use std::time::Duration;
use anyhow::{anyhow as e, Result};
use bstr::ByteSlice;
use crate::qw::frame;
use crate::{bytesextra, ktxstats_string, matchdate, serverinfo};
pub fn countdown_duration(data: &[u8]) -> Result<Duration> {
if serverinfo(data).is_ok_and(|s| s.mode == Some("hoonymode".to_string())) {
return Ok(Duration::ZERO);
}
let Some(offset) = data.find(matchdate::MATCHDATE_NEEDLE) else {
return Err(e!("Countdown not found"));
};
Ok(duration_until_offset(data, offset))
}
pub fn demo_duration(data: &[u8]) -> Result<Duration> {
const NEEDLE_MATCH_OVER: [u8; 0x11] = [
0x54, 0x68, 0x65, 0x20, 0x6D, 0x61, 0x74, 0x63, 0x68, 0x20, 0x69, 0x73, 0x20, 0x6F, 0x76,
0x65, 0x72, ];
const NEEDLE_STANDBY: [u8; 0x10] = [
0x34, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x00, 0x53, 0x74, 0x61, 0x6E, 0x64, 0x62, 0x79,
0x00, ];
let offset = data
.rfind(NEEDLE_MATCH_OVER)
.unwrap_or_else(|| data.rfind(NEEDLE_STANDBY).unwrap_or(data.len()));
Ok(duration_until_offset(data, offset))
}
pub fn match_duration(data: &[u8]) -> Result<Duration> {
if serverinfo(data).is_ok_and(|s| s.mode == Some("hoonymode".to_string())) {
return match_duration_from_seeking(data);
}
match_duration_from_ktxstats(data).or_else(|_| match_duration_from_seeking(data))
}
pub fn match_duration_from_seeking(data: &[u8]) -> Result<Duration> {
let end = demo_duration(data)?;
let begin = countdown_duration(data).unwrap_or(Duration::ZERO);
Ok(end - begin)
}
pub fn match_duration_from_ktxstats(data: &[u8]) -> Result<Duration> {
let ktxstats_s = ktxstats_string(data)?;
let Some((from, to)) =
bytesextra::offsets_between(ktxstats_s.as_bytes(), br#""duration": "#, b",")
else {
return Err(e!("Duration not found in ktxstats"));
};
let duration_f: f64 = ktxstats_s[from..to].parse()?;
Ok(Duration::from_secs_f64(duration_f))
}
fn duration_until_offset(data: &[u8], target_offset: usize) -> Duration {
let mut index = 0;
let mut total_ms: u32 = 0;
while let Ok(frame_info) = frame::Info::from_data_and_index(data, index) {
if index >= target_offset {
break;
}
total_ms += frame_info.duration;
index += frame_info.size;
}
Duration::from_secs_f32(total_ms as f32 / 1000.0)
}
#[cfg(test)]
mod tests {
use std::fs::read;
use anyhow::Result;
use pretty_assertions::assert_eq;
use super::*;
#[test]
#[rustfmt::skip]
fn test_countdown_duration() -> Result<()> {
assert_eq!(countdown_duration(&read("tests/files/1on1_milton_vs_mushi[tron]20240616-1719.mvd")?)?, Duration::ZERO);
assert_eq!(countdown_duration(&read("tests/files/ffa_5[dm4]20240501-1229.mvd")?)?.as_secs(), 10);
assert_eq!(countdown_duration(&read("tests/files/duel_equ_vs_kaboom[povdmm4]20240422-1038.mvd")?)?.as_secs(), 10);
assert_eq!(countdown_duration(&read("tests/files/duel_holy_vs_dago[bravado]20240426-1659.mvd")?)?.as_secs(), 10);
assert_eq!(countdown_duration(&read("tests/files/4on4_oeks_vs_tsq[dm2]20240426-1716.mvd")?)?.as_secs(), 10);
assert_eq!(countdown_duration(&read("tests/files/20260220-0409_4on4_pex_vs_red[dm3].mvd")?)?.as_secs(), 10);
assert_eq!(countdown_duration(&read("tests/files/ctf_blue_vs_red[ctf5]20240520-1925.mvd")?)?.as_secs(), 10);
assert_eq!(countdown_duration(&read("tests/files/wipeout_red_vs_blue[q3dm6qw]20240406-2028.mvd")?)?.as_secs(), 10);
Ok(())
}
#[test]
#[rustfmt::skip]
fn test_demo_duration() -> Result<()> {
assert_eq!(demo_duration(&read("tests/files/1on1_milton_vs_mushi[tron]20240616-1719.mvd")?)?.as_secs(), 174);
assert_eq!(demo_duration(&read("tests/files/ffa_5[dm4]20240501-1229.mvd")?)?.as_secs(), 71);
assert_eq!(demo_duration(&read("tests/files/duel_equ_vs_kaboom[povdmm4]20240422-1038.mvd")?)?.as_secs(), 190);
assert_eq!(demo_duration(&read("tests/files/duel_holy_vs_dago[bravado]20240426-1659.mvd")?)?.as_secs(), 610);
assert_eq!(demo_duration(&read("tests/files/4on4_oeks_vs_tsq[dm2]20240426-1716.mvd")?)?.as_secs(), 1210);
assert_eq!(demo_duration(&read("tests/files/20260220-0409_4on4_pex_vs_red[dm3].mvd")?)?.as_secs(), 1210);
assert_eq!(demo_duration(&read("tests/files/ctf_blue_vs_red[ctf5]20240520-1925.mvd")?)?.as_secs(), 610);
assert_eq!(demo_duration(&read( "tests/files/wipeout_red_vs_blue[q3dm6qw]20240406-2028.mvd")?)?.as_secs(), 231);
Ok(())
}
#[test]
#[rustfmt::skip]
fn test_match_duration() -> Result<()> {
assert_eq!(match_duration(&read("tests/files/ffa_5[dm4]20240501-1229.mvd")?)?.as_secs(), 61);
assert_eq!(match_duration(&read("tests/files/duel_equ_vs_kaboom[povdmm4]20240422-1038.mvd")?)?.as_secs(), 180);
assert_eq!(match_duration(&read("tests/files/duel_holy_vs_dago[bravado]20240426-1659.mvd")?)?.as_secs(), 600);
assert_eq!(match_duration(&read("tests/files/4on4_oeks_vs_tsq[dm2]20240426-1716.mvd")?)?.as_secs(), 1200);
assert_eq!(match_duration(&read("tests/files/20260220-0409_4on4_pex_vs_red[dm3].mvd")?)?.as_secs(), 1200);
assert_eq!(match_duration(&read("tests/files/ctf_blue_vs_red[ctf5]20240520-1925.mvd")?)?.as_secs(), 600);
assert_eq!(match_duration(&read("tests/files/wipeout_red_vs_blue[q3dm6qw]20240406-2028.mvd")?)?.as_secs(), 221);
Ok(())
}
}