summaryrefslogtreecommitdiff
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/Cargo.toml5
-rw-r--r--server/src/config/load.rs47
-rw-r--r--server/src/config/mod.rs13
-rw-r--r--server/src/error/mod.rs90
-rw-r--r--server/src/main.rs11
-rw-r--r--server/src/server/handle_request.rs57
-rw-r--r--server/src/server/listen.rs48
-rw-r--r--server/src/server/mod.rs55
-rw-r--r--server/src/server/run.rs23
-rw-r--r--server/src/session/mod.rs17
10 files changed, 342 insertions, 24 deletions
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 }
+}