use alloc::{string::String, vec::Vec};
#[cfg(feature = "arbitrary")]
use arbitrary::Arbitrary;
use semver::Version;
use crate::{
GameReplayMetadata, ReplaySerializeError,
consts::{BASE64_ZLIB_FIRST_BYTE, UNCOMPRESSED_FIRST_BYTE, ZLIB_HEADER_FIRST_BYTE},
errors::UnknownReplayKind,
serialize::ReplayEncoder,
};
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ReplayBufferKind {
Base64,
Compressed,
Uncompressed,
}
impl ReplayBufferKind {
#[must_use]
pub const fn is_binary(self) -> bool {
match self {
Self::Base64 => false,
Self::Compressed | Self::Uncompressed => true,
}
}
#[must_use]
pub const fn is_binary_compressed(self) -> bool {
match self {
Self::Compressed => true,
Self::Base64 | Self::Uncompressed => false,
}
}
#[must_use]
pub const fn is_binary_uncompressed(self) -> bool {
match self {
Self::Base64 | Self::Compressed => false,
Self::Uncompressed => true,
}
}
#[must_use]
pub const fn is_base64(self) -> bool {
match self {
Self::Base64 => true,
Self::Compressed | Self::Uncompressed => false,
}
}
#[must_use]
pub const fn is_compressed(self) -> bool {
match self {
Self::Base64 | Self::Compressed => true,
Self::Uncompressed => false,
}
}
pub const fn try_from_first_byte(byte: u8) -> Result<Self, UnknownReplayKind> {
match byte {
UNCOMPRESSED_FIRST_BYTE => Ok(Self::Uncompressed),
ZLIB_HEADER_FIRST_BYTE => Ok(Self::Compressed),
BASE64_ZLIB_FIRST_BYTE => Ok(Self::Base64),
_ => Err(UnknownReplayKind { first_byte: byte }),
}
}
}
#[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum InputParseMode {
Relative,
Absolute,
}
impl InputParseMode {
pub const ABSOLUTE_TIMING_START: Version = Version::new(0, 17, 22);
#[must_use]
pub fn try_infer_from_version(version: &str) -> Option<InputParseMode> {
let lower = version.to_ascii_lowercase();
let lower = lower
.trim_start_matches('v')
.trim_start_matches("alpha")
.trim_start();
if lower.contains("wtf") {
return Some(InputParseMode::Relative);
}
if lower.trim_start().starts_with("unofficial expansion") {
return Some(InputParseMode::Relative);
}
let lower = match lower.find('@') {
Some(idx) => &lower[..idx],
None => lower,
};
let lower = lower.split(' ').next().unwrap_or_default();
let filtered_version: String = lower
.chars()
.filter(|c| c.is_numeric() || *c == '.')
.collect();
let version = Version::parse(&filtered_version);
if let Ok(v) = version {
if v < Self::ABSOLUTE_TIMING_START {
return Some(InputParseMode::Relative);
}
return Some(InputParseMode::Absolute);
}
None
}
#[must_use]
#[deprecated = "this doesn't really fit in with the rest of the codebase that uses VlqReader instead"]
pub fn try_infer_from_input_data(input_slice: &[u64]) -> Option<InputParseMode> {
let mut prev_time = 0;
for &time in input_slice.iter().step_by(2) {
if time < prev_time {
return Some(InputParseMode::Relative);
}
prev_time = time;
}
None
}
}
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[derive(Clone, Debug)]
pub struct EncoderConfig {
pub(crate) replay_kind: ReplayBufferKind,
pub(crate) compression_level: u8,
pub(crate) input_mode_override: Option<InputParseMode>,
}
impl EncoderConfig {
pub const DEFAULT: Self = Self {
replay_kind: ReplayBufferKind::Compressed,
compression_level: 1,
input_mode_override: None,
};
#[must_use]
pub const fn new(replay_kind: ReplayBufferKind) -> Self {
Self::DEFAULT.kind(replay_kind)
}
#[must_use = "this function returns the modified self"]
pub const fn kind(mut self, replay_kind: ReplayBufferKind) -> Self {
self.replay_kind = replay_kind;
self
}
#[must_use]
pub const fn get_kind(&self) -> ReplayBufferKind {
self.replay_kind
}
#[must_use = "this function returns the modified self"]
pub const fn compression_level(mut self, level: u8) -> Self {
self.compression_level = level;
self
}
#[must_use]
pub const fn get_compression_level(&self) -> u8 {
self.compression_level
}
#[must_use = "this function returns the modified self"]
pub const fn input_mode(mut self, mode: Option<InputParseMode>) -> Self {
self.input_mode_override = mode;
self
}
#[must_use]
pub const fn get_input_mode(&self) -> Option<InputParseMode> {
self.input_mode_override
}
#[must_use = "encode a replay with the encoder"]
pub fn build(
&self,
metadata: &GameReplayMetadata,
) -> Result<(ReplayEncoder, Vec<u8>), ReplaySerializeError> {
ReplayEncoder::with_config(metadata, self)
}
}
impl Default for EncoderConfig {
fn default() -> Self {
Self::DEFAULT
}
}
#[cfg(test)]
mod tests {
use fastrand::Rng;
use strum::IntoEnumIterator;
use super::*;
use crate::replay::{
GameInputEvent,
action::{InputAction, InputActionKey, InputActionKind},
};
#[test]
fn test_inferred_mode() {
use InputParseMode::*;
let cases = [
("Techmino is fun!", None),
("Alpha v0.15.1", Some(Relative)),
("V0.16.2", Some(Relative)),
("0.17.22", Some(Absolute)),
("v0.17.6@26fc", Some(Relative)),
("v 1.2.3", Some(Absolute)),
("WTF", Some(Relative)),
("Unofficial Expansion v0.2.1", Some(Relative)),
(
"V0.17.22 IRSv1.1 PASSTHROUGHFIXv1.0 KOSv1.2beta TE:Cv1.0",
Some(Absolute),
),
("V0.17.22 + IRSv1.1.1", Some(Absolute)),
(
"V0.17.22 IRSv1.1 PASSTHROUGHFIXv1.0 KOCv0.1beta TE:Cv1.0",
Some(Absolute),
),
];
for (input, expected) in cases {
assert_eq!(InputParseMode::try_infer_from_version(input), expected);
}
}
#[cfg(feature = "strum")]
#[test]
fn test_event_roundtrip() {
const ROUNDS: usize = 10_000_000;
let mut rng = Rng::with_seed(0x4d59_5df4_d0f3_3173);
for i in 0..ROUNDS {
let kind: InputActionKind = rng.bool().into();
let key = rng.choice(InputActionKey::iter()).unwrap();
let action = InputAction { kind, key };
let frame = rng.u64(0..=GameInputEvent::MAX_FRAME);
let Ok(event) = GameInputEvent::new(frame, action) else {
panic!(
"Failed to create GameInputEvent from args:
Kind: {kind:?} = {kind_discriminant:?}
Key: {key:?} = {key_discriminant:?}
Frame: {frame} = {frame:x}",
kind_discriminant = core::mem::discriminant(&kind),
key_discriminant = core::mem::discriminant(&key),
);
};
let (rt_kind, rt_key, rt_frame) = (event.kind(), event.key(), event.frame());
assert_eq!(kind, rt_kind);
assert_eq!(key, rt_key);
assert_eq!(frame, rt_frame);
if i % 1_000_000 == 0 {
eprintln!("{i} of {ROUNDS}");
}
}
}
}