summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main.rs18
-rw-r--r--src/play.rs73
-rw-r--r--src/session.rs165
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)
);
}