use crate::error::Result;
use crate::{Chunk, Th, ThBufRead};
use std::collections::HashSet;
pub trait ThStream {
fn header(&mut self) -> Result<&[u8]>;
fn next_chunk(&mut self) -> Result<Chunk>;
fn reset_chunk(&mut self);
}
impl<R: ThBufRead> ThStream for Th<R> {
fn header(&mut self) -> Result<&[u8]> {
self.header()
}
fn next_chunk(&mut self) -> Result<Chunk> {
self.next_chunk()
}
fn reset_chunk(&mut self) {
self.reset_chunk();
}
}
pub struct ThCompat<R: ThBufRead> {
inner: ThCompatEnum<R>,
}
enum ThCompatEnum<R: ThBufRead> {
V2(ThMinor2Compat<R>),
V4(ThMinor4Compat<R>),
Newest(Th<R>),
}
#[derive(Debug, PartialEq)]
enum ThCompatVersion {
V2,
V4,
Newest,
}
impl<R: ThBufRead> ThCompat<R> {
pub fn parse(reader: R) -> Result<Self> {
let mut raw = Th::parse(reader)?;
let header = raw.header()?;
let compat_version = header_version(header);
let inner = match compat_version {
None | Some(ThCompatVersion::V2) => {
ThCompatEnum::V2(ThMinor2Compat::new(ThMinor4Compat::new(raw)))
}
Some(ThCompatVersion::V4) => ThCompatEnum::V4(ThMinor4Compat::new(raw)),
Some(ThCompatVersion::Newest) => ThCompatEnum::Newest(raw),
};
Ok(Self { inner })
}
}
impl<R: ThBufRead> ThStream for ThCompat<R> {
fn header(&mut self) -> Result<&[u8]> {
match &mut self.inner {
ThCompatEnum::V2(inner) => inner.header(),
ThCompatEnum::V4(inner) => inner.header(),
ThCompatEnum::Newest(inner) => inner.header(),
}
}
fn next_chunk(&mut self) -> Result<Chunk> {
match &mut self.inner {
ThCompatEnum::V2(inner) => inner.next_chunk(),
ThCompatEnum::V4(inner) => inner.next_chunk(),
ThCompatEnum::Newest(inner) => inner.next_chunk(),
}
}
fn reset_chunk(&mut self) {
match &mut self.inner {
ThCompatEnum::V2(inner) => inner.reset_chunk(),
ThCompatEnum::V4(inner) => inner.reset_chunk(),
ThCompatEnum::Newest(inner) => inner.reset_chunk(),
}
}
}
fn header_version(slice: &[u8]) -> Option<ThCompatVersion> {
let key_pattern = b"\"version_minor\"";
let key_pos = slice
.windows(key_pattern.len())
.position(|w| w == key_pattern)?;
let after_key_pos = key_pos + key_pattern.len();
let mut remaining = slice[after_key_pos..].iter();
let after_key_len = remaining.len();
loop {
let c = *remaining.next()?;
match c {
b' ' | b'\t' | b'\n' | b'\r' => continue,
b':' => break,
_ => return None,
}
}
loop {
let c = *remaining.next()?;
match c {
b' ' | b'\t' | b'\n' | b'\r' => continue,
b'"' => break,
_ => return None,
}
}
let version_start = after_key_pos + after_key_len - remaining.len();
loop {
let c = *remaining.next()?;
if c == b'"' {
break;
}
}
let version_end = after_key_pos + after_key_len - remaining.len() - 1;
let version_slice = &slice[version_start..version_end];
let version_str = std::str::from_utf8(version_slice).ok()?;
let version: u32 = version_str.parse().ok()?;
Some(match version {
0..=2 => ThCompatVersion::V2,
3..=4 => ThCompatVersion::V4,
_ => ThCompatVersion::Newest,
})
}
enum LastAction2 {
Add(i32),
Remove(i32),
Chunk,
}
struct ThMinor2Compat<R: ThBufRead> {
inner: ThMinor4Compat<R>,
had_player_ready: HashSet<i32>,
last_action: LastAction2,
}
impl<R: ThBufRead> ThMinor2Compat<R> {
fn new(reader: ThMinor4Compat<R>) -> Self {
Self {
inner: reader,
had_player_ready: HashSet::new(),
last_action: LastAction2::Chunk,
}
}
}
impl<R: ThBufRead> ThStream for ThMinor2Compat<R> {
fn header(&mut self) -> Result<&[u8]> {
self.inner.header()
}
fn next_chunk(&mut self) -> Result<Chunk> {
let reader = &mut self.inner as *mut dyn ThStream;
let cid;
match self.inner.next_chunk()? {
Chunk::PlayerNew(p) if !self.had_player_ready.contains(&p.cid) => {
self.had_player_ready.insert(p.cid);
self.last_action = LastAction2::Add(p.cid);
cid = p.cid;
}
next_chunk => {
self.last_action = LastAction2::Chunk;
if let Chunk::Drop(d) = &next_chunk {
self.had_player_ready.remove(&d.cid);
self.last_action = LastAction2::Remove(d.cid);
} else {
self.last_action = LastAction2::Chunk;
}
return Ok(next_chunk);
}
}
unsafe {
(*reader).reset_chunk();
}
Ok(Chunk::PlayerReady { cid })
}
fn reset_chunk(&mut self) {
match self.last_action {
LastAction2::Add(cid) => self.had_player_ready.remove(&cid),
LastAction2::Remove(cid) => self.had_player_ready.insert(cid),
LastAction2::Chunk => false,
};
self.inner.reset_chunk();
}
}
enum LastAction4 {
Add(i32),
Chunk,
}
struct ThMinor4Compat<R: ThBufRead> {
inner: Th<R>,
had_player_join: HashSet<i32>,
last_action: LastAction4,
}
impl<R: ThBufRead> ThMinor4Compat<R> {
fn new(reader: Th<R>) -> Self {
Self {
inner: reader,
had_player_join: Default::default(),
last_action: LastAction4::Chunk,
}
}
}
impl<R: ThBufRead> ThStream for ThMinor4Compat<R> {
fn header(&mut self) -> Result<&[u8]> {
self.inner.header()
}
fn next_chunk(&mut self) -> Result<Chunk> {
let reader = &mut self.inner as *mut dyn ThStream;
let cid;
let next_chunk = self.inner.next_chunk()?;
if let Some(p_cid) = next_chunk.cid() {
cid = p_cid;
if cid < 0 || self.had_player_join.contains(&cid) {
self.last_action = LastAction4::Chunk;
return Ok(next_chunk);
}
if matches!(
next_chunk,
Chunk::Join { cid: _ } | Chunk::JoinVer6 { cid: _ } | Chunk::JoinVer7 { cid: _ }
) {
self.had_player_join.insert(cid);
return Ok(next_chunk);
}
self.last_action = LastAction4::Add(cid);
self.had_player_join.insert(cid);
} else {
self.last_action = LastAction4::Chunk;
return Ok(next_chunk);
}
unsafe {
(*reader).reset_chunk();
}
Ok(Chunk::Join { cid })
}
fn reset_chunk(&mut self) {
match self.last_action {
LastAction4::Add(cid) => {
self.had_player_join.remove(&cid);
}
LastAction4::Chunk => {}
}
self.inner.reset_chunk();
}
}
#[cfg(test)]
mod test {
use super::header_version;
use super::ThCompatVersion;
#[test]
fn header_empty() {
assert_eq!(header_version(b""), None)
}
#[test]
fn header_empty_json() {
assert_eq!(header_version(b"{}"), None)
}
#[test]
fn header_simple() {
assert_eq!(
header_version(b"{\"version_minor\":\"2\"}"),
Some(ThCompatVersion::V2)
)
}
#[test]
fn header_simple2() {
assert_eq!(header_version(b"{\"version_minor\":\"2a\"}"), None)
}
#[test]
fn header_simple3() {
assert_eq!(
header_version(b"{\"version_minor\":\"5\"}"),
Some(ThCompatVersion::Newest)
)
}
#[test]
fn header_whitespace() {
assert_eq!(
header_version(b"{ \"version_minor\"\t\r: \n \"3\" }\n"),
Some(ThCompatVersion::V4)
)
}
}