summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Bjørnager Jensen2024-08-27 12:24:32 +0200
committerGabriel Bjørnager Jensen2024-08-27 12:24:32 +0200
commit31e2a98a974c9e1811391c979e9532956dfd763d (patch)
treeac6be354678297059a8bc34d41cf3da83d982176
parent7380386fd55bb75fc30c1fba5b1ec4340d837cde (diff)
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.HEAD0.13.0-2master
-rw-r--r--CHANGELOG.md27
-rw-r--r--Cargo.toml2
-rw-r--r--client/Cargo.toml2
-rw-r--r--client/src/client/run.rs25
-rw-r--r--core/Cargo.toml2
-rw-r--r--core/src/net/mod.rs1
-rw-r--r--core/src/net/request/mod.rs6
-rw-r--r--core/src/net/response/mod.rs8
-rw-r--r--core/src/net/server_status/mod.rs9
-rw-r--r--core/src/net/session_token/mod.rs13
-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
-rw-r--r--test-server.ini7
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
diff --git a/Cargo.toml b/Cargo.toml
index ce99e94..fd4dfb8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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