diff --git a/Cargo.lock b/Cargo.lock index 60827d49..a7330f54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1122,7 +1122,7 @@ dependencies = [ [[package]] name = "fonts" -version = "0.1.0" +version = "0.1.1" dependencies = [ "embedded-graphics", ] @@ -3465,7 +3465,7 @@ dependencies = [ [[package]] name = "uwh-common" -version = "0.1.0" +version = "0.1.1" dependencies = [ "arrayref", "arrayvec 0.7.2", @@ -3483,7 +3483,7 @@ dependencies = [ [[package]] name = "uwh-matrix-drawing" -version = "0.1.0" +version = "0.1.1" dependencies = [ "arrayref", "arrayvec 0.7.2", diff --git a/README.md b/README.md index 71bce91b..d90463be 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,21 @@ The main software component here is the [`uwh-refbox`](uwh-refbox) crate. The other crates are support crates that are also used by other binaries, not included here. -# Running +# Running the Binary + +On Windows and Mac the app can be run by downloading the latest relase from GitHub and following the bundled instructions. + +If you want to change the size of the simulated panels, you will need to run via the command line: + +## Windows + +1. Open `PowerShell` (to open `PowerShell`, start by typing "PowerShell" into the search bar by the windows icon, then clicking the app) +2. Drag the `.exe` from `File Explorer` into the `PowerShell` window (this will insert the location of the `.exe` into the command line) +3. Add the following to the command line: `-s N` (with a space before `-s`) where `N` is any positive decimal number (`4` and `4.0` are both acceptable). `N` sets the size of the panels, the default value is `4` +4. Confrim that the command line now looks something like this: `'' -s 5.5` +5. Press `enter` to start the program + +# Running From Source 1. You will need to [Install Rust](https://rustup.rs/) 2. Ensure that you have the following libraries installed: diff --git a/fonts/Cargo.toml b/fonts/Cargo.toml index 175f4ac5..a45265f4 100644 --- a/fonts/Cargo.toml +++ b/fonts/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fonts" -version = "0.1.0" +version = "0.1.1" authors = ["Atlantis Sports "] edition = "2021" diff --git a/uwh-common/Cargo.toml b/uwh-common/Cargo.toml index 73fef397..b310cc1b 100644 --- a/uwh-common/Cargo.toml +++ b/uwh-common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uwh-common" -version = "0.1.0" +version = "0.1.1" authors = ["Atlantis Sports "] edition = "2021" @@ -14,7 +14,7 @@ arrayvec = { version = "0.7.2", default-features = false, features = ["serde"] } defmt = "0.3.1" derivative = { version = "2.2.0", features = ["use_core"] } displaydoc = { version = "0.2.3", default-features = false } -fonts = { version = "0.1.0", path = "../fonts" } +fonts = { version = "0.1.1", path = "../fonts" } log = "0.4.16" serde = { version = "1.0", default-features = false } serde_derive = "1.0" diff --git a/uwh-common/src/config.rs b/uwh-common/src/config.rs index 8339980a..adc4a015 100644 --- a/uwh-common/src/config.rs +++ b/uwh-common/src/config.rs @@ -84,7 +84,7 @@ impl Default for Game { pre_overtime_break: Duration::from_secs(180), overtime_break_duration: Duration::from_secs(60), pre_sudden_death_duration: Duration::from_secs(60), - post_game_duration: Duration::from_secs(60), + post_game_duration: Duration::from_secs(120), nominal_break: Duration::from_secs(900), minimum_break: Duration::from_secs(240), } @@ -169,7 +169,7 @@ mod test { pre_sudden_death_duration = 60 sudden_death_allowed = true team_timeouts_per_half = 1 - post_game_duration = 60 + post_game_duration = 120 nominal_break = 900 minimum_break = 240"# ); diff --git a/uwh-common/src/game_snapshot.rs b/uwh-common/src/game_snapshot.rs index a4d8e23c..b89667ef 100644 --- a/uwh-common/src/game_snapshot.rs +++ b/uwh-common/src/game_snapshot.rs @@ -1,9 +1,9 @@ #[cfg(feature = "std")] -use crate::config::Game; +use crate::{config::Game, drawing_support::*}; use arrayref::array_ref; use arrayvec::ArrayVec; use core::{ - cmp::{Ordering, PartialOrd}, + cmp::{min, Ordering, PartialOrd}, time::Duration, }; #[cfg(not(target_os = "windows"))] @@ -15,8 +15,8 @@ use serde_derive::{Deserialize, Serialize}; const PANEL_PENALTY_COUNT: usize = 3; /// Game snapshot information that the LED matrices need. Excludes some fields, limits to three -/// penalties (the three with the lowest remaining time), and places the penalties on a stack based -/// `ArrayVec`, instead of the heap based `Vec` +/// penalties (the three with the lowest remaining time), and places the penalties on a stack-based +/// `ArrayVec`, instead of the heap-based `Vec` #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] pub struct GameSnapshotNoHeap { pub current_period: GamePeriod, @@ -35,7 +35,7 @@ pub struct GameSnapshotNoHeap { #[derive(Debug, PartialEq, Eq, Default, Clone, Serialize, Deserialize)] pub struct GameSnapshot { pub current_period: GamePeriod, - pub secs_in_period: u16, + pub secs_in_period: u32, pub timeout: TimeoutSnapshot, pub b_score: u8, pub w_score: u8, @@ -64,7 +64,13 @@ impl From for GameSnapshotNoHeap { Self { current_period: snapshot.current_period, - secs_in_period: snapshot.secs_in_period, + secs_in_period: min( + snapshot + .secs_in_period + .try_into() + .unwrap_or(MAX_STRINGABLE_SECS), + MAX_STRINGABLE_SECS, + ), timeout: snapshot.timeout, b_score: snapshot.b_score, w_score: snapshot.w_score, @@ -324,7 +330,7 @@ impl TimeoutSnapshot { match self { Self::None => Ok([0x00, 0x00]), Self::Black(time) | Self::White(time) | Self::Ref(time) | Self::PenaltyShot(time) => { - if *time > 5999 { + if *time > MAX_STRINGABLE_SECS { Err(EncodingError::TimeoutTimeTooLarge(*time)) } else { let variant = match self { diff --git a/uwh-common/src/lib.rs b/uwh-common/src/lib.rs index f324fe94..99e71177 100644 --- a/uwh-common/src/lib.rs +++ b/uwh-common/src/lib.rs @@ -4,3 +4,9 @@ pub mod game_snapshot; #[cfg(feature = "std")] pub mod config; + +pub mod drawing_support { + pub const MAX_STRINGABLE_SECS: u16 = 5999; + pub const MAX_LONG_STRINGABLE_SECS: u32 = 5_999_999; + pub const MAX_SHORT_STRINGABLE_SECS: u8 = 99; +} diff --git a/uwh-matrix-drawing/Cargo.toml b/uwh-matrix-drawing/Cargo.toml index 538698b4..3a76e960 100644 --- a/uwh-matrix-drawing/Cargo.toml +++ b/uwh-matrix-drawing/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uwh-matrix-drawing" -version = "0.1.0" +version = "0.1.1" authors = ["Atlantis Sports "] edition = "2021" @@ -12,8 +12,8 @@ std = ["arrayvec/std", "serde/std", "uwh-common/std"] arrayref = "0.3.6" arrayvec = { version = "0.7.2", default-features = false } embedded-graphics = "0.7.1" -fonts = { version = "0.1.0", path = "../fonts" } +fonts = { version = "0.1.1", path = "../fonts" } more-asserts = "0.2" serde = { version = "1.0", default-features = false } serde_derive = "1.0" -uwh-common = { version = "0.1.0", path = "../uwh-common", default-features = false } +uwh-common = { version = "0.1.1", path = "../uwh-common", default-features = false } diff --git a/uwh-matrix-drawing/src/drawing.rs b/uwh-matrix-drawing/src/drawing.rs index 395b3b6e..e9856ac4 100644 --- a/uwh-matrix-drawing/src/drawing.rs +++ b/uwh-matrix-drawing/src/drawing.rs @@ -11,7 +11,7 @@ use embedded_graphics::{ }; use fonts::fonts::{FONT_10X25, FONT_14X31, FONT_20X46, FONT_28X64, FONT_5X8, FONT_7X15}; use more_asserts::*; -use uwh_common::game_snapshot::*; +use uwh_common::{drawing_support::*, game_snapshot::*}; /// Draws all the details of the game onto the provided display. Assumes the dispaly is 256x64 /// @@ -307,8 +307,7 @@ pub fn draw_panels>( .draw(display)?; let time: ArrayString<4> = match penalty.time { PenaltyTime::Seconds(secs) => { - let i = if secs >= 60 { 1 } else { 2 }; - ArrayString::from(secs_to_time_string(secs).get(i..).unwrap()).unwrap() + ArrayString::from(secs_to_time_string(secs).trim()).unwrap() } PenaltyTime::TotalDismissal => ArrayString::from("DSMS").unwrap(), }; @@ -368,7 +367,7 @@ where ::Output: Display, ::Output: Display, { - assert_le!(secs, T::from(5999u16)); + assert_le!(secs, T::from(MAX_STRINGABLE_SECS)); let min = secs / T::from(60u16); let sec = secs % T::from(60u16); let mut time_string = ArrayString::new(); @@ -376,10 +375,25 @@ where time_string } +pub fn secs_to_long_time_string(secs: T) -> ArrayString<8> +where + T: Div + Rem + From + Copy + Ord + Debug, + ::Output: Display, + ::Output: Display, +{ + assert_le!(secs, T::from(MAX_LONG_STRINGABLE_SECS)); + let min = secs / T::from(60u32); + let sec = secs % T::from(60u32); + let mut time_string = ArrayString::new(); + write!(&mut time_string, "{:5}:{:02}", min, sec).unwrap(); + time_string +} + pub fn secs_to_short_time_string(secs: T) -> ArrayString<3> where - T: Copy + Display, + T: From + Ord + Copy + Display + Debug, { + assert_le!(secs, T::from(99u8)); let mut time_string = ArrayString::new(); write!(&mut time_string, ":{:02}", secs).unwrap(); time_string diff --git a/uwh-refbox/Cargo.toml b/uwh-refbox/Cargo.toml index 9ba3a715..092cb1ff 100644 --- a/uwh-refbox/Cargo.toml +++ b/uwh-refbox/Cargo.toml @@ -35,8 +35,8 @@ time = { version = "0.3", features = ["local-offset", "macros", "serde", "serde- tokio = { version = "1.18", features = ["io-util", "macros", "net", "sync", "time"] } tokio-serial = "5.4" toml = "0.5" -uwh-common = { version = "0.1.0", path = "../uwh-common"} -uwh-matrix-drawing = { version = "0.1.0", path = "../uwh-matrix-drawing"} +uwh-common = { version = "0.1.1", path = "../uwh-common"} +uwh-matrix-drawing = { version = "0.1.1", path = "../uwh-matrix-drawing"} [target.'cfg(windows)'.build-dependencies] winres = "0.1" diff --git a/uwh-refbox/src/app/mod.rs b/uwh-refbox/src/app/mod.rs index 80f19bcb..53035cfe 100644 --- a/uwh-refbox/src/app/mod.rs +++ b/uwh-refbox/src/app/mod.rs @@ -31,6 +31,7 @@ use tokio::{ use tokio_serial::SerialPortBuilder; use uwh_common::{ config::{Config, Game as GameConfig}, + drawing_support::*, game_snapshot::{Color as GameColor, GamePeriod, GameSnapshot, TimeoutSnapshot}, }; @@ -43,6 +44,9 @@ use style::{PADDING, SPACING, WINDOW_BACKGROUND}; pub mod update_sender; use update_sender::*; +const REQUEST_TIMEOUT: Duration = Duration::from_secs(10); +const MAX_RETRIES: usize = 6; + pub struct RefBoxApp { tm: Arc>, config: Config, @@ -52,6 +56,7 @@ pub struct RefBoxApp { pen_edit: PenaltyEditor, app_state: AppState, last_app_state: AppState, + last_message: Message, update_sender: UpdateSender, message_listener: MessageListener, msg_tx: mpsc::UnboundedSender, @@ -105,7 +110,7 @@ struct EditableSettings { games: Option>, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum KeypadPage { AddScore(GameColor), Penalty(Option<(GameColor, usize)>, GameColor, PenaltyKind), @@ -173,7 +178,7 @@ pub enum ScrollOption { GameParameter, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum Message { Init, NewSnapshot(GameSnapshot), @@ -242,6 +247,52 @@ pub enum Message { NoAction, // TODO: Remove once UI is functional } +impl Message { + fn is_repeatable(&self) -> bool { + match self { + Self::NewSnapshot(_) + | Self::ChangeTime { .. } + | Self::ChangeScore { .. } + | Self::Scroll { .. } + | Self::KeypadButtonPress(_) + | Self::ToggleBoolParameter(_) + | Self::RecvTournamentList(_) + | Self::RecvTournament(_) + | Self::RecvGameList(_) + | Self::RecvGame(_) + | Self::NoAction => true, + + Self::Init + | Self::EditTime + | Self::TimeEditComplete { .. } + | Self::StartPlayNow + | Self::EditScores + | Self::ScoreEditComplete { .. } + | Self::PenaltyOverview + | Self::PenaltyOverviewComplete { .. } + | Self::ChangeKind(_) + | Self::PenaltyEditComplete { .. } + | Self::KeypadPage(_) + | Self::ChangeColor(_) + | Self::AddScoreComplete { .. } + | Self::EditGameConfig + | Self::ConfigEditComplete { .. } + | Self::EditParameter(_) + | Self::SelectParameter(_) + | Self::ParameterEditComplete { .. } + | Self::ParameterSelected(_, _) + | Self::ConfirmationSelected(_) + | Self::BlackTimeout(_) + | Self::WhiteTimeout(_) + | Self::RefTimeout(_) + | Self::PenaltyShot(_) + | Self::EndTimeout + | Self::ConfirmScores(_) + | Self::ScoreConfirmation { .. } => false, + } + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum KeypadButton { Zero, @@ -269,7 +320,7 @@ impl RefBoxApp { fn apply_snapshot(&mut self, mut new_snapshot: GameSnapshot) { if new_snapshot.current_period != self.snapshot.current_period { if new_snapshot.current_period == GamePeriod::BetweenGames { - self.handle_game_end(); + self.handle_game_end(new_snapshot.next_game_number); } else if self.snapshot.current_period == GamePeriod::BetweenGames { self.handle_game_start(new_snapshot.game_number); } @@ -321,7 +372,7 @@ impl RefBoxApp { }; ( - prereqs && new_snapshot.secs_in_period <= NUM_SHORT_SOUNDS, + prereqs && new_snapshot.secs_in_period <= NUM_SHORT_SOUNDS as u32, prereqs && is_warn_period && new_snapshot.secs_in_period == 35, ) } @@ -363,33 +414,41 @@ impl RefBoxApp { let client_ = client.clone(); let msg_tx_ = self.msg_tx.clone(); task::spawn(async move { - let msg = match client_.execute(request).await { - Ok(resp) => { - if resp.status() != StatusCode::OK { - error!( - "Got bad status code from uwhscores when requesting {}: {}", - short_name, - resp.status() - ); - None - } else { - match resp.json::().await { - Ok(parsed) => Some(on_success(parsed)), - Err(e) => { - error!("Couldn't desesrialize {}: {e}", short_name); - None + let mut msg = None; + for _ in 0..MAX_RETRIES { + msg = match client_.execute(request.try_clone().unwrap()).await { + Ok(resp) => { + if resp.status() != StatusCode::OK { + error!( + "Got bad status code from uwhscores when requesting {}: {}", + short_name, + resp.status() + ); + info!("Maybe retrying"); + continue; + } else { + match resp.json::().await { + Ok(parsed) => Some(on_success(parsed)), + Err(e) => { + error!("Couldn't desesrialize {}: {e}", short_name); + None + } } } } - } - Err(e) => { - error!("Request for {} failed: {e}", short_name); - None - } - }; + Err(e) => { + error!("Request for {} failed: {e}", short_name); + info!("Maybe retrying"); + continue; + } + }; + break; + } if let Some(msg) = msg { msg_tx_.send(msg).unwrap(); + } else { + error!("Too many failures when requesting {short_name}, stopping"); } }); } @@ -459,30 +518,45 @@ impl RefBoxApp { let client_ = client.clone(); task::spawn(async move { - let login = match client_.execute(login_request).await { - Ok(resp) => { - if resp.status() != StatusCode::OK { - error!( - "Got bad status code from uwhscores when logging in: {}", - resp.status() - ); - return; - } else { - match resp.json::().await { - Ok(parsed) => parsed, - Err(e) => { - error!("Couldn't desesrialize login: {e}"); - return; + let mut login = None; + for _ in 0..MAX_RETRIES { + login = match client_.execute(login_request.try_clone().unwrap()).await { + Ok(resp) => { + if resp.status() != StatusCode::OK { + error!( + "Got bad status code from uwhscores when logging in: {}", + resp.status() + ); + info!("Maybe retrying"); + continue; + } else { + match resp.json::().await { + Ok(parsed) => Some(parsed), + Err(e) => { + error!("Couldn't desesrialize login: {e}"); + return; + } } } } - } - Err(e) => { - error!("Login request failed: {e}"); - return; - } + Err(e) => { + error!("Login request failed: {e}"); + info!("Maybe retrying"); + continue; + } + }; + break; + } + + let login = if let Some(l) = login { + l + } else { + error!("Too many failures when logging in to uwhscores, stopping"); + return; }; + info!("Posting score: {post_data:?}"); + let post_request = client_ .request(Method::POST, post_url) .basic_auth::<_, String>(login.token, None) @@ -490,19 +564,26 @@ impl RefBoxApp { .build() .unwrap(); - match client_.execute(post_request).await { - Ok(resp) => { - if resp.status() != StatusCode::OK { - error!( - "Got bad status code from uwhscores when posting score: {}", - resp.status() - ); + for _ in 0..MAX_RETRIES { + match client_.execute(post_request.try_clone().unwrap()).await { + Ok(resp) => { + if resp.status() != StatusCode::OK { + error!( + "Got bad status code from uwhscores when posting score: {}", + resp.status() + ); + info!("Maybe retrying"); + continue; + } } - } - Err(e) => { - error!("Post score request failed: {e}"); - } - }; + Err(e) => { + error!("Post score request failed: {e}"); + info!("Maybe retrying"); + continue; + } + }; + break; + } }); } } @@ -540,10 +621,10 @@ impl RefBoxApp { } } - fn handle_game_end(&self) { + fn handle_game_end(&self, next_game_num: u32) { if self.using_uwhscores { if let Some(tid) = self.current_tid { - self.request_game_details(tid, self.snapshot.game_number); + self.request_game_details(tid, next_game_num); } else { error!("Missing current tid to request game info"); } @@ -594,7 +675,11 @@ impl Application for RefBoxApp { tm.set_timezone(config.uwhscores.timezone); tm.start_clock(Instant::now()); - let client = match Client::builder().https_only(require_https).build() { + let client = match Client::builder() + .https_only(require_https) + .timeout(REQUEST_TIMEOUT) + .build() + { Ok(c) => Some(c), Err(e) => { error!("Failed to start HTTP Client: {e}"); @@ -623,6 +708,7 @@ impl Application for RefBoxApp { snapshot, app_state: AppState::MainPage, last_app_state: AppState::MainPage, + last_message: Message::NoAction, update_sender, message_listener, msg_tx, @@ -661,6 +747,15 @@ impl Application for RefBoxApp { fn update(&mut self, message: Message) -> Command { trace!("Handling message: {message:?}"); + + if !message.is_repeatable() && (message == self.last_message) { + warn!("Ignoring a repeated message: {message:?}"); + self.last_message = message.clone(); + return Command::none(); + } else { + self.last_message = message.clone(); + } + match message { Message::Init => self.request_tournament_list(), Message::NewSnapshot(snapshot) => { @@ -684,21 +779,25 @@ impl Application for RefBoxApp { secs, timeout, } => { - let dur = match self.app_state { + let (dur, large_max) = match self.app_state { AppState::TimeEdit(_, ref mut game_dur, ref mut timeout_dur) => { if timeout { - timeout_dur.as_mut().unwrap() + (timeout_dur.as_mut().unwrap(), false) } else { - game_dur + (game_dur, true) } } - AppState::ParameterEditor(_, ref mut dur) => dur, - AppState::KeypadPage(KeypadPage::TeamTimeouts(ref mut dur), _) => dur, + AppState::ParameterEditor(_, ref mut dur) => (dur, false), + AppState::KeypadPage(KeypadPage::TeamTimeouts(ref mut dur), _) => (dur, false), _ => unreachable!(), }; if increase { *dur = min( - Duration::from_secs(5999), + Duration::from_secs(if large_max { + MAX_LONG_STRINGABLE_SECS as u64 + } else { + MAX_STRINGABLE_SECS as u64 + }), dur.saturating_add(Duration::from_secs(secs)), ); } else { @@ -1072,6 +1171,12 @@ impl Application for RefBoxApp { start_time, }); + if edited_settings.using_uwhscores { + tm.apply_next_game_start(Instant::now()).unwrap(); + } else { + tm.clear_scheduled_game_start(); + } + let edited_settings = self.edited_settings.take().unwrap(); self.config.hardware.white_on_right = edited_settings.white_on_right; self.using_uwhscores = edited_settings.using_uwhscores; @@ -1117,6 +1222,11 @@ impl Application for RefBoxApp { }; tm.set_next_game(next_game_info); + + if edited_settings.using_uwhscores { + tm.apply_next_game_start(Instant::now()).unwrap(); + } + AppState::MainPage } } else { @@ -1175,8 +1285,14 @@ impl Application for RefBoxApp { .map(|(i, _)| i) }), ListableParameter::Game => self.games.as_ref().and_then(|games| { + let pool = self + .edited_settings + .as_ref() + .and_then(|edit| edit.current_pool.clone())?; + games .iter() + .filter(|(_, game)| game.pool == pool) .enumerate() .find(|(_, (gid, _))| { **gid == self.edited_settings.as_ref().unwrap().game_number @@ -1304,6 +1420,12 @@ impl Application for RefBoxApp { start_time, }); + if edited_settings.using_uwhscores { + tm.apply_next_game_start(Instant::now()).unwrap(); + } else { + tm.clear_scheduled_game_start(); + } + self.config.hardware.white_on_right = edited_settings.white_on_right; self.using_uwhscores = edited_settings.using_uwhscores; self.current_tid = edited_settings.current_tid; diff --git a/uwh-refbox/src/app/view_builders.rs b/uwh-refbox/src/app/view_builders.rs index dc364f98..42e3c4bd 100644 --- a/uwh-refbox/src/app/view_builders.rs +++ b/uwh-refbox/src/app/view_builders.rs @@ -29,7 +29,7 @@ use uwh_common::{ Color as GameColor, GamePeriod, GameSnapshot, PenaltySnapshot, PenaltyTime, TimeoutSnapshot, }, }; -use uwh_matrix_drawing::secs_to_time_string; +use uwh_matrix_drawing::{secs_to_long_time_string, secs_to_time_string}; pub(super) fn build_main_view<'a>( snapshot: &GameSnapshot, @@ -1912,7 +1912,7 @@ fn make_game_time_button<'a>( TimeoutSnapshot::None => None, }; - let time_text = secs_to_time_string(snapshot.secs_in_period); + let time_text = secs_to_long_time_string(snapshot.secs_in_period); let time_text = time_text.trim(); if tall { @@ -1959,6 +1959,8 @@ fn make_time_editor<'a, T: Into>( time: Duration, timeout: bool, ) -> Container<'a, Message> { + let wide = time > Duration::from_secs(MAX_STRINGABLE_SECS as u64); + container( column() .spacing(SPACING) @@ -1994,7 +1996,7 @@ fn make_time_editor<'a, T: Into>( text(time_string(time)) .size(LARGE_TEXT) .horizontal_alignment(Horizontal::Center) - .width(Length::Units(200)), + .width(Length::Units(if wide { 300 } else { 200 })), ) .push( column() @@ -2025,7 +2027,7 @@ fn make_time_editor<'a, T: Into>( } fn time_string(time: Duration) -> String { - secs_to_time_string(time.as_secs()).trim().to_string() + secs_to_long_time_string(time.as_secs()).trim().to_string() } fn timeout_time_string(snapshot: &GameSnapshot) -> String { diff --git a/uwh-refbox/src/tournament_manager.rs b/uwh-refbox/src/tournament_manager.rs index ca5b4661..7f46cbd3 100644 --- a/uwh-refbox/src/tournament_manager.rs +++ b/uwh-refbox/src/tournament_manager.rs @@ -13,6 +13,7 @@ use tokio::{ }; use uwh_common::{ config::Game as GameConfig, + drawing_support::*, game_snapshot::{ Color, GamePeriod, GameSnapshot, PenaltySnapshot, PenaltyTime, TimeoutSnapshot, }, @@ -20,7 +21,7 @@ use uwh_common::{ use crate::uwhscores::TimingRules; -const MAX_TIME_VAL: Duration = Duration::from_secs(5999); // 99:59 is the largest displayable value +const MAX_TIME_VAL: Duration = Duration::from_secs(MAX_LONG_STRINGABLE_SECS as u64); #[derive(Debug)] pub struct TournamentManager { @@ -144,6 +145,10 @@ impl TournamentManager { Ok(()) } + pub fn clear_scheduled_game_start(&mut self) { + self.next_scheduled_start = None; + } + pub fn game_number(&self) -> u32 { self.game_number } @@ -654,19 +659,8 @@ impl TournamentManager { Ok(()) } - fn end_game(&mut self, now: Instant) { - let was_running = self.clock_is_running(); - - self.current_period = GamePeriod::BetweenGames; - - info!( - "{} Ending game {}. Score is B({}), W({})", - self.status_string(now), - self.game_number, - self.b_score, - self.w_score - ); - + fn calc_time_to_next_game(&self, now: Instant, from_time: Instant) -> Duration { + info!("Next game info is: {:?}", self.next_game); let scheduled_start = if let Some(start_time) = self.next_game.as_ref().and_then(|info| info.start_time) { let cur_time = OffsetDateTime::now_utc().to_offset(self.timezone); @@ -690,6 +684,60 @@ impl TournamentManager { .unwrap_or(now + self.config.nominal_break) }; + let time_remaining_at_start = + if let Some(time_until_start) = scheduled_start.checked_duration_since(from_time) { + max(time_until_start, self.config.minimum_break) + } else { + self.config.minimum_break + }; + + // Make sure the value isn't too big + min(time_remaining_at_start, MAX_TIME_VAL) + } + + pub fn apply_next_game_start(&mut self, now: Instant) -> Result<()> { + if self.current_period != GamePeriod::BetweenGames { + return Err(TournamentManagerError::GameInProgress); + } + + let next_game_info = if let Some(info) = self.next_game.as_ref() { + info + } else { + return Err(TournamentManagerError::NoNextGameInfo); + }; + + if let Some(ref timing) = next_game_info.timing { + self.config = timing.clone().into(); + } + + let time_remaining_at_start = self.calc_time_to_next_game(now, now); + + info!( + "{} Setting between games time based on uwhscores info: {time_remaining_at_start:?}", + self.status_string(now), + ); + + self.clock_state = ClockState::CountingDown { + start_time: now, + time_remaining_at_start, + }; + + Ok(()) + } + + fn end_game(&mut self, now: Instant) { + let was_running = self.clock_is_running(); + + self.current_period = GamePeriod::BetweenGames; + + info!( + "{} Ending game {}. Score is B({}), W({})", + self.status_string(now), + self.game_number, + self.b_score, + self.w_score + ); + let game_end = match self.clock_state { ClockState::CountingDown { start_time, @@ -698,15 +746,7 @@ impl TournamentManager { ClockState::CountingUp { .. } | ClockState::Stopped { .. } => now, }; - let time_remaining_at_start = - if let Some(time_until_start) = scheduled_start.checked_duration_since(game_end) { - max(time_until_start, self.config.minimum_break) - } else { - self.config.minimum_break - }; - - // Make sure the value isn't too big - let time_remaining_at_start = min(time_remaining_at_start, MAX_TIME_VAL); + let time_remaining_at_start = self.calc_time_to_next_game(now, game_end); info!( "{} Entering between games, time to next game is {time_remaining_at_start:?}", @@ -1298,7 +1338,7 @@ impl TournamentManager { is_old_game: !self.has_reset, game_number: self.game_number(), next_game_number: self.next_game_number(), - tournament_id: 0, // TODO: placeholder + tournament_id: 0, }) } @@ -1634,6 +1674,8 @@ pub enum TournamentManagerError { InvalidIndex(Color, usize), #[error("Can't halt game from the current state")] InvalidState, + #[error("Next Game Info is needed to perform this action")] + NoNextGameInfo, } pub type Result = std::result::Result;