diff options
| -rw-r--r-- | src/main.rs | 18 | ||||
| -rw-r--r-- | src/play.rs | 73 | ||||
| -rw-r--r-- | src/session.rs | 165 |
3 files changed, 188 insertions, 68 deletions
diff --git a/src/main.rs b/src/main.rs index f4782ac..c05d3c6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,6 @@ #![warn( missing_docs, - missing_copy_implementations, missing_debug_implementations )] @@ -14,8 +13,9 @@ mod play; mod session; mod template; +use std::array; use crate::play::handle_play; -use crate::session::{HandObject, Session}; +use crate::session::{HandObject, PlayerColor, Session}; use crate::template::{IndexTemplate, SessionTemplate}; use askama::Template; use axum::extract::{Path, Query, State, WebSocketUpgrade}; @@ -132,11 +132,23 @@ async fn update_hands( ) -> StatusCode { let mut sessions = state.sessions.write().unwrap(); + let hand = array::from_fn(|i| { + let color = PlayerColor::try_from(i); + let color = color.as_ref().map(AsRef::as_ref); + if let Ok(color) = color { + // It would not be necessary to clone here if the vector could be moved, which could be + // done if payload was mutable + payload.get(color).cloned().unwrap_or_else(Vec::new) + } else { + Vec::new() + } + }); + match sessions.get_mut(&id) { Some(session) => { let mut session = session.lock().unwrap(); - session.update_hands(payload); + session.update_hands(hand); StatusCode::NO_CONTENT } diff --git a/src/play.rs b/src/play.rs index c635595..2f364c0 100644 --- a/src/play.rs +++ b/src/play.rs @@ -1,9 +1,8 @@ use crate::AppState; -use crate::session::{HandObject, Session}; +use crate::session::{HandObject, PlayerColor, Session}; use axum::extract::ws::{Message, Utf8Bytes, WebSocket}; use futures_util::{SinkExt, StreamExt}; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; use std::sync::{Arc, Mutex, RwLock, Weak}; use tokio::sync::broadcast::Receiver; use tokio::sync::broadcast::error::RecvError; @@ -27,7 +26,7 @@ enum OutgoingPlayMessage { #[derive(Clone)] pub enum PlayUpdate { - HandUpdate(HashMap<String, Vec<HandObject>>), + HandUpdate([Vec<HandObject>; PlayerColor::COUNT]), } pub async fn handle_play(socket: WebSocket, app_state: Arc<AppState>) { @@ -57,7 +56,7 @@ pub async fn handle_play(socket: WebSocket, app_state: Arc<AppState>) { let sender_tx = sender_tx.clone(); tokio::spawn(async move { let mut player_session = None; - let player_color = Arc::new(RwLock::new(String::new())); + let player_color = Arc::new(RwLock::new(PlayerColor::Grey)); while let Some(msg) = receiver.next().await { let Ok(Message::Text(text)) = msg else { @@ -70,7 +69,7 @@ pub async fn handle_play(socket: WebSocket, app_state: Arc<AppState>) { let sessions = app_state.sessions.read().unwrap(); sessions .get(&id) - .map(Arc::to_owned) + .map(Arc::clone) .ok_or("Session did not exist") }; @@ -79,8 +78,11 @@ pub async fn handle_play(socket: WebSocket, app_state: Arc<AppState>) { let (colors, update_rx) = { let session = session.lock().unwrap(); - let colors: Vec<String> = - session.seats.keys().cloned().collect(); + let colors: Vec<String> = session.seats.iter().enumerate() + .filter(|(_, hand)| hand.len() > 0) + .flat_map(|(index, _)| PlayerColor::try_from(index).ok()) + .map(|color| String::from(color.as_ref())) + .collect(); let update_rx = session.update_tx.subscribe(); (colors, update_rx) @@ -121,35 +123,23 @@ pub async fn handle_play(socket: WebSocket, app_state: Arc<AppState>) { let Some(session) = player_session.clone().and_then(|session| session.upgrade()) else { - let response = OutgoingPlayMessage::Error; - if sender_tx.send(response).await.is_err() { - break; - } - break; + let _ = sender_tx.send(OutgoingPlayMessage::Error).await; + break }; - let hand = session - .lock() - .unwrap() - .seats - .get(&color) - .map(|seat| seat.to_owned()); - match hand { - Some(hand) => { - *player_color.write().unwrap() = color; - if sender_tx - .send(OutgoingPlayMessage::Hand(hand)) - .await - .is_err() - { - break; - } - } - None => { - if sender_tx.send(OutgoingPlayMessage::Error).await.is_err() { - break; - } - } + let Ok(color) = PlayerColor::try_from(color.as_str()) else { + let _ = sender_tx.send(OutgoingPlayMessage::Error).await; + break }; + + let hand = session.lock().unwrap().seats[&color].clone(); + *player_color.write().unwrap() = color; + if sender_tx + .send(OutgoingPlayMessage::Hand(hand)) + .await + .is_err() + { + break; + } } Err(err) => { eprintln!( @@ -173,23 +163,26 @@ async fn handle_update( mut update_rx: Receiver<PlayUpdate>, sender_tx: Sender<OutgoingPlayMessage>, _player_session: Weak<Mutex<Session>>, - player_color: Arc<RwLock<String>>, + player_color: Arc<RwLock<PlayerColor>>, ) { loop { match update_rx.recv().await { Ok(PlayUpdate::HandUpdate(hands)) => { + let colors: Vec<String> = hands.iter().enumerate() + .filter(|(_, hand)| hand.len() > 0) + .flat_map(|(index, _)| PlayerColor::try_from(index).ok()) + .map(|color| String::from(color.as_ref())) + .collect(); let _ = sender_tx .send(OutgoingPlayMessage::Initialize { - colors: hands.keys().cloned().collect(), + colors, }) .await; let hand = { let color = player_color.read().unwrap(); - hands.get(&*color).map(ToOwned::to_owned) - }; - if let Some(hand) = hand { - let _ = sender_tx.send(OutgoingPlayMessage::Hand(hand)).await; + hands[usize::from(&*color)].to_owned() }; + let _ = sender_tx.send(OutgoingPlayMessage::Hand(hand)).await; } Err(RecvError::Closed) => break, Err(RecvError::Lagged(_)) => continue, diff --git a/src/session.rs b/src/session.rs index 93ea838..096ae1a 100644 --- a/src/session.rs +++ b/src/session.rs @@ -1,23 +1,27 @@ +use std::array; use crate::play::PlayUpdate; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::ops::Index; use tokio::sync::broadcast; #[derive(Debug)] pub struct Session { pub steam_name: String, - pub seats: HashMap<String, Vec<HandObject>>, + pub seats: [Vec<HandObject>; PlayerColor::COUNT], pub update_tx: broadcast::Sender<PlayUpdate>, } -#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] pub enum HandObject { CustomDeck(CustomDeck), } // TODO: These fields will be used in the future. When they are, the dead_code lint should no longer // be suppressed. -#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +/// Similar to the table defined at [Custom Deck](https://api.tabletopsimulator.com/custom-game-objects/#custom-deck) +/// in the Tabletop Simulator API knowledge base, but `card_id` is used to identify the card number +/// within the deck. +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] #[allow(dead_code)] pub struct CustomDeck { /// The path/URL of the face cardsheet. @@ -27,18 +31,40 @@ pub struct CustomDeck { /// If each card has a unique card back (via a cardsheet). pub unique_back: bool, /// The number of columns on the cardsheet. - pub width: f64, + pub width: u64, /// The number of rows on the cardsheet. - pub height: f64, + pub height: u64, /// The number of cards on the cardsheet. - pub number: f64, + pub number: u64, /// Whether the cards are horizontal, instead of vertical. pub sideways: bool, /// Whether the card back should be used as the hidden image (instead of the last slot of the /// `face` image). pub back_is_hidden: bool, - /// ID of the custom card within the deck. - pub card_id: f64, + /// ID of the custom card within the deck, starting from 1. + pub card_id: u64, +} + +/// One of the colors defined by Tabletop Simulator as a player color. All players are assigned +/// one of these twelve player colors. +/// +/// See [Player Colors](https://api.tabletopsimulator.com/player/colors/) from the Tabletop +/// Simulator API knowledge base. +#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub enum PlayerColor { + White, + Brown, + Red, + Orange, + Yellow, + Green, + Teal, + Blue, + Purple, + Pink, + #[default] + Grey, + Black, } impl Session { @@ -47,12 +73,12 @@ impl Session { Session { steam_name, - seats: HashMap::new(), + seats: array::from_fn(|_| Vec::new()), update_tx, } } - pub fn update_hands(&mut self, hands: HashMap<String, Vec<HandObject>>) { + pub fn update_hands(&mut self, hands: [Vec<HandObject>; PlayerColor::COUNT]) { self.seats = hands.to_owned(); // Updating the hand is a success regardless of whether there are players connected to // receive a hand update @@ -60,6 +86,100 @@ impl Session { } } +impl PlayerColor { + pub const COUNT: usize = 12; +} + +impl AsRef<str> for PlayerColor { + fn as_ref(&self) -> &str { + match self { + PlayerColor::White => "White", + PlayerColor::Brown => "Brown", + PlayerColor::Red => "Red", + PlayerColor::Orange => "Orange", + PlayerColor::Yellow => "Yellow", + PlayerColor::Green => "Green", + PlayerColor::Teal => "Teal", + PlayerColor::Blue => "Blue", + PlayerColor::Purple => "Purple", + PlayerColor::Pink => "Pink", + PlayerColor::Grey => "Grey", + PlayerColor::Black => "Black", + } + } +} + +impl From<&PlayerColor> for usize { + fn from(value: &PlayerColor) -> Self { + match value { + PlayerColor::White => 0, + PlayerColor::Brown => 1, + PlayerColor::Red => 2, + PlayerColor::Orange => 3, + PlayerColor::Yellow => 4, + PlayerColor::Green => 5, + PlayerColor::Teal => 6, + PlayerColor::Blue => 7, + PlayerColor::Purple => 8, + PlayerColor::Pink => 9, + PlayerColor::Grey => 10, + PlayerColor::Black => 11, + } + } +} + +impl<T> Index<&PlayerColor> for [T] { + type Output = T; + + fn index(&self, index: &PlayerColor) -> &Self::Output { + &self[usize::from(index)] + } +} + +impl TryFrom<&str> for PlayerColor { + type Error = (); + + fn try_from(value: &str) -> Result<Self, Self::Error> { + match value { + "White" => Ok(Self::White), + "Brown" => Ok(Self::Brown), + "Red" => Ok(Self::Red), + "Orange" => Ok(Self::Orange), + "Yellow" => Ok(Self::Yellow), + "Green" => Ok(Self::Green), + "Teal" => Ok(Self::Teal), + "Blue" => Ok(Self::Blue), + "Purple" => Ok(Self::Purple), + "Pink" => Ok(Self::Pink), + "Grey" => Ok(Self::Grey), + "Black" => Ok(Self::Black), + _ => Err(()) + } + } +} + +impl TryFrom<usize> for PlayerColor { + type Error = (); + + fn try_from(value: usize) -> Result<Self, Self::Error> { + match value { + 0 => Ok(Self::White), + 1 => Ok(Self::Brown), + 2 => Ok(Self::Red), + 3 => Ok(Self::Orange), + 4 => Ok(Self::Yellow), + 5 => Ok(Self::Green), + 6 => Ok(Self::Teal), + 7 => Ok(Self::Blue), + 8 => Ok(Self::Purple), + 9 => Ok(Self::Pink), + 10 => Ok(Self::Grey), + 11 => Ok(Self::Black), + _ => Err(()) + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -73,28 +193,23 @@ mod tests { face: "https://steamusercontent-a.akamaihd.net/ugc/1663479592506990057/B6EEB9A683A57C9A41CC9782993A8BAF9DCD72A1/".to_string(), back: "https://steamusercontent-a.akamaihd.net/ugc/1663479592507076702/D16FFBC8D87B4D4FB21C0057F2BBC9DC4D4FD379/".to_string(), unique_back: false, - width: 5.0, - height: 7.0, - number: 6.0, + width: 5, + height: 7, + number: 6, sideways: false, back_is_hidden: false, - card_id: 0.0, + card_id: 1, }; - let hands = HashMap::from([( - "red".to_string(), - vec![HandObject::CustomDeck(card.to_owned())], - )]); + let mut hands: [Vec<HandObject>; PlayerColor::COUNT] = array::from_fn(|_| Vec::new()); + hands[PlayerColor::Red as usize].push(HandObject::CustomDeck(card.to_owned())); + session.update_hands(hands); - // TODO: This lint allow be removed when PlayUpdate has more variants - #[allow(irrefutable_let_patterns)] - let PlayUpdate::HandUpdate(hand) = update_rx.recv().await.unwrap() else { - panic!("Received update was not a HandUpdate"); - }; + let PlayUpdate::HandUpdate(hand) = update_rx.recv().await.unwrap(); assert_eq!( - hand.get("red").unwrap().first().unwrap().to_owned(), + hand[PlayerColor::Red as usize].first().unwrap().to_owned(), HandObject::CustomDeck(card) ); } |
