use alloc::vec::Vec;
use core::fmt;
use crate::rules::PlayerAction;
use crate::types::{Button, Buttons};
const DOOM_1_9: u8 = 109;
const DEMO_TERMINATOR: u8 = 0x80;
const BT_ATTACK: u8 = 1 << 0;
const BT_USE: u8 = 1 << 1;
const BT_CHANGE: u8 = 1 << 2;
const BT_WEAPONMASK: u8 = 0b0011_1000; const BT_WEAPONSHIFT: u32 = 3;
#[derive(Clone, Debug)]
pub struct Demo {
pub skill: u8,
pub episode: u8,
pub map: u8,
pub deathmatch: bool,
pub respawn: bool,
pub fast: bool,
pub no_monsters: bool,
pub console_player: u8,
pub players_in_game: [bool; 4],
actions: Vec<PlayerAction>,
}
impl Demo {
pub fn parse(bytes: &[u8]) -> Result<Self, DemoError> {
let Some(header) = bytes.first_chunk::<13>() else {
return Err(DemoError::TooShort);
};
let [version, skill, episode, map, dm, resp, fast, nomonst,
console_player, p1, p2, p3, p4] = *header;
if version != DOOM_1_9 {
return Err(DemoError::UnsupportedVersion(version));
}
let deathmatch = dm != 0;
let respawn = resp != 0;
let fast = fast != 0;
let no_monsters = nomonst != 0;
let players_in_game = [p1 != 0, p2 != 0, p3 != 0, p4 != 0];
let num_players = players_in_game.iter().filter(|p| **p).count();
if num_players == 0 {
return Err(DemoError::NoPlayers);
}
if let Some(&present) = players_in_game.get(console_player as usize) {
if !present {
return Err(DemoError::ConsolePlayerAbsent);
}
} else {
return Err(DemoError::ConsolePlayerAbsent);
}
let body = bytes.get(13..).unwrap_or_default();
let stride = 4 * num_players;
let console_offset = 4 * console_player as usize;
let mut actions = Vec::new();
let mut i = 0;
loop {
let Some(&first) = body.get(i) else {
break; };
if first == DEMO_TERMINATOR {
break;
}
let tic_start = i + console_offset;
let Some(t) = body.get(tic_start..tic_start + 4) else {
return Err(DemoError::TruncatedTicStream);
};
actions.push(decode_ticcmd(t));
i += stride;
}
Ok(Self {
skill,
episode,
map,
deathmatch,
respawn,
fast,
no_monsters,
console_player,
players_in_game,
actions,
})
}
#[inline]
pub fn actions(&self) -> &[PlayerAction] {
&self.actions
}
#[inline]
pub fn len(&self) -> usize {
self.actions.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.actions.is_empty()
}
}
fn decode_ticcmd(bytes: &[u8]) -> PlayerAction {
let &[fwd, side, angle, raw_buttons, ..] = bytes else {
return PlayerAction::default();
};
let forward = fwd as i8;
let side = side as i8;
let raw_angle = angle as i8;
let mut buttons = Buttons::empty();
if raw_buttons & BT_ATTACK != 0 {
buttons |= Button::Attack;
}
if raw_buttons & BT_USE != 0 {
buttons |= Button::Use;
}
let weapon_select = if raw_buttons & BT_CHANGE != 0 {
(((raw_buttons & BT_WEAPONMASK) >> BT_WEAPONSHIFT) + 1).min(8)
} else {
0
};
PlayerAction {
forward_move: forward,
side_move: side,
angle_turn: (raw_angle as i16) << 8,
buttons,
weapon_select,
}
}
#[derive(Debug)]
pub enum DemoError {
TooShort,
UnsupportedVersion(u8),
NoPlayers,
ConsolePlayerAbsent,
TruncatedTicStream,
}
impl fmt::Display for DemoError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TooShort => write!(f, "demo lump shorter than 13-byte header"),
Self::UnsupportedVersion(v) => {
write!(f, "unsupported demo version 0x{v:02x} (expected 0x6d / Doom 1.9)")
}
Self::NoPlayers => write!(f, "demo header marks no players in game"),
Self::ConsolePlayerAbsent => write!(f, "console_player index is not marked in-game"),
Self::TruncatedTicStream => write!(f, "demo body ended mid-ticcmd (no 0x80 terminator)"),
}
}
}
impl core::error::Error for DemoError {}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::indexing_slicing)] mod tests {
use super::*;
fn hdr(episode: u8, map: u8) -> [u8; 13] {
[
DOOM_1_9, 2, episode, map, 0, 0, 0, 0, 0, 1, 0, 0, 0,
]
}
#[test]
fn parses_empty_stream() {
let mut bytes = hdr(1, 5).to_vec();
bytes.push(DEMO_TERMINATOR);
let d = Demo::parse(&bytes).expect("parse");
assert_eq!(d.episode, 1);
assert_eq!(d.map, 5);
assert!(d.is_empty());
}
#[test]
fn decodes_one_ticcmd() {
let mut bytes = hdr(1, 1).to_vec();
bytes.extend_from_slice(&[10, (-5i8) as u8, 0x10, 0b0001_0111]);
bytes.push(DEMO_TERMINATOR);
let d = Demo::parse(&bytes).expect("parse");
assert_eq!(d.len(), 1);
let a = d.actions()[0];
assert_eq!(a.forward_move, 10);
assert_eq!(a.side_move, -5);
assert_eq!(a.angle_turn, 0x1000);
assert!(a.buttons.contains(Button::Attack));
assert!(a.buttons.contains(Button::Use));
assert_eq!(a.weapon_select, 3); }
#[test]
fn rejects_unknown_version() {
let mut bytes = hdr(1, 1).to_vec();
bytes[0] = 0x6e;
bytes.push(DEMO_TERMINATOR);
assert!(matches!(
Demo::parse(&bytes),
Err(DemoError::UnsupportedVersion(0x6e))
));
}
#[test]
fn rejects_too_short() {
assert!(matches!(Demo::parse(&[0; 5]), Err(DemoError::TooShort)));
}
}