freenukum 0.4.0

A clone of the 1991 DOS game Duke Nukem 1
Documentation
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-FileCopyrightText: Wolfgang Silbermayr <wolfgang@silbermayr.at>

use anyhow::{Error, Result};
use clap::Parser;
use freenukum::data::original_data_dir;
use freenukum::graphics::load_default_font;
use freenukum::hero::Hero;
use freenukum::level::{tiles::Tile, Level};
use freenukum::rendering::{CanvasRenderer, MovePositionRenderer};
use freenukum::settings::Settings;
use freenukum::tilecache::TileCache;
use freenukum::UserEvent;
use freenukum::{
    game, DefaultSizes, Sizes, LEVEL_HEIGHT, LEVEL_WIDTH, WINDOW_HEIGHT,
    WINDOW_WIDTH,
};
use sdl2::{
    event::{Event, WindowEvent},
    keyboard::Keycode,
    mouse::MouseButton,
    pixels::Color,
    rect::Rect,
};
use std::fs::File;
use std::num::NonZeroUsize;
use std::num::ParseIntError;

/// Show an original Duke Nukem 1 level.
#[derive(Parser, Debug)]
struct Arguments {
    /// The number of the level in hexadecimal format.
    /// This is usually in the range from 1 to 'c'.
    #[clap(parse(try_from_str=parse_hex))]
    level_number: usize,

    /// The episode number. Usually in the range 1 to 3.
    #[clap(default_value = "1", long, name = "EPISODE_NUMBER")]
    episode: NonZeroUsize,
}

fn parse_hex(src: &str) -> Result<usize, ParseIntError> {
    usize::from_str_radix(src, 16)
}

