diff --git a/src/actions/dig.rs b/src/actions/dig.rs new file mode 100644 index 0000000..bac1dcb --- /dev/null +++ b/src/actions/dig.rs @@ -0,0 +1,127 @@ +use std::collections::HashMap; +use std::str::FromStr; + +use mio::Token; + +use crate::actions::look; +use crate::command::Command; +use crate::database::Db; +use crate::queue::SendQueue; +use crate::world::{Direction, Exit, Room}; +use crate::{try_option_send_error, try_send_error}; + +/// Look at the room. Provide room name, description, and exits. +pub fn dig(command: &Command, args: String, token: Token, db: &mut Db) -> SendQueue { + let mut send_queue = SendQueue::new(); + + // find the player + let mut player = try_option_send_error!(token, db.get_connected_player(token)); + + // get the direction to dig + let direction = match Direction::from_str(&args) { + Ok(d) => d, + Err(e) => { + send_queue.push(token, format!("{}", e), true); + return send_queue; + } + }; + + // get starting room + let mut start_room = try_option_send_error!(token, db.load_room(player.location)); + + // make sure exit doesn't already exist + if start_room.exits.contains_key(&direction) { + send_queue.push(token, "Exit already exists", true); + return send_queue; + } + + // get starting zone + let mut zone = try_option_send_error!(token, db.load_zone(start_room.zone)); + + let new_room_id = try_send_error!(token, db.new_area_id()); + + // create a new, empty room + let mut new_room = Room { + id: new_room_id, + zone: start_room.zone, + name: format!("New Room {}", new_room_id), + description: Vec::new(), + users_visible: true, + exits: HashMap::new(), + }; + + // add exit from start room to new room + let _ = start_room.exits.insert( + direction, + Exit { + target: new_room.id, + direction, + }, + ); + + // add exit from new room to start room + let _ = new_room.exits.insert( + direction.opposite(), + Exit { + target: start_room.id, + direction: direction.opposite(), + }, + ); + + // add new room to zone + let _ = zone.areas.insert(new_room.id); + + // save the new room + if db.save_room(&new_room).is_ok() { + send_queue.push(token, "New room saved\n", false); + } else { + send_queue.push(token, "Unable to save new room", true); + return send_queue; + } + + // save the start room + if db.save_room(&start_room).is_ok() { + send_queue.push(token, "Start room saved\n", false); + } else { + send_queue.push(token, "Unable to save start room", true); + return send_queue; + } + + // save the zone + if db.save_zone(&zone).is_ok() { + send_queue.push(token, "Zone saved\n", false); + } else { + send_queue.push(token, "Unable to save zone", true); + return send_queue; + } + + // move and save the player + player.location = new_room.id; + if db.save_player(&player).is_ok() { + if db.save_connected_player(token, &player).is_ok() { + send_queue.push(token, format!("You dig {}.\n\n", direction.long()), false); + } else { + send_queue.push(token, "Unable to save connected player", true); + } + } else { + send_queue.push(token, "Unable to save player", true); + return send_queue; + } + + // inform people about what just took place + for (neighbor_token, _) in + try_send_error!(token, db.find_connected_players_by_location(start_room.id)) + { + if neighbor_token != token { + send_queue.push( + neighbor_token, + format!("{} digs {}.", player.name, direction.long()), + true, + ); + } + } + + send_queue.append(&mut look(&command, args, token, db)); + + send_queue +} diff --git a/src/actions/help.rs b/src/actions/help.rs new file mode 100644 index 0000000..2f250a9 --- /dev/null +++ b/src/actions/help.rs @@ -0,0 +1,11 @@ +use mio::Token; + +use crate::command::Command; +use crate::database::Db; +use crate::queue::SendQueue; + +pub fn help(command: &Command, args: String, token: Token, db: &mut Db) -> SendQueue { + let mut send_queue = SendQueue::new(); + + send_queue +} diff --git a/src/actions/look.rs b/src/actions/look.rs new file mode 100644 index 0000000..534695b --- /dev/null +++ b/src/actions/look.rs @@ -0,0 +1,55 @@ +use colored::Colorize; +use mio::Token; + +use crate::command::Command; +use crate::database::Db; +use crate::queue::SendQueue; +use crate::{try_option_send_error, try_send_error}; + +/// Look at the room. Provide room name, description, and exits. +pub fn look(_: &Command, _: String, token: Token, db: &mut Db) -> SendQueue { + let mut send_queue = SendQueue::new(); + + // get the player + let player = try_option_send_error!(token, db.get_connected_player(token)); + + // get the room + let room = try_option_send_error!(token, db.load_room(player.location)); + + // room name + send_queue.push(token, format!("{}\n", room.name.cyan().to_string()), false); + + // room description + send_queue.push( + token, + format!("{}\n", { + let mut s = room.description.join("\n"); + s.push_str("\n"); + s + }), + false, + ); + + // exits + send_queue.push( + token, + format!("[ obvious exits: {} ]\n", room.exit_string()) + .cyan() + .to_string(), + false, + ); + + // other people in room + for (neighbor_token, neighbor_player) in try_send_error!( + token, + db.find_connected_players_by_location(player.location) + ) { + if neighbor_token != token { + send_queue.push(token, format!("{} is here", neighbor_player.name), false); + } + } + + send_queue.push(token, "", true); + + send_queue +} diff --git a/src/actions/mod.rs b/src/actions/mod.rs new file mode 100644 index 0000000..859ee30 --- /dev/null +++ b/src/actions/mod.rs @@ -0,0 +1,13 @@ +mod dig; +mod help; +mod look; +mod move_room; +mod save; +mod say; + +pub use dig::dig; +pub use help::help; +pub use look::look; +pub use move_room::move_room; +pub use save::save; +pub use say::say; diff --git a/src/actions/move_room.rs b/src/actions/move_room.rs new file mode 100644 index 0000000..386a42b --- /dev/null +++ b/src/actions/move_room.rs @@ -0,0 +1,84 @@ +use log::warn; +use mio::Token; + +use crate::actions::look; +use crate::command::Command; +use crate::database::Db; +use crate::queue::SendQueue; +use crate::world::*; +use crate::{try_option_send_error, try_send_error}; + +pub fn move_room(command: &Command, _args: String, token: Token, db: &mut Db) -> SendQueue { + let mut send_queue = SendQueue::new(); + + let direction: Direction = match command { + Command::N | Command::North => Direction::North, + Command::S | Command::South => Direction::South, + Command::E | Command::East => Direction::East, + Command::W | Command::West => Direction::West, + Command::U | Command::Up => Direction::Up, + Command::D | Command::Down => Direction::Down, + _ => { + warn!("Can't figure out direction: {:?}", command); + return send_queue; + } + }; + + // find the player + let mut player = try_option_send_error!(token, db.get_connected_player(token)); + + // get starting room + let start_room = try_option_send_error!(token, db.load_room(player.location)); + + // get the exit + let exit = if let Some(exit) = start_room.exits.get(&direction) { + exit + } else { + send_queue.push(token, "You can't go that way.", true); + return send_queue; + }; + + // get the target room + let target_room = try_option_send_error!(token, db.load_room(exit.target)); + + // move and save the player + player.location = target_room.id; + let _ = try_send_error!(token, db.save_player(&player)); + let _ = try_send_error!(token, db.save_connected_player(token, &player)); + send_queue.push(token, format!("You leave {}.\n\n", direction.long()), false); + + // tell people about leaving + for (neighbor_token, _) in + try_send_error!(token, db.find_connected_players_by_location(start_room.id)) + { + if neighbor_token != token { + send_queue.push( + neighbor_token, + format!("{} leaves {}.", player.name, direction.long()), + true, + ); + } + } + + // tell people about entering + for (neighbor_token, _) in + try_send_error!(token, db.find_connected_players_by_location(target_room.id)) + { + if neighbor_token != token { + send_queue.push( + neighbor_token, + format!( + "{} arrives from the {}.", + player.name, + direction.opposite().long() + ), + true, + ); + } + } + + // look around + send_queue.append(&mut look(&command, String::new(), token, db)); + + send_queue +} diff --git a/src/actions/save.rs b/src/actions/save.rs new file mode 100644 index 0000000..f0c41e0 --- /dev/null +++ b/src/actions/save.rs @@ -0,0 +1,18 @@ +use mio::Token; + +use crate::command::Command; +use crate::database::Db; +use crate::queue::SendQueue; +use crate::{try_option_send_error, try_send_error}; + +/// Save the player information to disk. +pub fn save(_: &Command, _: String, token: Token, db: &mut Db) -> SendQueue { + let mut send_queue = SendQueue::new(); + + let player = try_option_send_error!(token, db.get_connected_player(token)); + let _ = try_send_error!(token, db.save_player(&player)); + let _ = try_send_error!(token, db.save_connected_player(token, &player)); + + send_queue.push(token, "Ok", true); + send_queue +} diff --git a/src/actions/say.rs b/src/actions/say.rs new file mode 100644 index 0000000..9092cb8 --- /dev/null +++ b/src/actions/say.rs @@ -0,0 +1,30 @@ +use mio::Token; + +use crate::command::Command; +use crate::database::Db; +use crate::queue::SendQueue; +use crate::{try_option_send_error, try_send_error}; + +/// Say something to anyone in the room. +pub fn say(_: &Command, args: String, token: Token, db: &mut Db) -> SendQueue { + let mut send_queue = SendQueue::new(); + + let player = try_option_send_error!(token, db.get_connected_player(token)); + + for (neighbor_token, _) in try_send_error!( + token, + db.find_connected_players_by_location(player.location) + ) { + send_queue.push( + neighbor_token, + if neighbor_token == token { + format!("You say, \"{}\"\n", args) + } else { + format!("{} says, \"{}\"\n", player.name, args) + }, + true, + ); + } + + send_queue +} diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..2ce8379 --- /dev/null +++ b/src/client.rs @@ -0,0 +1,84 @@ +use std::fmt; +use std::io::{BufRead, BufReader, BufWriter, ErrorKind, Write}; +use std::net::SocketAddr; + +use log::error; + +use mio::net::TcpStream; +use mio::Token; + +use crate::result::RudeResult; +use crate::state::*; + +/// `Client` struct for storing information about a connected client, also +/// a few helper functions for communicating with the client. +#[derive(Debug)] +pub struct Client { + /// TCP socket + pub socket: TcpStream, + + /// Identifier token + pub token: Token, + + /// IP information + pub addr: SocketAddr, + + /// Client's play state + pub state: State, +} + +impl fmt::Display for Client { + /// Only need the ip and port to be printed. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}:{}", self.addr.ip(), self.addr.port()) + } +} + +impl Into for Client { + /// Convert the ip and port into a string. + fn into(self) -> String { + format!("{}:{}", self.addr.ip(), self.addr.port()) + } +} + +impl Client { + /// Read a message from the client + pub fn read(&self) -> RudeResult { + let reader = BufReader::new(&self.socket); + let mut buf = String::new(); + + for line in reader.lines() { + match line { + Ok(line) => buf += &line, + Err(e) => match e.kind() { + ErrorKind::WouldBlock => break, + _ => return Err(e.into()), + }, + } + } + + Ok(buf) + } + + /// Send a string to the client. + pub fn send_without_prompt>(&mut self, message: T) { + let message = message.into(); + let mut writer = BufWriter::new(&self.socket); + if let Err(e) = writer.write_all(message.as_bytes()) { + error!("Unable to send message to client ({:?}): {}", self, e); + } + } + + /// Send a string to the client, followed by a prompt. + pub fn send_with_prompt>(&mut self, message: T) { + let message = message.into() + self.prompt(); + let mut writer = BufWriter::new(&self.socket); + if let Err(e) = writer.write_all(message.as_bytes()) { + error!("Unable to send message to client ({:?}): {}", self, e); + } + } + + fn prompt(&self) -> &str { + "\n> " + } +} diff --git a/src/command/command.rs b/src/command/command.rs new file mode 100644 index 0000000..17207a3 --- /dev/null +++ b/src/command/command.rs @@ -0,0 +1,107 @@ +use std::default::Default; + +use mio::Token; +use strum_macros::{Display, EnumIter}; + +use crate::actions; +use crate::command::{CommandSet, Parse, ParserError}; +use crate::database::Db; +use crate::queue::SendQueue; + +#[derive(Clone, Debug, Display, EnumIter, Eq, Ord, PartialEq, PartialOrd)] +pub enum Command { + N, + S, + E, + W, + U, + D, + North, + South, + East, + West, + Up, + Down, + Dig, + Help, + Look, + Save, + Say, + Set(CommandSet), + Default, +} + +impl Default for Command { + fn default() -> Self { + Self::Default + } +} + +impl Parse for Command { + fn help(&self) -> &str { + match self { + Self::N => "n :: Move one room to the north.", + Self::S => "s :: Move one room to the south.", + Self::E => "e :: Move one room to the east.", + Self::W => "w :: Move one room to the west.", + Self::U => "u :: Move one room up.", + Self::D => "d :: Move one room down.", + Self::North => "north :: Move one room to the north.", + Self::South => "south :: Move one room to the south.", + Self::East => "east :: Move one room to the east.", + Self::West => "west :: Move one room to the west.", + Self::Up => "up :: Move one room up.", + Self::Down => "down :: Move one room down.", + Self::Dig => "dig DIRECTION:: Dig a new path to a new room in the provided direction.", + Self::Help => "help [COMMAND]:: Provide help on a specific command, or an overview.", + Self::Look => "look :: Take a look around the current room.", + Self::Save => "save :: Write player information to disk.", + Self::Say => "say MESSAGE:: Say something to the room.", + Self::Set(_) => "set OPTIONS :: Set various parameters.", + Self::Default => "", + } + } + + fn parse_subcommand(&self, s: String) -> Result<(Self, String), ParserError> { + match self { + Self::Set(_) => { + let (command, args) = CommandSet::parse(s)?; + Ok((Self::Set(command), args)) + } + Self::Default => Err(ParserError::Default), + _ => Ok((self.clone(), s)), + } + } + + fn subcommand_help(&self, _: String) -> Option { + match self { + Self::Set(command_set) => Some(command_set.help_all()), + _ => None, + } + } + + fn dispatch_map_subcommand(&self, args: String, token: Token, db: &mut Db) -> SendQueue { + match self { + Self::Set(command_set) => command_set.dispatch(command_set, args, token, db), + _ => SendQueue::new(), + } + } + + fn dispatch_map(&self) -> fn(&Self, String, Token, &mut Db) -> SendQueue { + match self { + Self::N | Self::North => actions::move_room, + Self::S | Self::South => actions::move_room, + Self::E | Self::East => actions::move_room, + Self::W | Self::West => actions::move_room, + Self::U | Self::Up => actions::move_room, + Self::D | Self::Down => actions::move_room, + Self::Dig => actions::dig, + Self::Help => actions::help, + Self::Look => actions::look, + Self::Save => actions::save, + Self::Say => actions::say, + Self::Set(_) => Self::dispatch_map_subcommand, + Self::Default => Self::dispatch_default, + } + } +} diff --git a/src/command/mod.rs b/src/command/mod.rs new file mode 100644 index 0000000..ca67c96 --- /dev/null +++ b/src/command/mod.rs @@ -0,0 +1,9 @@ +mod command; +mod parse; +mod parser_error; +mod set; + +pub use command::Command; +pub use parse::Parse; +pub use parser_error::ParserError; +pub use set::*; diff --git a/src/command/parse.rs b/src/command/parse.rs new file mode 100644 index 0000000..8246e66 --- /dev/null +++ b/src/command/parse.rs @@ -0,0 +1,131 @@ +use std::string::ToString; + +use mio::Token; +use strum::IntoEnumIterator; + +use crate::command::ParserError; +use crate::database::Db; +use crate::queue::SendQueue; + +/// Command parser. Commands consist of the text from the beginning of a string, up to but not +/// including the first space character or newline. If a space, that space is removed and the +/// rest of the string is put into args. If a newline, args will be empty. The command part of the +/// string is then parsed for a match with the variants of the enum. After that, control is passed +/// to `parse_subcommand()` to parse a subcommand if necessary. Also integrated into this is the +/// help system. +pub trait Parse: Clone + Default + IntoEnumIterator + PartialEq + ToString { + /// Help text for a single command. + fn help(&self) -> &str; + + /// Check if there's a subcommand. The default implementation does not support subcommands. + /// If subcommands are necessary, you must implement the `subcommand()` function. + fn parse_subcommand(&self, s: String) -> Result<(Self, String), ParserError> { + Ok((self.clone(), s)) + } + + /// Gets the `help_all()` for a subcommand. The default implementation does not support + /// subcommands. You must implement the `subcommand_help()` function if you want sumcommands. + fn subcommand_help(&self, _: String) -> Option { + None + } + + /// Map of variants to functions to be called with 'dispatch()'. + fn dispatch_map(&self) -> fn(&Self, String, Token, &mut Db) -> SendQueue; + + /// Must be implemented if there are subcommands. + fn dispatch_map_subcommand(&self, _: String, _: Token, _: &mut Db) -> SendQueue { + SendQueue::new() + } + + /// This is to call the function associated with a command. + fn dispatch( + &self, + command: &Self, + command_text: String, + token: Token, + db: &mut Db, + ) -> SendQueue { + let function = self.dispatch_map(); + function(&command, command_text, token, db) + } + + /// Boilerplate for default variant. Shouldn't ever be called. Probably should log if it is. + fn dispatch_default(&self, _: String, _: Token, _: &mut Db) -> SendQueue { + SendQueue::new() + } + + /// Help text for all commands + fn help_all(&self) -> String { + Self::iter() + .filter_map(|a| { + if a == Self::default() { + None + } else { + Some(a.help().into()) + } + }) + .collect::>() + .join("\n") + } + + /// List of all commands + fn commands(&self) -> Vec { + Self::iter() + .filter_map(|a| { + if a == Self::default() { + None + } else { + Some(a.to_string().to_lowercase()) + } + }) + .collect() + } + + /// Parse a `Into` into a command. + fn parse>(s: S) -> Result<(Self, String), ParserError> { + let mut s = s.into(); + + // get the text command and the args + let mut args: String = if let Some(pos) = s.find(' ') { + s.split_off(pos) + } else { + String::new() + }; + + if args.starts_with(' ') { + args = args.split_off(1); + } + + // no command + if s.is_empty() { + return Err(ParserError::Empty); + } + + let mut matches: Vec = Vec::new(); + + // look for matches + for action in Self::iter() { + if s == action.to_string() { + matches = vec![action]; + break; + } else if action.to_string().to_lowercase().starts_with(s.as_str()) { + matches.push(action); + } + } + + // check if there was a match + if matches.is_empty() { + return Err(ParserError::Unknown); + } + + // sort so the first match is the best + matches.sort_by(|a, b| a.to_string().cmp(&b.to_string())); + + // default is an error + if matches[0] == Self::default() { + Err(ParserError::Default) + } else { + Ok(matches[0].parse_subcommand(args)?) + } + } +} diff --git a/src/command/parser_error.rs b/src/command/parser_error.rs new file mode 100644 index 0000000..2841d54 --- /dev/null +++ b/src/command/parser_error.rs @@ -0,0 +1,21 @@ +use std::error::Error; +use std::fmt; + +#[derive(Debug)] +pub enum ParserError { + Empty, + Unknown, + Default, +} + +impl fmt::Display for ParserError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Empty => fmt::Display::fmt("No command provided", f), + Self::Unknown => fmt::Display::fmt("Unknown command", f), + Self::Default => fmt::Display::fmt("Internal error", f), + } + } +} + +impl Error for ParserError {} diff --git a/src/command/set/mod.rs b/src/command/set/mod.rs new file mode 100644 index 0000000..2702c06 --- /dev/null +++ b/src/command/set/mod.rs @@ -0,0 +1,9 @@ +mod player; +mod room; +mod set; +mod zone; + +pub use player::*; +pub use room::*; +pub use set::*; +pub use zone::*; diff --git a/src/command/set/player/mod.rs b/src/command/set/player/mod.rs new file mode 100644 index 0000000..6ffd43b --- /dev/null +++ b/src/command/set/player/mod.rs @@ -0,0 +1,5 @@ +mod name; +mod player; + +pub use name::*; +pub use player::*; diff --git a/src/command/set/player/name.rs b/src/command/set/player/name.rs new file mode 100644 index 0000000..0e98900 --- /dev/null +++ b/src/command/set/player/name.rs @@ -0,0 +1,25 @@ +use mio::Token; + +use crate::command::CommandSetPlayer; +use crate::database::Db; +use crate::queue::SendQueue; +use crate::{try_option_send_error, try_send_error}; + +impl CommandSetPlayer { + pub fn dispatch_name(&self, args: String, token: Token, db: &mut Db) -> SendQueue { + let mut player = try_option_send_error!(token, db.get_connected_player(token)); + + let new_name = args.trim(); + if new_name.is_empty() { + //return SendQueue::from_string(token, "Name can't be empty") + return SendQueue(vec![(token, "Name can't be empty".into(), true)].into()); + } + + player.name = new_name.to_string(); + + let _ = try_send_error!(token, db.save_player(&player)); + let _ = try_send_error!(token, db.save_connected_player(token, &player)); + + SendQueue::ok(token) + } +} diff --git a/src/command/set/player/player.rs b/src/command/set/player/player.rs new file mode 100644 index 0000000..62708d2 --- /dev/null +++ b/src/command/set/player/player.rs @@ -0,0 +1,36 @@ +use std::default::Default; + +use mio::Token; +use strum_macros::{Display, EnumIter}; + +use crate::command::Parse; +use crate::database::Db; +use crate::queue::SendQueue; + +#[derive(Clone, Debug, Display, EnumIter, Eq, Ord, PartialEq, PartialOrd)] +pub enum CommandSetPlayer { + Name, + Default, +} + +impl Default for CommandSetPlayer { + fn default() -> Self { + Self::Default + } +} + +impl Parse for CommandSetPlayer { + fn help(&self) -> &str { + match self { + Self::Name => "set player name NEW_NAME :: Set player name to NEW_NAME.", + Self::Default => "", + } + } + + fn dispatch_map(&self) -> fn(&Self, String, Token, &mut Db) -> SendQueue { + match self { + Self::Name => Self::dispatch_name, + Self::Default => Self::dispatch_default, + } + } +} diff --git a/src/command/set/room/mod.rs b/src/command/set/room/mod.rs new file mode 100644 index 0000000..3c78711 --- /dev/null +++ b/src/command/set/room/mod.rs @@ -0,0 +1,5 @@ +mod name; +mod room; + +pub use name::*; +pub use room::*; diff --git a/src/command/set/room/name.rs b/src/command/set/room/name.rs new file mode 100644 index 0000000..9c8932d --- /dev/null +++ b/src/command/set/room/name.rs @@ -0,0 +1,19 @@ +use mio::Token; + +use crate::command::CommandSetRoom; +use crate::database::Db; +use crate::queue::SendQueue; +use crate::{try_option_send_error, try_send_error}; + +impl CommandSetRoom { + pub fn dispatch_name(&self, args: String, token: Token, db: &mut Db) -> SendQueue { + let player = try_option_send_error!(token, db.get_connected_player(token)); + + let mut room = try_option_send_error!(token, db.load_room(player.location)); + room.name = args; + + let _ = try_send_error!(token, db.save_room(&room)); + + SendQueue::ok(token) + } +} diff --git a/src/command/set/room/room.rs b/src/command/set/room/room.rs new file mode 100644 index 0000000..0fd1152 --- /dev/null +++ b/src/command/set/room/room.rs @@ -0,0 +1,36 @@ +use std::default::Default; + +use mio::Token; +use strum_macros::{Display, EnumIter}; + +use crate::command::Parse; +use crate::database::Db; +use crate::queue::SendQueue; + +#[derive(Clone, Debug, Display, EnumIter, Eq, Ord, PartialEq, PartialOrd)] +pub enum CommandSetRoom { + Name, + Default, +} + +impl Default for CommandSetRoom { + fn default() -> Self { + Self::Default + } +} + +impl Parse for CommandSetRoom { + fn help(&self) -> &str { + match self { + Self::Name => "set room name NEW_NAME :: Set room name to NEW_NAME.", + Self::Default => "", + } + } + + fn dispatch_map(&self) -> fn(&Self, String, Token, &mut Db) -> SendQueue { + match self { + Self::Name => Self::dispatch_name, + Self::Default => Self::dispatch_default, + } + } +} diff --git a/src/command/set/set.rs b/src/command/set/set.rs new file mode 100644 index 0000000..43e78d4 --- /dev/null +++ b/src/command/set/set.rs @@ -0,0 +1,75 @@ +use std::default::Default; + +use mio::Token; +use strum_macros::{Display, EnumIter}; + +use crate::command::{CommandSetPlayer, CommandSetRoom, CommandSetZone, Parse, ParserError}; +use crate::database::Db; +use crate::queue::SendQueue; + +#[derive(Clone, Debug, Display, EnumIter, Eq, Ord, PartialEq, PartialOrd)] +pub enum CommandSet { + Player(CommandSetPlayer), + Room(CommandSetRoom), + Zone(CommandSetZone), + Default, +} + +impl Default for CommandSet { + fn default() -> Self { + Self::Default + } +} + +impl Parse for CommandSet { + fn help(&self) -> &str { + match self { + Self::Player(_) => "set player :: Set player options.", + Self::Room(_) => "set room :: Set room options.", + Self::Zone(_) => "set zone :: Set zone options.", + Self::Default => "", + } + } + + fn parse_subcommand(&self, s: String) -> Result<(Self, String), ParserError> { + match self { + Self::Player(_) => { + let (command, args) = CommandSetPlayer::parse(s)?; + Ok((Self::Player(command), args)) + } + Self::Room(_) => { + let (command, args) = CommandSetRoom::parse(s)?; + Ok((Self::Room(command), args)) + } + Self::Zone(_) => { + let (command, args) = CommandSetZone::parse(s)?; + Ok((Self::Zone(command), args)) + } + Self::Default => Err(ParserError::Default), + } + } + + fn dispatch_map_subcommand(&self, args: String, token: Token, db: &mut Db) -> SendQueue { + match self { + Self::Player(command_set_player) => { + command_set_player.dispatch(command_set_player, args, token, db) + } + Self::Room(command_set_room) => { + command_set_room.dispatch(command_set_room, args, token, db) + } + Self::Zone(command_set_zone) => { + command_set_zone.dispatch(command_set_zone, args, token, db) + } + _ => SendQueue::new(), + } + } + + fn dispatch_map(&self) -> fn(&Self, String, Token, &mut Db) -> SendQueue { + match self { + Self::Player(_) => Self::dispatch_map_subcommand, + Self::Room(_) => Self::dispatch_map_subcommand, + Self::Zone(_) => Self::dispatch_map_subcommand, + Self::Default => Self::dispatch_default, + } + } +} diff --git a/src/command/set/zone/mod.rs b/src/command/set/zone/mod.rs new file mode 100644 index 0000000..d7e2202 --- /dev/null +++ b/src/command/set/zone/mod.rs @@ -0,0 +1,5 @@ +mod name; +mod zone; + +pub use name::*; +pub use zone::*; diff --git a/src/command/set/zone/name.rs b/src/command/set/zone/name.rs new file mode 100644 index 0000000..c228654 --- /dev/null +++ b/src/command/set/zone/name.rs @@ -0,0 +1,19 @@ +use mio::Token; + +use crate::command::CommandSetZone; +use crate::database::Db; +use crate::queue::SendQueue; +use crate::{try_option_send_error, try_send_error}; + +impl CommandSetZone { + pub fn dispatch_name(&self, args: String, token: Token, db: &mut Db) -> SendQueue { + let player = try_option_send_error!(token, db.get_connected_player(token)); + + let mut zone = try_option_send_error!(token, db.load_zone(player.location)); + zone.name = args; + + let _ = try_send_error!(token, db.save_zone(&zone)); + + SendQueue::ok(token) + } +} diff --git a/src/command/set/zone/zone.rs b/src/command/set/zone/zone.rs new file mode 100644 index 0000000..bace83b --- /dev/null +++ b/src/command/set/zone/zone.rs @@ -0,0 +1,36 @@ +use std::default::Default; + +use mio::Token; +use strum_macros::{Display, EnumIter}; + +use crate::command::Parse; +use crate::database::Db; +use crate::queue::SendQueue; + +#[derive(Clone, Debug, Display, EnumIter, Eq, Ord, PartialEq, PartialOrd)] +pub enum CommandSetZone { + Name, + Default, +} + +impl Default for CommandSetZone { + fn default() -> Self { + Self::Default + } +} + +impl Parse for CommandSetZone { + fn help(&self) -> &str { + match self { + Self::Name => "set zone name NEW_NAME :: Set zone name to NEW_NAME.", + Self::Default => "", + } + } + + fn dispatch_map(&self) -> fn(&Self, String, Token, &mut Db) -> SendQueue { + match self { + Self::Name => Self::dispatch_name, + Self::Default => Self::dispatch_default, + } + } +} diff --git a/src/config/config.rs b/src/config/config.rs new file mode 100644 index 0000000..d26aba4 --- /dev/null +++ b/src/config/config.rs @@ -0,0 +1,31 @@ +use std::path::PathBuf; + +use serde_derive::Deserialize; + +use crate::config::*; +use crate::file; +use crate::id::Id; +use crate::result::RudeResult; + +/// Game configuration +#[derive(Clone, Debug, Deserialize)] +pub struct Config { + /// Directory to hold world and players + pub database: PathBuf, + + /// Server configuration + pub server: Server, + + /// Logging configuration + pub logging: Logging, + + /// Default starting location + pub starting_location: Id, +} + +impl Config { + /// Load and deserialize the toml configuration from the given file. + pub fn load>(path: P) -> RudeResult { + Ok(file::read_print(&path.into())?) + } +} diff --git a/src/config/logging.rs b/src/config/logging.rs new file mode 100644 index 0000000..3d4a248 --- /dev/null +++ b/src/config/logging.rs @@ -0,0 +1,10 @@ +use serde_derive::Deserialize; + +use crate::config::LogLevel; + +/// Logging configuration +#[derive(Clone, Debug, Deserialize)] +pub struct Logging { + /// Level of log verbosity. + pub level: LogLevel, +} diff --git a/src/config/loglevel.rs b/src/config/loglevel.rs new file mode 100644 index 0000000..f91ab5f --- /dev/null +++ b/src/config/loglevel.rs @@ -0,0 +1,45 @@ +use log::LevelFilter; +use serde_derive::Deserialize; + +/// Helper enum to handle deserialization from toml because `LevelFilter` +/// from the log crate does not. +#[derive(Clone, Debug, Deserialize)] +pub enum LogLevel { + /// log::LevelFilter::Off + #[allow(non_camel_case_types)] + off, + + /// log::LevelFilter::Error + #[allow(non_camel_case_types)] + error, + + /// log::LevelFilter::Warn + #[allow(non_camel_case_types)] + warn, + + /// log::LevelFilter::Info + #[allow(non_camel_case_types)] + info, + + /// log::LevelFilter::Debug + #[allow(non_camel_case_types)] + debug, + + /// log::LevelFilter::Trace + #[allow(non_camel_case_types)] + trace, +} + +impl LogLevel { + /// Return the associated `LevelFilter`. + pub fn level(&self) -> LevelFilter { + match &self { + Self::off => LevelFilter::Off, + Self::error => LevelFilter::Error, + Self::warn => LevelFilter::Warn, + Self::info => LevelFilter::Info, + Self::debug => LevelFilter::Debug, + Self::trace => LevelFilter::Trace, + } + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..72808cb --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,12 @@ +//! Game configuration + +#[allow(clippy::module_inception)] +mod config; +mod logging; +mod loglevel; +mod server; + +pub use config::Config; +pub use logging::Logging; +pub use loglevel::LogLevel; +pub use server::Server; diff --git a/src/config/server.rs b/src/config/server.rs new file mode 100644 index 0000000..41be8af --- /dev/null +++ b/src/config/server.rs @@ -0,0 +1,17 @@ +use serde_derive::Deserialize; + +/// Server configuration +#[derive(Clone, Debug, Deserialize)] +pub struct Server { + /// IP address to listen on. + pub ip: String, + + /// Port to listen on. + pub port: i64, + + /// Max number of connected players. + pub connection_limit: usize, + + /// Socket event capacity. + pub event_capacity: usize, +} diff --git a/src/database/connected_players.rs b/src/database/connected_players.rs new file mode 100644 index 0000000..a97e287 --- /dev/null +++ b/src/database/connected_players.rs @@ -0,0 +1,101 @@ +use std::convert::TryFrom; + +use mio::Token; +use rusqlite::params; + +use crate::database::Db; +use crate::id::Id; +use crate::player::Player; +use crate::result::RudeResult; +use crate::try_log; + +impl Db { + /// Get all connected players in a room + pub fn find_connected_players_by_location( + &self, + location: Id, + ) -> RudeResult> { + let mut statement = try_log!( + self.0.prepare( + "select connected_players.token, players.id, players.name, players.password, players.created, players.location from connected_players, players where players.location = ? and connected_players.player = players.id;" + ), + "Unable to prepare sql statement" + ); + + let mut rows = try_log!( + statement.query(params![location]), + "Unable to perform query" + ); + + let mut v = Vec::<(Token, Player)>::new(); + while let Some(row) = try_log!(rows.next(), "Unable to retrieve row") { + let row_token: i64 = try_log!(row.get("token"), "Unable to get token"); + let token: Token = Token::from(usize::from_le_bytes(row_token.to_le_bytes())); + let player = try_log!(Player::try_from(row), "Unable to get Player from Row"); + v.push((token, player)); + } + + Ok(v) + } + + /// Load a player from the database. + pub fn get_connected_player(&self, token: Token) -> RudeResult> { + let mut statement = try_log!( + self.0.prepare( + "select connected_players.token, players.id, players.name, players.password, players.created, players.location from connected_players, players where connected_players.token = ? and connected_players.player = players.id;" + ), + "Unable to prepare sql statement" + ); + + let mut rows = try_log!( + statement.query(params![i64::from_le_bytes(token.0.to_le_bytes())]), + "Unable to perform query" + ); + + if let Some(row) = try_log!(rows.next(), "Unable to retrieve row") { + Ok(Some(try_log!( + Player::try_from(row), + "Unable to get Player from Row" + ))) + } else { + Ok(None) + } + } + + /// Save a connected player to the database. + pub fn save_connected_player(&self, token: Token, player: &Player) -> RudeResult<()> { + let mut statement = try_log!( + self.0.prepare( + "insert into connected_players (token, player) values (?, ?) on conflict(token) do update set player=?;" + ), + "Unable to prepare sql statement" + ); + + let _ = try_log!( + statement.execute(params![ + i64::from_le_bytes(token.0.to_le_bytes()), + player.id, + player.id, + ]), + "Unable to perform query" + ); + + Ok(()) + } + + /// Remove player from connected_players table + pub fn remove_connected_player(&self, token: Token) -> RudeResult<()> { + let mut statement = try_log!( + self.0 + .prepare("delete from connected_players where token = ?;"), + "Unable to prepare sql statement" + ); + + let _ = try_log!( + statement.execute(params![i64::from_le_bytes(token.0.to_le_bytes())]), + "Unable to execute sql statement" + ); + + Ok(()) + } +} diff --git a/src/database/db.rs b/src/database/db.rs new file mode 100644 index 0000000..240274b --- /dev/null +++ b/src/database/db.rs @@ -0,0 +1,84 @@ +use std::path::Path; + +use mio::Token; +use rusqlite::{params, Connection}; + +use crate::id::Id; +use crate::player::Player; +use crate::result::RudeResult; +use crate::try_log; + +pub struct Db(pub Connection); + +impl Db { + /// Open the database + pub fn open>(path: P) -> RudeResult { + let connection = try_log!(Connection::open(path), "Unable to open database"); + + { + let mut statement = try_log!( + connection.prepare("delete from connected_players;"), + "Unable to prepare sql statement" + ); + + let _ = try_log!( + statement.execute(params![]), + "Unable to execute sql statement" + ); + } + + Ok(Self(connection)) + } + + pub fn single_save_player(&self, token: Token, player: &Player) -> RudeResult<()> { + let _ = self.save_player(&player)?; + self.save_connected_player(token, &player) + } + + /// Get a new player id, checked to ensure uniqueness. + pub fn new_player_id(&self) -> RudeResult { + let mut statement = try_log!( + self.0.prepare("select * from players where id=?;"), + "Unable to prepare sql statement" + ); + + loop { + let id = Id::new(); + let mut rows = try_log!(statement.query(params![id]), "Unable to perform query"); + if try_log!(rows.next(), "Unable to retrieve row").is_none() { + return Ok(id); + } + } + } + + /// Get a new area id, checked to ensure uniqueness. + pub fn new_area_id(&self) -> RudeResult { + let mut zones_statement = try_log!( + self.0.prepare("select * from zones where id = ?;"), + "Unable to prepare sql statement" + ); + + let mut rooms_statement = try_log!( + self.0.prepare("select * from rooms where id = ?;"), + "Unable to prepare sql statement" + ); + + loop { + let id = Id::new(); + + let mut rows = try_log!( + zones_statement.query(params![id]), + "Unable to perform query" + ); + if try_log!(rows.next(), "Unable to retrieve row").is_none() { + let mut rows = try_log!( + rooms_statement.query(params![id]), + "Unable to perform sql query" + ); + if try_log!(rows.next(), "Unable to retrieve row").is_none() { + return Ok(id); + } + } + } + } +} diff --git a/src/database/mod.rs b/src/database/mod.rs new file mode 100644 index 0000000..4ea56a5 --- /dev/null +++ b/src/database/mod.rs @@ -0,0 +1,7 @@ +mod connected_players; +mod db; +mod players; +mod rooms; +mod zones; + +pub use db::Db; diff --git a/src/database/players.rs b/src/database/players.rs new file mode 100644 index 0000000..6c42ac0 --- /dev/null +++ b/src/database/players.rs @@ -0,0 +1,87 @@ +use std::convert::TryFrom; + +use rusqlite::params; + +use crate::database::Db; +use crate::id::Id; +use crate::player::Player; +use crate::result::RudeResult; +use crate::try_log; + +impl Db { + /// Load a player from the database. + pub fn load_player(&self, id: Id) -> RudeResult> { + let mut statement = try_log!( + self.0 + .prepare("select id, name, password, created, location from players where id = ?"), + "Unable to prepare sql statement" + ); + + let mut rows = try_log!(statement.query(params![id]), "Unable to perform query"); + + Ok( + if let Some(row) = try_log!(rows.next(), "Unable to retrieve row") { + Some(try_log!( + Player::try_from(row), + "Unable to get Player from Row" + )) + } else { + None + }, + ) + } + + /// Find a player by the name + pub fn find_player_by_name>(&self, name: S) -> RudeResult> { + let mut statement = try_log!( + self.0.prepare( + "select id, name, password, created, location from players where name = ?" + ), + "Unable to prepare sql statement" + ); + + let mut rows = try_log!( + statement.query(params![name.into()]), + "Unable to perform query" + ); + + Ok( + if let Some(row) = try_log!(rows.next(), "Unable to retrieve row") { + Some(try_log!( + Player::try_from(row), + "Unable to get Player from Row" + )) + } else { + None + }, + ) + } + + /// Save a player to the database. + pub fn save_player(&self, player: &Player) -> RudeResult<()> { + let mut statement = try_log!( + self.0.prepare( + "insert into players (id, name, password, created, location) values (?, ?, ?, ?, ?) on conflict(id) do update set id=?, name=?, password=?, created=?, location=?;" + ), + "Unable to prepare statement" + ); + + let _ = try_log!( + statement.execute(params![ + player.id, + player.name, + player.password, + player.created, + player.location, + player.id, + player.name, + player.password, + player.created, + player.location, + ]), + "Unable to perform query" + ); + + Ok(()) + } +} diff --git a/src/database/rooms.rs b/src/database/rooms.rs new file mode 100644 index 0000000..21b5453 --- /dev/null +++ b/src/database/rooms.rs @@ -0,0 +1,92 @@ +use std::collections::HashMap; +use std::convert::TryFrom; + +use rusqlite::params; + +use crate::database::Db; +use crate::id::Id; +use crate::result::RudeResult; +use crate::try_log; +use crate::world::{Exit, Room}; + +impl Db { + /// Save a room to the database. + pub fn save_room(&self, room: &Room) -> RudeResult<()> { + let mut statement = try_log!( + self.0.prepare( + "insert into rooms (id, zone, name, description, users_visible) values (?, ?, ?, ?, ?) on conflict(id) do update set id=?, zone=?, name=?, description=?, users_visible=?;" + ), + "Unable to prepare sql statement" + ); + + let _ = try_log!( + statement.execute(params![ + room.id, + room.zone, + room.name, + room.description.join("\n"), + room.users_visible, + room.id, + room.zone, + room.name, + room.description.join("\n"), + room.users_visible, + ]), + "Unable to perform query" + ); + + for (direction, exit) in room.exits.iter() { + let mut statement = try_log!( + self.0 + .prepare("insert into exits (room, target, direction) values (?, ?, ?);"), + "Unable to prepare sql statement" + ); + + let _ = try_log!( + statement.execute(params![room.id, exit.target, exit.direction,]), + "Unable to perform query" + ); + } + + Ok(()) + } + + /// Load a room from the database. + pub fn load_room(&self, id: Id) -> RudeResult> { + let mut statement = try_log!( + self.0.prepare( + "select id, zone, name, description, users_visible from rooms where id = ?" + ), + "Unable to prepare sql statement" + ); + + let mut rows = try_log!(statement.query(params![id]), "Unable to perform query"); + + let mut room = if let Some(row) = try_log!(rows.next(), "Unable to retrieve row") { + try_log!(Room::try_from(row), "Unable to get Room from Row") + } else { + return Ok(None); + }; + + room.exits = { + let mut e = HashMap::new(); + + let mut statement = try_log!( + self.0 + .prepare("select room, target, direction from exits where room = ?"), + "Unable to prepare sql statement" + ); + + let mut rows = try_log!(statement.query(params![id]), "Unable to perform query"); + + while let Some(row) = try_log!(rows.next(), "Unable to retrieve row") { + let exit = try_log!(Exit::try_from(row), "Unable to get Exit from Row"); + e.insert(exit.direction, exit); + } + + e + }; + + Ok(Some(room)) + } +} diff --git a/src/database/zones.rs b/src/database/zones.rs new file mode 100644 index 0000000..f288a34 --- /dev/null +++ b/src/database/zones.rs @@ -0,0 +1,88 @@ +use std::collections::HashSet; +use std::convert::TryFrom; + +use rusqlite::params; + +use crate::database::Db; +use crate::id::Id; +use crate::result::RudeResult; +use crate::try_log; +use crate::world::Zone; + +impl Db { + /// Save a zone to the database. + pub fn save_zone(&self, zone: &Zone) -> RudeResult<()> { + let mut statement = try_log!( + self.0.prepare( + "insert into zones (id, parent, name, users_visible) values (?, ?, ?, ?) on conflict(id) do update set id=?, parent=?, name=?, users_visible=?;" + ), + "Unable to prepare sql statement" + ); + + let _ = try_log!( + statement.execute(params![ + zone.id, + zone.parent, + zone.name, + zone.users_visible, + zone.id, + zone.parent, + zone.name, + zone.users_visible, + ]), + "Unable to perform query" + ); + + Ok(()) + } + + /// Load a zone from the database. + pub fn load_zone(&self, id: Id) -> RudeResult> { + let mut statement = try_log!( + self.0 + .prepare("select id, parent, name, users_visible from zones where id = ?"), + "Unable to prepare sql statement" + ); + + let mut rows = try_log!(statement.query(params![id]), "Unable to perform query"); + + let mut zone = if let Some(row) = try_log!(rows.next(), "Unable to retrieve row") { + try_log!(Zone::try_from(row), "Unable to get Zone from Row") + } else { + return Ok(None); + }; + + zone.areas = { + let mut a = HashSet::new(); + + let mut statement = try_log!( + self.0.prepare("select id from zones where parent = ?;"), + "Unable to prepare sql statement" + ); + + let mut rows = try_log!(statement.query(params![id]), "Unable to perform query"); + + while let Some(row) = try_log!(rows.next(), "Unable to retrieve row") { + let new_id = try_log!(row.get(0), "Unable to retrieve field"); + if new_id != id { + a.insert(new_id); + } + } + + let mut statement = try_log!( + self.0.prepare("select id from rooms where zone = ?;"), + "Unable to prepare sql statement" + ); + + let mut rows = try_log!(statement.query(params![id]), "Unable to perform query"); + + while let Some(row) = try_log!(rows.next(), "Unable to retrieve row") { + a.insert(try_log!(row.get(0), "Unable to retrieve field")); + } + + a + }; + + Ok(Some(zone)) + } +} diff --git a/src/file.rs b/src/file.rs new file mode 100644 index 0000000..26eb235 --- /dev/null +++ b/src/file.rs @@ -0,0 +1,43 @@ +use std::fs::File; +use std::io::{BufReader, Read}; +use std::path::PathBuf; + +use log::debug; +use serde::de::Deserialize; + +use crate::result::*; + +/// Open a file from the filesystem, read the contents, parse toml and return +/// the corresponding toml object. Errors will be printed as well as returned. +pub fn read_print<'de, P: Into, T: Deserialize<'de>>(path: P) -> RudeResult { + read_file(path, false) +} + +/// The actual function to read a file. +fn read_file<'de, P: Into, T: Deserialize<'de>>(path: P, log: bool) -> RudeResult { + let path = path.into(); + debug!("Reading file {}", path.display()); + + let file: File = try_error( + File::open(path.clone()), + format!("Unable to open file: {}", path.display()), + log, + )?; + + let mut buffer = BufReader::new(file); + let mut file_contents = String::new(); + + try_error( + buffer.read_to_string(&mut file_contents), + format!("Unable to read file: {}", path.display()), + log, + )?; + + // this Box::leak() may not be great? something about leaking memory? + // i don't know what i'm doing here? + try_error( + toml::from_str(Box::leak(file_contents.into_boxed_str())), + format!("Unable to parse toml: {}", path.display()), + log, + ) +} diff --git a/src/game/game.rs b/src/game/game.rs new file mode 100644 index 0000000..659b143 --- /dev/null +++ b/src/game/game.rs @@ -0,0 +1,32 @@ +use std::collections::{HashMap, VecDeque}; + +use mio::{Events, Poll, Token}; + +use crate::client::Client; +use crate::config::Config; +use crate::database::Db; +use crate::server::Server; + +/// The `Game` struct is everything. +pub struct Game { + /// Game configuration + pub config: Config, + + /// Server socket + pub server: Server, + + /// Poll object + pub poll: Poll, + + /// Socket events + pub events: Events, + + /// Available tokens + pub tokens: VecDeque, + + /// Connected clients + pub clients: HashMap, + + /// Database connection pool + pub db: Db, +} diff --git a/src/game/handle_events.rs b/src/game/handle_events.rs new file mode 100644 index 0000000..03ad038 --- /dev/null +++ b/src/game/handle_events.rs @@ -0,0 +1,87 @@ +use std::net::Shutdown; + +use log::{debug, info, warn}; + +use mio::unix::UnixReady; +use mio::{PollOpt, Ready}; + +use crate::game::Game; +use crate::queue::RecvQueue; + +impl Game { + pub fn handle_events(&mut self) -> RecvQueue { + let mut recv_queue = RecvQueue::new(); + let events = self.events.iter().clone(); + + for event in events { + if UnixReady::from(event.readiness()).is_hup() { + // remote host has disconnected + let address: String = match self.clients.remove(&event.token()) { + Some(client) => client.into(), + None => "None".into(), + }; + info!("Disconnect from {}", address); + let _ = self.db.remove_connected_player(event.token()); + self.tokens.push_back(event.token()); + } else if event.token() == self.server.token { + // new connection + if let Some(token) = self.tokens.pop_front() { + // got a token so haven't reached connection limit + match self.server.accept(token) { + Ok(client) => { + match self.poll.register( + &client.socket, + client.token, + Ready::readable() | UnixReady::hup(), + PollOpt::edge(), + ) { + Ok(_) => { + // new connection + info!("Connect from {}", &client); + recv_queue.push(token, ""); + self.clients.insert(token, client); + } + Err(e) => { + warn!("Register failed: {}: {}", client, e); + self.tokens.push_back(token); + } + }; + } + Err(e) => { + warn!("Accept failed: {}", e); + self.tokens.push_back(token); + } + }; + } else { + // connection limit reached + warn!("Maximum connections reached"); + + let token = event.token(); + + // send message and close connection + if let Ok(mut client) = self.server.accept(token) { + client.send_without_prompt("Maximum connections reached.\n"); + let _ = client.socket.shutdown(Shutdown::Both); + info!("Rejected connection from {}", &client); + } + + // put the token back + self.tokens.push_back(token); + } + } else { + // there is something to read from a client + if let Some(client) = self.clients.get_mut(&event.token()) { + let r = client.read(); + match r { + Ok(message) => { + recv_queue.push(event.token(), message); + } + Err(e) => debug!("Read from client failed: {}: {}", client, e), + }; + } + } + } + + recv_queue + } +} diff --git a/src/game/iter_once.rs b/src/game/iter_once.rs new file mode 100644 index 0000000..ac57da7 --- /dev/null +++ b/src/game/iter_once.rs @@ -0,0 +1,41 @@ +use log::debug; + +use crate::game::Game; +use crate::queue::SendQueue; +use crate::result::RudeResult; +use crate::try_log; + +impl Game { + /// One iteration of the game loop + pub fn iter_once(&mut self) -> RudeResult { + // poll for events + try_log!(self.poll.poll(&mut self.events, None), "Poll failed"); + + // handle socket events and build receive queue + let mut recv_queue = self.handle_events(); + + // get the send queue ready + let mut send_queue = SendQueue::new(); + + // process the receive queue and fill the send queue + while let Some((token, message)) = recv_queue.pop() { + let mut queue = self.process_recv_message(token, message); + send_queue.append(&mut queue); + } + + // send everything in the send queue + while let Some((token, message, prompt)) = send_queue.pop() { + if let Some(client) = self.clients.get_mut(&token) { + if prompt { + client.send_with_prompt(message); + } else { + client.send_without_prompt(message); + } + } else { + debug!("no client?"); + } + } + + Ok(true) + } +} diff --git a/src/game/mod.rs b/src/game/mod.rs new file mode 100644 index 0000000..a1f3f3a --- /dev/null +++ b/src/game/mod.rs @@ -0,0 +1,9 @@ +#[allow(clippy::module_inception)] +mod game; +mod handle_events; +mod iter_once; +mod new; +mod process_recv_message; +mod state; + +pub use game::Game; diff --git a/src/game/new.rs b/src/game/new.rs new file mode 100644 index 0000000..495d3e5 --- /dev/null +++ b/src/game/new.rs @@ -0,0 +1,56 @@ +use std::collections::{HashMap, VecDeque}; + +use log::{debug, info}; +use mio::{Events, Poll, PollOpt, Ready, Token}; + +use crate::client::Client; +use crate::config::Config; +use crate::database::Db; +use crate::game::Game; +use crate::logger; +use crate::result::*; +use crate::server::Server; +use crate::try_log; + +impl Game { + pub fn new() -> RudeResult { + let config = Config::load("config.toml")?; + println!("Loading configuration from config.toml"); + + let log_level = config.logging.level.clone(); + try_print(logger::init(log_level), "Unable to initialize logger")?; + debug!("Initialized logging facility"); + + debug!("Opening database"); + let db = Db::open(&config.database)?; + + let events = Events::with_capacity(config.server.event_capacity); + let clients: HashMap = HashMap::new(); + let mut tokens: VecDeque = VecDeque::with_capacity(config.server.connection_limit); + + for i in 1..=config.server.connection_limit { + tokens.push_back(Token(i)); + } + + let server_address = format!("{}:{}", config.server.ip, config.server.port); + let server = Server::listen(server_address.clone(), Token(0))?; + info!("Listening on {}", &server_address); + + let poll = try_log!(Poll::new(), "Unable to create Poll"); + try_log!( + poll.register(&server.socket, Token(0), Ready::readable(), PollOpt::edge()), + "Unable to register poll" + ); + debug!("Accepting connections"); + + Ok(Self { + config, + server, + poll, + events, + tokens, + clients, + db, + }) + } +} diff --git a/src/game/process_recv_message.rs b/src/game/process_recv_message.rs new file mode 100644 index 0000000..bce090c --- /dev/null +++ b/src/game/process_recv_message.rs @@ -0,0 +1,37 @@ +use log::warn; +use mio::Token; + +use crate::game::Game; +use crate::queue::SendQueue; +use crate::state::*; + +impl Game { + /// Process the received message from the client. + pub fn process_recv_message(&mut self, token: Token, message: String) -> SendQueue { + let mut send_queue = SendQueue::new(); + + let client_state = { + if let Some(client) = self.clients.get(&token) { + client.state.clone() + } else { + // should probably do something here like give the client a + // state or close the connection or something? + warn!("Client has no state: {:?}", token); + return send_queue; + } + }; + + match &client_state { + State::Login(login_state) => { + let mut queue = self.login(token, message, login_state); + send_queue.append(&mut queue); + } + State::Action => { + let mut queue = self.action(token, message); + send_queue.append(&mut queue); + } + }; + + send_queue + } +} diff --git a/src/game/state/action.rs b/src/game/state/action.rs new file mode 100644 index 0000000..8fdf5f0 --- /dev/null +++ b/src/game/state/action.rs @@ -0,0 +1,48 @@ +use log::{debug, warn}; +use mio::Token; + +use crate::command::{Command, Parse}; +use crate::game::Game; +use crate::queue::SendQueue; + +impl Game { + /// Figure out the action to be taken and take it. + pub fn action(&mut self, token: Token, message: String) -> SendQueue { + let mut send_queue = SendQueue::new(); + + // get the player information + let player = if let Ok(Some(player)) = self.db.get_connected_player(token) { + player.clone() + } else { + warn!("No connected player found: {:?}", &token); + return SendQueue::error(token); + }; + + // break up the message into lines + let lines: Vec = message.lines().map(|s| s.into()).collect(); + + // no need to do anything else if there's nothing to process + if lines.is_empty() { + send_queue.push(token, "", true); + return send_queue; + } + + // process each line + for line in lines { + // parse the command + let (command, args) = match Command::parse(&line) { + Ok(c) => c, + Err(e) => { + send_queue.push(token, format!("{}", e), true); + continue; + } + }; + + debug!("{} : {:?}", player.name, command); + + send_queue.append(&mut command.dispatch(&command, args, token, &mut self.db)); + } + + send_queue + } +} diff --git a/src/game/state/login.rs b/src/game/state/login.rs new file mode 100644 index 0000000..f746124 --- /dev/null +++ b/src/game/state/login.rs @@ -0,0 +1,155 @@ +use chrono::Utc; +use log::warn; +use mio::Token; + +use crate::actions; +use crate::command::Command; +use crate::game::Game; +use crate::player::Player; +use crate::queue::SendQueue; +use crate::state::*; + +impl Game { + pub fn login(&mut self, token: Token, message: String, state: &Login) -> SendQueue { + let mut send_queue = SendQueue::new(); + let mut client = { + if let Some(client) = self.clients.remove(&token) { + client + } else { + warn!("Can't find a client with token: {:?}", &token); + return send_queue; + } + }; + + match state { + // get the username + Login::Username => { + if message.is_empty() { + send_queue.push(token, "Username: ", false); + } else { + match self.db.find_player_by_name(&message) { + Ok(Some(_)) => { + send_queue.push(token, "\nPassword: ", false); + client.state = State::Login(Login::Password(message)); + } + Ok(None) => { + send_queue.push( + token, + format!("\nCreate {}? [y/N]: ", message.clone()), + false, + ); + client.state = State::Login(Login::CreateUser(message)); + } + Err(_) => { + send_queue.push(token, "\nError\n\nUsername: ", false); + } + } + } + } + + // username not found + Login::CreateUser(username) => { + if !message.clone().is_empty() && message != "n" { + send_queue.push(token, "\nNew password: ", false); + client.state = State::Login(Login::CreatePassword(username.to_owned())); + } else { + send_queue.push(token, "\n\nUsername: ", false); + client.state = State::Login(Login::Username); + } + } + + // first new user password + Login::CreatePassword(username) => { + if message.is_empty() { + send_queue.push(token, "\n\nUsername: ", false); + client.state = State::Login(Login::Username); + } else { + send_queue.push(token, "\nNew password again: ", false); + client.state = + State::Login(Login::CreatePassword2((username.to_owned(), message))); + } + } + + Login::CreatePassword2((username, pass)) => { + let pass = pass.to_owned(); + if message.is_empty() || message != pass { + send_queue.push(token, "\n\nUsername: ", false); + client.state = State::Login(Login::Username); + } else { + if let Ok(id) = self.db.new_player_id() { + let player = Player { + id, + name: username.clone(), + password: pass, + created: Utc::now(), + location: self.config.starting_location.clone(), + }; + + if self.db.single_save_player(token, &player).is_ok() { + send_queue.push(token, format!("Welcome, {}\n", username), false); + send_queue.push(token, "", true); + + send_queue.append(&mut actions::look( + &Command::default(), + String::new(), + token, + &mut self.db, + )); + + client.state = State::Action; + } else { + send_queue.push(token, "Error", true); + } + } else { + send_queue.push(token, "Error", true); + } + } + } + + Login::Password(username) => { + if message.is_empty() { + send_queue.push(token, "\n\nUsername: ", false); + client.state = State::Login(Login::Username); + } else { + match self.db.find_player_by_name(username) { + Ok(Some(player)) => { + if message == player.password { + if self.db.save_connected_player(token, &player).is_ok() { + send_queue.push( + token, + format!("Welcome back, {}\n\n", username), + false, + ); + client.state = State::Action; + + send_queue.append(&mut actions::look( + &Command::default(), + String::new(), + token, + &mut self.db, + )); + } else { + send_queue.push(token, "Unable to login\n", false); + send_queue.push(token, "\n\nUsername: ", false); + client.state = State::Login(Login::Username); + } + } else { + send_queue.push(token, "Incorrect password\n", false); + send_queue.push(token, "\n\nUsername: ", false); + client.state = State::Login(Login::Username); + } + } + Ok(None) | Err(_) => { + send_queue.push(token, "Error\n", false); + send_queue.push(token, "\n\nUsername: ", false); + client.state = State::Login(Login::Username); + } + } + } + } + }; + + self.clients.insert(token, client); + send_queue + } +} diff --git a/src/game/state/mod.rs b/src/game/state/mod.rs new file mode 100644 index 0000000..34a205c --- /dev/null +++ b/src/game/state/mod.rs @@ -0,0 +1,2 @@ +mod action; +mod login; diff --git a/src/id.rs b/src/id.rs new file mode 100644 index 0000000..126a3f5 --- /dev/null +++ b/src/id.rs @@ -0,0 +1,74 @@ +use std::cmp::{Eq, PartialEq}; +use std::fmt; +use std::hash::{Hash, Hasher}; + +use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput, ValueRef}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +pub struct Id(Uuid); + +impl Id { + pub fn new() -> Self { + Self(Uuid::new_v4()) + } +} + +impl fmt::Display for Id { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl Hash for Id { + fn hash(&self, state: &mut H) { + self.0.hash(state); + } +} + +impl Into for Id { + fn into(self) -> Uuid { + self.0 + } +} + +impl From for Id { + fn from(u: Uuid) -> Self { + Self(u) + } +} + +impl Eq for Id {} + +impl PartialEq for Id { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl FromSql for Id { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + let s = match value.as_str() { + Ok(s) => s, + Err(e) => { + log::error!("{}({}) :: {} :: {}", file!(), line!(), "value.as_str()", e); + return Err(e); + } + }; + + match Uuid::parse_str(s) { + Ok(id) => return Ok(Self(id)), + Err(e) => Err(FromSqlError::Other(Box::from(e))), + } + } +} + +impl ToSql for Id { + fn to_sql(&self) -> rusqlite::Result> { + let h = self.0.to_hyphenated(); + let mut buf = Uuid::encode_buffer(); + let s = h.encode_lower(&mut buf); + Ok(ToSqlOutput::from(s.to_string())) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..8e771c7 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,19 @@ +//! The Rude Mud + +pub mod actions; +pub mod client; +pub mod command; +pub mod config; +pub mod database; +pub mod file; +pub mod game; +pub mod id; +pub mod logger; +#[macro_use] +pub mod macros; +pub mod player; +pub mod queue; +pub mod result; +pub mod server; +pub mod state; +pub mod world; diff --git a/src/logger.rs b/src/logger.rs new file mode 100644 index 0000000..13894fc --- /dev/null +++ b/src/logger.rs @@ -0,0 +1,38 @@ +use chrono::Local; +use colored::Colorize; + +use fern::colors::{Color, ColoredLevelConfig}; +use fern::Dispatch; + +use crate::config::LogLevel; +use crate::result::RudeResult; + +/// Initialize the logging facilities. `LevelFilter` must be specified and will +/// determine what level of logs will be shown.` +pub fn init(level: LogLevel) -> RudeResult<()> { + Dispatch::new() + .format(|out, message, record| { + let colors = ColoredLevelConfig::new() + .error(Color::Red) + .warn(Color::Magenta) + .info(Color::Cyan) + .debug(Color::Yellow) + .trace(Color::Green); + + out.finish(format_args!( + "{} {} {}", + Local::now() + .format("%Y-%m-%dT%H:%M:%S%.3f%z") + .to_string() + .white() + .bold(), + colors.color(record.level()), + message + )) + }) + .level(level.level()) + .chain(std::io::stdout()) + .apply()?; + + Ok(()) +} diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..43baae7 --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,82 @@ +// #[macro_export] +// macro_rules! try_log { +// ($e:expr, $l:literal) => { +// match $e { +// Ok(r) => r, +// Err(e) => { +// log::error!("{}({}) :: {} :: {}", file!(), line!(), $l, e); +// return Err(Box::from(e)); +// } +// } +// }; +// } + +#[macro_export] +macro_rules! try_log { + ($e:expr, $($arg:tt)*) => { + match $e { + Ok(r) => r, + Err(e) => { + let s = std::fmt::format(format_args!($($arg)*)); + log::error!("{}({}) :: {} :: {}", file!(), line!(), s, e); + return Err(Box::from(e)); + } + } + }; +} + +/// Unwrap a Result, if it's an error then let client know and return. +#[macro_export] +macro_rules! try_send_error { + ($i:ident, $e:expr) => { + match $e { + Ok(r) => r, + Err(e) => { + log::error!( + "{}({}) :: returning SendQueue::error() :: {}", + file!(), + line!(), + e + ); + return SendQueue::error($i); + } + } + }; +} + +#[macro_export] +macro_rules! try_option_send_error { + ($i:ident, $e:expr) => { + match $e { + Ok(Some(r)) => r, + Ok(None) => { + log::error!( + "{}({}) :: returning SendQueue::error() :: None value", + file!(), + line!() + ); + return SendQueue::error($i); + } + Err(e) => { + log::error!( + "{}({}) :: returning SendQueue::error() :: {}", + file!(), + line!(), + e + ); + return SendQueue::error($i); + } + } + }; +} + +// #[macro_export] +// macro_rules! try_option_send_error { +// ($i:ident, $e:expr) => { +// if let Ok(Some(r)) = $e { +// r +// } else { +// return SendQueue::error($i); +// } +// }; +// } diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..a083f8c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,36 @@ +mod actions; +mod client; +mod command; +mod config; +mod database; +mod file; +mod game; +mod id; +mod logger; +#[macro_use] +mod macros; +mod player; +mod queue; +mod result; +mod server; +mod state; +mod world; + +use log::*; + +use crate::game::Game; +use crate::result::RudeResult; + +fn main() -> RudeResult<()> { + let mut game = Game::new()?; + + loop { + match game.iter_once() { + Ok(true) => continue, + Ok(false) => break, + Err(e) => return Err(e), + } + } + + Ok(()) +} diff --git a/src/player.rs b/src/player.rs new file mode 100644 index 0000000..23317b3 --- /dev/null +++ b/src/player.rs @@ -0,0 +1,43 @@ +//! Player information + +use std::convert::TryFrom; +use std::error::Error; + +use chrono::{DateTime, Utc}; +use rusqlite::{types::FromSql, Row}; +use serde_derive::{Deserialize, Serialize}; + +use crate::id::Id; + +/// Player information +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Player { + /// Unique identifier + pub id: Id, + + /// Player name + pub name: String, + + /// Player's password (this needs to be properly salted and hashed but isn't) + pub password: String, + + /// Creation DateTime + pub created: DateTime, + + /// Player's location + pub location: Id, +} + +impl<'a> TryFrom<&Row<'a>> for Player { + type Error = Box; + + fn try_from(row: &Row) -> Result { + Ok(Self { + id: try_log!(row.get("id"), "id"), + name: try_log!(row.get("name"), "name"), + password: try_log!(row.get("password"), "password"), + created: try_log!(row.get("created"), "created"), + location: try_log!(row.get("location"), "location"), + }) + } +} diff --git a/src/players.rs b/src/players.rs new file mode 100644 index 0000000..756a053 --- /dev/null +++ b/src/players.rs @@ -0,0 +1,26 @@ +//! All the players of the game + +use std::collections::HashMap; + +use uuid::Uuid; + +use crate::player::Player; + +/// Map of each player id to [`Player`](../Player/struct.Player.html) object. +pub type Players = HashMap; + +/// Methods for the [`Players`](type.Players.html) type +pub trait PlayersMethods { + fn find_by_name>(&self, name: S) -> Option; +} + +impl PlayersMethods for Players { + /// Find a [`Player`](../Player/struct.Player.html) by the player name. + fn find_by_name>(&self, name: S) -> Option { + let name = name.into(); + match self.iter().find(|(_id, player)| player.name == name) { + Some((_id, player)) => Some(player.clone()), + None => None, + } + } +} diff --git a/src/queue.rs b/src/queue.rs new file mode 100644 index 0000000..dfdbcc6 --- /dev/null +++ b/src/queue.rs @@ -0,0 +1,65 @@ +//! Queues for recv and send +use std::collections::VecDeque; + +use mio::Token; + +/// Queue of clients with a message to receive. +/// +/// * The client is designated with the [`Token`](../../mio/struct.Token.html). +/// * [`String`](https://doc.rust-lang.org/nightly/alloc/string/struct.String.html) +/// is used to store the message. +#[derive(Debug)] +pub struct RecvQueue(VecDeque<(Token, String)>); + +impl RecvQueue { + /// Create a new, empty `RecvQueue`. + pub fn new() -> Self { + RecvQueue(VecDeque::new()) + } + + /// Remove and return the first message in the `RecvQueue`. + pub fn pop(&mut self) -> Option<(Token, String)> { + self.0.pop_front() + } + + /// Add a message to the end of the `RecvQueue`. + pub fn push>(&mut self, token: Token, s: S) { + self.0.push_back((token, s.into())); + } +} + +/// Queue of messages to send to clients. +/// +/// * The client is designated with the [`Token`](../../mio/struct.Token.html). +/// * [`String`](https://doc.rust-lang.org/nightly/alloc/string/struct.String.html) +/// is used to store the message. +/// * [`bool`](https://doc.rust-lang.org/nightly/std/primitive.bool.html) is set to `true` if a +/// prompt is to be displayed following the message. +#[derive(Debug)] +pub struct SendQueue(pub VecDeque<(Token, String, bool)>); + +impl SendQueue { + pub fn new() -> Self { + SendQueue(VecDeque::new()) + } + + pub fn append(&mut self, queue: &mut Self) { + self.0.append(&mut queue.0); + } + + pub fn pop(&mut self) -> Option<(Token, String, bool)> { + self.0.pop_front() + } + + pub fn push>(&mut self, token: Token, s: S, prompt: bool) { + self.0.push_back((token, s.into(), prompt)); + } + + pub fn error(token: Token) -> Self { + Self(vec![(token, "Error".to_string(), true)].into()) + } + + pub fn ok(token: Token) -> Self { + Self(vec![(token, "Ok".to_string(), true)].into()) + } +} diff --git a/src/result.rs b/src/result.rs new file mode 100644 index 0000000..6f214eb --- /dev/null +++ b/src/result.rs @@ -0,0 +1,56 @@ +use std::error::Error; + +use log::error; + +/// Wrapper for `Result>`. +/// +/// # Example +/// ``` +/// use rude::result::RudeResult; +/// +/// fn example_result() -> RudeResult<()> { +/// Ok(()) +/// } +/// ``` +pub type RudeResult = Result>; + +/// Wrap `?` with an error message into the log. +/*pub fn try_log>, S: Into>( + result: Result, + message: S, +) -> RudeResult { + try_error(result, message, true) +}*/ + +/// Wrap `?` with an error message to stdout. +pub fn try_print>, S: Into>( + result: Result, + message: S, +) -> RudeResult { + try_error(result, message, false) +} + +/// Wrap `?` with an error message to either stdout or the log. +/// +/// * `result` - `Result` type to check for `Error`. +/// * `message` - Message to send to either log or stdout if `result` is `Error`. +/// * `log` - `true` to send `message` to log, otherwise send to stdout. +//pub fn try_error>(result: Result, message: S, log: bool) -> Result { +pub fn try_error>, S: Into>( + result: Result, + message: S, + log: bool, +) -> RudeResult { + match result { + Ok(r) => Ok(r), + Err(e) => { + let e = e.into(); + if log { + error!("{} :: {}", message.into(), e); + } else { + println!("{} :: {}", message.into(), e); + } + Err(e.into()) + } + } +} diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..f3b7017 --- /dev/null +++ b/src/server.rs @@ -0,0 +1,82 @@ +//! Server connection information. + +use std::io; +use std::net::SocketAddr; + +use mio::event::Evented; +use mio::net::TcpListener; +use mio::{Poll, PollOpt, Ready, Token}; + +use crate::client::Client; +use crate::result::*; +use crate::state::*; + +/// Connection information for the server. +#[derive(Debug)] +pub struct Server { + /// listen socket + pub socket: TcpListener, + + /// token identifier (0) + pub token: Token, + + /// ip address/port + pub addr: SocketAddr, +} + +impl Server { + /// Bind to the provided address + pub fn listen<'a>(addr: String, token: Token) -> RudeResult { + let addr: SocketAddr = try_log!(addr.parse(), "Unable to parse server address: {}", &addr); + + let socket: TcpListener = try_log!( + TcpListener::bind(&addr), + "Unable to bind to address: {}", + &addr, + ); + + Ok(Server { + socket, + token, + addr, + }) + } + + /// Accept a new client connection + pub fn accept(&self, token: Token) -> RudeResult { + let (socket, addr) = self.socket.accept()?; + + Ok(Client { + socket, + token, + addr, + state: State::Login(Login::Username), + }) + } +} + +impl Evented for Server { + fn register( + &self, + poll: &Poll, + token: Token, + interest: Ready, + opts: PollOpt, + ) -> io::Result<()> { + self.socket.register(poll, token, interest, opts) + } + + fn reregister( + &self, + poll: &Poll, + token: Token, + interest: Ready, + opts: PollOpt, + ) -> io::Result<()> { + self.socket.reregister(poll, token, interest, opts) + } + + fn deregister(&self, poll: &Poll) -> io::Result<()> { + self.socket.deregister(poll) + } +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..a689de3 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,30 @@ +//! State information for a connected `Client`. + +/// Play state for a conected `Client`. +#[derive(Clone, Debug, PartialEq)] +pub enum State { + /// Logging in + Login(Login), + + /// Performing an action + Action, +} + +/// Login state +#[derive(Clone, Debug, PartialEq)] +pub enum Login { + /// Username + Username, + + /// Unknown user + CreateUser(String), + + /// New user password + CreatePassword(String), + + /// New user password again + CreatePassword2((String, String)), + + /// Password for existing user + Password(String), +} diff --git a/src/world/area.rs b/src/world/area.rs new file mode 100644 index 0000000..dcfc21b --- /dev/null +++ b/src/world/area.rs @@ -0,0 +1,36 @@ +use crate::id::Id; + +/// The type of the `Area`. +#[derive(Debug)] +pub enum AreaType { + Room, + Zone, +} + +/// An 'Area' is an identifiable location, either a specific `Room` or maybe a +/// grouping of `Area`s. +pub trait Area: std::fmt::Debug { + /// Returns the unique identifier of the `Area`. + fn id(&self) -> Id; + + /// Return the unique identifier of the parent `Area`. + fn parent(&self) -> Id; + + /// Text name of the `Area`. + fn name(&self) -> String; + + /// Specifies whether users in this `Area` will be visibile (appearing in + /// the area `self.name()`) to the parent `Area` and its children. This + /// allows for granularity over the where command. + fn visible(&self) -> bool; + + /// The type of area, either it's a `Room` or a `Zone`. + fn area_type(&self) -> AreaType; + + /// Returns true if this `Area` is the root, or the world. This is + /// determined to be the case if `self.id()` and `self.parent()` return + /// the same `Uuid`. + fn is_world(&self) -> bool { + self.id() == self.parent() + } +} diff --git a/src/world/direction.rs b/src/world/direction.rs new file mode 100644 index 0000000..e3b1f1f --- /dev/null +++ b/src/world/direction.rs @@ -0,0 +1,193 @@ +use std::default::Default; +use std::error::Error; +use std::str::FromStr; + +use lazy_static::lazy_static; +use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput, ValueRef}; +use serde_derive::{Deserialize, Serialize}; + +use crate::command::ParserError; +use crate::result::RudeResult; + +lazy_static! { + /// List of text directions and the associated Direction + pub static ref DIRECTION_LIST: Vec<(&'static str, Direction)> = vec![ + ("north", Direction::North), + ("south", Direction::South), + ("east", Direction::East), + ("west", Direction::West), + ("up", Direction::Up), + ("down", Direction::Down), + ]; +} + +/// Directions for an exit from a room. +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] +pub enum Direction { + /// Cardinal north + North = 0, + + /// Cardinal east + East, + + /// Cardinal south + South, + + /// Cardinal west + West, + + /// Up (towards the sky) + Up, + + /// Down (towards the ground) + Down, + + /// Default (internal usage only) + Default, +} + +impl Default for Direction { + fn default() -> Self { + Self::Default + } +} + +impl FromSql for Direction { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + let s = match value.as_str() { + Ok(s) => s, + Err(e) => { + log::error!("{}({}) :: {} :: {}", file!(), line!(), "value.as_str()", e); + return Err(e); + } + }; + + match Self::from_str(s) { + Ok(direction) => return Ok(direction), + Err(e) => Err(FromSqlError::Other(e)), + } + } +} + +impl ToSql for Direction { + fn to_sql(&self) -> rusqlite::Result> { + Ok(ToSqlOutput::from(self.long())) + } +} + +impl FromStr for Direction { + type Err = Box; + + fn from_str(s: &str) -> std::result::Result { + let mut s: String = s.into(); + + // get rid of extra characters + if let Some(pos) = s.find(' ') { + s.split_off(pos); + } + + // no command + if s.is_empty() { + return Err(Box::from(ParserError::Empty)); + } + + // match directions with input + let mut matches: Vec<(&str, Self)> = DIRECTION_LIST + .iter() + .filter_map(|(text, dir)| { + if text.starts_with(&s) { + Some((text.clone(), dir.clone())) + } else { + None + } + }) + .collect(); + + // No matches here means unknown direction + if matches.is_empty() { + return Err(Box::from(ParserError::Unknown)); + } + + // exact match, do no more + if matches.len() == 1 { + return Ok(matches[0].1); + } + + // look for directions that match + for (text, direction) in DIRECTION_LIST.iter() { + let direction: Self = direction.clone(); + if *text == s { + // exact match + matches = vec![(text, direction)]; + break; + } else if text.starts_with(s.as_str()) { + // starts the same, add to the list + matches.push((text, direction)); + } + } + + // check if there was a match + if matches.is_empty() { + return Err(Box::from(ParserError::Unknown)); + } + + // sort and take the first match + // this allows possibly more directions, the original set shouldn't ever have multiple + matches.sort(); + Ok(matches[0].1) + } +} + +impl Direction { + /// Provide the opposite direction + pub fn opposite(self) -> Direction { + match self { + Self::North => Self::South, + Self::East => Self::West, + Self::South => Self::North, + Self::West => Self::East, + Self::Up => Self::Down, + Self::Down => Self::Up, + Self::Default => Self::Default, + } + } + + /// Single character identifier for the direction. + pub fn short(&self) -> &str { + match self { + Self::North => "N", + Self::East => "E", + Self::South => "S", + Self::West => "W", + Self::Up => "U", + Self::Down => "D", + Self::Default => "", + } + } + + /// String identifier for the direction. + pub fn long(&self) -> &str { + match self { + Self::North => "north", + Self::East => "east", + Self::South => "south", + Self::West => "west", + Self::Up => "up", + Self::Down => "down", + Self::Default => "", + } + } + + pub fn try_from_long>(s: S) -> RudeResult { + let s = s.as_ref(); + match s { + "north" => Ok(Self::North), + "east" => Ok(Self::East), + "south" => Ok(Self::South), + "west" => Ok(Self::West), + "up" => Ok(Self::Up), + "down" => Ok(Self::Down), + _ => Err(format!("Invalid direction: {}", s).into()), + } + } +} diff --git a/src/world/exit.rs b/src/world/exit.rs new file mode 100644 index 0000000..6f759f5 --- /dev/null +++ b/src/world/exit.rs @@ -0,0 +1,29 @@ +use std::convert::TryFrom; +use std::error::Error; + +use rusqlite::Row; +use serde_derive::{Deserialize, Serialize}; + +use crate::id::Id; +use crate::world::Direction; + +/// A one way exit, from one room to another, with a direction. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Exit { + /// Target room ID + pub target: Id, + + /// Exit direction + pub direction: Direction, +} + +impl<'a> TryFrom<&Row<'a>> for Exit { + type Error = Box; + + fn try_from(row: &Row) -> Result { + Ok(Self { + target: row.get("target")?, + direction: Direction::try_from_long(row.get::<&str, String>("direction")?)?, + }) + } +} diff --git a/src/world/mod.rs b/src/world/mod.rs new file mode 100644 index 0000000..682b9a9 --- /dev/null +++ b/src/world/mod.rs @@ -0,0 +1,13 @@ +//! The game world. Regions, zones, and rooms. + +mod area; +mod direction; +mod exit; +mod room; +mod zone; + +pub use area::*; +pub use direction::*; +pub use exit::Exit; +pub use room::Room; +pub use zone::Zone; diff --git a/src/world/room.rs b/src/world/room.rs new file mode 100644 index 0000000..99b620a --- /dev/null +++ b/src/world/room.rs @@ -0,0 +1,83 @@ +use std::collections::HashMap; +use std::convert::TryFrom; +use std::error::Error; + +use rusqlite::Row; +use serde_derive::{Deserialize, Serialize}; + +use crate::id::Id; +use crate::try_log; +use crate::world::{Area, AreaType, Direction, Exit, DIRECTION_LIST}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Room { + pub id: Id, + pub zone: Id, + pub name: String, + pub description: Vec, + pub users_visible: bool, + pub exits: HashMap, +} + +impl Room { + pub fn exit_string(&self) -> String { + if self.exits.is_empty() { + return String::new(); + } + + DIRECTION_LIST + .iter() + .filter_map(|(_, direction)| { + if self.exits.contains_key(direction) { + Some(direction.short()) + } else { + None + } + }) + .collect::>() + .join(" ") + } +} + +impl Area for Room { + fn id(&self) -> Id { + self.id + } + + fn parent(&self) -> Id { + self.zone + } + + fn name(&self) -> String { + self.name.to_owned() + } + + fn visible(&self) -> bool { + self.users_visible + } + + fn area_type(&self) -> AreaType { + AreaType::Room + } +} + +impl<'a> TryFrom<&Row<'a>> for Room { + type Error = Box; + + fn try_from(row: &Row) -> Result { + //let orig_id: String = try_log!(row.get("id"), "id"); + //let new_id: Uuid = try_log!(Uuid::parse_str(&orig_id), "parse"); + + Ok(Self { + id: try_log!(row.get("id"), "id"), + zone: try_log!(row.get("zone"), "zone"), + name: try_log!(row.get("name"), "name"), + description: try_log!(row.get::<&str, String>("description"), "description") + .lines() + .map(|s| String::from(s)) + .collect(), + users_visible: try_log!(row.get("users_visible"), "users_visible"), + exits: HashMap::new(), + }) + } +} diff --git a/src/world/zone.rs b/src/world/zone.rs new file mode 100644 index 0000000..8c57e26 --- /dev/null +++ b/src/world/zone.rs @@ -0,0 +1,54 @@ +use std::collections::HashSet; +use std::convert::TryFrom; +use std::error::Error; + +use rusqlite::Row; +use serde_derive::{Deserialize, Serialize}; + +use crate::id::Id; +use crate::world::{Area, AreaType}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Zone { + pub id: Id, + pub parent: Id, + pub name: String, + pub users_visible: bool, + pub areas: HashSet, +} + +impl Area for Zone { + fn id(&self) -> Id { + self.id + } + + fn parent(&self) -> Id { + self.parent + } + + fn name(&self) -> String { + self.name.to_owned() + } + + fn visible(&self) -> bool { + self.users_visible + } + + fn area_type(&self) -> AreaType { + AreaType::Zone + } +} + +impl<'a> TryFrom<&Row<'a>> for Zone { + type Error = Box; + + fn try_from(row: &Row) -> Result { + Ok(Self { + id: row.get("id")?, + parent: row.get("parent")?, + name: row.get("name")?, + users_visible: row.get("users_visible")?, + areas: HashSet::new(), + }) + } +}