update deps, fix stuff and add some comments

This commit is contained in:
Rusty Striker 2025-06-14 19:53:23 +03:00
parent dbeda509fc
commit 3cfabcc39f
Signed by: RustyStriker
GPG key ID: 87E4D691632DFF15
5 changed files with 477 additions and 466 deletions

View file

@ -1,7 +1,11 @@
use std::collections::HashMap;
use api::game_actions::SpawnToken;
use game::{chat_message::ChatMessage, entry::{ActionDefinition, DiceRoll}, Game, GameImpl};
use game::{
Game, GameImpl,
chat_message::ChatMessage,
entry::{ActionDefinition, DiceRoll},
};
use serde::{Deserialize, Serialize};
use tokio::sync::{broadcast, mpsc};
@ -14,35 +18,39 @@ pub mod user;
pub struct GameServer {
game: Game<pathfinder2r_impl::Pathfinder2rCharacterSheet, pathfinder2r_impl::entry::Entry>,
chat: Vec<(String, ChatMessage)>,
#[serde(skip)] // we dont want to save the logged users as it will always be empty when the server restarts
// we dont want to save the logged users as it will always be empty when the server restarts
#[serde(skip)]
/// Logged in users, bool value used to know if the user is an admin or not
users: HashMap<String, bool>,
// TODO: JEESH REPLACE THIS WITH A DATABASE AND PROPER SECURITY ONCE ITS DONE PLEASE GOD
// TODO: JEESH REPLACE THIS WITH A DATABASE AND PROPER SECURITY ONCE ITS DONE PLEASE GOD
creds: HashMap<String, String>,
}
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());
creds.insert("rusty".to_string(), "rusty".to_string());
creds.insert("test".to_string(), "test".to_string());
creds.insert("dragonfly".to_string(), "dragonfly".to_string());
Self {
game: Game::new(),
chat: vec![
(
"Server".to_string(),
ChatMessage::new("a weapon description".to_string())
.id(1)
.character(Some("Sword or something".to_string()))
.with_action(
ActionDefinition::new("weapon/attack".to_string())
chat: vec![(
"Server".to_string(),
ChatMessage::new("a weapon description".to_string())
.id(1)
.character(Some("Sword or something".to_string()))
.with_action(
ActionDefinition::new("weapon/attack".to_string())
.display_name(Some("Attack +7".to_string()))
.with_roll(DiceRoll::new("Pierce".to_string(), 12, 1).constant(1))
.with_roll(DiceRoll::new("Fire".to_string(), 4, 2))
)
.with_action(ActionDefinition::new("Attack +3".to_string()).with_roll(DiceRoll::new("Base".to_string(), 20, 1)))
.with_action(ActionDefinition::new("Attack -1".to_string()))
)
],
.with_roll(DiceRoll::new("Fire".to_string(), 4, 2)),
)
.with_action(ActionDefinition::new("Attack +3".to_string()).with_roll(DiceRoll::new(
"Base".to_string(),
20,
1,
)))
.with_action(ActionDefinition::new("Attack -1".to_string())),
)],
users: HashMap::new(),
creds,
}
@ -63,20 +71,34 @@ impl GameServer {
api::Request::Error => {}
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) {
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 })));
_ = 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
}
// TODO: check if the editor is an admin as well
else if id == self.chat[msg.id - 1].0 {
} else if id == self.chat[msg.id - 1].0 || *self.users.get(&id).unwrap_or(&false) {
self.chat[msg.id - 1] = (id.clone(), msg.clone());
} else {
// if its an edit message and editor is not the owner, skip
@ -116,46 +138,65 @@ impl GameServer {
} else {
self.chat.len() - amount
};
let history: Vec<ChatMessage> =
self.chat.iter().skip(start).map(|m| m.1.clone()).collect();
let history: Vec<ChatMessage> = self.chat.iter().skip(start).map(|m| m.1.clone()).collect();
_ = broadcast.send((Some(id), api::Response::GetChatHistory(history)));
},
}
api::Request::GetTokens { scene } => {
for token_id in self.game.available_tokens(scene) {
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 })));
_ = broadcast.send((
Some(id.clone()),
api::Response::SpawnToken(SpawnToken {
token_id: token_id,
x: ti.x,
y: ti.y,
img,
}),
));
}
}
},
api::Request::SpawnToken { map_id, character, x, y, img_path } => {
}
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 })));
},
_ = 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 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 }));
}
},
}
api::Request::ActionResult(result) => {
let msg = ChatMessage::new(
result.results.iter()
result
.results
.iter()
// .map(|d| &d.result_text)
.fold(String::new(), |a,b| a + &format!("{}: {} = {}\n", &b.name, &b.result_text, b.result))
)
.character(Some(result.name))
.source(id.clone())
.targets(Some(result.targets))
.id(self.chat.len() + 1);
.fold(String::new(), |a, b| {
a + &format!("{}: {} = {}\n", &b.name, &b.result_text, b.result)
}),
)
.character(Some(result.name))
.source(id.clone())
.targets(Some(result.targets))
.id(self.chat.len() + 1);
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) {
@ -163,13 +204,13 @@ impl GameServer {
// return the new id with the character i think
_ = broadcast.send((Some(id), api::Response::CharacterCreated(new_id)));
}
},
}
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);

