clemsbot/src/app/web/route/livesplit.rs

170 lines
5.0 KiB
Rust

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<State>) -> 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<State>, 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<State>, data: LiveSplitBody, paused: bool) -> Vec<anyhow::Result<()>> {
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<State>, data: LiveSplitBody, paused: bool) -> impl Future<Output=Result<(), Rejection>> {
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<State>) -> BoxedFilter<(impl Reply, )> {
livesplit_route(state, "start", false)
}
fn livesplit_split(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
livesplit_route(state, "split", false)
}
fn livesplit_reset(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
livesplit_route(state, "reset", true)
}
fn livesplit_finish(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
livesplit_route(state, "finish", true)
}
fn livesplit_route(state: Arc<State>, 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<String>,
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<String>,
category_name: Option<String>,
offset: String,
attempt_count: usize,
segment_names: Vec<String>,
}
#[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<String>,
game_time: Option<String>,
}