diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/_if.rs | 20 | ||||
-rw-r--r-- | src/_match.rs | 52 | ||||
-rw-r--r-- | src/bin/game.rs | 5 | ||||
-rw-r--r-- | src/bin/server.rs | 6 | ||||
-rw-r--r-- | src/config.rs | 41 | ||||
-rw-r--r-- | src/data.rs | 17 | ||||
-rw-r--r-- | src/lib.rs | 20 | ||||
-rw-r--r-- | src/lobby.rs | 136 | ||||
-rw-r--r-- | src/main.rs | 42 | ||||
-rw-r--r-- | src/map.rs | 29 | ||||
-rw-r--r-- | src/mapstore.rs | 17 | ||||
-rw-r--r-- | src/server.rs | 45 | ||||
-rw-r--r-- | src/users.rs | 10 | ||||
-rw-r--r-- | src/wire/mod.rs | 23 | ||||
-rw-r--r-- | src/wire/resp.rs | 17 |
15 files changed, 362 insertions, 118 deletions
diff --git a/src/_if.rs b/src/_if.rs index 91cb3a2daf18..59d80fb165af 100644 --- a/src/_if.rs +++ b/src/_if.rs @@ -1,11 +1,11 @@ //! A common trait interface between the server and the client use crate::wire::{ - Action, AuthErr, Lobby, LobbyId, MatchErr, MatchId, RegErr, RoomErr, RoomUpdate, UpdateState, + Action, AuthErr, Lobby, LobbyErr, LobbyId, LobbyUpdate, MatchErr, MatchId, RegErr, UpdateState, User, UserId, }; -use async_trait::async_trait; use async_std::sync::Arc; +use async_trait::async_trait; use chrono::{DateTime, Utc}; /// The main game interface implemented by the server and client @@ -24,19 +24,25 @@ pub trait GameIf { async fn anonymous(self: Arc<Self>, name: String) -> Result<User, AuthErr>; /// Join a match-making lobby - async fn join(self: Arc<Self>, user: User, lobby: LobbyId) -> Result<Lobby, RoomErr>; + async fn join(self: Arc<Self>, user: User, lobby: LobbyId) -> Result<Lobby, LobbyErr>; /// Leave a match-making lobby - async fn leave(self: Arc<Self>, user: User, lobby: LobbyId) -> Result<(), RoomErr>; + async fn leave(self: Arc<Self>, user: User, lobby: LobbyId) -> Result<(), LobbyErr>; /// Set the player's ready state - async fn ready(self: Arc<Self>, user: User, lobby: LobbyId, ready: bool) -> RoomUpdate; + async fn ready( + self: Arc<Self>, + user: User, + lobby: LobbyId, + ready: bool, + ) -> Result<LobbyUpdate, LobbyErr>; /// Send a start request (as lobby admin) - async fn start_req(self: Arc<Self>, user: User, lobby: LobbyId) -> DateTime<Utc>; + async fn start_req(self: Arc<Self>, user: UserId, lobby: LobbyId) -> Result<DateTime<Utc>, LobbyErr>; /// Perform a game action as a user - async fn perform_action(self: Arc<Self>, user: User, mtch: MatchId, act: Action) -> UpdateState; + async fn perform_action(self: Arc<Self>, user: User, mtch: MatchId, act: Action) + -> UpdateState; /// Leave a match async fn leave_match(self: Arc<Self>, user: User, mtch: MatchId) -> Result<(), MatchErr>; diff --git a/src/_match.rs b/src/_match.rs new file mode 100644 index 000000000000..23d3be8aeb0d --- /dev/null +++ b/src/_match.rs @@ -0,0 +1,52 @@ +use crate::{ + data::Player, + lobby::MetaLobby, + map::Map, + wire::{LobbyUser, MatchId, UserId}, +}; +use async_std::sync::Arc; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +/// Describes a match for the server +pub struct Match { + /// The match id + pub id: MatchId, + /// The list of active players + pub players: Vec<Player>, + /// The active game map + pub map: Map, + /// The time the match was initialised + pub init_t: DateTime<Utc>, + /// The synced time the match was started + pub start_t: Option<DateTime<Utc>>, +} + +impl From<MetaLobby> for Match { + fn from(ml: MetaLobby) -> Self { + Self { + id: ml.inner.id, + players: ml + .inner + .players + .into_iter() + .map(|lu| Player { + id: lu.id, + name: lu.name, + color: lu.color, + money: 0.into(), + }) + .collect(), + map: Map::new(), + init_t: Utc::now(), + start_t: None, + } + } +} + +impl Match { + /// Set the start time of the match, which may be in the future + pub fn set_start(&mut self, t: DateTime<Utc>) { + self.start_t = Some(t); + } +} diff --git a/src/bin/game.rs b/src/bin/game.rs new file mode 100644 index 000000000000..9bf9fc9f9631 --- /dev/null +++ b/src/bin/game.rs @@ -0,0 +1,5 @@ +//! The main game UI client + +fn main() { + +} diff --git a/src/bin/server.rs b/src/bin/server.rs new file mode 100644 index 000000000000..47db85926ac1 --- /dev/null +++ b/src/bin/server.rs @@ -0,0 +1,6 @@ +//! The dedicated server binary + + +fn main() { + +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 000000000000..8a1b0b4deb0a --- /dev/null +++ b/src/config.rs @@ -0,0 +1,41 @@ +//! The file formats backing maps and other configs + +use crate::data::{NodeId, LinkId}; +use serde::{Serialize, Deserialize}; +use std::path::Path; + +/// A config tree that describes a map +#[derive(Serialize, Deserialize)] +pub struct MapCfg { + /// The set of nodes + pub nodes: Vec<NodeCfg>, + /// Links connecting nodes + pub links: Vec<LinkCfg>, + /// Default spawn points (player count) + pub spawns: Vec<SpawnCfg>, +} + +#[derive(Serialize, Deserialize)] +pub struct NodeCfg { + /// Node ID + pub id: NodeId, + /// Render/world position + pub x: f64, + pub y: f64, +} + +#[derive(Serialize, Deserialize)] +pub struct LinkCfg { + /// The link ID + id: LinkId, + /// List of connectioned nodes + con: Vec<NodeId>, +} + +#[derive(Serialize, Deserialize)] +pub struct SpawnCfg { + /// The node of the spawn point + pub n_id: NodeId, + /// At what number of players is this spawn available? + pub max_players: usize, +} diff --git a/src/data.rs b/src/data.rs index 4cfe0c09e463..940bda10e7e8 100644 --- a/src/data.rs +++ b/src/data.rs @@ -78,13 +78,13 @@ pub type PlayerId = usize; #[derive(Serialize, Deserialize)] pub struct Player { /// A unique player ID (per match) - id: PlayerId, + pub id: PlayerId, /// The player name - name: String, + pub name: String, /// Player color - color: Color, + pub color: Color, /// The player's money - money: AtomicU16, + pub money: AtomicU16, } /// Optionally, players can create teams @@ -144,7 +144,7 @@ pub trait ColorPalette { /// Create a new color palette fn palette() -> Self; /// Get a palette without a certain colour - fn without(b: &Color) -> Self; + fn without(&mut self, b: &Color); /// Mix a color back into the available palette fn remix(&mut self, new: Color); } @@ -167,8 +167,11 @@ impl ColorPalette for Vec<Color> { pal } - fn without(b: &Color) -> Self { - Self::palette().into_iter().filter(|a| a == b).collect() + /// Drop a colour from the palette + fn without(&mut self, b: &Color) { + if let Some(pos) = self.into_iter().rposition(|a| a == b) { + self.remove(pos); + } } fn remix(&mut self, new: Color) { diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 000000000000..d6e3f246a08d --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,20 @@ +#![allow(warnings)] + +mod _if; +pub use _if::GameIf; + +pub mod data; +pub mod gens; + +mod _match; +mod config; +mod io; +mod lobby; +mod map; +mod mapstore; +mod server; +mod stats; +mod users; +mod wire; + +pub use identity::Identity as Id; diff --git a/src/lobby.rs b/src/lobby.rs index ac6ab305414a..496f98bd7b6f 100644 --- a/src/lobby.rs +++ b/src/lobby.rs @@ -3,7 +3,7 @@ use crate::{ data::{Color, ColorPalette}, users::MetaUser, - wire::{Lobby, LobbyId, LobbyUser}, + wire::{Lobby, LobbyErr, LobbyId, LobbyUpdate, LobbyUser, User, UserId}, }; use async_std::sync::{Arc, RwLock}; use std::{ @@ -14,15 +14,60 @@ use std::{ /// A list of all the lobbies on the server pub struct LobbyList { max: AtomicUsize, - users: RwLock<BTreeMap<LobbyId, Arc<MetaLobby>>>, + lobbies: RwLock<BTreeMap<LobbyId, MetaLobby>>, +} + +impl LobbyList { + pub fn new() -> Self { + Self { + max: 0.into(), + lobbies: Default::default(), + } + } + + /// Create a new lobby + pub async fn create(&self, map: String) -> LobbyId { + let id = self.max.fetch_add(1, Ordering::Relaxed); + self.lobbies + .write() + .await + .insert(id, MetaLobby::create(id, map)); + id + } + + /// Remove a lobby by ID + pub async fn destroy(&self, id: LobbyId) -> Result<(), LobbyErr> { + self.consume(id).await.map(|_| ()) + } + + /// Remove and return the lobby + pub async fn consume(&self, id: LobbyId) -> Result<MetaLobby, LobbyErr> { + self.lobbies + .write() + .await + .remove(&id) + .map_or(Err(LobbyErr::NoSuchRoom), |l| Ok(l)) + } + + /// Get mutable access to a lobby + pub async fn get_mut<F, T>(&self, id: LobbyId, cb: F) -> Result<T, LobbyErr> + where + F: Fn(&mut MetaLobby) -> T, + { + self.lobbies + .write() + .await + .get_mut(&id) + .map_or(Err(LobbyErr::OtherError), |ref mut l| Ok(cb(l))) + } } /// Additional state held by the server /// /// The meta lobby will also sync updates to all connected users, when updates are made to the lobby pub struct MetaLobby { - palette: Vec<Color>, - inner: Lobby, + pub palette: Vec<Color>, + pub inner: Lobby, } impl MetaLobby { @@ -38,21 +83,19 @@ impl MetaLobby { } } - pub fn join(&mut self, user: MetaUser) { + pub fn join(&mut self, user: &MetaUser) -> Lobby { let color = if &user.name == "spacekookie" { let color = Color::blue(); + self.palette.without(&color); - let num = self.inner.players.len(); - self.palette = Vec::without(&color); - let in_use = self.palette.split_off(num); - - self.inner + if let Some(user) = self + .inner .players .iter_mut() - .zip(in_use.into_iter()) - .for_each(|(user, color)| { - user.color = color; - }); + .find(|u| u.color == Color::blue()) + { + user.color = self.palette.remove(0); + } color } else { @@ -60,14 +103,17 @@ impl MetaLobby { }; self.inner.players.push(LobbyUser { + admin: false, id: user.id, - name: user.name, + name: user.name.clone(), ready: false, color, }); + + self.inner.clone() } - pub fn leave(&mut self, user: MetaUser) { + pub fn leave(&mut self, user: &MetaUser) { let (pos, user) = self .inner .players @@ -84,4 +130,62 @@ impl MetaLobby { self.palette.remix(user.color); self.inner.players.remove(pos); } + + /// Check if a user is even present in a lobby + /// + /// Perform this prerequisite check before making other user-specific changes to the lobby + pub fn in_lobby(&self, user: UserId) -> bool { + self.inner + .players + .iter() + .find(|u| u.id == user) + .map(|_| true) + .unwrap_or(false) + } + + /// Set the ready state for a user + pub fn ready(&mut self, user: User, ready: bool) -> LobbyUpdate { + if let Some(user) = self + .inner + .players + .iter_mut() + .find(|u| u.id == user.id) + .as_mut() + { + user.ready = ready; + } + + LobbyUpdate::Ready( + self.inner + .players + .iter() + .filter_map(|u| if u.ready { Some(u.id) } else { None }) + .collect(), + ) + } + + /// Try to start a game, if the user can and everybody is ready + pub fn start(&mut self, user: UserId) -> Result<(), LobbyErr> { + if let Some(_) = self + .inner + .players + .iter() + .filter(|u| u.admin) + .find(|u| u.id == user) + { + return Err(LobbyErr::NotAuthorized); + }; + + match self + .inner + .players + .iter() + .filter(|u| !u.ready) + .collect::<Vec<_>>() + .len() + { + 0 => Err(LobbyErr::NotAllReady), + _ => Ok(()), + } + } } diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 976a99af2895..000000000000 --- a/src/main.rs +++ /dev/null @@ -1,42 +0,0 @@ -#![allow(warnings)] - -mod _if; -mod data; -mod gens; -mod io; -mod lobby; -mod map; -mod server; -mod stats; -mod users; -mod wire; - -pub(crate) use identity::Identity as Id; - -use ggez::{ - self, conf, - event::{self, EventHandler}, - Context, ContextBuilder, GameResult, -}; - -struct GameState; - -impl EventHandler for GameState { - fn update(&mut self, _: &mut Context) -> GameResult { - Ok(()) - } - - fn draw(&mut self, _: &mut Context) -> GameResult { - Ok(()) - } -} - -fn main() -> GameResult { - let cb = ContextBuilder::new("RstNode", "Katharina Fey") - .window_setup(conf::WindowSetup::default().title("RstNode").vsync(true)) - .window_mode(conf::WindowMode::default().dimensions(800.0, 600.0)); - - let (ctx, el) = &mut cb.build()?; - let mut game = GameState; // smash the state - event::run(ctx, el, &mut game) -} diff --git a/src/map.rs b/src/map.rs index f977bec93e1d..212ce75100ad 100644 --- a/src/map.rs +++ b/src/map.rs @@ -1,31 +1,42 @@ //! Implements a map graph and world logic use crate::{ - data::{Link, Node}, - server::{ServerResult, ServerErr}, - wire::Response + config::{LinkCfg, MapCfg, NodeCfg}, + data::{Link, Node, NodeId}, + server::{ServerErr, ServerResult}, + wire::Response, }; use async_std::sync::Arc; -use serde::{Deserialize, Serialize}; +use quadtree_rs::Quadtree; use std::collections::BTreeMap; +pub struct MapNode { + pos: (f64, f64), + inner: Node, +} + /// A map that people fight on /// /// A map is defined by it's graph relationships, but also where on /// the map nodes are placed, how much spacing there is, etc. All /// this information is encoded in the same structs because it's /// static, and just more convenient. -#[derive(Default, Serialize, Deserialize)] pub struct Map { - /// Node IDs mapped to node objects - nodes: BTreeMap<u16, Arc<Node>>, + /// Node IDs mapped to coordinates + nodes: BTreeMap<NodeId, (i64, i64)>, /// Link IDs mapped to link objects links: BTreeMap<u16, Arc<Link>>, + /// A coordinate map for the network + coord: Quadtree<i64, Arc<MapNode>>, } impl Map { - pub fn new() -> Arc<Self> { - Arc::new(Self::default()) + pub fn new() -> Self { + Self { + nodes: BTreeMap::new(), + links: BTreeMap::new(), + coord: Quadtree::new(2), + } } pub fn update<F>(&mut self, cb: F) -> ServerResult<Response> diff --git a/src/mapstore.rs b/src/mapstore.rs new file mode 100644 index 000000000000..85c5e36ef93b --- /dev/null +++ b/src/mapstore.rs @@ -0,0 +1,17 @@ +//! Map store + +use crate::config::MapCfg; +use std::{collections::BTreeMap, fs, path::Path}; + +pub struct MapStore { + configs: BTreeMap<String, MapCfg>, +} + +impl MapStore { + /// Load a set of map configs + pub fn load_path(&mut self, path: &Path) { + fs::read_dir(&path).unwrap().for_each(|d| { + let name = d.unwrap().file_name().into_string().unwrap(); + }); + } +} diff --git a/src/server.rs b/src/server.rs index 6ddfb70a9d41..7a2fad27bb1f 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,10 +1,13 @@ use crate::{ _if::GameIf, + _match::Match, + data::Player, + lobby::LobbyList, map::Map, users::UserStore, wire::{ - Action, AuthErr, Lobby, LobbyId, Match, MatchErr, MatchId, MatchUser, RegErr, Response, - RoomErr, RoomUpdate, UpdateState, User, UserId, + Action, AuthErr, Lobby, LobbyErr, LobbyId, LobbyUpdate, MatchErr, MatchId, RegErr, + Response, UpdateState, User, UserId, }, }; use async_std::sync::{Arc, Mutex, RwLock}; @@ -27,6 +30,7 @@ pub enum ServerErr { pub struct Server { matches: BTreeMap<MatchId, Mutex<Match>>, users: UserStore, + lobbies: LobbyList, } impl Server { @@ -35,6 +39,7 @@ impl Server { Self { matches: Default::default(), users: UserStore::new(), + lobbies: LobbyList::new(), } } @@ -63,14 +68,14 @@ impl Server { F: Fn(&mut Map) -> ServerResult<Response>, { match self.matches.get(&id) { - Some(ref mut m) => m.lock().await.map.update(cb), + Some(ref m) => m.lock().await.map.update(cb), None => Err(ServerErr::NoSuchMatch), } } pub async fn update_players<F>(self: Arc<Self>, id: MatchId, cb: F) -> ServerResult<Response> where - F: Fn(&mut Vec<MatchUser>) -> ServerResult<Response>, + F: Fn(&mut Vec<Player>) -> ServerResult<Response>, { match self.matches.get(&id) { Some(ref mut m) => cb(&mut m.lock().await.players), @@ -98,20 +103,36 @@ impl GameIf for Server { Ok(auth) } - async fn join(self: Arc<Self>, user: User, lobby: LobbyId) -> Result<Lobby, RoomErr> { - unimplemented!() + async fn join(self: Arc<Self>, user: User, lobby: LobbyId) -> Result<Lobby, LobbyErr> { + let mu = self.users.get(&user).await?; + self.lobbies.get_mut(lobby, |mut l| l.join(&mu)).await } - async fn leave(self: Arc<Self>, user: User, lobby: LobbyId) -> Result<(), RoomErr> { - unimplemented!() + async fn leave(self: Arc<Self>, user: User, lobby: LobbyId) -> Result<(), LobbyErr> { + let mu = self.users.get(&user).await?; + self.lobbies.get_mut(lobby, |mut l| l.leave(&mu)).await } - async fn ready(self: Arc<Self>, user: User, lobby: LobbyId, ready: bool) -> RoomUpdate { - unimplemented!() + async fn ready( + self: Arc<Self>, + user: User, + lobby: LobbyId, + ready: bool, + ) -> Result<LobbyUpdate, LobbyErr> { + self.lobbies + .get_mut(lobby, |mut l| l.ready(user, ready)) + .await } - async fn start_req(self: Arc<Self>, user: User, lobby: LobbyId) -> DateTime<Utc> { - unimplemented!() + /// A start request was received + async fn start_req( + self: Arc<Self>, + user: UserId, + lobby: LobbyId, + ) -> Result<DateTime<Utc>, LobbyErr> { + self.lobbies.get_mut(lobby, |mut l| l.start(user)).await?; + let lob = self.lobbies.consume(lobby).await?; + Ok(Utc::now()) } async fn perform_action( diff --git a/src/users.rs b/src/users.rs index edc3c13fe3b1..0c93b83ec1da 100644 --- a/src/users.rs +++ b/src/users.rs @@ -1,7 +1,7 @@ //! A users abstraction module use crate::{ - wire::{User, UserId}, + wire::{LobbyErr, User, UserId}, Id, }; use async_std::sync::{Arc, RwLock}; @@ -31,6 +31,14 @@ impl UserStore { } } + /// Get the metadata user for a login user + pub async fn get(&self, user: &User) -> Result<Arc<MetaUser>, LobbyErr> { + match self.users.read().await.get(&user.id) { + Some(ref u) => Ok(Arc::clone(u)), + None => Err(LobbyErr::OtherError), + } + } + pub async fn add<S: Into<Option<String>>>( &self, name: String, diff --git a/src/wire/mod.rs b/src/wire/mod.rs index bf8dea0e8131..b1c231b44580 100644 --- a/src/wire/mod.rs +++ b/src/wire/mod.rs @@ -23,7 +23,7 @@ use serde::{Deserialize, Serialize}; pub type UserId = usize; /// Represents a user payload -#[derive(Clone, Serialize, Deserialize)] +#[derive(Copy, Clone, Serialize, Deserialize)] pub struct User { /// The internal user ID pub id: UserId, @@ -34,7 +34,7 @@ pub struct User { } /// A more lobby specific abstraction for a user -#[derive(Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] pub struct LobbyUser { /// The user ID pub id: UserId, @@ -42,6 +42,8 @@ pub struct LobbyUser { pub name: String, /// Are they ready? pub ready: bool, + /// Are they the lobby admin? + pub admin: bool, /// The colour they will be in the match pub color: Color, } @@ -50,7 +52,7 @@ pub struct LobbyUser { pub type LobbyId = usize; /// Represent a lobby -#[derive(Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] pub struct Lobby { /// The ID of the lobby pub id: LobbyId, @@ -65,19 +67,4 @@ pub struct Lobby { /// An alias for a match ID pub type MatchId = usize; -/// Mapping users to a player in game -#[derive(Serialize, Deserialize)] -pub struct MatchUser { - pub user: User, - pub player: Player, -} -#[derive(Serialize, Deserialize)] -pub struct Match { - /// The match id - pub id: MatchId, - /// The list of active players - pub players: Vec<MatchUser>, - /// The active game map - pub map: Map, -} diff --git a/src/wire/resp.rs b/src/wire/resp.rs index b9f41fbe93e4..4812baf13e39 100644 --- a/src/wire/resp.rs +++ b/src/wire/resp.rs @@ -16,18 +16,17 @@ pub enum Response { /// Get a list of available <name-room> pairs Rooms(Vec<(String, LobbyId)>), /// A user joins a game lobby - Join(Result<Lobby, RoomErr>), + Join(Result<Lobby, LobbyErr>), /// A user leaves a game lobby - Leave(Result<(), RoomErr>), + Leave(Result<(), LobbyErr>), /// Get the new set of ready states - Ready(RoomUpdate), + Ready(LobbyUpdate), /// Receiving a start request time StartReq(DateTime<Utc>), /// Response to the action with the state update GameUpdate(UpdateState), /// Leave the match (forfeit) LeaveGame(Result<(), MatchErr>), - } #[derive(Serialize, Deserialize)] @@ -53,17 +52,23 @@ pub enum AuthErr { } #[derive(Serialize, Deserialize)] -pub enum RoomErr { +pub enum LobbyErr { /// The requested room is already full RoomFull, /// The room id is unknown NoSuchRoom, /// Previously not in room NotInRoom, + /// Not everybody was ready + NotAllReady, + /// A request was sent by someone who isn't authorised + NotAuthorized, + /// Other internal error, try again? + OtherError, } #[derive(Serialize, Deserialize)] -pub enum RoomUpdate { +pub enum LobbyUpdate { /// The set of ready users Ready(Vec<UserId>), } |