use minifb::{Key, Window, WindowOptions};
use neurodoom::demo::Demo;
use neurodoom::engine::ClassicEngine;
use neurodoom::math::{SCREENHEIGHT, SCREENWIDTH};
use neurodoom::render::SemanticClass;
use neurodoom::wad::Wad;
use neurodoom::world::{LevelExit, PeerId};
#[path = "common.rs"]
mod common;
const SCALE: usize = 3;
const DEMO_LUMPS: &[&str] = &["DEMO1", "DEMO2", "DEMO3"];
fn main() {
let (wad_data, wad_path) = common::load_wad()
.unwrap_or_else(|e| panic!("failed to read WAD at {wad_path:?}: {e}",
wad_path = std::env::var_os("DOOM_WAD").unwrap_or_else(|| common::DEFAULT_WAD_PATH.into())));
eprintln!("Loaded WAD {wad_path:?}");
let mut window = Window::new(
"neurodoom demo replay — Space: next, Tab: view, Esc: quit",
SCREENWIDTH * SCALE,
SCREENHEIGHT * SCALE,
WindowOptions::default(),
)
.expect("failed to create window");
window.set_target_fps(35);
let mut fb = vec![0u32; SCREENWIDTH * SCREENHEIGHT];
let mut view_mode: u8 = 0;
let mut lump_idx = 0;
'outer: while window.is_open() && !window.is_key_down(Key::Escape) {
let lump_name = DEMO_LUMPS[lump_idx % DEMO_LUMPS.len()];
let (demo, mut engine) = match load_demo(&wad_data, lump_name) {
Ok(pair) => pair,
Err(e) => {
eprintln!("skipping {lump_name}: {e}");
lump_idx += 1;
continue;
}
};
eprintln!(
"Playing {lump_name}: E{ep}M{map}, skill {s}, {n} tics ({sec:.1}s)",
ep = demo.episode,
map = demo.map,
s = demo.skill,
n = demo.len(),
sec = demo.len() as f32 / 35.0
);
let reason = play_demo(&mut engine, &demo, &mut window, &mut fb, &mut view_mode);
match reason {
StopReason::Quit => break 'outer,
StopReason::Skipped => eprintln!(" -> skipped by user"),
StopReason::PlayerDied { tic } => {
eprintln!(" -> desynced (player died on tic {tic}/{total})", total = demo.len());
}
StopReason::LevelComplete { tic } => {
eprintln!(" -> reached exit on tic {tic}/{total}", total = demo.len());
}
StopReason::Finished => eprintln!(" -> finished"),
}
drain_skip_keys(&mut window);
lump_idx += 1;
}
}
enum StopReason {
Finished,
Quit,
Skipped,
PlayerDied { tic: usize },
LevelComplete { tic: usize },
}
fn play_demo(
engine: &mut ClassicEngine,
demo: &Demo,
window: &mut Window,
fb: &mut [u32],
view_mode: &mut u8,
) -> StopReason {
for (tic, &action) in demo.actions().iter().enumerate() {
if !window.is_open() || window.is_key_down(Key::Escape) {
return StopReason::Quit;
}
if window.is_key_pressed(Key::Space, minifb::KeyRepeat::No)
|| window.is_key_pressed(Key::N, minifb::KeyRepeat::No)
{
return StopReason::Skipped;
}
if window.is_key_pressed(Key::Tab, minifb::KeyRepeat::No) {
*view_mode = (*view_mode + 1) % 3;
}
engine.tick_single(PeerId(0), action);
paint_frame(engine, *view_mode, fb);
window
.update_with_buffer(fb, SCREENWIDTH, SCREENHEIGHT)
.unwrap();
if engine.level_exit() != LevelExit::None {
return StopReason::LevelComplete { tic };
}
if let Some(eid) = engine.world().controlled_by(PeerId(0))
&& let Some(e) = engine.world().get(eid)
&& e.health <= 0
{
return StopReason::PlayerDied { tic };
}
}
StopReason::Finished
}
fn drain_skip_keys(window: &mut Window) {
while window.is_open()
&& !window.is_key_down(Key::Escape)
&& (window.is_key_down(Key::Space)
|| window.is_key_down(Key::N)
|| window.is_key_down(Key::Tab))
{
window.update();
std::thread::sleep(std::time::Duration::from_millis(16));
}
}
fn load_demo(wad_data: &[u8], lump_name: &str) -> Result<(Demo, ClassicEngine), Box<dyn std::error::Error>> {
let wad = Wad::parse(wad_data)?;
let lump_idx = wad
.find_lump(lump_name)
.ok_or_else(|| format!("lump {lump_name} not found"))?;
let demo = Demo::parse(wad.lump_data(lump_idx))?;
let map = format!("E{}M{}", demo.episode, demo.map);
let engine = ClassicEngine::new(wad_data, &map)?;
Ok((demo, engine))
}
fn paint_frame(engine: &ClassicEngine, view_mode: u8, fb: &mut [u32]) {
match view_mode {
1 => {
for (i, &class) in engine.semantic_buffer().iter().enumerate() {
fb[i] = SemanticClass::from_u8(class).to_rgb();
}
}
2 => {
for (i, &d) in engine.depth_buffer().iter().enumerate() {
fb[i] = common::depth_to_rgb(d);
}
}
_ => {
let rgba = engine.framebuffer();
for (i, pixel) in fb.iter_mut().enumerate() {
let base = i * 4;
let r = rgba[base] as u32;
let g = rgba[base + 1] as u32;
let b = rgba[base + 2] as u32;
*pixel = (r << 16) | (g << 8) | b;
}
}
}
}