View file

@ -1,150 +1,192 @@
use axum::{
extract::ws::{self,Message}, response, routing, Router
Router,
extract::ws::{self, Message},
response, routing,
};
use futures_util::{
SinkExt, StreamExt,
stream::{SplitSink, SplitStream},
};
use futures_util::{stream::{SplitSink, SplitStream}, SinkExt, StreamExt};
use open_tavern::api::{Request, RequestError, Response};
use tokio::sync::{broadcast, mpsc};
#[tokio::main]
async fn main() {
let (bsend, _) = broadcast::channel(10);
let (msend, mrecv) = mpsc::channel(50);
let bsend2 = bsend.clone();
let app = Router::new()
.route("/", routing::get(root))
.route("/tavern.js", routing::get(socket))
.route("/app.js", routing::get(app_js))
.route("/style.css", routing::get(style))
.route("/ws", routing::get(move |w| ws_handler(w, msend, bsend2.clone().subscribe())));
let (bsend, _) = broadcast::channel(10);
let (msend, mrecv) = mpsc::channel(50);
let bsend2 = bsend.clone();
let app = Router::new()
.route("/", routing::get(root))
.route("/tavern.js", routing::get(socket))
.route("/app.js", routing::get(app_js))
.route("/style.css", routing::get(style))
.route(
"/ws",
routing::get(move |w| ws_handler(w, msend, bsend2.clone().subscribe())),
);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3001").await.unwrap();
let game = open_tavern::GameServer::new();
tokio::spawn(game.server_loop(mrecv, bsend));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3001").await.unwrap();
let game = open_tavern::GameServer::new();
tokio::spawn(game.server_loop(mrecv, bsend));
axum::serve(listener, app).await.expect("axum server crashed, yaaaaay (unless i crashed him that yay)");
axum::serve(listener, app)
.await
.expect("axum server crashed, yaaaaay (unless i crashed him that yay)");
}
async fn ws_handler(ws: ws::WebSocketUpgrade, msend: mpsc::Sender<(String, Request)>, brecv: broadcast::Receiver<(Option<String>, Response)>) -> impl axum::response::IntoResponse {
ws.on_upgrade(|w| handle_socket(w, msend, brecv))
/// Executes on a new WebSocket request, set update to [handle_socket]
async fn ws_handler(
ws: ws::WebSocketUpgrade,
msend: mpsc::Sender<(String, Request)>,
brecv: broadcast::Receiver<(Option<String>, Response)>,
) -> impl axum::response::IntoResponse {
ws.on_upgrade(|w| handle_socket(w, msend, brecv))
}
/// Receiver of a websocket after [handle_socket] handle the login
async fn socket_receiver(mut recv: SplitStream<ws::WebSocket>, msend: mpsc::Sender<(String, Request)>, id: String) {
while let Some(msg) = recv.next().await {
if let Ok(msg) = msg {
match msg {
Message::Text(t) => {
println!("Got message from {}: {}", &id, &t);
let req = serde_json::from_str::<Request>(&t).unwrap_or_default();
let erred = msend.send((id.clone(), req)).await.is_err();
if erred {
break;
}
}
ws::Message::Binary(_) => todo!(),
ws::Message::Ping(_) => todo!(),
ws::Message::Pong(_) => todo!(),
ws::Message::Close(_) => {
// dont care if we fail the send as we are quitting regardless
_ = msend.send((id.clone(), open_tavern::api::Request::Quit)).await;
break;
},
}
}
}
while let Some(msg) = recv.next().await {
if let Ok(msg) = msg {
match msg {
Message::Text(t) => {
println!("Got message from {}: {}", &id, &t);
let req = serde_json::from_str::<Request>(&t).unwrap_or_default();
let erred = msend.send((id.clone(), req)).await.is_err();
if erred {
break;
}
}
ws::Message::Binary(_) => todo!(),
ws::Message::Ping(_) => todo!(),
ws::Message::Pong(_) => todo!(),
ws::Message::Close(_) => {
// dont care if we fail the send as we are quitting regardless
_ = msend.send((id.clone(), open_tavern::api::Request::Quit)).await;
break;
}
}
}
}
}
async fn socket_sender(id: String, mut send: SplitSink<ws::WebSocket, ws::Message>, mut brecv: broadcast::Receiver<(Option<String>, Response)>) {
while let Ok((to_id, msg)) = brecv.recv().await {
if to_id.is_none() || to_id.map(|t| t == id).unwrap_or(false) {
println!("Sending a message to {}: {:?}", &id, &msg);
let err = send.send(ws::Message::Text(serde_json::to_string(&msg).unwrap())).await.is_err();
if err || matches!(msg, Response::Shutdown) {
_ = send.close().await;
break;
}
}
}
/// Sender of a websocket after [handle_socket] handles the login
async fn socket_sender(
id: String,
mut send: SplitSink<ws::WebSocket, ws::Message>,
mut brecv: broadcast::Receiver<(Option<String>, Response)>,
) {
while let Ok((to_id, msg)) = brecv.recv().await {
if to_id.is_none() || to_id.map(|t| t == id).unwrap_or(false) {
println!("Sending a message to {}: {:?}", &id, &msg);
let err = send
.send(ws::Message::Text(serde_json::to_string(&msg).unwrap().into()))
.await
.is_err();
if err || matches!(msg, Response::Shutdown) {
_ = send.close().await;
break;
}
}
}
}
async fn handle_socket(mut socket: ws::WebSocket, msend: mpsc::Sender<(String, Request)>, mut brecv: broadcast::Receiver<(Option<String>, Response)>) {
let mut id: Option<String> = 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 {
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;
}
}
}
},
msg = socket.recv() => {
if let Some(msg) = msg.map(|m| m.ok()).flatten() {
match msg {
Message::Text(t) => {
let req = serde_json::from_str::<Request>(&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;
},
_ => {},
};
}
}
};
}
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()));
tokio::spawn(socket_sender(id, send, brecv));
}
println!("Done with so-cat");
/// Socket login handler, upon a websocket upgrade it will check for login requests until a login
/// request succeeds, then launch [socket_sender] and [socket_receiver] to handle the actual gameplay part
async fn handle_socket(
mut socket: ws::WebSocket,
msend: mpsc::Sender<(String, Request)>,
mut brecv: broadcast::Receiver<(Option<String>, Response)>,
) {
let mut id: Option<String> = 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 {
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().into())).await;
break;
}
else {
id = None;
_ = socket.send(Message::Text(serde_json::to_string(&msg).unwrap_or_default().into())).await;
}
}
}
},
msg = socket.recv() => {
if let Some(msg) = msg.map(|m| m.ok()).flatten() {
match msg {
Message::Text(t) => {
let req = serde_json::from_str::<Request>(&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().into())).await;
},
}
}
ws::Message::Close(_) => {
id = None;
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()));
tokio::spawn(socket_sender(id, send, brecv));
}
println!("Done with so-cat");
}
async fn root() -> axum::response::Html<&'static str> {
response::Html(include_str!("../assets/web/index.html"))
response::Html(include_str!("../assets/web/index.html"))
}
async fn socket() -> impl response::IntoResponse {
([(axum::http::header::CONTENT_TYPE, "text/javascript")], include_str!("../assets/web/tavern.js"))
(
[(axum::http::header::CONTENT_TYPE, "text/javascript")],
include_str!("../assets/web/tavern.js"),
)
}
async fn app_js() -> impl response::IntoResponse {
([(axum::http::header::CONTENT_TYPE, "text/javascript")], include_str!("../assets/web/app.js"))
(
[(axum::http::header::CONTENT_TYPE, "text/javascript")],
include_str!("../assets/web/app.js"),
)
}
async fn style() -> impl response::IntoResponse {
([(axum::http::header::CONTENT_TYPE, "text/css")], include_str!("../assets/web/style.css"))
}
(
[(axum::http::header::CONTENT_TYPE, "text/css")],
include_str!("../assets/web/style.css"),
)
}