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