170 lines
5.0 KiB
Rust
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>,
|
|
}
|