fn main() -> Result<()> {
    const VERSION: &str = env!("CARGO_PKG_VERSION");
    let args = Arguments::parse();

    let settings = Settings::load_or_create();
    let sdl_context = sdl2::init().map_err(Error::msg)?;
    let video_subsystem = sdl_context.video().map_err(Error::msg)?;
    let ttf_context = sdl2::ttf::init()?;
    let event_subsystem = sdl_context.event().map_err(Error::msg)?;
    let mut event_pump = sdl_context.event_pump().map_err(Error::msg)?;
    event_subsystem
        .register_custom_event::<UserEvent>()
        .map_err(Error::msg)?;

    let event_sender = event_subsystem.event_sender();

    let window = game::create_window(
        WINDOW_WIDTH,
        WINDOW_HEIGHT,
        settings.fullscreen,
        &format!("Freenukum {} level loader example", VERSION),
        &video_subsystem,
    )?;
    let (win_w, win_h) = window.size();

    let mut canvas = window.into_canvas().present_vsync().build()?;
    canvas.set_draw_color(Color::RGB(0, 0, 0));
    canvas.clear();
    canvas.present();
    let texture_creator = canvas.texture_creator();

    let mut episodes = game::check_episodes(
        &mut canvas,
        &load_default_font(&ttf_context)?,
        &texture_creator,
        &mut event_pump,
    )?;

    let tilecache = TileCache::load_from_path(&original_data_dir())?;
    episodes.switch_to(args.episode.get() - 1)?;

    let level_file = format!(
        "worldal{:1x}.{}",
        args.level_number,
        episodes.file_extension()
    );

    let mut file = File::open(&original_data_dir().join(level_file))?;
    let sizes = DefaultSizes;

    let mut hero = Hero::new(&sizes);
    let mut level = Level::load(&mut file, &mut hero, &sizes)?;

    let mut r = Rect::new(0, 0, win_w, win_h);
    let level_rect = Rect::new(
        0,
        0,
        sizes.width() * LEVEL_WIDTH,
        sizes.height() * LEVEL_HEIGHT,
    );

    event_sender
        .push_custom_event(UserEvent::Redraw)
        .map_err(Error::msg)?;

    let mut multiply = 10;
    'event_loop: loop {
        match event_pump.wait_event() {
            Event::KeyDown {
                keycode: Some(Keycode::Up),
                ..
            } => {
                let (x, y) = (0, -1);
                scroll(x * multiply, y * multiply, &mut r, level_rect);
                event_sender
                    .push_custom_event(UserEvent::Redraw)
                    .map_err(Error::msg)?;
            }
            Event::KeyDown {
                keycode: Some(Keycode::Down),
                ..
            } => {
                let (x, y) = (0, 1);
                scroll(x * multiply, y * multiply, &mut r, level_rect);
                event_sender
                    .push_custom_event(UserEvent::Redraw)
                    .map_err(Error::msg)?;
            }
            Event::KeyDown {
                keycode: Some(Keycode::Left),
                ..
            } => {
                let (x, y) = (-1, 0);
                scroll(x * multiply, y * multiply, &mut r, level_rect);
                event_sender
                    .push_custom_event(UserEvent::Redraw)
                    .map_err(Error::msg)?;
            }
            Event::KeyDown {
                keycode: Some(Keycode::Right),
                ..
            } => {
                let (x, y) = (1, 0);
                scroll(x * multiply, y * multiply, &mut r, level_rect);
                event_sender
                    .push_custom_event(UserEvent::Redraw)
                    .map_err(Error::msg)?;
            }
            Event::KeyDown {
                keycode: Some(Keycode::LShift),
                ..
            } => {
                multiply = 50;
            }
            Event::KeyUp {
                keycode: Some(Keycode::LShift),
                ..
            } => {
                multiply = 10;
            }
            Event::MouseButtonDown {
                mouse_btn: MouseButton::Left,
                x,
                y,
                ..
            } => {
                let global_x = r.x() + x;
                let global_y = r.y() + y;
                let tile_x = global_x / sizes.width() as i32;
                let tile_y = global_y / sizes.height() as i32;

                if let Ok(Tile {
                    raw_number,
                    effective_number,
                    solid,
                }) = level.tiles.get(tile_x, tile_y)
                {
                    println!(
                    "Tile at (x={}, y={}): (Raw: 0x{:04x}, Effective: 0x{:04x}). Solid: {}.",
                    tile_x, tile_y, raw_number, effective_number,solid
                );
                }
            }
            Event::Quit { .. }
            | Event::KeyDown {
                keycode: Some(Keycode::Escape),
                ..
            }
            | Event::KeyDown {
                keycode: Some(Keycode::Q),
                ..
            } => break 'event_loop,
            Event::Window {
                win_event: WindowEvent::Exposed,
                ..
            }
            | Event::Window {
                win_event: WindowEvent::Shown,
                ..
            } => canvas.present(),
            e if e.is_user_event() => {
                if e.as_user_event_type::<UserEvent>()
                    == Some(UserEvent::Redraw)
                {
                    let mut renderer = CanvasRenderer {
                        canvas: &mut canvas,
                        texture_creator: &texture_creator,
                        tileprovider: &tilecache,
                    };
                    let mut renderer = MovePositionRenderer {
                        offset_x: -r.x(),
                        offset_y: -r.y(),
                        upstream: &mut renderer,
                    };
                    level.render(
                        &mut renderer,
                        &sizes,
                        &mut hero,
                        settings.draw_collision_bounds,
                        r,
                        None,
                        None,
                    )?;
                }
                canvas.present()
            }
            _ => {}
        }
    }
    Ok(())
}

fn scroll(x_dist: i32, y_dist: i32, r: &mut Rect, level_rect: Rect) {
    r.offset(x_dist, y_dist);

    if r.left() < 0 {
        r.set_x(0);
    }
    if r.right() > level_rect.right() {
        r.set_right(level_rect.right());
    }

    if r.top() < 0 {
        r.set_y(0);
    }
    if r.bottom() > level_rect.bottom() {
        r.set_bottom(level_rect.bottom());
    }
}