update deps, fix stuff and add some comments
This commit is contained in:
parent
dbeda509fc
commit
3cfabcc39f
5 changed files with 477 additions and 466 deletions
507
Cargo.lock
generated
507
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,15 +1,14 @@
|
|||
[package]
|
||||
name = "open_tavern"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.7.7", features = ["ws"] }
|
||||
axum = { version = "0.8.4", features = ["ws"] }
|
||||
serde = "1.*.*"
|
||||
serde_json = "1.*.*"
|
||||
tokio = { version = "1.*.*", features = ["full"] }
|
||||
sqlx = { version = "0.8.2", features = ["runtime-tokio", "sqlite"] }
|
||||
sqlx = { version = "0.8.6", features = ["runtime-tokio", "sqlite"] }
|
||||
tavern_macros = { version = "0.1.0", path = "tavern_macros" }
|
||||
parking_lot = "0.12.3"
|
||||
futures-util = "0.3.30"
|
||||
futures-util = "0.3.31"
|
||||
base64 = "0.22.1"
|
||||
|
|
2
rustfmt.toml
Normal file
2
rustfmt.toml
Normal file
|
@ -0,0 +1,2 @@
|
|||
binop_separator = "Back"
|
||||
max_width = 120
|
111
src/lib.rs
111
src/lib.rs
|
@ -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,7 +18,9 @@ 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
|
||||
creds: HashMap<String, String>,
|
||||
|
@ -22,13 +28,12 @@ pub struct GameServer {
|
|||
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![
|
||||
(
|
||||
chat: vec![(
|
||||
"Server".to_string(),
|
||||
ChatMessage::new("a weapon description".to_string())
|
||||
.id(1)
|
||||
|
@ -37,12 +42,15 @@ impl GameServer {
|
|||
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_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_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,37 +138,57 @@ 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))
|
||||
.fold(String::new(), |a, b| {
|
||||
a + &format!("{}: {} = {}\n", &b.name, &b.result_text, b.result)
|
||||
}),
|
||||
)
|
||||
.character(Some(result.name))
|
||||
.source(id.clone())
|
||||
|
@ -154,8 +196,7 @@ impl GameServer {
|
|||
.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);
|
||||
|
|
80
src/main.rs
80
src/main.rs
|
@ -1,7 +1,12 @@
|
|||
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};
|
||||
|
||||
|
@ -15,19 +20,30 @@ async fn main() {
|
|||
.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())));
|
||||
.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));
|
||||
|
||||
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 {
|
||||
/// 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 {
|
||||
|
@ -47,17 +63,24 @@ async fn socket_receiver(mut recv: SplitStream<ws::WebSocket>, msend: mpsc::Send
|
|||
// 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)>) {
|
||||
/// 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())).await.is_err();
|
||||
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;
|
||||
|
@ -65,11 +88,22 @@ async fn socket_sender(id: String, mut send: SplitSink<ws::WebSocket, ws::Messag
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_socket(mut socket: ws::WebSocket, msend: mpsc::Sender<(String, Request)>, mut brecv: broadcast::Receiver<(Option<String>, Response)>) {
|
||||
/// 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 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! {
|
||||
|
@ -79,12 +113,12 @@ async fn handle_socket(mut socket: ws::WebSocket, msend: mpsc::Sender<(String, R
|
|||
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;
|
||||
_ = 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())).await;
|
||||
_ = socket.send(Message::Text(serde_json::to_string(&msg).unwrap_or_default().into())).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -109,7 +143,7 @@ async fn handle_socket(mut socket: ws::WebSocket, msend: mpsc::Sender<(String, R
|
|||
}
|
||||
},
|
||||
_ => {
|
||||
_ = socket.send(Message::Text(serde_json::to_string(&Response::Error(RequestError::InvalidRequest)).unwrap())).await;
|
||||
_ = socket.send(Message::Text(serde_json::to_string(&Response::Error(RequestError::InvalidRequest)).unwrap().into())).await;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -136,15 +170,23 @@ async fn handle_socket(mut socket: ws::WebSocket, msend: mpsc::Sender<(String, R
|
|||
async fn root() -> axum::response::Html<&'static str> {
|
||||
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"),
|
||||
)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue