parent
2e12903c23
commit
8165be78f1
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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;
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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<String> 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<String> {
|
||||
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<T: Into<String>>(&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<T: Into<String>>(&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> "
|
||||
}
|
||||
}
|
@ -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<String> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
@ -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::*;
|
@ -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<String> {
|
||||
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::<Vec<String>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
/// List of all commands
|
||||
fn commands(&self) -> Vec<String> {
|
||||
Self::iter()
|
||||
.filter_map(|a| {
|
||||
if a == Self::default() {
|
||||
None
|
||||
} else {
|
||||
Some(a.to_string().to_lowercase())
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Parse a `Into<String>` into a command.
|
||||
fn parse<S: Into<String>>(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<Self> = 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)?)
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {}
|
@ -0,0 +1,9 @@
|
||||
mod player;
|
||||
mod room;
|
||||
mod set;
|
||||
mod zone;
|
||||
|
||||
pub use player::*;
|
||||
pub use room::*;
|
||||
pub use set::*;
|
||||
pub use zone::*;
|
@ -0,0 +1,5 @@
|
||||
mod name;
|
||||
mod player;
|
||||
|
||||
pub use name::*;
|
||||
pub use player::*;
|
@ -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)
|
||||
}
|
||||
}
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
mod name;
|
||||
mod room;
|
||||
|
||||
pub use name::*;
|
||||
pub use room::*;
|
@ -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)
|
||||
}
|
||||
}
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
mod name;
|
||||
mod zone;
|
||||
|
||||
pub use name::*;
|
||||
pub use zone::*;
|
@ -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)
|
||||
}
|
||||
}
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
@ -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<P: Into<PathBuf>>(path: P) -> RudeResult<Self> {
|
||||
Ok(file::read_print(&path.into())?)
|
||||
}
|
||||
}
|
@ -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,
|
||||
}
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
@ -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,
|
||||
}
|
@ -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<Vec<(Token, Player)>> {
|
||||
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<Option<Player>> {
|
||||
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(())
|
||||
}
|
||||
}
|
@ -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<P: AsRef<Path>>(path: P) -> RudeResult<Self> {
|
||||
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<Id> {
|
||||
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<Id> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
mod connected_players;
|
||||
mod db;
|
||||
mod players;
|
||||
mod rooms;
|
||||
mod zones;
|
||||
|
||||
pub use db::Db;
|
@ -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<Option<Player>> {
|
||||
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<S: Into<String>>(&self, name: S) -> RudeResult<Option<Player>> {
|
||||
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(())
|
||||
}
|
||||
}
|
@ -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<Option<Room>> {
|
||||
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))
|
||||
}
|
||||
}
|
@ -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<Option<Zone>> {
|
||||
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))
|
||||
}
|
||||
}
|
@ -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<PathBuf>, T: Deserialize<'de>>(path: P) -> RudeResult<T> {
|
||||
read_file(path, false)
|
||||
}
|
||||
|
||||
/// The actual function to read a file.
|
||||
fn read_file<'de, P: Into<PathBuf>, T: Deserialize<'de>>(path: P, log: bool) -> RudeResult<T> {
|
||||
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,
|
||||
)
|
||||
}
|
@ -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<Token>,
|
||||
|
||||
/// Connected clients
|
||||
pub clients: HashMap<Token, Client>,
|
||||
|
||||
/// Database connection pool
|
||||
pub db: Db,
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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<bool> {
|
||||
// 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)
|
||||
}
|
||||
}
|
@ -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;
|
@ -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<Self> {
|
||||
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<Token, Client> = HashMap::new();
|
||||
let mut tokens: VecDeque<Token> = 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,
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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<String> = 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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
mod action;
|
||||
mod login;
|
@ -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<H: Hasher>(&self, state: &mut H) {
|
||||
self.0.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<Uuid> for Id {
|
||||
fn into(self) -> Uuid {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Uuid> 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<Self> {
|
||||
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<ToSqlOutput<'_>> {
|
||||
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()))
|
||||
}
|
||||
}
|
@ -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;
|
@ -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(())
|
||||
}
|
@ -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<T, E>, 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);
|
||||
// }
|
||||
// };
|
||||
// }
|
@ -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(())
|
||||
}
|
@ -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<Utc>,
|
||||
|
||||
/// Player's location
|
||||
pub location: Id,
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&Row<'a>> for Player {
|
||||
type Error = Box<dyn Error>;
|
||||
|
||||
fn try_from(row: &Row) -> Result<Self, Self::Error> {
|
||||
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"),
|
||||
})
|
||||
}
|
||||
}
|
@ -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<Uuid, Player>;
|
||||
|
||||
/// Methods for the [`Players`](type.Players.html) type
|
||||
pub trait PlayersMethods {
|
||||
fn find_by_name<S: Into<String>>(&self, name: S) -> Option<Player>;
|
||||
}
|
||||
|
||||
impl PlayersMethods for Players {
|
||||
/// Find a [`Player`](../Player/struct.Player.html) by the player name.
|
||||
fn find_by_name<S: Into<String>>(&self, name: S) -> Option<Player> {
|
||||
let name = name.into();
|
||||
match self.iter().find(|(_id, player)| player.name == name) {
|
||||
Some((_id, player)) => Some(player.clone()),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
@ -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<S: Into<String>>(&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<S: Into<String>>(&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())
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
use std::error::Error;
|
||||
|
||||
use log::error;
|
||||
|
||||
/// Wrapper for `Result<T, Box<dyn Error>>`.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use rude::result::RudeResult;
|
||||
///
|
||||
/// fn example_result() -> RudeResult<()> {
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
pub type RudeResult<T> = Result<T, Box<dyn Error>>;
|
||||
|
||||
/// Wrap `?` with an error message into the log.
|
||||
/*pub fn try_log<T, E: Into<Box<dyn Error>>, S: Into<String>>(
|
||||
result: Result<T, E>,
|
||||
message: S,
|
||||
) -> RudeResult<T> {
|
||||
try_error(result, message, true)
|
||||
}*/
|
||||
|
||||
/// Wrap `?` with an error message to stdout.
|
||||
pub fn try_print<T, E: Into<Box<dyn Error>>, S: Into<String>>(
|
||||
result: Result<T, E>,
|
||||
message: S,
|
||||
) -> RudeResult<T> {
|
||||
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<T, S: Into<String>>(result: Result<T>, message: S, log: bool) -> Result<T> {
|
||||
pub fn try_error<T, E: Into<Box<dyn Error>>, S: Into<String>>(
|
||||
result: Result<T, E>,
|
||||
message: S,
|
||||
log: bool,
|
||||
) -> RudeResult<T> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
@ -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<Server> {
|
||||
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<Client> {
|
||||
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)
|
||||
}
|
||||
}
|
@ -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),
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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<Self> {
|
||||
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<ToSqlOutput<'_>> {
|
||||
Ok(ToSqlOutput::from(self.long()))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Direction {
|
||||
type Err = Box<dyn Error + Send + Sync + 'static>;
|
||||
|
||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||
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: AsRef<str>>(s: S) -> RudeResult<Self> {
|
||||
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()),
|
||||
}
|
||||
}
|
||||
}
|
@ -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<dyn Error>;
|
||||
|
||||
fn try_from(row: &Row) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
target: row.get("target")?,
|
||||
direction: Direction::try_from_long(row.get::<&str, String>("direction")?)?,
|
||||
})
|
||||
}
|
||||
}
|
@ -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;
|
@ -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<String>,
|
||||
pub users_visible: bool,
|
||||
pub exits: HashMap<Direction, Exit>,
|
||||
}
|
||||
|
||||
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::<Vec<&str>>()
|
||||
.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<dyn Error>;
|
||||
|
||||
fn try_from(row: &Row) -> Result<Self, Self::Error> {
|
||||
//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(),
|
||||
})
|
||||
}
|
||||
}
|
@ -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<Id>,
|
||||
}
|
||||
|
||||
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<dyn Error>;
|
||||
|
||||
fn try_from(row: &Row) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
id: row.get("id")?,
|
||||
parent: row.get("parent")?,
|
||||
name: row.get("name")?,
|
||||
users_visible: row.get("users_visible")?,
|
||||
areas: HashSet::new(),
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in new issue