srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/server/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/server/src')
-rw-r--r--crates/server/src/main.rs19
-rw-r--r--crates/server/src/multiplayer.rs28
-rw-r--r--crates/server/src/state.rs13
3 files changed, 48 insertions, 12 deletions
diff --git a/crates/server/src/main.rs b/crates/server/src/main.rs
index 7e9e9a8..2769d4a 100644
--- a/crates/server/src/main.rs
+++ b/crates/server/src/main.rs
@@ -10,6 +10,7 @@ mod spotify;
mod state;
mod stats;
+use crate::state::RaceText;
use axum::http::{HeaderValue, Method};
use axum::routing::get;
use axum::Router;
@@ -24,6 +25,8 @@ use tower_http::trace::TraceLayer;
#[derive(Deserialize)]
struct TextEntry {
content: String,
+ #[serde(default)]
+ attribution: Option<String>,
}
// Every multiplayer room draws from this same pool so every player in a
@@ -32,15 +35,18 @@ struct TextEntry {
// falls back to a couple of plain sentences if the file isn't reachable
// (e.g. the server binary run from somewhere other than the repo root),
// so a room can still start rather than erroring on an empty pool.
-fn load_race_texts() -> Vec<String> {
+fn load_race_texts() -> Vec<RaceText> {
let path = std::env::var("TEXTS_JSON_PATH").unwrap_or_else(|_| "texts.json".to_string());
match std::fs::read_to_string(&path).ok().and_then(|raw| serde_json::from_str::<Vec<TextEntry>>(&raw).ok()) {
- Some(entries) if !entries.is_empty() => entries.into_iter().map(|e| e.content).collect(),
+ Some(entries) if !entries.is_empty() => entries
+ .into_iter()
+ .map(|e| RaceText { text: e.content, attribution: e.attribution })
+ .collect(),
_ => {
tracing::warn!("could not load race texts from {path} - using a small built-in fallback pool");
vec![
- "The quick brown fox jumps over the lazy dog.".to_string(),
- "Pack my box with five dozen liquor jugs.".to_string(),
+ RaceText { text: "The quick brown fox jumps over the lazy dog.".to_string(), attribution: None },
+ RaceText { text: "Pack my box with five dozen liquor jugs.".to_string(), attribution: None },
]
}
}
@@ -136,7 +142,10 @@ mod tests {
let app_state = Arc::new(AppState::new(
db,
false,
- vec!["The quick brown fox jumps over the lazy dog.".to_string()],
+ vec![RaceText {
+ text: "The quick brown fox jumps over the lazy dog.".to_string(),
+ attribution: None,
+ }],
SpotifyConfig::default(),
"http://localhost:4173".to_string(),
));
diff --git a/crates/server/src/multiplayer.rs b/crates/server/src/multiplayer.rs
index 8974c8c..7fca7c3 100644
--- a/crates/server/src/multiplayer.rs
+++ b/crates/server/src/multiplayer.rs
@@ -56,6 +56,10 @@ const BOT_NAMES: &[&str] = &[
"Pixel", "Quartz", "Sigil", "Tessa", "Umbra",
];
+// Long enough to read the passage's opening words and get your hands in
+// position. Three seconds was barely enough to register that a race had
+// started, let alone look at the text.
+const COUNTDOWN_SECONDS: u32 = 5;
const ROOM_CODE_CHARS: &[u8] = b"ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // no 0/O/1/I - easy to misread aloud
#[derive(Debug, Clone, PartialEq)]
@@ -318,16 +322,30 @@ async fn start_race(room_arc: Arc<Mutex<Room>>, state: Arc<AppState>) {
return;
}
room.status = RoomStatus::Countdown;
- let text = state
+ let picked = state
.race_texts
.get(rand::thread_rng().gen_range(0..state.race_texts.len()))
.cloned()
- .unwrap_or_default();
- room.text = Some(text.clone());
- text
+ .unwrap_or_else(|| crate::state::RaceText {
+ text: String::new(),
+ attribution: None,
+ });
+ room.text = Some(picked.text.clone());
+ // Sent before the countdown so every client can put the passage on
+ // screen and let players read it while they wait, the way TypeRacer
+ // and 10FastFingers both do.
+ broadcast(
+ &room,
+ &ServerMessage::RaceText {
+ text: picked.text.clone(),
+ attribution: picked.attribution.clone(),
+ },
+ )
+ .await;
+ picked.text
};
- for seconds in (1..=3).rev() {
+ for seconds in (1..=COUNTDOWN_SECONDS).rev() {
{
let room = room_arc.lock().await;
broadcast(&room, &ServerMessage::Countdown { seconds }).await;
diff --git a/crates/server/src/state.rs b/crates/server/src/state.rs
index 267b6fb..7244955 100644
--- a/crates/server/src/state.rs
+++ b/crates/server/src/state.rs
@@ -17,6 +17,15 @@ impl SpotifyConfig {
}
}
+/// A passage a race can be run on, with whoever wrote it. The attribution
+/// travels with the text so every racer can see where the passage came from,
+/// the same way single player does.
+#[derive(Debug, Clone)]
+pub struct RaceText {
+ pub text: String,
+ pub attribution: Option<String>,
+}
+
#[derive(Clone)]
pub struct AppState {
pub db: SqlitePool,
@@ -33,7 +42,7 @@ pub struct AppState {
/// Race passages every multiplayer room draws from, so every player in
/// a room types the identical text - loaded once at startup rather
/// than per-room, since the pool itself never changes at runtime.
- pub race_texts: Vec<String>,
+ pub race_texts: Vec<RaceText>,
pub spotify: SpotifyConfig,
pub frontend_origin: String,
pub http: Client,
@@ -43,7 +52,7 @@ impl AppState {
pub fn new(
db: SqlitePool,
cookie_secure: bool,
- race_texts: Vec<String>,
+ race_texts: Vec<RaceText>,
spotify: SpotifyConfig,
frontend_origin: String,
) -> Self {