diff --git a/rustyhack_client/src/consts.rs b/rustyhack_client/src/consts.rs index ad5e12a..3100032 100644 --- a/rustyhack_client/src/consts.rs +++ b/rustyhack_client/src/consts.rs @@ -1,6 +1,6 @@ -pub const VIEWPORT_WIDTH: u32 = 41; -pub const VIEWPORT_HEIGHT: u32 = 15; -pub const TARGET_FPS: u32 = 10; -pub const LOG_NAME: &str = "rustyhack_client.log"; -pub const GAME_TITLE: &str = "Rustyhack MMO"; -pub const VALID_NAME_REGEX: &str = "^[[:alpha:]]+$"; +pub(crate) const VIEWPORT_WIDTH: u32 = 41; +pub(crate) const VIEWPORT_HEIGHT: u32 = 15; +pub(crate) const TARGET_FPS: u32 = 10; +pub(crate) const LOG_NAME: &str = "rustyhack_client.log"; +pub(crate) const GAME_TITLE: &str = "Rustyhack MMO"; +pub(crate) const VALID_NAME_REGEX: &str = "^[[:alpha:]]+$"; diff --git a/rustyhack_client/src/engine.rs b/rustyhack_client/src/engine.rs deleted file mode 100644 index f2fc00b..0000000 --- a/rustyhack_client/src/engine.rs +++ /dev/null @@ -1,284 +0,0 @@ -use crate::consts::{GAME_TITLE, TARGET_FPS, VIEWPORT_HEIGHT, VIEWPORT_WIDTH}; -use crate::message_handler; -use crate::viewport::Viewport; -use bincode::serialize; -use console_engine::{ConsoleEngine, KeyCode, KeyModifiers}; -use crossbeam_channel::{Receiver, Sender}; -use laminar::{Packet, SocketEvent}; -use rustyhack_lib::background_map::AllMaps; -use rustyhack_lib::ecs::components::Velocity; -use rustyhack_lib::ecs::player::Player; -use rustyhack_lib::message_handler::player_message::{ - CreatePlayerMessage, EntityUpdates, PlayerMessage, PlayerReply, VelocityMessage, -}; -use std::collections::HashMap; -use std::time::Duration; -use std::{process, thread}; - -pub fn run( - sender: Sender, - receiver: Receiver, - server_addr: &str, - client_addr: &str, - player_name: &str, -) { - let (player_update_sender, player_update_receiver) = crossbeam_channel::unbounded(); - let (entity_update_sender, entity_update_receiver) = crossbeam_channel::unbounded(); - debug!("Spawned thread channels."); - let local_sender = sender.clone(); - thread::spawn(move || { - message_handler::run(sender, receiver, player_update_sender, entity_update_sender) - }); - - let all_maps = request_all_maps_data(&local_sender, &server_addr, &player_update_receiver); - let mut player = send_new_player_request( - &local_sender, - player_name, - &server_addr, - &client_addr, - &player_update_receiver, - ); - info!("player_name is: {}", player.player_details.player_name); - debug!("All maps is: {:?}", all_maps); - - let mut viewport = Viewport::new(VIEWPORT_WIDTH, VIEWPORT_HEIGHT, TARGET_FPS); - let mut console = - console_engine::ConsoleEngine::init(viewport.width, viewport.height, viewport.target_fps); - console.set_title(GAME_TITLE); - info!("Initialised console engine."); - - let mut other_entities = EntityUpdates { - updates: HashMap::new(), - }; - - loop { - console.wait_frame(); - console.clear_screen(); - - debug!("About to send player velocity update."); - send_player_updates(&local_sender, &console, &player, &server_addr); - - debug!("About to wait for entity updates from server."); - player = check_for_received_player_updates(&player_update_receiver, player); - other_entities = check_for_received_entity_updates(&entity_update_receiver, other_entities); - - viewport.draw_viewport_contents( - &mut console, - &player, - all_maps.get(&player.position.map).unwrap_or_else(|| { - error!( - "There is no map for current player position: {}", - &player.position.map - ); - process::exit(1); - }), - &other_entities, - ); - - if should_quit(&console) { - info!("Ctrl-q detected - quitting app."); - break; - } - } -} - -fn send_new_player_request( - sender: &Sender, - player_name: &str, - server_addr: &str, - client_addr: &str, - channel_receiver: &Receiver, -) -> Player { - let create_player_request_packet = Packet::reliable_unordered( - server_addr - .parse() - .expect("Server address format is invalid."), - serialize(&PlayerMessage::PlayerJoin(CreatePlayerMessage { - client_addr: client_addr.to_string(), - player_name: player_name.to_string(), - })) - .unwrap(), - ); - message_handler::send_packet(create_player_request_packet, sender); - info!("Sent new player request to server."); - wait_for_new_player_response(channel_receiver) -} - -fn request_all_maps_data( - sender: &Sender, - server_addr: &str, - channel_receiver: &Receiver, -) -> AllMaps { - let get_all_maps_request_packet = Packet::reliable_ordered( - server_addr - .parse() - .expect("Server address format is invalid."), - serialize(&PlayerMessage::GetAllMaps).expect("Error serialising GetAllMaps request."), - Some(1), - ); - message_handler::send_packet(get_all_maps_request_packet, sender); - info!("Requested all maps data from server."); - wait_for_all_maps_response(channel_receiver) -} - -fn wait_for_all_maps_response(channel_receiver: &Receiver) -> AllMaps { - let mut all_maps_downloaded = false; - let mut all_maps = HashMap::new(); - loop { - let received = channel_receiver.recv(); - if let Ok(received_message) = received { - match received_message { - PlayerReply::AllMaps(message) => { - info!("All maps downloaded from server."); - all_maps_downloaded = true; - all_maps = message; - } - _ => { - info!( - "Ignoring other message types until maps downloaded. {:?}", - received_message - ) - } - } - } - if all_maps_downloaded { - info!("Got all maps data."); - break; - } - thread::sleep(Duration::from_millis(1)); - } - all_maps -} - -fn wait_for_new_player_response(channel_receiver: &Receiver) -> Player { - let mut new_player_confirmed = false; - let mut player = Player::default(); - loop { - let received = channel_receiver.recv(); - if let Ok(received_message) = received { - match received_message { - PlayerReply::PlayerJoined(message) => { - info!("New player creation confirmed."); - new_player_confirmed = true; - player = message; - } - PlayerReply::PlayerAlreadyOnline => { - error!( - "This player name is already taken, and the player is currently online." - ); - process::exit(1); - } - _ => { - info!( - "Ignoring other message types until new player confirmed. {:?}", - received_message - ) - } - } - } - if new_player_confirmed { - info!("Got all data needed to begin game."); - break; - } - thread::sleep(Duration::from_millis(1)); - } - player -} - -fn send_player_updates( - sender: &Sender, - console: &ConsoleEngine, - player: &Player, - server_addr: &str, -) { - let mut velocity = Velocity { x: 0, y: 0 }; - if console.is_key_held(KeyCode::Up) { - velocity.y = -1; - } else if console.is_key_held(KeyCode::Down) { - velocity.y = 1; - } else if console.is_key_held(KeyCode::Left) { - velocity.x = -1; - } else if console.is_key_held(KeyCode::Right) { - velocity.x = 1; - } - - if velocity.y != 0 || velocity.x != 0 { - debug!("Movement detected, sending velocity packet to server."); - send_velocity_packet(sender, server_addr, player, velocity); - } -} - -fn send_velocity_packet( - sender: &Sender, - server_addr: &str, - player: &Player, - velocity: Velocity, -) { - let packet = Packet::unreliable_sequenced( - server_addr - .parse() - .expect("Server address format is invalid."), - serialize(&PlayerMessage::UpdateVelocity(VelocityMessage { - player_name: player.player_details.player_name.clone(), - velocity, - })) - .unwrap(), - Some(10), - ); - message_handler::send_packet(packet, sender); - debug!("Sent velocity packet to server."); -} - -fn check_for_received_player_updates( - channel_receiver: &Receiver, - mut player: Player, -) -> Player { - debug!("Checking for received player position from server."); - while !channel_receiver.is_empty() { - let received = channel_receiver.recv(); - if let Ok(received_message) = received { - match received_message { - PlayerReply::UpdatePosition(new_position) => { - debug!("Player position update received: {:?}", &new_position); - player.position = new_position - } - _ => { - warn!( - "Unexpected message on channel from message handler: {:?}", - received_message - ) - } - } - } - } - player -} - -fn check_for_received_entity_updates( - channel_receiver: &Receiver, - mut entity_updates: EntityUpdates, -) -> EntityUpdates { - debug!("Checking for received entity updates from server."); - while !channel_receiver.is_empty() { - let received = channel_receiver.recv(); - if let Ok(received_message) = received { - match received_message { - PlayerReply::UpdateOtherEntities(new_updates) => { - debug!("Entity updates received: {:?}", &new_updates); - entity_updates = new_updates; - } - _ => { - warn!( - "Unexpected message on channel from message handler: {:?}", - received_message - ) - } - } - } - } - entity_updates -} - -fn should_quit(console: &ConsoleEngine) -> bool { - console.is_key_pressed_with_modifier(KeyCode::Char('q'), KeyModifiers::CONTROL) -} diff --git a/rustyhack_client/src/game.rs b/rustyhack_client/src/game.rs new file mode 100644 index 0000000..2f60e41 --- /dev/null +++ b/rustyhack_client/src/game.rs @@ -0,0 +1,96 @@ +mod map_handler; +mod new_player; +mod updates_handler; +pub(crate) mod viewport; + +use crate::consts::{GAME_TITLE, TARGET_FPS, VIEWPORT_HEIGHT, VIEWPORT_WIDTH}; +use crate::game::viewport::Viewport; +use crate::networking::message_handler; +use console_engine::{ConsoleEngine, KeyCode, KeyModifiers}; +use crossbeam_channel::{Receiver, Sender}; +use laminar::{Packet, SocketEvent}; +use rustyhack_lib::message_handler::player_message::EntityUpdates; +use std::collections::HashMap; +use std::process; + +pub(crate) fn run( + sender: Sender, + receiver: Receiver, + server_addr: &str, + client_addr: &str, + player_name: &str, +) { + let (player_update_sender, player_update_receiver) = crossbeam_channel::unbounded(); + let (entity_update_sender, entity_update_receiver) = crossbeam_channel::unbounded(); + debug!("Spawned thread channels."); + let local_sender = sender.clone(); + + message_handler::spawn_message_handler_thread( + sender, + receiver, + player_update_sender, + entity_update_sender, + ); + + let all_maps = + map_handler::request_all_maps_data(&local_sender, &server_addr, &player_update_receiver); + + let mut player = new_player::send_new_player_request( + &local_sender, + player_name, + &server_addr, + &client_addr, + &player_update_receiver, + ); + + let mut viewport = Viewport::new(VIEWPORT_WIDTH, VIEWPORT_HEIGHT, TARGET_FPS); + + let mut console = + console_engine::ConsoleEngine::init(viewport.width, viewport.height, viewport.target_fps); + console.set_title(GAME_TITLE); + info!("Initialised console engine."); + + let mut other_entities = EntityUpdates { + position_updates: HashMap::new(), + display_details: HashMap::new(), + }; + + info!("Starting game loop"); + loop { + console.wait_frame(); + console.clear_screen(); + + debug!("About to send player velocity update."); + updates_handler::send_player_updates(&local_sender, &console, &player, &server_addr); + + debug!("About to wait for entity updates from server."); + player = + updates_handler::check_for_received_player_updates(&player_update_receiver, player); + other_entities = updates_handler::check_for_received_entity_updates( + &entity_update_receiver, + other_entities, + ); + + viewport.draw_viewport_contents( + &mut console, + &player, + all_maps.get(&player.position.map).unwrap_or_else(|| { + error!( + "There is no map for current player position: {}", + &player.position.map + ); + process::exit(1); + }), + &other_entities, + ); + + if should_quit(&console) { + info!("Ctrl-q detected - quitting app."); + break; + } + } +} + +fn should_quit(console: &ConsoleEngine) -> bool { + console.is_key_pressed_with_modifier(KeyCode::Char('q'), KeyModifiers::CONTROL) +} diff --git a/rustyhack_client/src/game/map_handler.rs b/rustyhack_client/src/game/map_handler.rs new file mode 100644 index 0000000..6def22d --- /dev/null +++ b/rustyhack_client/src/game/map_handler.rs @@ -0,0 +1,56 @@ +use crate::networking::message_handler; +use bincode::serialize; +use crossbeam_channel::{Receiver, Sender}; +use laminar::Packet; +use rustyhack_lib::background_map::AllMaps; +use rustyhack_lib::message_handler::player_message::{PlayerMessage, PlayerReply}; +use std::collections::HashMap; +use std::thread; +use std::time::Duration; + +pub(crate) fn request_all_maps_data( + sender: &Sender, + server_addr: &str, + channel_receiver: &Receiver, +) -> AllMaps { + let get_all_maps_request_packet = Packet::reliable_ordered( + server_addr + .parse() + .expect("Server address format is invalid."), + serialize(&PlayerMessage::GetAllMaps).expect("Error serialising GetAllMaps request."), + Some(1), + ); + message_handler::send_packet(get_all_maps_request_packet, sender); + info!("Requested all maps data from server."); + wait_for_all_maps_response(channel_receiver) +} + +fn wait_for_all_maps_response(channel_receiver: &Receiver) -> AllMaps { + let mut all_maps_downloaded = false; + let mut all_maps = HashMap::new(); + loop { + let received = channel_receiver.recv(); + if let Ok(received_message) = received { + match received_message { + PlayerReply::AllMaps(message) => { + info!("All maps downloaded from server."); + all_maps_downloaded = true; + all_maps = message; + } + _ => { + info!( + "Ignoring other message types until maps downloaded. {:?}", + received_message + ) + } + } + } + if all_maps_downloaded { + info!("Got all maps data."); + break; + } + thread::sleep(Duration::from_millis(1)); + } + debug!("All maps is: {:?}", all_maps); + all_maps +} diff --git a/rustyhack_client/src/game/new_player.rs b/rustyhack_client/src/game/new_player.rs new file mode 100644 index 0000000..90e2d97 --- /dev/null +++ b/rustyhack_client/src/game/new_player.rs @@ -0,0 +1,68 @@ +use crate::networking::message_handler; +use bincode::serialize; +use crossbeam_channel::{Receiver, Sender}; +use laminar::Packet; +use rustyhack_lib::ecs::player::Player; +use rustyhack_lib::message_handler::player_message::{ + CreatePlayerMessage, PlayerMessage, PlayerReply, +}; +use std::time::Duration; +use std::{process, thread}; + +pub(crate) fn send_new_player_request( + sender: &Sender, + player_name: &str, + server_addr: &str, + client_addr: &str, + channel_receiver: &Receiver, +) -> Player { + let create_player_request_packet = Packet::reliable_unordered( + server_addr + .parse() + .expect("Server address format is invalid."), + serialize(&PlayerMessage::PlayerJoin(CreatePlayerMessage { + client_addr: client_addr.to_string(), + player_name: player_name.to_string(), + })) + .unwrap(), + ); + message_handler::send_packet(create_player_request_packet, sender); + info!("Sent new player request to server."); + wait_for_new_player_response(channel_receiver) +} + +fn wait_for_new_player_response(channel_receiver: &Receiver) -> Player { + let mut new_player_confirmed = false; + let mut player = Player::default(); + loop { + let received = channel_receiver.recv(); + if let Ok(received_message) = received { + match received_message { + PlayerReply::PlayerJoined(message) => { + info!("New player creation confirmed."); + new_player_confirmed = true; + player = message; + } + PlayerReply::PlayerAlreadyOnline => { + error!( + "This player name is already taken, and the player is currently online." + ); + process::exit(1); + } + _ => { + info!( + "Ignoring other message types until new player confirmed. {:?}", + received_message + ) + } + } + } + if new_player_confirmed { + info!("Got all data needed to begin game."); + break; + } + thread::sleep(Duration::from_millis(1)); + } + info!("player_name is: {}", player.player_details.player_name); + player +} diff --git a/rustyhack_client/src/game/updates_handler.rs b/rustyhack_client/src/game/updates_handler.rs new file mode 100644 index 0000000..b090d3c --- /dev/null +++ b/rustyhack_client/src/game/updates_handler.rs @@ -0,0 +1,104 @@ +use crate::networking::message_handler; +use bincode::serialize; +use console_engine::{ConsoleEngine, KeyCode}; +use crossbeam_channel::{Receiver, Sender}; +use laminar::Packet; +use rustyhack_lib::ecs::components::Velocity; +use rustyhack_lib::ecs::player::Player; +use rustyhack_lib::message_handler::player_message::{ + EntityUpdates, PlayerMessage, PlayerReply, VelocityMessage, +}; + +pub(crate) fn send_player_updates( + sender: &Sender, + console: &ConsoleEngine, + player: &Player, + server_addr: &str, +) { + let mut velocity = Velocity { x: 0, y: 0 }; + if console.is_key_held(KeyCode::Up) { + velocity.y = -1; + } else if console.is_key_held(KeyCode::Down) { + velocity.y = 1; + } else if console.is_key_held(KeyCode::Left) { + velocity.x = -1; + } else if console.is_key_held(KeyCode::Right) { + velocity.x = 1; + } + + if velocity.y != 0 || velocity.x != 0 { + debug!("Movement detected, sending velocity packet to server."); + send_velocity_packet(sender, server_addr, player, velocity); + } +} + +fn send_velocity_packet( + sender: &Sender, + server_addr: &str, + player: &Player, + velocity: Velocity, +) { + let packet = Packet::unreliable_sequenced( + server_addr + .parse() + .expect("Server address format is invalid."), + serialize(&PlayerMessage::UpdateVelocity(VelocityMessage { + player_name: player.player_details.player_name.clone(), + velocity, + })) + .unwrap(), + Some(10), + ); + message_handler::send_packet(packet, sender); + debug!("Sent velocity packet to server."); +} + +pub(crate) fn check_for_received_player_updates( + channel_receiver: &Receiver, + mut player: Player, +) -> Player { + debug!("Checking for received player position from server."); + while !channel_receiver.is_empty() { + let received = channel_receiver.recv(); + if let Ok(received_message) = received { + match received_message { + PlayerReply::UpdatePosition(new_position) => { + debug!("Player position update received: {:?}", &new_position); + player.position = new_position + } + _ => { + warn!( + "Unexpected message on channel from message handler: {:?}", + received_message + ) + } + } + } + } + player +} + +pub(crate) fn check_for_received_entity_updates( + channel_receiver: &Receiver, + mut entity_updates: EntityUpdates, +) -> EntityUpdates { + debug!("Checking for received entity updates from server."); + while !channel_receiver.is_empty() { + let received = channel_receiver.recv(); + if let Ok(received_message) = received { + match received_message { + PlayerReply::UpdateOtherEntities(new_updates) => { + debug!("Entity updates received: {:?}", &new_updates); + entity_updates = new_updates; + } + _ => { + warn!( + "Unexpected message on channel from message handler: {:?}", + received_message + ) + } + } + } + } + entity_updates +} diff --git a/rustyhack_client/src/viewport.rs b/rustyhack_client/src/game/viewport.rs similarity index 84% rename from rustyhack_client/src/viewport.rs rename to rustyhack_client/src/game/viewport.rs index f9a0152..ad4a089 100644 --- a/rustyhack_client/src/viewport.rs +++ b/rustyhack_client/src/game/viewport.rs @@ -1,18 +1,19 @@ -use console_engine::{pixel, Color, ConsoleEngine}; +use console_engine::{pixel, ConsoleEngine}; use rustyhack_lib::background_map::tiles::{Tile, TilePosition}; use rustyhack_lib::background_map::BackgroundMap; +use rustyhack_lib::ecs::components::DisplayDetails; use rustyhack_lib::ecs::player::Player; use rustyhack_lib::message_handler::player_message::EntityUpdates; -pub struct Viewport { - pub width: u32, - pub height: u32, - pub target_fps: u32, - pub viewable_map_topleft: TilePosition, +pub(crate) struct Viewport { + pub(crate) width: u32, + pub(crate) height: u32, + pub(crate) target_fps: u32, + pub(crate) viewable_map_topleft: TilePosition, } impl Viewport { - pub fn new(width: u32, height: u32, target_fps: u32) -> Viewport { + pub(crate) fn new(width: u32, height: u32, target_fps: u32) -> Viewport { Viewport { width, height, @@ -21,7 +22,7 @@ impl Viewport { } } - pub fn draw_viewport_contents( + pub(crate) fn draw_viewport_contents( &mut self, console: &mut ConsoleEngine, player: &Player, @@ -53,7 +54,8 @@ fn draw_other_entities( viewport: &Viewport, ) { debug!("Drawing other entities."); - let updates = entity_updates.updates.clone(); + let default_display_details = DisplayDetails::default(); + let updates = entity_updates.position_updates.clone(); for (name, position) in updates { if name != player.player_details.player_name { let relative_entity_position = TilePosition { @@ -66,11 +68,14 @@ fn draw_other_entities( && relative_entity_position.x < (viewport.width - 1) as i32 && relative_entity_position.y < (viewport.height - 1) as i32 { + let display_details = entity_updates.display_details.get(&name).unwrap_or_else(|| { + warn!("Entity update for {} doesn't have a corresponding display detail, using default.", &name); + &default_display_details}); + console.set_pxl( relative_entity_position.x, relative_entity_position.y, - //todo don't hardcode this - pixel::pxl_fg('@', Color::Magenta), + pixel::pxl_fg(display_details.icon, display_details.colour), ) } } diff --git a/rustyhack_client/src/main.rs b/rustyhack_client/src/main.rs index 7ac6b84..811b037 100644 --- a/rustyhack_client/src/main.rs +++ b/rustyhack_client/src/main.rs @@ -1,156 +1,21 @@ +use std::env; + mod consts; -mod engine; -mod message_handler; -mod viewport; +mod game; +mod networking; +mod setup; #[macro_use] extern crate log; extern crate simplelog; -use crate::consts::VALID_NAME_REGEX; -use laminar::Socket; -use regex::Regex; -use simplelog::*; -use std::fs::File; -use std::net::SocketAddr; -use std::{env, io, process, thread}; - fn main() { - let args: Vec = env::args().collect(); - initialise_log(&args); - let (server_addr, client_addr) = get_server_addr(); - info!("Server address is set to: {}", &server_addr); - info!("Client listen address is set to: {}", &client_addr); - - let player_name = get_player_name(); - - info!("Attempting to bind listen socket to: {}", &client_addr); - let mut socket = Socket::bind_with_config(&client_addr, message_handler::get_laminar_config()) - .unwrap_or_else(|err| { - error!("Unable to bind socket to {}, error: {}", &client_addr, err); - process::exit(1); - }); - info!("Successfully bound socket."); - - let sender = socket.get_packet_sender(); - let receiver = socket.get_event_receiver(); - let _thread = thread::spawn(move || socket.start_polling()); - info!("Spawned socket polling thread."); - - engine::run(sender, receiver, &server_addr, &client_addr, &player_name); -} - -fn get_server_addr() -> (String, String) { - println!("--Rustyhack MMO Client Setup--"); - - let mut server_addr; - loop { - server_addr = String::new(); - println!("1) Connect to which server? (default: 127.0.0.1:50201)"); - io::stdin() - .read_line(&mut server_addr) - .expect("Failed to read input"); - - if server_addr.trim() == "" { - println!("Using default server address."); - println!(); - server_addr = String::from("127.0.0.1:50201"); - break; - } + setup::initialise_log(env::args().collect()); - let server_socket_addr: SocketAddr = match server_addr.trim().parse() { - Ok(value) => value, - Err(err) => { - println!( - "Not a valid socket address (e.g. 127.0.0.1:50201 ): {}", - err - ); - continue; - } - }; - server_addr = server_socket_addr.to_string(); - break; - } - - //handle client port allocation automatically - //maybe need to revisit in future if it causes problems - let client_addr = String::from("0.0.0.0:0"); - - (server_addr, client_addr) -} - -fn get_player_name() -> String { - let mut player_name; - loop { - player_name = String::new(); - println!("3) What is your character name?"); - io::stdin() - .read_line(&mut player_name) - .expect("Failed to read input"); - - let parsed_player_name: String = match player_name.trim().parse() { - Ok(value) => value, - Err(err) => { - println!("Must be a valid String: {}", err); - println!(); - continue; - } - }; - - //must be 20 characters or less - if parsed_player_name.chars().count() > 20 { - println!("Character name must be 20 characters or less."); - println!(); - continue; - } - - //must only contain letters - let regex = Regex::new(VALID_NAME_REGEX).expect("Player name regex is invalid."); - if !regex.is_match(&parsed_player_name) { - println!("Character name must only contain letters."); - println!(); - continue; - } - - player_name = parsed_player_name; - break; - } - player_name -} + let (server_addr, client_addr, player_name) = setup::get_player_setup_details(); + let (sender, receiver) = networking::bind_to_socket(&client_addr); -fn initialise_log(args: &[String]) { - let mut log_level = LevelFilter::Info; - if args.len() > 1 && args[1] == "--debug" { - println!("Debug logging enabled."); - log_level = LevelFilter::Debug; - } + game::run(sender, receiver, &server_addr, &client_addr, &player_name); - let mut file_location = env::current_exe().unwrap_or_else(|err| { - eprintln!("Problem getting current executable location: {}", err); - process::exit(1); - }); - file_location.pop(); - file_location.push(consts::LOG_NAME); - CombinedLogger::init(vec![ - TermLogger::new( - LevelFilter::Warn, - simplelog::Config::default(), - TerminalMode::Mixed, - ), - WriteLogger::new( - log_level, - simplelog::Config::default(), - File::create(file_location.as_path()).unwrap_or_else(|err| { - eprintln!("Unable to create log file: {}", err); - process::exit(1); - }), - ), - ]) - .unwrap_or_else(|err| { - eprintln!( - "Something went wrong when initialising the logging system: {}", - err - ); - process::exit(1); - }); + info!("Program terminated."); } diff --git a/rustyhack_client/src/networking.rs b/rustyhack_client/src/networking.rs new file mode 100644 index 0000000..55c1ccf --- /dev/null +++ b/rustyhack_client/src/networking.rs @@ -0,0 +1,37 @@ +use crossbeam_channel::{Receiver, Sender}; +use laminar::{Packet, Socket, SocketEvent}; +use std::time::Duration; +use std::{process, thread}; + +pub(crate) mod message_handler; + +pub(crate) fn bind_to_socket(client_addr: &str) -> (Sender, Receiver) { + info!("Attempting to bind listen socket to: {}", &client_addr); + let socket = + Socket::bind_with_config(&client_addr, get_laminar_config()).unwrap_or_else(|err| { + error!("Unable to bind socket to {}, error: {}", &client_addr, err); + process::exit(1); + }); + info!("Successfully bound socket."); + + let sender = socket.get_packet_sender(); + let receiver = socket.get_event_receiver(); + + start_polling(socket); + + (sender, receiver) +} + +fn get_laminar_config() -> laminar::Config { + laminar::Config { + idle_connection_timeout: Duration::from_secs(10), + heartbeat_interval: Some(Duration::from_secs(2)), + max_fragments: 255, + ..Default::default() + } +} + +fn start_polling(mut socket: Socket) { + let _thread = thread::spawn(move || socket.start_polling()); + info!("Spawned socket polling thread."); +} diff --git a/rustyhack_client/src/message_handler.rs b/rustyhack_client/src/networking/message_handler.rs similarity index 88% rename from rustyhack_client/src/message_handler.rs rename to rustyhack_client/src/networking/message_handler.rs index 41a81c1..c20f85b 100644 --- a/rustyhack_client/src/message_handler.rs +++ b/rustyhack_client/src/networking/message_handler.rs @@ -1,11 +1,22 @@ +use crate::networking::message_handler; use bincode::deserialize; use crossbeam_channel::{Receiver, Sender}; use laminar::{Packet, SocketEvent}; use rustyhack_lib::message_handler::player_message::PlayerReply; -use std::process; -use std::time::Duration; +use std::{process, thread}; -pub fn run( +pub(crate) fn spawn_message_handler_thread( + sender: Sender, + receiver: Receiver, + player_update_sender: Sender, + entity_update_sender: Sender, +) { + thread::spawn(move || { + message_handler::run(sender, receiver, player_update_sender, entity_update_sender) + }); +} + +pub(crate) fn run( _sender: Sender, receiver: Receiver, player_update_sender: Sender, @@ -83,7 +94,7 @@ pub fn run( } } -pub fn send_packet(packet: Packet, sender: &Sender) { +pub(crate) fn send_packet(packet: Packet, sender: &Sender) { let send_result = sender.send(packet); match send_result { Ok(_) => { @@ -95,12 +106,3 @@ pub fn send_packet(packet: Packet, sender: &Sender) { } } } - -pub fn get_laminar_config() -> laminar::Config { - laminar::Config { - idle_connection_timeout: Duration::from_secs(10), - heartbeat_interval: Some(Duration::from_secs(2)), - max_fragments: 255, - ..Default::default() - } -} diff --git a/rustyhack_client/src/setup.rs b/rustyhack_client/src/setup.rs new file mode 100644 index 0000000..29930c0 --- /dev/null +++ b/rustyhack_client/src/setup.rs @@ -0,0 +1,130 @@ +use crate::consts; +use regex::Regex; +use simplelog::{CombinedLogger, LevelFilter, TermLogger, TerminalMode, WriteLogger}; +use std::fs::File; +use std::net::SocketAddr; +use std::{env, io, process}; + +pub(crate) fn initialise_log(args: Vec) { + let mut log_level = LevelFilter::Info; + if args.len() > 1 && args[1] == "--debug" { + println!("Debug logging enabled."); + log_level = LevelFilter::Debug; + } + + let mut file_location = env::current_exe().unwrap_or_else(|err| { + eprintln!("Problem getting current executable location: {}", err); + process::exit(1); + }); + file_location.pop(); + file_location.push(consts::LOG_NAME); + CombinedLogger::init(vec![ + TermLogger::new( + LevelFilter::Warn, + simplelog::Config::default(), + TerminalMode::Mixed, + ), + WriteLogger::new( + log_level, + simplelog::Config::default(), + File::create(file_location.as_path()).unwrap_or_else(|err| { + eprintln!("Unable to create log file: {}", err); + process::exit(1); + }), + ), + ]) + .unwrap_or_else(|err| { + eprintln!( + "Something went wrong when initialising the logging system: {}", + err + ); + process::exit(1); + }); +} + +pub(crate) fn get_player_setup_details() -> (String, String, String) { + let (server_addr, client_addr) = get_server_addr(); + let player_name = get_player_name(); + (server_addr, client_addr, player_name) +} + +fn get_server_addr() -> (String, String) { + println!("--Rustyhack MMO Client Setup--"); + + let mut server_addr; + loop { + server_addr = String::new(); + println!("1) Connect to which server? (default: 127.0.0.1:50201)"); + io::stdin() + .read_line(&mut server_addr) + .expect("Failed to read input"); + + if server_addr.trim() == "" { + println!("Using default server address."); + println!(); + server_addr = String::from("127.0.0.1:50201"); + break; + } + + let server_socket_addr: SocketAddr = match server_addr.trim().parse() { + Ok(value) => value, + Err(err) => { + println!( + "Not a valid socket address (e.g. 127.0.0.1:50201 ): {}", + err + ); + continue; + } + }; + server_addr = server_socket_addr.to_string(); + break; + } + + //handle client port allocation automatically + //maybe need to revisit in future if it causes problems + let client_addr = String::from("0.0.0.0:0"); + + info!("Server address is set to: {}", &server_addr); + info!("Client listen address is set to: {}", &client_addr); + (server_addr, client_addr) +} + +fn get_player_name() -> String { + let mut player_name; + loop { + player_name = String::new(); + println!("3) What is your character name?"); + io::stdin() + .read_line(&mut player_name) + .expect("Failed to read input"); + + let parsed_player_name: String = match player_name.trim().parse() { + Ok(value) => value, + Err(err) => { + println!("Must be a valid String: {}", err); + println!(); + continue; + } + }; + + //must be 20 characters or less + if parsed_player_name.chars().count() > 20 { + println!("Character name must be 20 characters or less."); + println!(); + continue; + } + + //must only contain letters + let regex = Regex::new(consts::VALID_NAME_REGEX).expect("Player name regex is invalid."); + if !regex.is_match(&parsed_player_name) { + println!("Character name must only contain letters."); + println!(); + continue; + } + + player_name = parsed_player_name; + break; + } + info!("Requested player name is: {}", &player_name); + player_name +} diff --git a/rustyhack_lib/src/ecs/components.rs b/rustyhack_lib/src/ecs/components.rs index 27d1838..e5ed51f 100644 --- a/rustyhack_lib/src/ecs/components.rs +++ b/rustyhack_lib/src/ecs/components.rs @@ -1,3 +1,4 @@ +use crate::consts::{DEFAULT_PLAYER_COLOUR, DEFAULT_PLAYER_ICON}; use console_engine::Color; use serde::{Deserialize, Serialize}; @@ -27,6 +28,17 @@ pub struct DisplayDetails { pub collidable: bool, } +impl Default for DisplayDetails { + fn default() -> Self { + DisplayDetails { + icon: DEFAULT_PLAYER_ICON, + colour: DEFAULT_PLAYER_COLOUR, + visible: true, + collidable: true, + } + } +} + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct PlayerDetails { pub player_name: String, diff --git a/rustyhack_lib/src/ecs/player.rs b/rustyhack_lib/src/ecs/player.rs index be60bb9..403190b 100644 --- a/rustyhack_lib/src/ecs/player.rs +++ b/rustyhack_lib/src/ecs/player.rs @@ -1,7 +1,4 @@ -use crate::consts::{ - DEFAULT_MAP, DEFAULT_PLAYER_COLOUR, DEFAULT_PLAYER_ICON, DEFAULT_PLAYER_POSITION_X, - DEFAULT_PLAYER_POSITION_Y, -}; +use crate::consts::{DEFAULT_MAP, DEFAULT_PLAYER_POSITION_X, DEFAULT_PLAYER_POSITION_Y}; use crate::ecs::components::{DisplayDetails, PlayerDetails, Position}; use serde::{Deserialize, Serialize}; @@ -20,12 +17,7 @@ impl Default for Player { client_addr: "".to_string(), currently_online: false, }, - display_details: DisplayDetails { - icon: DEFAULT_PLAYER_ICON, - colour: DEFAULT_PLAYER_COLOUR, - visible: true, - collidable: true, - }, + display_details: DisplayDetails::default(), position: Position { x: DEFAULT_PLAYER_POSITION_X, y: DEFAULT_PLAYER_POSITION_Y, diff --git a/rustyhack_lib/src/lib.rs b/rustyhack_lib/src/lib.rs index 7c22d6d..ebaa094 100644 --- a/rustyhack_lib/src/lib.rs +++ b/rustyhack_lib/src/lib.rs @@ -1,6 +1,3 @@ -pub use background_map::character_map; -pub use background_map::tiles; - pub mod background_map; pub mod consts; pub mod ecs; diff --git a/rustyhack_lib/src/message_handler/player_message.rs b/rustyhack_lib/src/message_handler/player_message.rs index a77df21..813cd04 100644 --- a/rustyhack_lib/src/message_handler/player_message.rs +++ b/rustyhack_lib/src/message_handler/player_message.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; use crate::background_map::AllMaps; -use crate::ecs::components::{Position, Velocity}; +use crate::ecs::components::{DisplayDetails, Position, Velocity}; use crate::ecs::player::Player; #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -37,5 +37,6 @@ pub struct VelocityMessage { #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct EntityUpdates { - pub updates: HashMap, + pub position_updates: HashMap, + pub display_details: HashMap, } diff --git a/rustyhack_server/src/consts.rs b/rustyhack_server/src/consts.rs index f69c37d..50c0630 100644 --- a/rustyhack_server/src/consts.rs +++ b/rustyhack_server/src/consts.rs @@ -1,5 +1,5 @@ use std::time::Duration; -pub const LOG_NAME: &str = "rustyhack_server.log"; -pub const ENTITY_UPDATE_TICK: Duration = Duration::from_millis(50); -pub const LOOP_TICK: Duration = Duration::from_millis(10); +pub(crate) const LOG_NAME: &str = "rustyhack_server.log"; +pub(crate) const ENTITY_UPDATE_TICK: Duration = Duration::from_millis(50); +pub(crate) const LOOP_TICK: Duration = Duration::from_millis(10); diff --git a/rustyhack_server/src/game.rs b/rustyhack_server/src/game.rs new file mode 100644 index 0000000..4c13c82 --- /dev/null +++ b/rustyhack_server/src/game.rs @@ -0,0 +1,88 @@ +use std::collections::HashMap; +use std::thread; +use std::time::Instant; + +use crossbeam_channel::{Receiver, Sender}; +use laminar::{Packet, SocketEvent}; +use legion::*; + +use rustyhack_lib::ecs::components::*; + +use crate::consts; +use crate::networking::message_handler; + +mod background_map; +mod player_updates; +mod systems; + +pub(crate) fn run(sender: Sender, receiver: Receiver) { + let all_maps_resource = background_map::initialise_all_maps(); + let all_maps = background_map::initialise_all_maps(); + + let (channel_sender, channel_receiver) = crossbeam_channel::unbounded(); + info!("Created thread channel sender and receiver."); + let local_sender = sender.clone(); + + message_handler::spawn_message_handler_thread(sender, receiver, all_maps, channel_sender); + + let mut world = World::default(); + info!("Initialised ECS World"); + + let mut schedule = systems::build_schedule(); + + let mut resources = Resources::default(); + resources.insert(all_maps_resource); + info!("Finished loading all_maps into world resources."); + + let mut player_velocity_updates: HashMap = HashMap::new(); + + let mut entity_tick_time = Instant::now(); + let mut loop_tick_time = Instant::now(); + info!("Starting game loop"); + loop { + player_velocity_updates = player_updates::process_player_messages( + &mut world, + &channel_receiver, + &local_sender, + player_velocity_updates, + ); + + if !player_velocity_updates.is_empty() { + debug!("Player velocity updates available, proceeding with world update."); + resources.insert(player_velocity_updates.to_owned()); + debug!("Added player velocity updates to world resources."); + + debug!("Executing schedule..."); + schedule.execute(&mut world, &mut resources); + debug!("Schedule executed successfully."); + + player_velocity_updates = player_updates::send_player_updates( + &mut world, + &local_sender, + player_velocity_updates, + ); + } + + //do every 50ms + if entity_tick_time.elapsed() > consts::ENTITY_UPDATE_TICK { + player_updates::send_other_entities_updates(&mut world, &local_sender); + entity_tick_time = Instant::now(); + } + + if loop_tick_time.elapsed() > consts::LOOP_TICK { + warn!( + "Loop took longer than specified tick time, expected: {:?}, actual: {:?}", + consts::LOOP_TICK, + loop_tick_time.elapsed() + ); + loop_tick_time = Instant::now(); + continue; + } else { + let duration_to_sleep = consts::LOOP_TICK - loop_tick_time.elapsed(); + if duration_to_sleep.as_nanos() > 0 { + thread::sleep(duration_to_sleep); + } + loop_tick_time = Instant::now(); + } + } +} diff --git a/rustyhack_server/src/background_map.rs b/rustyhack_server/src/game/background_map.rs similarity index 98% rename from rustyhack_server/src/background_map.rs rename to rustyhack_server/src/game/background_map.rs index 575e3e7..8383e1a 100644 --- a/rustyhack_server/src/background_map.rs +++ b/rustyhack_server/src/game/background_map.rs @@ -5,7 +5,7 @@ use std::collections::HashMap; use std::path::PathBuf; use std::{env, fs, process}; -pub fn initialise_all_maps() -> AllMaps { +pub(crate) fn initialise_all_maps() -> AllMaps { info!("About to initialise all maps"); let mut all_maps: AllMaps = HashMap::new(); let file_location = get_maps_directory_location(); diff --git a/rustyhack_server/src/engine.rs b/rustyhack_server/src/game/player_updates.rs similarity index 55% rename from rustyhack_server/src/engine.rs rename to rustyhack_server/src/game/player_updates.rs index 62e764a..516c52b 100644 --- a/rustyhack_server/src/engine.rs +++ b/rustyhack_server/src/game/player_updates.rs @@ -1,106 +1,16 @@ -use crate::message_handler; -use crate::message_handler::get_laminar_config; -use crate::{background_map, consts}; +use crate::networking::message_handler; use bincode::serialize; use crossbeam_channel::{Receiver, Sender}; -use laminar::{Packet, Socket}; -use legion::*; -use rustyhack_lib::background_map::tiles::{Collidable, Tile}; -use rustyhack_lib::background_map::AllMaps; -use rustyhack_lib::consts::DEFAULT_MAP; +use laminar::Packet; +use legion::{IntoQuery, World}; use rustyhack_lib::ecs::components; -use rustyhack_lib::ecs::components::*; +use rustyhack_lib::ecs::components::{DisplayDetails, PlayerDetails, Position, Velocity}; use rustyhack_lib::ecs::player::Player; use rustyhack_lib::message_handler::player_message::{EntityUpdates, PlayerMessage, PlayerReply}; use std::collections::HashMap; -use std::time::Instant; -use std::{process, thread}; +use std::process; -pub fn run(server_addr: &str) { - info!("Attempting to bind socket to: {}", &server_addr); - let mut socket = - Socket::bind_with_config(&server_addr, get_laminar_config()).unwrap_or_else(|err| { - error!("Unable to bind socket to {}, error: {}", &server_addr, err); - process::exit(1); - }); - info!("Bound to socket successfully."); - - let (sender, receiver) = (socket.get_packet_sender(), socket.get_event_receiver()); - let local_sender = sender.clone(); - thread::spawn(move || socket.start_polling()); - info!("Spawned socket polling thread."); - - let mut world = World::default(); - info!("Initialised ECS World"); - - let all_maps_resource = background_map::initialise_all_maps(); - let all_maps = background_map::initialise_all_maps(); - - let (channel_sender, channel_receiver) = crossbeam_channel::unbounded(); - info!("Created thread channel sender and receiver."); - thread::spawn(move || message_handler::run(&sender, &receiver, &all_maps, channel_sender)); - - let mut schedule = Schedule::builder() - .add_system(update_player_input_system()) - .add_system(update_entities_position_system()) - .build(); - info!("Built system schedule."); - - let mut resources = Resources::default(); - resources.insert(all_maps_resource); - info!("Finished loading all maps resource."); - - let mut player_velocity_updates: HashMap = HashMap::new(); - - let mut entity_tick_time = Instant::now(); - let mut loop_tick_time = Instant::now(); - loop { - player_velocity_updates = process_player_messages( - &mut world, - &channel_receiver, - &local_sender, - player_velocity_updates, - ); - - if !player_velocity_updates.is_empty() { - debug!("Player velocity updates available, proceeding with world update."); - resources.insert(player_velocity_updates.to_owned()); - debug!("Added player velocity updates to world resources."); - - debug!("Executing schedule..."); - schedule.execute(&mut world, &mut resources); - debug!("Schedule executed successfully."); - - player_velocity_updates = - send_player_updates(&mut world, &local_sender, player_velocity_updates); - } - - //do every 50ms - if entity_tick_time.elapsed() > consts::ENTITY_UPDATE_TICK { - send_other_entities_updates(&mut world, &local_sender); - entity_tick_time = Instant::now(); - } - - //todo: tune this, else it eats up cpu - if loop_tick_time.elapsed() > consts::LOOP_TICK { - warn!( - "Loop took longer than specified tick time, expected: {:?}, actual: {:?}", - consts::LOOP_TICK, - loop_tick_time.elapsed() - ); - loop_tick_time = Instant::now(); - continue; - } else { - let duration_to_sleep = consts::LOOP_TICK - loop_tick_time.elapsed(); - if duration_to_sleep.as_nanos() > 0 { - thread::sleep(duration_to_sleep); - } - loop_tick_time = Instant::now(); - } - } -} - -fn process_player_messages( +pub(crate) fn process_player_messages( world: &mut World, channel_receiver: &Receiver, sender: &Sender, @@ -152,7 +62,7 @@ fn set_player_disconnected(world: &mut World, address: String) { } } -pub fn join_player(world: &mut World, name: String, client_addr: String, sender: &Sender) { +fn join_player(world: &mut World, name: String, client_addr: String, sender: &Sender) { let mut query = <(&mut PlayerDetails, &DisplayDetails, &Position)>::query(); let mut should_create_new_player = true; for (player_details, display_details, position) in query.iter_mut(world) { @@ -193,12 +103,7 @@ pub fn join_player(world: &mut World, name: String, client_addr: String, sender: } } -pub fn create_player( - world: &mut World, - name: String, - client_addr: String, - sender: &Sender, -) { +fn create_player(world: &mut World, name: String, client_addr: String, sender: &Sender) { let player = Player { player_details: PlayerDetails { player_name: name.clone(), @@ -236,58 +141,7 @@ fn send_player_joined_response(player: Player, sender: &Sender) { ); } -#[system(par_for_each)] -fn update_player_input( - player_details: &PlayerDetails, - velocity: &mut Velocity, - #[resource] player_updates: &HashMap, -) { - debug!("Adding player velocity updates to world."); - for (update_entity_name, update_velocity) in player_updates { - if update_entity_name == &player_details.player_name { - velocity.x = update_velocity.x; - velocity.y = update_velocity.y; - } - } -} - -#[system(par_for_each)] -#[filter(maybe_changed::())] -fn update_entities_position( - velocity: &mut Velocity, - position: &mut Position, - #[resource] all_maps: &AllMaps, -) { - debug!("Updating world entities positions after velocity updates."); - let current_map = all_maps.get(&position.map).unwrap_or_else(|| { - error!( - "Entity is located on a map that does not exist: {}", - &position.map - ); - warn!("Will return the default map, but this may cause problems."); - all_maps.get(DEFAULT_MAP).unwrap() - }); - if !entity_is_colliding_with_tile(current_map.get_tile_at( - (position.x + velocity.x) as usize, - (position.y + velocity.y) as usize, - )) { - position.x += velocity.x; - position.y += velocity.y; - } - velocity.x = 0; - velocity.y = 0; -} - -fn entity_is_colliding_with_tile(tile: Tile) -> bool { - match tile { - Tile::Door(door) => door.collidable == Collidable::True, - Tile::Wall(wall) => wall.collidable == Collidable::True, - Tile::Boundary => true, - _ => false, - } -} - -fn send_player_updates( +pub(crate) fn send_player_updates( world: &mut World, sender: &Sender, mut player_velocity_updates: HashMap, @@ -324,13 +178,15 @@ fn send_player_updates( player_velocity_updates } -fn send_other_entities_updates(world: &mut World, sender: &Sender) { - let mut updates: HashMap = HashMap::new(); - let mut query = <(&PlayerDetails, &mut Position)>::query(); +pub(crate) fn send_other_entities_updates(world: &mut World, sender: &Sender) { + let mut position_updates: HashMap = HashMap::new(); + let mut display_details: HashMap = HashMap::new(); + let mut query = <(&PlayerDetails, &mut Position, &DisplayDetails)>::query(); debug!("Getting all players positions"); - for (player_details, position) in query.iter_mut(world) { + for (player_details, position, display) in query.iter_mut(world) { if player_details.currently_online { - updates.insert(player_details.player_name.clone(), position.clone()); + position_updates.insert(player_details.player_name.clone(), position.clone()); + display_details.insert(player_details.player_name.clone(), *display); } } @@ -340,12 +196,13 @@ fn send_other_entities_updates(world: &mut World, sender: &Sender) { if player_details.currently_online { debug!("Sending entity updates to: {}", &player_details.client_addr); let response = serialize(&PlayerReply::UpdateOtherEntities(EntityUpdates { - updates: updates.clone(), + position_updates: position_updates.clone(), + display_details: display_details.clone(), })) .unwrap_or_else(|err| { error!( "Failed to serialise entity updates: {:?}, error: {}", - &updates, err + &position_updates, err ); process::exit(1); }); diff --git a/rustyhack_server/src/game/systems.rs b/rustyhack_server/src/game/systems.rs new file mode 100644 index 0000000..4242a2f --- /dev/null +++ b/rustyhack_server/src/game/systems.rs @@ -0,0 +1,94 @@ +use legion::world::SubWorld; +use legion::*; +use rustyhack_lib::background_map::tiles::{Collidable, Tile}; +use rustyhack_lib::background_map::AllMaps; +use rustyhack_lib::consts::DEFAULT_MAP; +use rustyhack_lib::ecs::components::{DisplayDetails, PlayerDetails, Position, Velocity}; +use std::collections::HashMap; + +pub(crate) fn build_schedule() -> Schedule { + let schedule = Schedule::builder() + .add_system(update_player_input_system()) + .add_system(update_entities_position_system()) + .build(); + info!("Built system schedule."); + schedule +} + +#[system(par_for_each)] +fn update_player_input( + player_details: &PlayerDetails, + velocity: &mut Velocity, + #[resource] player_updates: &HashMap, +) { + debug!("Adding player velocity updates to world."); + for (update_entity_name, update_velocity) in player_updates { + if update_entity_name == &player_details.player_name { + velocity.x = update_velocity.x; + velocity.y = update_velocity.y; + } + } +} + +#[system] +#[write_component(Velocity)] +#[write_component(Position)] +#[read_component(DisplayDetails)] +fn update_entities_position(world: &mut SubWorld, #[resource] all_maps: &AllMaps) { + let mut query = <(&mut Velocity, &mut Position)>::query(); + let world2 = &world.clone(); + for (velocity, position) in query.iter_mut(world) { + debug!("Updating world entities positions after velocity updates."); + let current_map = all_maps.get(&position.map).unwrap_or_else(|| { + error!( + "Entity is located on a map that does not exist: {}", + &position.map + ); + warn!("Will return the default map, but this may cause problems."); + all_maps.get(DEFAULT_MAP).unwrap() + }); + if !entity_is_colliding_with_tile(current_map.get_tile_at( + (position.x + velocity.x) as usize, + (position.y + velocity.y) as usize, + )) && !entity_is_colliding_with_entity( + position.x + velocity.x, + position.y + velocity.y, + world2, + &position.map, + ) { + position.x += velocity.x; + position.y += velocity.y; + } + velocity.x = 0; + velocity.y = 0; + } +} + +fn entity_is_colliding_with_entity( + player_x: i32, + player_y: i32, + world: &SubWorld, + current_map: &str, +) -> bool { + let mut result = false; + let mut query = <(&Position, &DisplayDetails)>::query(); + for (position, display_details) in query.iter(world) { + if position.map == current_map + && display_details.collidable + && position.x == player_x + && position.y == player_y + { + result = true; + } + } + result +} + +fn entity_is_colliding_with_tile(tile: Tile) -> bool { + match tile { + Tile::Door(door) => door.collidable == Collidable::True, + Tile::Wall(wall) => wall.collidable == Collidable::True, + Tile::Boundary => true, + _ => false, + } +} diff --git a/rustyhack_server/src/main.rs b/rustyhack_server/src/main.rs index 0e766cc..748e395 100644 --- a/rustyhack_server/src/main.rs +++ b/rustyhack_server/src/main.rs @@ -1,85 +1,20 @@ -use simplelog::*; -use std::fs::File; -use std::net::SocketAddr; -use std::{env, io, process}; +use std::env; -mod background_map; mod consts; -mod engine; -mod message_handler; +mod game; +mod networking; +mod setup; #[macro_use] extern crate log; extern crate simplelog; fn main() { - let args: Vec = env::args().collect(); - initialise_log(&args); - let server_addr = get_server_addr(); - info!("Server listen port is set to: {}", &server_addr); - engine::run(&server_addr); - info!("Program terminated."); -} - -fn get_server_addr() -> String { - println!("--Rustyhack MMO Server Setup--"); + setup::initialise_log(env::args().collect()); - let mut server_addr; - loop { - server_addr = String::new(); - println!("1) What is the server listen port? (default: 50201)"); - io::stdin() - .read_line(&mut server_addr) - .expect("Failed to read line"); + let (sender, receiver) = networking::bind_to_socket(setup::get_server_addr()); - if server_addr.trim() == "" { - println!("Using default server listen port."); - server_addr = String::from("0.0.0.0:50201"); - break; - } + game::run(sender, receiver); - server_addr = String::from("0.0.0.0:") + &*server_addr; - let server_socket_addr: SocketAddr = match server_addr.trim().parse() { - Ok(value) => value, - Err(err) => { - println!("Not a valid port (e.g. 50201 ): {}", err); - continue; - } - }; - server_addr = server_socket_addr.to_string(); - break; - } - server_addr -} - -fn initialise_log(args: &[String]) { - let mut log_level = LevelFilter::Info; - if args.len() > 1 && args[1] == "--debug" { - println!("Debug logging enabled."); - log_level = LevelFilter::Debug; - } - let mut file_location = env::current_exe().unwrap_or_else(|err| { - eprintln!("Problem getting current executable location: {}", err); - process::exit(1); - }); - file_location.pop(); - file_location.push(consts::LOG_NAME); - CombinedLogger::init(vec![ - TermLogger::new(LevelFilter::Info, Config::default(), TerminalMode::Mixed), - WriteLogger::new( - log_level, - Config::default(), - File::create(file_location.as_path()).unwrap_or_else(|err| { - eprintln!("Unable to create log file: {}", err); - process::exit(1); - }), - ), - ]) - .unwrap_or_else(|err| { - eprintln!( - "Something went wrong when initialising the logging system: {}", - err - ); - process::exit(1); - }); + info!("Program terminated."); } diff --git a/rustyhack_server/src/networking.rs b/rustyhack_server/src/networking.rs new file mode 100644 index 0000000..f2dadec --- /dev/null +++ b/rustyhack_server/src/networking.rs @@ -0,0 +1,32 @@ +use crossbeam_channel::{Receiver, Sender}; +use laminar::{Packet, Socket, SocketEvent}; +use std::time::Duration; +use std::{process, thread}; + +pub(crate) mod message_handler; + +pub(crate) fn bind_to_socket(server_addr: String) -> (Sender, Receiver) { + info!("Attempting to bind socket to: {}", &server_addr); + let mut socket = + Socket::bind_with_config(&server_addr, get_laminar_config()).unwrap_or_else(|err| { + error!("Unable to bind socket to {}, error: {}", &server_addr, err); + process::exit(1); + }); + info!("Bound to socket successfully."); + + let sender = socket.get_packet_sender(); + let receiver = socket.get_event_receiver(); + + thread::spawn(move || socket.start_polling()); + info!("Spawned socket polling thread."); + + (sender, receiver) +} + +fn get_laminar_config() -> laminar::Config { + laminar::Config { + idle_connection_timeout: Duration::from_secs(10), + max_fragments: 255, + ..Default::default() + } +} diff --git a/rustyhack_server/src/message_handler.rs b/rustyhack_server/src/networking/message_handler.rs similarity index 89% rename from rustyhack_server/src/message_handler.rs rename to rustyhack_server/src/networking/message_handler.rs index 24707e1..96bac01 100644 --- a/rustyhack_server/src/message_handler.rs +++ b/rustyhack_server/src/networking/message_handler.rs @@ -3,12 +3,21 @@ use crossbeam_channel::{Receiver, Sender}; use laminar::{Packet, SocketEvent}; use rustyhack_lib::background_map::AllMaps; use rustyhack_lib::message_handler::player_message::{PlayerMessage, PlayerReply}; -use std::time::Duration; +use std::thread; -pub fn run( - sender: &Sender, - receiver: &Receiver, - all_maps: &AllMaps, +pub(crate) fn spawn_message_handler_thread( + sender: Sender, + receiver: Receiver, + all_maps: AllMaps, + channel_sender: Sender, +) { + thread::spawn(move || run(sender, receiver, all_maps, channel_sender)); +} + +pub(crate) fn run( + sender: Sender, + receiver: Receiver, + all_maps: AllMaps, channel_sender: Sender, ) { info!("Spawned message handler thread."); @@ -79,7 +88,7 @@ pub fn run( } } -pub fn send_packet(packet: Packet, sender: &Sender) { +pub(crate) fn send_packet(packet: Packet, sender: &Sender) { let send_result = sender.send(packet); match send_result { Ok(_) => { @@ -104,11 +113,3 @@ fn send_channel_message(message: PlayerMessage, sender: &Sender) } } } - -pub fn get_laminar_config() -> laminar::Config { - laminar::Config { - idle_connection_timeout: Duration::from_secs(10), - max_fragments: 255, - ..Default::default() - } -} diff --git a/rustyhack_server/src/setup.rs b/rustyhack_server/src/setup.rs new file mode 100644 index 0000000..5dc5cc1 --- /dev/null +++ b/rustyhack_server/src/setup.rs @@ -0,0 +1,69 @@ +use crate::consts; +use simplelog::{CombinedLogger, Config, LevelFilter, TermLogger, TerminalMode, WriteLogger}; +use std::fs::File; +use std::net::SocketAddr; +use std::{env, io, process}; + +pub(crate) fn initialise_log(args: Vec) { + let mut log_level = LevelFilter::Info; + if args.len() > 1 && args[1] == "--debug" { + println!("Debug logging enabled."); + log_level = LevelFilter::Debug; + } + let mut file_location = env::current_exe().unwrap_or_else(|err| { + eprintln!("Problem getting current executable location: {}", err); + process::exit(1); + }); + file_location.pop(); + file_location.push(consts::LOG_NAME); + CombinedLogger::init(vec![ + TermLogger::new(LevelFilter::Info, Config::default(), TerminalMode::Mixed), + WriteLogger::new( + log_level, + Config::default(), + File::create(file_location.as_path()).unwrap_or_else(|err| { + eprintln!("Unable to create log file: {}", err); + process::exit(1); + }), + ), + ]) + .unwrap_or_else(|err| { + eprintln!( + "Something went wrong when initialising the logging system: {}", + err + ); + process::exit(1); + }); +} + +pub(crate) fn get_server_addr() -> String { + println!("--Rustyhack MMO Server Setup--"); + + let mut server_addr; + loop { + server_addr = String::new(); + println!("1) What is the server listen port? (default: 50201)"); + io::stdin() + .read_line(&mut server_addr) + .expect("Failed to read line"); + + if server_addr.trim() == "" { + println!("Using default server listen port."); + server_addr = String::from("0.0.0.0:50201"); + break; + } + + server_addr = String::from("0.0.0.0:") + &*server_addr; + let server_socket_addr: SocketAddr = match server_addr.trim().parse() { + Ok(value) => value, + Err(err) => { + println!("Not a valid port (e.g. 50201 ): {}", err); + continue; + } + }; + server_addr = server_socket_addr.to_string(); + break; + } + info!("Server listen port is set to: {}", &server_addr); + server_addr +}