diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/bitmap.rs | 191 | ||||
| -rw-r--r-- | src/grp.rs | 45 | ||||
| -rw-r--r-- | src/main.rs | 13 | ||||
| -rw-r--r-- | src/renderer.rs | 47 | ||||
| -rw-r--r-- | src/timer.rs | 49 | ||||
| -rw-r--r-- | src/world.rs | 187 |
6 files changed, 302 insertions, 230 deletions
diff --git a/src/bitmap.rs b/src/bitmap.rs index 8773a66..d9ec391 100644 --- a/src/bitmap.rs +++ b/src/bitmap.rs @@ -23,7 +23,29 @@ use std::io::{Cursor, Read, Seek, SeekFrom}; use self::byteorder::{LE, ReadBytesExt}; use grp::GroupManager; -/// Rectangular chunk of ARGB data. +/// A rectangular chunk of raw ARGB8888 data. That is, each byte carries 8 bits +/// of information about the intensity of a certain color. The following is a +/// list of bitmasks and which color ("channel," in the vernacular) they +/// represent. +/// +/// 0x000000ff - Blue +/// 0x0000ff00 - Green +/// 0x00ff0000 - Red +/// 0xff000000 - Alpha (transparency) +/// +/// # Examples +/// +/// Colors are extracted from each individual integer via bitwise operations. +/// +/// ``` +/// let bitmap = bitmaps.get(277); // Arbitrary choice of tile number. +/// let corner = bitmap.data[0]; +/// +/// let b = corner & 0xff; +/// let g = (corner >> 8) & 0xff; +/// let r = (corner >> 16) & 0xff; +/// let a = (corner >> 24) & 0xff; +/// ``` #[derive(Clone)] pub struct Bitmap { pub width: u16, @@ -31,14 +53,13 @@ pub struct Bitmap { pub data: Vec<u32>, } -/// Manages access to bitmap tiles in a GRP archive. +/// Implementation of a bitmap cache, which is used for obtaining a bitmap +/// conversion of the individual tiles in a group file. pub struct BitmapManager { bitmaps: Vec<Bitmap>, } impl BitmapManager { - // FIXME: Should we have a hardcoded palette to fall back on if there is no - // PALETTE.DAT? That might make sense for Blood. /// Create a new BitmapManager, loading all of the bitmap tiles in the given /// GRP archive. /// @@ -98,22 +119,26 @@ impl BitmapManager { // - 'paletteLoadFromDisk' in EDuke's 'build/src/palette.cpp' // - 'loadpalette' in Build's 'ENGINE.C' // - 'LoadPalette' in Transfusion's 'arttools/art2tga.c' + let palette = if let Some(data) = grp.get("PALETTE.DAT") { let mut palette = Vec::new(); - // FIXME: As of right now it's RGB, not ARGB. for i in 0..256 { - let r = data[(i * 3)] << 2; - let g = data[(i * 3) + 1] << 2; - let b = data[(i * 3) + 2] << 2; - - // FIXME: Also, I'm not a fan of this manual shifting shit. - palette.push(((r as u32) << 16) | ((g as u32) << 8) | (b as u32)); + // The VGA 262,144 color palette appears darker than intended + // when interpreted ARGB8888 output, so the intensity of the + // red, green, and blue channels are scaled by a uniform factor. + let a = (0xff) as u32; + let r = (data[(i * 3)] << 2) as u32; + let g = (data[(i * 3) + 1] << 2) as u32; + let b = (data[(i * 3) + 2] << 2) as u32; + palette.push(a << 24 | r << 16 | g << 8 | b); } palette } else { - bail!("No PALETTE.DAT"); + // FIXME: There should be a default palette to fall back on. Update + // the documentation for this method when that's implemented. + bail!("No PALETTE.DAT in GRP archive."); }; // From BUILDINF.TXT @@ -121,20 +146,20 @@ impl BitmapManager { // All art files must have xxxxx###.ART. When loading an art file you // should keep trying to open new xxxxx###'s, incrementing the number, // until an art file is not found. + for i in 0.. { if let Some(data) = grp.get(&format!("TILES{:03}.ART", i)) { - // FIXME: Hoo, boy. Passing that Error back up the stack is - // probably a pretty bad way of handling an invalid header. - bitmaps.extend_from_slice(&BitmapManager::load_art(data, &palette)?); + // FIXME: Passing the Error back up the stack is suboptimal (?) + let parsed = BitmapManager::load_art(data, &palette)?; + bitmaps.extend_from_slice(&parsed); } else { - // Indicates that there was no TILES000.ART, implying that - // literally NO bitmaps were loaded. That probably isn't right. + // Indicates that we didn't even load TILES000.ART, meaning that + // literally NO bitmaps were loaded. That's a pretty big issue. if i == 0 { - bail!("No TILES000.ART"); + bail!("No TILES000.ART in GRP archive."); } - // But if we get here, it means that we just hit the last - // tilesheet and it's not worth our time to continue checking. + // But if i > 0, we simply hit the last tilesheet. No problem. break; } } @@ -144,9 +169,11 @@ impl BitmapManager { /// Load the tile given by the specified index, or None if no tile with the /// specified index exists. - pub fn get_tile(&self, index: i32) -> Option<&Bitmap> { - if (index as usize) < self.bitmaps.len() { - Some(&self.bitmaps[(index as usize)]) + pub fn get(&self, index: i32) -> Option<&Bitmap> { + let index = index as usize; + + if index < self.bitmaps.len() { + Some(&self.bitmaps[index]) } else { None } @@ -154,6 +181,12 @@ impl BitmapManager { /// Loads the tiles in an TILES###.ART file. fn load_art(data: &[u8], palette: &[u32]) -> Result<Vec<Bitmap>, Box<Error>> { + let len = data.len() as u32; + + if len < 16 { + bail!("ART does not contain a valid header."); + } + let mut data = Cursor::new(data); let mut bitmaps = Vec::new(); @@ -164,6 +197,7 @@ impl BitmapManager { // The first 4 bytes in the art format are the version number. The // current current art version is now 1. If artversion is not 1 then // either it's the wrong art version or something is wrong. + let version = data.read_u32::<LE>()?; if version != 1 { @@ -177,7 +211,8 @@ impl BitmapManager { // would need this variable, but it turned it is was unnecessary. To get // the number of tiles, you should search all art files, and check the // localtilestart and localtileend values for each file. - let _ = data.read_u32::<LE>()?; + + let _count = data.read_u32::<LE>()?; // 3. long localtilestart; // @@ -194,10 +229,15 @@ impl BitmapManager { // TILES001.ART -> localtilestart = 256, localtileend = 511 // TILES002.ART -> localtilestart = 512, localtileend = 767 // TILES003.ART -> localtilestart = 768, localtileend = 1023 + let first_tile_index = data.read_u32::<LE>()?; let last_tile_index = data.read_u32::<LE>()?; let count = last_tile_index - first_tile_index + 1; + if len < 8 * count + 16 { + bail!(format!("Invalid number of tiles (given: {})", count)); + } + // 5. short tilesizx[localtileend-localtilestart+1]; // // This is an array of shorts of all the x dimensions of the tiles in this art @@ -238,49 +278,106 @@ impl BitmapManager { // memory is stored. Example on a 4*4 file: // // Offsets: - // ----------------- - // | 0 | 4 | 8 |12 | - // ----------------- - // | 1 | 5 | 9 |13 | - // ----------------- - // | 2 | 6 |10 |14 | - // ----------------- - // | 3 | 7 |11 |15 | - // ----------------- - let mut data_off = (16 + 2 * count + 2 * count + 4 * count) as u64; - - for i in 0..count { - // The header is 16 bytes, the width and height arrays are both 2 - // bytes per value, and the attribute array is 4 bytes per value. - // The raw bitmap data follows immediately and the bounds of each - // bitmap are inferred from the width and height. - let width_array_off = (2 * i + 16) as u64; - let height_array_off = (2 * i + 2 * count + 16) as u64; + // --------------------- + // | 0 | 4 | 8 | 12 | + // --------------------- + // | 1 | 5 | 9 | 13 | + // --------------------- + // | 2 | 6 | 10 | 14 | + // --------------------- + // | 3 | 7 | 11 | 15 | + // --------------------- + + // 16 bytes for the header, 2 bytes per entry for the array of bitmap + // widths, 2 bytes per entry for the array of bitmap heights, and 4 + // bytes per entry for the array of bitmap attributes. + + let data_off = 16 + 2 * count + 2 * count + 4 * count; + let mut data_off = data_off as u64; + + if (len as u64) < data_off { + bail!(format!("Invalid number of tiles (given: {})", count)); + } + for i in 0..count { + let width_array_off = 2 * i + 16; + let width_array_off = width_array_off as u64; data.seek(SeekFrom::Start(width_array_off))?; - let width = data.read_u16::<LE>()?; + let width = data.read_u16::<LE>()? as usize; + let height_array_off = 2 * i + 2 * count + 16; + let height_array_off = height_array_off as u64; data.seek(SeekFrom::Start(height_array_off))?; - let height = data.read_u16::<LE>()?; + let height = data.read_u16::<LE>()? as usize; - let mut indices = vec![0; (width as usize) * (height as usize)]; + let mut indices = vec![0; width * height]; data.seek(SeekFrom::Start(data_off))?; data.read(&mut indices)?; data_off += indices.len() as u64; let mut data = Vec::new(); - // FIXME: Casting this as usize is just nasty, man. for column in 0..width { for row in 0..height { - let index = indices[(row as usize) * (width as usize) + (column as usize)]; + let index = indices[row * width + column]; data.push(palette[index as usize]); } } + let width = width as u16; + let height = height as u16; + bitmaps.push(Bitmap { width, height, data }); } Ok(bitmaps) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_load_slice() { + // Generated palette blob. All colors in the palette are white (HTML hex + // ffffffff). + let palette = vec![0xffffffff; 256]; + + // Binary blob containing an ART test vector, made by me. Contains one + // tile, a single black pixel. + let data = vec![ + 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, + ]; + + let parsed = BitmapManager::load_art(&data, &palette).unwrap(); + + assert_eq!(parsed.len(), 1); + + let tile = &parsed[0]; + + assert_eq!(tile.width, 1); + assert_eq!(tile.height, 1); + assert_eq!(tile.data[0], 0xffffffff); + } + + #[test] + fn test_incomplete_header() { + let palette = vec![0xffffffff; 256]; + + // Binary blob similar to the ART test vector above, but without the + // actual bitmap data. + let data = vec![ + 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + + if let Ok(_) = BitmapManager::load_art(&data, &palette) { + panic!("Parser accepted invalid ART file."); + } + } +} @@ -22,7 +22,7 @@ use std::error::Error; use std::fs::File; use std::io::Read; -use self::byteorder::{ByteOrder, LittleEndian}; +use self::byteorder::{ByteOrder, LE}; use path::PathManager; // What's the .GRP file format? @@ -40,13 +40,15 @@ use path::PathManager; /// Silverman's original code goes about loading game data. #[derive(Debug)] pub struct GroupManager { - path_manager: PathManager, + paths: PathManager, files: HashMap<String, Vec<u8>>, } impl GroupManager { - pub fn new(path_manager: PathManager) -> GroupManager { - GroupManager { path_manager, files: HashMap::new() } + /// Creates a new GroupManager that uses the given PathManager to resolve + /// the locations of named GRP files. + pub fn new(paths: PathManager) -> GroupManager { + GroupManager { paths, files: HashMap::new() } } /// Loads the contents of an in-memory group file into the cache. @@ -68,14 +70,14 @@ impl GroupManager { bail!("Invalid GRP header."); } - let file_count = LittleEndian::read_u32(&data[12..16]) as usize; + let file_count = LE::read_u32(&data[12..16]) as usize; // 16 bytes for the header, and 16 bytes for each table entry. The raw // data will follow. let data_start = 16 * (file_count + 1) as usize; if data_start >= len { - bail!("Invalid number of files."); + bail!(format!("Invalid number of files. (given: {})", file_count)); } let mut data_off = data_start; @@ -94,7 +96,7 @@ impl GroupManager { }; let size = &data[table_off+12..table_off+16]; - let size = LittleEndian::read_u32(size) as usize; + let size = LE::read_u32(size) as usize; if data_off + size > len { bail!("`data_off >= len` - Table was likely corrupted."); @@ -116,7 +118,7 @@ impl GroupManager { /// /// A return value of 'Err' indicates that the given path did not exist. pub fn load_file(&mut self, name: &str) -> Result<(), Box<Error>> { - if let Some(path) = self.path_manager.find(name) { + if let Some(path) = self.paths.find(name) { let mut file = File::open(path)?; let mut bytes: Vec<u8> = Vec::new(); @@ -136,7 +138,7 @@ impl GroupManager { } #[cfg(test)] -mod grp_tests { +mod tests { use super::*; #[test] @@ -161,12 +163,11 @@ mod grp_tests { 0x01, 0x02, 0x02, 0x03, 0x03, 0x03, ]; - let path_manager = PathManager::new(); - let mut group_manager = GroupManager::new(path_manager); + let paths = PathManager::new(); + let mut group_manager = GroupManager::new(paths); - match group_manager.load_data(&data) { - Err(e) => panic!("{}", e), - Ok(_) => (), + if let Err(e) = group_manager.load_data(&data) { + panic!("{}", e); } let data = match group_manager.get("TESTFILEA") { @@ -205,8 +206,8 @@ mod grp_tests { b'J', b'a', b'k', b'o', b'b', ]; - let path_manager = PathManager::new(); - let mut group_manager = GroupManager::new(path_manager); + let paths = PathManager::new(); + let mut group_manager = GroupManager::new(paths); if let Ok(_) = group_manager.load_data(&data) { panic!("Accepted invalid header."); @@ -225,8 +226,8 @@ mod grp_tests { 0x01, ]; - let path_manager = PathManager::new(); - let mut group_manager = GroupManager::new(path_manager); + let paths = PathManager::new(); + let mut group_manager = GroupManager::new(paths); if let Ok(_) = group_manager.load_data(&data) { panic!("Accepted invalid header."); @@ -246,8 +247,8 @@ mod grp_tests { 0x01, ]; - let path_manager = PathManager::new(); - let mut group_manager = GroupManager::new(path_manager); + let paths = PathManager::new(); + let mut group_manager = GroupManager::new(paths); if let Ok(_) = group_manager.load_data(&data) { panic!("Accepted invalid header."); @@ -266,8 +267,8 @@ mod grp_tests { 0x01, ]; - let path_manager = PathManager::new(); - let mut group_manager = GroupManager::new(path_manager); + let paths = PathManager::new(); + let mut group_manager = GroupManager::new(paths); if let Ok(_) = group_manager.load_data(&data) { panic!("Accepted invalid header."); diff --git a/src/main.rs b/src/main.rs index 3a95beb..7173f47 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,19 +25,18 @@ mod path; mod world; fn main() { - let path_manager = path::PathManager::new(); + let paths = path::PathManager::new(); let filename = "DUKE3D.GRP"; - let mut group_manager = grp::GroupManager::new(path_manager); + let mut groups = grp::GroupManager::new(paths); - if let Err(e) = group_manager.load_file(filename) { + if let Err(e) = groups.load_file(filename) { println!("Couldn't open {}: {}", filename, e); process::exit(1); } - let map = group_manager.get("E1L1.MAP").unwrap(); - let _world = world::World::from_map(map); + let map = groups.get("E1L1.MAP").unwrap(); + let world = world::World::from_map(map).unwrap(); - let bitmap_manager = bitmap::BitmapManager::new(&group_manager).unwrap(); - let _tile = bitmap_manager.get_tile(277).unwrap(); + let bitmaps = bitmap::BitmapManager::new(&groups).unwrap(); } diff --git a/src/renderer.rs b/src/renderer.rs deleted file mode 100644 index 1f0d09f..0000000 --- a/src/renderer.rs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (C) 2018 Jakob L. Kreuze, All Rights Reserved. -// -// This file is part of rebuild. -// -// rebuild is free software: you can redistribute it and/or modify it under the -// terms of the GNU General Public License as published by the Free Software -// Foundation, either version 3 of the License, or (at your option) any later -// version. -// -// rebuild is distributed in the hope that it will be useful, but WITHOUT ANY -// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR -// A PARTICULAR PURPOSE. See the GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along with -// rebuild. If not, see <http://www.gnu.org/licenses/>. - -extern crate sdl2; - -pub trait Renderer { - pub fn new() -> Self; - - // FIXME: Error handling? - pub fn window_title(title: &str); - - pub fn draw_rooms(player_num: i32, smooth_ratio: i32); -} - -// TODO: Document this. -pub struct ClassicRenderer; - -impl Renderer for ClassicRenderer { - // TODO: Document this. - // FIXME: Proper error handling. - pub fn new() -> ClassicRenderer { - // let context = sdl2::init().unwrap(); - // let video_subsystem = context.video().unwrap(); - - // let display = video_subsystem.window("SDL2", 800, 600) - // // .resizable() - // // .build_glium() - // .unwrap(); - - ClassicRenderer { } - } - - pub fn window_title() -} diff --git a/src/timer.rs b/src/timer.rs deleted file mode 100644 index 1ad1ac5..0000000 --- a/src/timer.rs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (C) 2018 Jakob L. Kreuze, All Rights Reserved. -// -// This file is part of rebuild. -// -// rebuild is free software: you can redistribute it and/or modify it under the -// terms of the GNU General Public License as published by the Free Software -// Foundation, either version 3 of the License, or (at your option) any later -// version. -// -// rebuild is distributed in the hope that it will be useful, but WITHOUT ANY -// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR -// A PARTICULAR PURPOSE. See the GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along with -// rebuild. If not, see <http://www.gnu.org/licenses/>. - -extern crate sdl2; - -use sdl2::TimerSubsystem; - -static const FREQUENCY = 1000; - -/// Implementation of a timer for managing game ticks. -pub struct Timer { - last_sample: u32, - ticks_per_second: u32, - ms_per_u64_tick: f64, - - sdl_timer: TimerSubsystem, -} - -impl Timer { - /// Create a new timer with the given frequency. - pub fn new(ticks_per_second: u32, sdl_timer: TimerSubsystem) -> Timer { - let last_sample = sdl_timer.ticks() * tics_per_second / FREQUENCY; - let ms_per_u64_tick = 1000.0 / sdl_timer.performance_frequency(); - Timer { last_sample, ticks_per_second, ms_per_u64_tick, sdl_timer } - } - - // TODO: Document this. - pub fn update(&mut self) { - let ms = self.sdl_timer.ticks(); - let ticks = ms * self.ticks_per_second / FREQUENCY - self.last_sample; - - if ticks > 0 { - self.last_sample += ticks; - } - } -} diff --git a/src/world.rs b/src/world.rs index 34a11f4..113e315 100644 --- a/src/world.rs +++ b/src/world.rs @@ -24,20 +24,67 @@ use std::io::Cursor; use self::byteorder::{LE, ReadBytesExt}; -/// Maintains the current state of the game world - the map geometry and -/// everything contained within it. +/// The basic element of a level, as understood by the BUILD engine - a logical +/// collection of a floor, a ceiling, and some number of walls. +#[derive(Debug)] +pub struct Sector { + first_wall: i16, + wall_count: i16, + visibility: u8, + tags: (i16, i16, i16), + + ceiling_height: i32, + ceiling_slope: i16, + ceiling_status: i16, + ceiling_bitmap: i16, + ceiling_shade: i8, + ceiling_palette: u8, + ceiling_panning: (u8, u8), + + floor_height: i32, + floor_slope: i16, + floor_status: i16, + floor_bitmap: i16, + floor_shade: i8, + floor_palette: u8, + floor_panning: (u8, u8), +} + +/// A "wall," taken to be some line segment as part of a sector's enclosing +/// geometry. Portals, openings between sectors, are represented as walls, even +/// though that isn't in-line with our typical definition of a "wall" in the +/// real world. +#[derive(Debug)] +pub struct Wall { + position: (i32, i32), + + adjacent_wall_index: i16, + opposite_wall_index: i16, + into_sector_index: i16, + + bitmap: i16, + bitmap_overlay: i16, + shade: i8, + palette: u8, + stretch: (u8, u8), + panning: (u8, u8), + + status: i16, + tags: (i16, i16, i16), +} + +/// Maintains and advances the state of the entire game world, indluding the +/// geometry of the current map. #[derive(Debug)] pub struct World { - sectors: Vec<Sector>, - walls: Vec<Wall>, + pub sectors: Vec<Sector>, + pub walls: Vec<Wall>, } impl World { - // TODO: Document this. - // FIXME: Doesn't do any sort of sanity checks on length. + /// Create a new World from the geometry and sprites specified in the given + /// MAP file. pub fn from_map(data: &[u8]) -> Result<World, Box<Error>> { - let mut data = Cursor::new(data); - // From BUILDINF.TXT // // Here is Ken's documentation on the COMPLETE BUILD map format: @@ -72,13 +119,26 @@ impl World { // close(fil); // } + let len = data.len(); + + // This is the absolute minimum possible size for a MAP, containing the + // header and a 0 short for each of the three arrays. This will be + // incremented as we find out more information about the MAP file - + // specifically, the number of sectors, walls, and sprites we expect. + let mut expected_len = 22; + + if len < expected_len { + bail!("File too small to possibly contain valid MAP."); + } + + let mut data = Cursor::new(data); let version = data.read_u32::<LE>()?; if version != 7 { bail!("Unsupported MAP version."); } - // TODO: Use this to position the world's player (?) + // TODO: Use this to position when initializing the player. let _start_x = data.read_i32::<LE>()?; let _start_y = data.read_i32::<LE>()?; let _start_z = data.read_i32::<LE>()?; @@ -137,6 +197,11 @@ impl World { let mut sectors = Vec::new(); let sector_count = data.read_u16::<LE>()?; + expected_len += 40 * (sector_count as usize); + + if len < expected_len { + bail!(format!("Invalid sector count (given: {})", sector_count)); + } for _ in 0..sector_count { let first_wall = data.read_i16::<LE>()?; @@ -235,6 +300,11 @@ impl World { let mut walls = Vec::new(); let wall_count = data.read_u16::<LE>()?; + expected_len += 32 * (wall_count as usize); + + if len < expected_len { + bail!(format!("Invalid wall count (given: {})", wall_count)); + } for _ in 0..wall_count { let position_x = data.read_i32::<LE>()?; @@ -298,19 +368,21 @@ impl World { // } spritetype; // spritetype sprite[4096]; // - // x, y, z - position of sprite - can be defined at center bottom or center + // x, y, z - position of sprite - can be defined at center bottom or + // center + // // cstat: - // bit 0: 1 = Blocking sprite (use with clipmove, getzrange) "B" - // bit 1: 1 = transluscence, 0 = normal "T" - // bit 2: 1 = x-flipped, 0 = normal "F" - // bit 3: 1 = y-flipped, 0 = normal "F" - // bits 5-4: 00 = FACE sprite (default) "R" - // 01 = WALL sprite (like masked walls) - // 10 = FLOOR sprite (parallel to ceilings&floors) - // bit 6: 1 = 1-sided sprite, 0 = normal "1" - // bit 7: 1 = Real centered centering, 0 = foot center "C" - // bit 8: 1 = Blocking sprite (use with hitscan / cliptype 1) "H" - // bit 9: 1 = Transluscence reversing, 0 = normal "T" + // bit 0: 1 = Blocking sprite (use with clipmove, getzrange) "B" + // bit 1: 1 = transluscence, 0 = normal "T" + // bit 2: 1 = x-flipped, 0 = normal "F" + // bit 3: 1 = y-flipped, 0 = normal "F" + // bits 5-4: 00 = FACE sprite (default) "R" + // 01 = WALL sprite (like masked walls) + // 10 = FLOOR sprite (parallel to ceilings&floors) + // bit 6: 1 = 1-sided sprite, 0 = normal "1" + // bit 7: 1 = Real centered centering, 0 = foot center "C" + // bit 8: 1 = Blocking sprite (use with hitscan/cliptype 1) "H" + // bit 9: 1 = Transluscence reversing, 0 = normal "T" // bits 10-14: reserved // bit 15: 1 = Invisible sprite, 0 = not invisible // picnum - texture index into art file @@ -324,10 +396,16 @@ impl World { // statnum - current status of sprite (inactive/monster/bullet, etc.) // // ang - angle the sprite is facing - // owner, xvel, yvel, zvel, lotag, hitag, extra - These variables used by the game programmer only + // owner, xvel, yvel, zvel, lotag, hitag, extra - These variables used + // by the game programmer only let mut sprites = Vec::new(); let sprite_count = data.read_u16::<LE>()?; + expected_len += 44 * (sprite_count as usize); + + if len < expected_len { + bail!(format!("Invalid sprite count (given: {})", sprite_count)); + } for _ in 0..sprite_count { let position_x = data.read_i32::<LE>()?; @@ -380,47 +458,40 @@ impl World { } } -#[derive(Debug)] -struct Sector { - first_wall: i16, - wall_count: i16, - visibility: u8, - tags: (i16, i16, i16), +#[cfg(test)] +mod tests { + use super::*; - ceiling_height: i32, - ceiling_slope: i16, - ceiling_status: i16, - ceiling_bitmap: i16, - ceiling_shade: i8, - ceiling_palette: u8, - ceiling_panning: (u8, u8), - - floor_height: i32, - floor_slope: i16, - floor_status: i16, - floor_bitmap: i16, - floor_shade: i8, - floor_palette: u8, - floor_panning: (u8, u8), -} + #[test] + fn test_load_slice() { + // Binary blob containing a MAP test vector, made by me. Contains an + // arbitrary header, no walls, no sectors, and no sprites. + let data = vec![ + 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, + ]; -#[derive(Debug)] -struct Wall { - position: (i32, i32), + if let Err(e) = World::from_map(&data) { + panic!("{}", e); + } - adjacent_wall_index: i16, - opposite_wall_index: i16, - into_sector_index: i16, + // TODO: Test the contents of the map. + } - bitmap: i16, - bitmap_overlay: i16, - shade: i8, - palette: u8, - stretch: (u8, u8), - panning: (u8, u8), + #[test] + fn test_incomplete_header() { + // Binary blob similar to the MAP test vector above, but with a header + // that would be too small to be valid. + let data = vec![ + 0x07, 0x00, 0x00, 0x00, + ]; - status: i16, - tags: (i16, i16, i16), + if let Ok(_) = World::from_map(&data) { + panic!("Accepted invalid header."); + } + } } #[derive(Debug)] |