diff options
-rw-r--r-- | CHANGELOG.md | 27 | ||||
-rw-r--r-- | Cargo.toml | 2 | ||||
-rw-r--r-- | client/Cargo.toml | 2 | ||||
-rw-r--r-- | client/src/client/run.rs | 25 | ||||
-rw-r--r-- | core/Cargo.toml | 2 | ||||
-rw-r--r-- | core/src/net/mod.rs | 1 | ||||
-rw-r--r-- | core/src/net/request/mod.rs | 6 | ||||
-rw-r--r-- | core/src/net/response/mod.rs | 8 | ||||
-rw-r--r-- | core/src/net/server_status/mod.rs | 9 | ||||
-rw-r--r-- | core/src/net/session_token/mod.rs | 13 | ||||
-rw-r--r-- | server/Cargo.toml | 5 | ||||
-rw-r--r-- | server/src/config/load.rs | 47 | ||||
-rw-r--r-- | server/src/config/mod.rs | 13 | ||||
-rw-r--r-- | server/src/error/mod.rs | 90 | ||||
-rw-r--r-- | server/src/main.rs | 11 | ||||
-rw-r--r-- | server/src/server/handle_request.rs | 57 | ||||
-rw-r--r-- | server/src/server/listen.rs | 48 | ||||
-rw-r--r-- | server/src/server/mod.rs | 55 | ||||
-rw-r--r-- | server/src/server/run.rs | 23 | ||||
-rw-r--r-- | server/src/session/mod.rs | 17 | ||||
-rw-r--r-- | test-server.ini | 7 |
21 files changed, 432 insertions, 36 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 878709c..5bf7bc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,33 @@ This is the changelog of Bowshock. See `"README.md"` for more information. +## 0.13.0-2 + +* Fix weird crate versions +* Bump dependency versions + +### Core + +* Implement `Clone`, `Copy`, `Display`, `Eq`, and `PartialEq` for `net::SessionToken` +* Add more variants to `Response` +* Add `net::ServerStatus` type + +### Client + +* Refactor + +### Server + +* Add `Session` type +* Restructure `Server` +* Handle and respond to requests +* Add `Error` and `Result` types +* Handle errors (instead of just panicking) +* Depend on `rand` +* Add `Config` structure +* Depend on `configparser` +* Add test configuration + ## 0.13.0-1 * Update logo @@ -3,7 +3,7 @@ members = ["client", "core", "server"] resolver = "2" [workspace.package] -version = "3.0.0" +version = "0.13.0" authors = ["Gabriel Bjørnager Jensen"] homepage = "https://achernar.dk/index.php?p=bowshock" repository = "https://mandelbrot.dk/bowshock/" diff --git a/client/Cargo.toml b/client/Cargo.toml index 9791b14..e8c57ac 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -11,7 +11,7 @@ repository.workspace = true [dependencies] bowshock = { path = "../core" } -bzipper = { version = "0.6.0", features = ["alloc"]} +bzipper = { version = "0.6.2", features = ["alloc"]} [lints] workspace = true diff --git a/client/src/client/run.rs b/client/src/client/run.rs index d955cea..88cc056 100644 --- a/client/src/client/run.rs +++ b/client/src/client/run.rs @@ -2,7 +2,7 @@ use crate::{Client, ServerConnection}; -use bowshock::net::{DEFAULT_SERVER_PORT, Request}; +use bowshock::net::{Request, Response, DEFAULT_SERVER_PORT}; use bzipper::FixedString; impl Client { @@ -12,8 +12,27 @@ impl Client { let mut connection = ServerConnection::new(server_addr); - let join_request = Request::PlayerJoin { username: FixedString::try_from("delta").unwrap() }; - connection.send_request(&join_request); + let mut request = Request::Ping; + connection.send_request(&request); + + let mut response = connection.recieve_response(); + + eprintln!("got response {response:?}"); + + request = Request::Join { username: FixedString::try_from("delta").unwrap() }; + connection.send_request(&request); + + response = connection.recieve_response(); + + eprintln!("got response {response:?}"); + + let Response::JoinAccepted { token } = response else { + eprintln!("expected approved join :("); + return Err(0x1); + }; + + request = Request::Quit { token }; + connection.send_request(&request); Ok(()) } diff --git a/core/Cargo.toml b/core/Cargo.toml index d835350..e14b1e3 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -9,7 +9,7 @@ homepage.workspace = true repository.workspace = true [dependencies] -bzipper = "0.6.0" +bzipper = "0.6.2" [lints] workspace = true diff --git a/core/src/net/mod.rs b/core/src/net/mod.rs index f1d7f0d..39916c9 100644 --- a/core/src/net/mod.rs +++ b/core/src/net/mod.rs @@ -3,6 +3,7 @@ use crate::use_mod; use_mod!(pub request); use_mod!(pub response); +use_mod!(pub server_status); use_mod!(pub session_token); // HIP 37279, a.k.a. Procyon. diff --git a/core/src/net/request/mod.rs b/core/src/net/request/mod.rs index ed803ac..40114d9 100644 --- a/core/src/net/request/mod.rs +++ b/core/src/net/request/mod.rs @@ -6,7 +6,9 @@ use bzipper::{Deserialise, FixedString, Serialise}; #[derive(Debug, Deserialise, Serialise)] pub enum Request { - PlayerJoin { username: FixedString<0x10> }, + Ping, - PlayerQuit { token: SessionToken }, + Join { username: FixedString<0x10> }, + + Quit { token: SessionToken }, } diff --git a/core/src/net/response/mod.rs b/core/src/net/response/mod.rs index 9c9a4cb..86e7db9 100644 --- a/core/src/net/response/mod.rs +++ b/core/src/net/response/mod.rs @@ -1,12 +1,14 @@ // Copyright 2022-2024 Gabriel Bjørnager Jensen. -use crate::net::{Request, SessionToken}; +use crate::net::{ServerStatus, SessionToken}; use bzipper::{Deserialise, FixedString, Serialise}; #[derive(Debug, Deserialise, Serialise)] pub enum Response { - PlayerJoinAccepted { token: SessionToken }, + JoinAccepted { token: SessionToken }, - RequestDenied { request: Request, reason: FixedString<0x20> }, + JoinDenied { reason: FixedString<0x20> }, + + ServerStatus(ServerStatus), } diff --git a/core/src/net/server_status/mod.rs b/core/src/net/server_status/mod.rs new file mode 100644 index 0000000..e74e4dc --- /dev/null +++ b/core/src/net/server_status/mod.rs @@ -0,0 +1,9 @@ +// Copyright 2022-2024 Gabriel Bjørnager Jensen. + +use bzipper::{Deserialise, Serialise}; + +#[derive(Debug, Deserialise, Serialise)] +pub struct ServerStatus { + pub player_count: u32, + pub max_player_count: u32, +} diff --git a/core/src/net/session_token/mod.rs b/core/src/net/session_token/mod.rs index cb3ab8c..0c839af 100644 --- a/core/src/net/session_token/mod.rs +++ b/core/src/net/session_token/mod.rs @@ -1,10 +1,21 @@ // Copyright 2022-2024 Gabriel Bjørnager Jensen. use bzipper::{Deserialise, Serialise}; +use std::fmt::{Debug, Display, Formatter}; -#[derive(Debug, Deserialise, Serialise)] +#[derive(Clone, Copy, Eq, Deserialise, PartialEq, Serialise)] #[repr(align(0x10))] pub struct SessionToken { pub time: u64, pub key: u64, } + +impl Debug for SessionToken { + #[inline(always)] + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { write!(f, "{self}") } +} + +impl Display for SessionToken { + #[inline(always)] + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { write!(f, "{:016X}:{:016X}", self.time, self.key) } +} diff --git a/server/Cargo.toml b/server/Cargo.toml index 30d6e61..ad3d31d 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -11,7 +11,10 @@ repository.workspace = true [dependencies] bowshock = { path = "../core" } -bzipper = "0.6.0" +configparser = "3.1.0" +rand = "0.8.5" + +bzipper = { version = "0.6.2", features = ["alloc"] } [lints] workspace = true diff --git a/server/src/config/load.rs b/server/src/config/load.rs new file mode 100644 index 0000000..7566ad9 --- /dev/null +++ b/server/src/config/load.rs @@ -0,0 +1,47 @@ +// Copyright 2022-2024 Gabriel Bjørnager Jensen. + +use crate::{Config, Error, Result}; + +use configparser::ini::Ini; +use std::env::args; +use std::path::PathBuf; + +impl Config { + pub fn load() -> Result<Self> { + let path = { + let mut args = args().skip(0x1); + + let Some(path) = args.next() else { return Err(Error::MissingConfig) }; + + if let Some(arg) = args.next() { return Err(Error::IllegalArgument { arg }) }; + + PathBuf::from(path) + }; + + let mut config = Ini::new(); + config + .load(path) + .map_err(|e| Error::ConfigError { message: e })?; + + macro_rules! get_field { + ($section:ident.$key:ident) => {{ + const FIELD: &str = concat!(stringify!($section), ".", stringify!($key)); + + if let Some(value) = config.get(stringify!($section), stringify!($key)) { + value + .parse() + .map_err(|e| Error::IllegalFieldValue { field: FIELD, value, source: Box::new(e) }) + } else { + Err(Error::MissingField { field: FIELD }) + } + }}; + } + + Ok(Self { + name: get_field!(server.name)?, + port: get_field!(server.port)?, + + max_player_count: get_field!(server.max_player_count)?, + }) + } +} diff --git a/server/src/config/mod.rs b/server/src/config/mod.rs new file mode 100644 index 0000000..72d3860 --- /dev/null +++ b/server/src/config/mod.rs @@ -0,0 +1,13 @@ +// Copyright 2022-2024 Gabriel Bjørnager Jensen. + +use bzipper::FixedString; + +mod load; + +#[derive(Debug)] +pub struct Config { + pub name: FixedString<0x10>, + pub port: u16, + + pub max_player_count: u32, +} diff --git a/server/src/error/mod.rs b/server/src/error/mod.rs new file mode 100644 index 0000000..281a074 --- /dev/null +++ b/server/src/error/mod.rs @@ -0,0 +1,90 @@ +// Copyright 2022-2024 Gabriel Bjørnager Jensen. + +use std::error::Error as StdError; +use std::fmt::{Display, Formatter}; +use std::process::{ExitCode, Termination}; + +pub type Result<T> = std::result::Result<T, Error>; + +#[derive(Debug)] +pub enum Error { + ConfigError { message: String }, + + IllegalArgument { arg: String }, + + IllegalFieldValue { field: &'static str, value: String, source: Box<dyn StdError> }, + + MissingConfig, + + MissingField { field: &'static str }, + + NetworkError { source: std::io::Error }, + + SerialiseError { source: bzipper::Error }, +} + +impl Display for Error { + #[inline] + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + use Error::*; + + match *self { + ConfigError { ref message } + => write!(f, "unable to load configuration: \"{message}\""), + + IllegalArgument { ref arg } + => write!(f, "illegal argument `{arg}` provided"), + + IllegalFieldValue { field, ref value, ref source } + => write!(f, "illegal configuration value {value:?} for field `{field}`: \"{source}\""), + + MissingConfig + => write!(f, "no configuration provided"), + + MissingField { field } + => write!(f, "missing configuration field `{field}`"), + + NetworkError { ref source } + => write!(f, "network error: \"{source}\""), + + SerialiseError { ref source } + => write!(f, "error when serialising response: \"{source}\""), + } + } +} + +impl StdError for Error { + #[allow(clippy::match_same_arms)] + #[inline] + fn source(&self) -> Option<&(dyn StdError + 'static)> { + use Error::*; + + match *self { + IllegalFieldValue { ref source, .. } => Some(source.as_ref()), + + NetworkError { ref source } => Some(source), + + SerialiseError { ref source } => Some(source), + + _ => None, + } + } +} + +impl Termination for Error { + #[inline(always)] + fn report(self) -> ExitCode { + use Error::*; + + match self { + | ConfigError { .. } + | IllegalArgument { .. } + | IllegalFieldValue { .. } + | MissingConfig + | MissingField { .. } + => 0x2, + + _ => 0x1, + }.into() + } +} diff --git a/server/src/main.rs b/server/src/main.rs index 5bd8b28..d375060 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,6 +1,15 @@ // Copyright 2022-2024 Gabriel Bjørnager Jensen. use bowshock::use_mod; +use_mod!(pub config); +use_mod!(pub error); use_mod!(pub server); +use_mod!(pub session); -fn main() -> Result<(), i32> { Server::new().run() } +fn main() -> Result<()> { + let config = Config::load()?; + let result = Server::new(config).run(); + + if let Err(ref e) = result { eprintln!("unable to run server: {e}"); } + result +} diff --git a/server/src/server/handle_request.rs b/server/src/server/handle_request.rs new file mode 100644 index 0000000..da20ee9 --- /dev/null +++ b/server/src/server/handle_request.rs @@ -0,0 +1,57 @@ +// Copyright 2022-2024 Gabriel Bjørnager Jensen. + +use crate::{Result, Server, Session}; + +use bowshock::net::{Request, Response}; +use std::net::SocketAddr; + +impl Server { + pub(in super) fn handle_request(&mut self, request: Request, addr: SocketAddr) -> Result<Option<Response>> { + eprintln!("got request {request:?} from {addr}"); + + let response = match request { + Request::Ping => { + let status = self.get_status(); + + Some(Response::ServerStatus(status)) + }, + + Request::Join { username } => { + for session in &self.sessions { + if username == session.username { + let response = Response::JoinDenied { + reason: "username already taken".parse().unwrap() + }; + + return Ok(Some(response)); + } + }; + + let token = self.generate_session_token(); + + self.sessions.push(Session { + token, + username, + + addr, + }); + + Some(Response::JoinAccepted { token }) + }, + + Request::Quit { token } => { + let index = self.sessions.iter().position(|v| *v == token); + + if let Some(index) = index { + let _ = self.sessions.remove(index); + } else { + eprintln!("got quit request for unknown session {token:?}"); + } + + None + }, + }; + + Ok(response) + } +} diff --git a/server/src/server/listen.rs b/server/src/server/listen.rs new file mode 100644 index 0000000..5e1e171 --- /dev/null +++ b/server/src/server/listen.rs @@ -0,0 +1,48 @@ +// Copyright 2022-2024 Gabriel Bjørnager Jensen. + +use crate::{Error, Result, Server}; + +use bowshock::net::{Request, Response}; +use bzipper::Buffer; +use std::net::UdpSocket; + +impl Server { + pub(in super) fn listen(&mut self, socket: &UdpSocket) -> Result<()> { + let mut request_buf = Buffer::<Request>::new(); + let mut response_buf = Buffer::<Response>::new(); + + eprintln!("listening at `{}`...", socket.local_addr().expect("socket without address provided")); + + loop { + let (len, client_addr) = socket + .recv_from(&mut request_buf) + .map_err(|e| Error::NetworkError { source: e })?; + + if len != request_buf.len() { + eprintln!("request had invalid size ({len}), skipping..."); + continue; + } + + let request = match request_buf.read() { + Ok(v) => v, + + Err(e) => { + eprintln!("invalid request provided: \"{e}\""); + continue; + } + }; + + let response = self.handle_request(request, client_addr)?; + + if let Some(response) = response { + response_buf + .write(&response) + .map_err(|e| Error::SerialiseError { source: e })?; + + socket + .send_to(&response_buf, client_addr) + .map_err(|e| Error::NetworkError { source: e })?; + } + } + } +} diff --git a/server/src/server/mod.rs b/server/src/server/mod.rs index 69b0842..0a7817f 100644 --- a/server/src/server/mod.rs +++ b/server/src/server/mod.rs @@ -1,15 +1,58 @@ // Copyright 2022-2024 Gabriel Bjørnager Jensen. +use crate::{Config, Session}; + +use bowshock::net::{ServerStatus, SessionToken}; +use rand::prelude::*; +use std::time::{SystemTime, UNIX_EPOCH}; + +mod handle_request; +mod listen; mod run; -pub struct Server; +pub struct Server { + config: Config, + + sessions: Vec<Session>, +} impl Server { #[must_use] - pub fn new() -> Self { Self } -} + pub fn new(config: Config) -> Self { + let sessions_len = config.max_player_count + .try_into() + .expect("`max_player_count` must be coercible to `usize`"); + + let sessions = Vec::with_capacity(sessions_len); + + Self { + config, + + sessions, + } + } + + #[must_use] + fn get_status(&self) -> ServerStatus { + let player_count = self.sessions + .len() + .try_into() + .expect("length of sessions must be coercible to `u32`"); + + let max_player_count = self.config.max_player_count; + + ServerStatus{ player_count, max_player_count } + } + + #[must_use] + fn generate_session_token(&self) -> SessionToken { + let time = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + + let key = rand::thread_rng().gen(); -impl Default for Server { - #[inline(always)] - fn default() -> Self { Self::new() } + SessionToken { time, key } + } } diff --git a/server/src/server/run.rs b/server/src/server/run.rs index 55d81ee..06e19ec 100644 --- a/server/src/server/run.rs +++ b/server/src/server/run.rs @@ -1,26 +1,17 @@ // Copyright 2022-2024 Gabriel Bjørnager Jensen. -use crate::Server; +use crate::{Error, Result, Server}; -use bowshock::net::{DEFAULT_SERVER_PORT, Request}; -use bzipper::Buffer; use std::net::UdpSocket; impl Server { - pub fn run(self) -> Result<(), i32> { - let server_addr = format!("127.0.0.1:{DEFAULT_SERVER_PORT}"); - let socket = UdpSocket::bind(&server_addr).unwrap(); + pub fn run(mut self) -> Result<()> { + let server_addr = format!("127.0.0.1:{}", self.config.port); + let socket = UdpSocket::bind(&server_addr) + .map_err(|e| Error::NetworkError { source: e })?; - let mut buf = Buffer::<Request>::new(); + self.listen(&socket)?; - eprintln!("listening at {server_addr}..."); - - loop { - let (len, client_addr) = socket.recv_from(&mut buf).unwrap(); - assert_eq!(len, buf.len()); - - let request = buf.read().unwrap(); - eprintln!("got request {request:?} from {client_addr}"); - } + Ok(()) } } diff --git a/server/src/session/mod.rs b/server/src/session/mod.rs new file mode 100644 index 0000000..b6ea014 --- /dev/null +++ b/server/src/session/mod.rs @@ -0,0 +1,17 @@ +// Copyright 2022-2024 Gabriel Bjørnager Jensen. + +use bowshock::net::SessionToken; +use bzipper::FixedString; +use std::net::SocketAddr; + +pub struct Session { + pub token: SessionToken, + pub username: FixedString<0x10>, + + pub addr: SocketAddr, +} + +impl PartialEq<SessionToken> for Session { + #[inline(always)] + fn eq(&self, other: &SessionToken) -> bool { self.token == *other } +} diff --git a/test-server.ini b/test-server.ini new file mode 100644 index 0000000..55c9a31 --- /dev/null +++ b/test-server.ini @@ -0,0 +1,7 @@ +[server] +name = "bowshock-test" + +root = "test-server/" +port = 37279 # Default port. + +max_player_count = 16 |