use futures::Future; use serde::Deserialize; use twitch_api2::helix::points::{UpdateCustomRewardRequest, UpdateCustomRewardBody}; use warp::{Filter, Reply, filters::BoxedFilter, Rejection}; use crate::app::{ State, web::CustomRejection, }; use std::sync::Arc; pub fn livesplit_routes(state: Arc) -> BoxedFilter<(impl Reply, )> { warp::post() .and( livesplit_start(Arc::clone(&state)) .or(livesplit_split(Arc::clone(&state))) .or(livesplit_reset(Arc::clone(&state))) .or(livesplit_finish(Arc::clone(&state))) ) .boxed() } struct RewardPauseInfo { id: &'static str, games: &'static [(&'static str, isize)], } const REWARDS: &[RewardPauseInfo] = &[ // reset RewardPauseInfo { id: "8ac47f16-2396-4c2d-9c43-d30bd9074a4f", games: &[ // in RCT1, only enable after the third split ("RollerCoaster Tycoon 1", 3), ], }, // no reset RewardPauseInfo { id: "0da50268-3237-4832-9849-9baac518e4de", games: &[ // in RCT1, only enable after the third split ("RollerCoaster Tycoon 1", 3), ], }, ]; async fn set_reward_paused(state: Arc, id: String, paused: bool) -> anyhow::Result<()> { let request = UpdateCustomRewardRequest::builder() .broadcaster_id(state.user_config.twitch.channel_id.to_string()) .id(id) .build(); let body = UpdateCustomRewardBody::builder() .is_paused(paused) .build(); state.twitch.client.helix.req_patch(request, body, &state.twitch.user_token).await?; Ok(()) } async fn set_rewards_paused(state: Arc, data: LiveSplitBody, paused: bool) -> Vec> { let mut results = Vec::with_capacity(REWARDS.len()); for info in REWARDS { let is_paused = state.rewards_paused.read().await.get(info.id).copied(); let should_apply = match info.games.iter().find(|(name, _)| data.run.game_name.as_deref().map(|run_name| run_name == *name).unwrap_or_default()) { Some((_, split_idx)) => { // if we're unpausing and the current split index is gte to the configured index for this game if !paused && data.current_split_index >= *split_idx { true } else if !paused && data.current_split_index < *split_idx { false } else { true } }, _ => true, }; if !should_apply || is_paused == Some(paused) { continue; } state.rewards_paused.write().await.insert(info.id.to_string(), paused); results.push(set_reward_paused(Arc::clone(&state), info.id.to_string(), paused).await); } results } fn rewards_filter(state: Arc, data: LiveSplitBody, paused: bool) -> impl Future> { async move { for result in set_rewards_paused(state, data, paused).await { if result.is_err() { return Err(warp::reject::custom(CustomRejection::TwitchError)); } } Ok(()) } } fn livesplit_start(state: Arc) -> BoxedFilter<(impl Reply, )> { livesplit_route(state, "start", false) } fn livesplit_split(state: Arc) -> BoxedFilter<(impl Reply, )> { livesplit_route(state, "split", false) } fn livesplit_reset(state: Arc) -> BoxedFilter<(impl Reply, )> { livesplit_route(state, "reset", true) } fn livesplit_finish(state: Arc) -> BoxedFilter<(impl Reply, )> { livesplit_route(state, "finish", true) } fn livesplit_route(state: Arc, path: &'static str, paused: bool) -> BoxedFilter<(impl Reply,)> { warp::path("livesplit") .and(warp::path(path)) .and(warp::path::end()) .and(warp::body::json()) .and_then(move |body: LiveSplitBody| { let state = Arc::clone(&state); rewards_filter(state, body, paused) }) .untuple_one() .map(|| warp::reply()) .boxed() } #[derive(Deserialize)] #[serde(rename_all = "PascalCase")] #[allow(dead_code)] struct LiveSplitBody { current_phase: usize, run: LiveSplitRun, attempt_started: LiveSplitAtomicTime, attempt_ended: LiveSplitAtomicTime, current_split_index: isize, current_split_name: Option, current_time: LiveSplitTime, current_attempt_duration: String, current_timing_method: usize, } #[derive(Deserialize)] #[serde(rename_all = "PascalCase")] #[allow(dead_code)] struct LiveSplitRun { game_name: Option, category_name: Option, offset: String, attempt_count: usize, segment_names: Vec, } #[derive(Deserialize)] #[serde(rename_all = "PascalCase")] #[allow(dead_code)] struct LiveSplitAtomicTime { time: String, synced_with_atomic_clock: bool, } #[derive(Deserialize)] #[serde(rename_all = "PascalCase")] #[allow(dead_code)] struct LiveSplitTime { real_time: Option, game_time: Option, }