diff --git a/readme.md b/readme.md index 0299519..3fac625 100644 --- a/readme.md +++ b/readme.md @@ -1,5 +1,23 @@ # WTF how do i implement a game??? +## Short range todo list to keep me going + +[x] Move tokens into `Game` from `GameServer` +[x] Move login into `GameServer` from the web server socket handler (and prevent multiple logins for the same user) +[ ] Add admin settings panel + [ ] Only show to admins + [ ] Adding new players (as in, login stuff, but this will be unimplemented in the actual server until i do the login database or something similar) + [ ] Creating new characters +[ ] Allow creation of characters for admins + +shit that needs to be done with characters handling: + +[ ] Create character: admins only +[ ] view character list (all pc characters i think, or maybe only scene available characters?) +[ ] get character info +[ ] set character info +[ ] assign character to user + ## Features [x] Simple chat diff --git a/src/api/game_actions.rs b/src/api/game_actions.rs index ebcf9b4..af4bdb0 100644 --- a/src/api/game_actions.rs +++ b/src/api/game_actions.rs @@ -20,8 +20,8 @@ pub struct ShowImage { #[derive(Serialize, Deserialize, Clone)] pub struct SpawnToken { pub token_id: usize, - pub x: i32, - pub y: i32, + pub x: f32, + pub y: f32, pub img: String } impl std::fmt::Debug for SpawnToken { diff --git a/src/api/login.rs b/src/api/login.rs index beb702a..be9ed06 100644 --- a/src/api/login.rs +++ b/src/api/login.rs @@ -9,5 +9,6 @@ pub struct LoginRequest { #[derive(Serialize, Clone, Debug)] pub struct LoginResult { pub success: bool, + pub username: String, // TODO: Figure out what the user needs on successful login to reduce traffic } \ No newline at end of file diff --git a/src/api/mod.rs b/src/api/mod.rs index 3e58be9..45a0e5f 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -5,7 +5,7 @@ pub mod map_actions; use game_actions::SpawnToken; use serde::{Deserialize, Serialize}; -use crate::game::{chat_message::ChatMessage, entry::ActionResult}; +use crate::game::{character_sheet::EntryType, chat_message::ChatMessage, entry::ActionResult}; #[derive(Serialize, Deserialize, Default, Debug)] #[serde(rename_all = "snake_case")] @@ -17,17 +17,25 @@ pub enum Request { Quit, Kick(String), Shutdown, + // Character stuff + CreateCharacter, + CharacterDisplay { id: usize }, + CharacterInputs { id: usize }, + CharacterGetField { id: usize, field: String }, + CharacterSetField { id: usize, field: String, val: EntryType }, + CharacterAssign { id: usize, user: String }, // Chat requests Message(ChatMessage), GetChatHistory { amount: usize, from: usize }, GetLastMessages { amount: usize, }, // Map requests GetTokens, - SpawnToken { x: i32, y: i32, img_path: String }, - MoveToken { token_id: usize, x: i32, y: i32 }, + SpawnToken { map_id: usize, character: String, x: f32, y: f32, img_path: String }, + MoveToken { token_id: usize, x: f32, y: f32 }, // Actions requests ActionResult(ActionResult) } + #[derive(Serialize, Clone, Debug)] #[serde(rename_all = "snake_case")] pub enum Response { @@ -35,7 +43,7 @@ pub enum Response { Login(login::LoginResult), Message(ChatMessage), GetChatHistory(Vec), - MoveToken { token_id: usize, x: i32, y: i32 }, + MoveToken { token_id: usize, x: f32, y: f32 }, SpawnToken(SpawnToken), Quit { id: String }, Shutdown, diff --git a/src/game/character_sheet.rs b/src/game/character_sheet.rs index 84170a0..27c5664 100644 --- a/src/game/character_sheet.rs +++ b/src/game/character_sheet.rs @@ -1,5 +1,7 @@ use std::fmt::Debug; +use serde::{Deserialize, Serialize}; + use super::{entry::{ActionDefinition, ActionResult, GameEntry}, chat_message::ChatMessage}; pub trait Character : CharacterSheet { @@ -38,7 +40,7 @@ impl AccessLevel { } } -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize)] pub enum EntryType { Number(i32), Text(String), diff --git a/src/game/mod.rs b/src/game/mod.rs index 984ae7b..3bd0ce5 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -1,36 +1,89 @@ //! Game Parser and Data types -use std::marker::PhantomData; +use std::{collections::HashMap, marker::PhantomData}; use character_sheet::Character; -use serde::Serialize; +use serde::{Deserialize, Serialize}; pub mod character_sheet; pub mod chat_message; pub mod entry; -pub trait GameImpl + Serialize, A: entry::GameEntry + Serialize> { +pub trait GameImpl<'a, C: Character + Serialize + Deserialize<'a>, A: entry::GameEntry + Serialize + Deserialize<'a>> { /// Creates a new game fn new() -> Self; /// Creates a new character, returning the character id fn create_character(&mut self) -> usize; + fn create_token(&mut self, map_id: usize, character: String, img_source: String, x: f32, y: f32) -> usize; + fn move_token(&mut self, map_id: usize, token_id: usize, x: f32, y: f32) -> bool; + fn token_info(&self, map_id: usize, token_id: usize) -> Option<&TokenInfo>; + fn available_tokens(&self) -> impl Iterator; } +#[derive(Serialize, Deserialize)] pub struct Game + Serialize, A: entry::GameEntry + Serialize> { - _c: PhantomData, _a: PhantomData, characters: Vec, + tokens: HashMap, } -impl + Serialize, A: entry::GameEntry + Serialize> GameImpl for Game { +impl<'a, C: Character + Serialize + Deserialize<'a>, A: entry::GameEntry + Serialize + Deserialize<'a>> GameImpl<'a, C, A> for Game { fn new() -> Self { Self { - _c: PhantomData, _a: PhantomData, characters: Vec::new(), + tokens: HashMap::new(), } } - fn create_character(&mut self) -> usize { self.characters.push(C::default()); self.characters.len() - 1 } + fn move_token(&mut self, _map_id: usize, token_id: usize, x: f32, y: f32) -> bool { + if let Some(ti) = self.tokens.get_mut(&token_id) { + ti.x = x; + ti.y = y; + true + } + else { + false + } + } + fn token_info(&self, _map_id: usize, token_id: usize) -> Option<&TokenInfo> { + if let Some(ti) = self.tokens.get(&token_id) { + Some(ti) + } + else { + None + } + } + + fn available_tokens(&self) -> impl Iterator { + self.tokens + .keys() + .into_iter() + .map(|k| *k) // this map feels stupid but keys() turns into a &usize iterator so :shrug: + } + + fn create_token(&mut self, map_id: usize, character: String, img_source: String, x: f32, y: f32) -> usize { + let mut id = 0; + while self.tokens.contains_key(&id) { + id += 1; + } + self.tokens.insert(id, TokenInfo { character, map_id, img_source, x, y }); + id + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct TokenInfo { + /// Which character the token refers to + pub character: String, + /// Which map does the token exists in (allowing multiple tokens in multiple maps) + pub map_id: usize, + /// Token image source, as path relative to the data directory + pub img_source: String, + // x, y are floats to allow 'free movement' + /// X position, in grid slots units (integers are grid aligned) + pub x: f32, + /// Y position, in grid slots units (integers are grid aligned) + pub y: f32, } \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 7d46bd6..300e336 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,8 @@ +use std::collections::HashMap; + use api::game_actions::SpawnToken; use game::{chat_message::ChatMessage, entry::{ActionDefinition, DiceRoll}, Game, GameImpl}; +use serde::{Deserialize, Serialize}; use tokio::sync::{broadcast, mpsc}; pub mod api; @@ -8,16 +11,23 @@ pub mod pathfinder2r_impl; pub mod table; pub mod user; +#[derive(Serialize, Deserialize)] pub struct GameServer { - _game: Game, - tokens: Vec<(String, i32, i32)>, + game: Game, chat: Vec<(String, ChatMessage)>, + #[serde(skip)] // we dont want to save the logged users as it will always be empty when the server restarts + users: HashMap, + // TODO: JEESH REPLACE THIS WITH A DATABASE AND PROPER SECURITY ONCE ITS DONE PLEASE GOD + creds: HashMap, } impl GameServer { pub fn new() -> Self { + let mut creds = HashMap::new(); + creds.insert("rusty".to_string(), String::new()); + creds.insert("artist".to_string(), "artist".to_string()); + creds.insert("dragonfly".to_string(), "cool".to_string()); Self { - _game: Game::new(), - tokens: vec![("assets/pf2r/tokens/louise.jpg".to_string(), 2, 2)], + game: Game::new(), chat: vec![ ( "Server".to_string(), @@ -34,6 +44,8 @@ impl GameServer { .with_action(ActionDefinition::new("Attack -1".to_string())) ) ], + users: HashMap::new(), + creds, } } @@ -48,9 +60,18 @@ impl GameServer { println!("Got message from {}: {:?}", &id, &req); match req { - // ignore errors and re-login requests + // ignore errors, should probably be blocked before they are sent here api::Request::Error => {} - api::Request::Login(_) => {} + api::Request::Login(login) => { + println!("login req from {}: {:?}", &id, &login); + if !self.users.contains_key(&login.username) && self.creds.get(&login.username).map(|p| p == &login.password).unwrap_or(false) { + self.users.insert(login.username.clone(), login.username == "rusty"); // rusty will be admin for now :) + _ = broadcast.send((Some(id), api::Response::Login(api::login::LoginResult { success: true, username: login.username }))); + } + else { + _ = broadcast.send((Some(id.clone()), api::Response::Login(api::login::LoginResult { success: false, username: login.username }))); + } + } api::Request::Message(mut msg) => { if msg.id == 0 || msg.id > self.chat.len() { msg.id = self.chat.len() + 1; // set the message id, 0 is invalid @@ -101,24 +122,24 @@ impl GameServer { _ = broadcast.send((Some(id), api::Response::GetChatHistory(history))); }, api::Request::GetTokens => { - for (i, (path, x, y)) in self.tokens.iter().enumerate() { - let bits = std::fs::read(path).expect("FAILED READING TOKEN IMAGE"); - let img = base64::Engine::encode(&base64::prelude::BASE64_STANDARD, &bits); - _ = broadcast.send((Some(id.clone()), api::Response::SpawnToken(SpawnToken { token_id: i, x: *x, y: *y, img }))); + for token_id in self.game.available_tokens() { + if let Some(ti) = self.game.token_info(0, token_id) { + let bits = std::fs::read(&ti.img_source).expect("FAILED READING TOKEN IMAGE"); + let img = base64::Engine::encode(&base64::prelude::BASE64_STANDARD, &bits); + _ = broadcast.send((Some(id.clone()), api::Response::SpawnToken(SpawnToken { token_id: token_id, x: ti.x, y: ti.y, img }))); + } } }, - api::Request::SpawnToken { x, y, img_path } => { - let token_id = self.tokens.len(); - self.tokens.push((img_path.clone(), x, y)); - let bits = std::fs::read(img_path).expect("FAILED READING TOKEN IMAGE"); + api::Request::SpawnToken { map_id, character, x, y, img_path } => { + let token_id = self.game.create_token(map_id, character, img_path.clone(), x, y); + let bits = std::fs::read(&img_path).expect("FAILED READING TOKEN IMAGE"); let img = base64::Engine::encode(&base64::prelude::BASE64_STANDARD, &bits); _ = broadcast.send((Some(id.clone()), api::Response::SpawnToken(SpawnToken { token_id, x, y, img }))); }, api::Request::MoveToken { token_id, x, y } => { // TODO: add check to make sure the actor is authorized to move the token - if token_id < self.tokens.len() { - self.tokens[token_id].1 = x; - self.tokens[token_id].2 = y; + if self.game.move_token(0, token_id, x, y) { + // TODO: maybe chage move_token to return optional x,y values if succeeded to make sure the token is where it was going to be _ = broadcast.send((None, api::Response::MoveToken { token_id, x, y })); } }, @@ -135,10 +156,32 @@ impl GameServer { self.chat.push((id, msg.clone())); _ = broadcast.send((None, api::Response::Message(msg))); + }, + api::Request::CreateCharacter => { + // check if user is admin + if self.users.get(&id).map(|a| *a).unwrap_or(false) { + let new_id = self.game.create_character(); + // return the new id with the character i think + } + }, + api::Request::Quit => { + if self.users.contains_key(&id) { + self.users.remove(&id); + } + _ = broadcast.send((None, api::Response::Quit { id })); + }, + api::Request::Kick(id) => { + if self.users.contains_key(&id) { + self.users.remove(&id); + } + _ = broadcast.send((Some(id), api::Response::Shutdown)); } - api::Request::Quit => _ = broadcast.send((None, api::Response::Quit { id })), - api::Request::Kick(id) => _ = broadcast.send((Some(id), api::Response::Shutdown)), api::Request::Shutdown => break, + api::Request::CharacterDisplay { id } => todo!(), + api::Request::CharacterInputs { id } => todo!(), + api::Request::CharacterGetField { id, field } => todo!(), + api::Request::CharacterSetField { id, field, val } => todo!(), + api::Request::CharacterAssign { id, user } => todo!(), } } _ = broadcast.send((None, api::Response::Shutdown)); diff --git a/src/main.rs b/src/main.rs index ff5f1f9..0149d09 100644 --- a/src/main.rs +++ b/src/main.rs @@ -64,46 +64,65 @@ async fn socket_sender(id: String, mut send: SplitSink, brecv: broadcast::Receiver<(Option, Response)>) { +async fn handle_socket(mut socket: ws::WebSocket, msend: mpsc::Sender<(String, Request)>, mut brecv: broadcast::Receiver<(Option, Response)>) { let mut id: Option = None; + // this is a temp id, and as long as 2 people dont try to connect to the socket at the same milisecond and to the same user it would be fine (i hope) + let temp_id = format!("temp_id_{}", std::time::SystemTime::now().duration_since(std::time::SystemTime::UNIX_EPOCH).map(|t| t.as_micros()).unwrap_or(0)); + let mut last_login_req = std::time::Instant::now(); loop { - if let Some(msg) = socket.recv().await { - if let Ok(msg) = msg { - match msg { - Message::Text(t) => { - let req = serde_json::from_str::(&t).unwrap_or_default(); - println!("Got unauthorized message: {:?}", t); - match req { - // TODO: Actual signing in mechanism with multiple ids :) - Request::Login(r) => if r.username == "rusty" || r.username == "honey" { - _ = socket.send(Message::Text( - serde_json::to_string(&Response::Login(open_tavern::api::login::LoginResult { success: true })).unwrap() - )).await; - id = Some(String::from(r.username)); - break; - } - else { - _ = socket.send(Message::Text( - serde_json::to_string(&Response::Login(open_tavern::api::login::LoginResult { success: false })).unwrap() - )).await; - }, - _ => { - _ = socket.send(Message::Text(serde_json::to_string(&Response::Error(RequestError::InvalidRequest)).unwrap())).await; - }, + tokio::select! { + b = brecv.recv() => { + println!("{} trying to log in: {:?}", &temp_id, &b); + if let Ok((to_id, msg)) = b { + if let Response::Login(open_tavern::api::login::LoginResult { success, username }) = msg.clone() { + let to_id = to_id.map(|ti| ti == temp_id).unwrap_or(false); + if to_id && success && id.as_ref().map(|id| id == &username).unwrap_or(false) { + _ = socket.send(Message::Text(serde_json::to_string(&msg).unwrap_or_default())).await; + break; + } + else { + id = None; + _ = socket.send(Message::Text(serde_json::to_string(&msg).unwrap_or_default())).await; } } - ws::Message::Binary(_) => todo!(), - ws::Message::Ping(_) => todo!(), - ws::Message::Pong(_) => todo!(), - ws::Message::Close(_) => break, - }; + } + }, + msg = socket.recv() => { + if let Some(msg) = msg.map(|m| m.ok()).flatten() { + match msg { + Message::Text(t) => { + let req = serde_json::from_str::(&t).unwrap_or_default(); + println!("{} trying to log in incoming: {:?}", &temp_id, t); + match req { + // TODO: Actual signing in mechanism with multiple ids :) + Request::Login(r) => { + // allow 1 login attempt every 3 seconds + if last_login_req.elapsed() > std::time::Duration::from_secs(1) { + last_login_req = std::time::Instant::now(); + id = Some(r.username.clone()); + _ = msend.send((temp_id.clone(), Request::Login(r))).await; + } + else { + println!("Failed too early"); + } + }, + _ => { + _ = socket.send(Message::Text(serde_json::to_string(&Response::Error(RequestError::InvalidRequest)).unwrap())).await; + }, + } + } + ws::Message::Close(_) => { + id = None; + break; + }, + _ => {}, + }; + } } - } - else { - break; - } + }; } if let Some(id) = id { + // maybe i should not spawn 2 different routines and just have 1 routine that uses tokio::select! println!("Got id for socket: {}", &id); let (send, recv) = socket.split(); tokio::spawn(socket_receiver(recv, msend, id.clone())); diff --git a/src/pathfinder2r_impl/mod.rs b/src/pathfinder2r_impl/mod.rs index 3a54241..ef60276 100644 --- a/src/pathfinder2r_impl/mod.rs +++ b/src/pathfinder2r_impl/mod.rs @@ -1,11 +1,11 @@ use crate::game::{character_sheet::*, chat_message::ChatMessage, entry::{ActionDefinition, ActionResult, DiceRoll, GameEntry}}; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use tavern_macros::CharacterSheet; pub mod entry; use entry::{Entry, Weapon}; -#[derive(Default, CharacterSheet, Serialize)] +#[derive(Default, CharacterSheet, Serialize, Deserialize)] pub struct Pathfinder2rCharacterSheet { // Genral stuff #[Input("Name")]