// 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 .
// FIXME: Signedness of 'char' is ambiguous from documentation.
extern crate byteorder;
extern crate simple_error;
use std::error::Error;
use std::io::Cursor;
use self::byteorder::{LE, ReadBytesExt};
/// 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 {
pub sectors: Vec,
pub walls: Vec,
}
impl World {
/// Create a new World from the geometry and sprites specified in the given
/// MAP file.
pub fn from_map(data: &[u8]) -> Result> {
// From BUILDINF.TXT
//
// Here is Ken's documentation on the COMPLETE BUILD map format:
// BUILD engine and editor programmed completely by Ken Silverman
//
// Here's how you should read a BUILD map file:
// {
// fil = open(???);
//
// // Load map version number (current version is 7L)
// read(fil,&mapversion,4);
//
// // Load starting position
// read(fil,posx,4);
// read(fil,posy,4);
// read(fil,posz,4); // Note: Z coordinates are all shifted up 4
// read(fil,ang,2); // All angles are from 0-2047, clockwise
// read(fil,cursectnum,2); // Sector of starting point
//
// // Load all sectors (see sector structure described below)
// read(fil,&numsectors,2);
// read(fil,§or[0],sizeof(sectortype)*numsectors);
//
// // Load all walls (see wall structure described below)
// read(fil,&numwalls,2);
// read(fil,&wall[0],sizeof(walltype)*numwalls);
//
// // Load all sprites (see sprite structure described below)
// read(fil,&numsprites,2);
// read(fil,&sprite[0],sizeof(spritetype)*numsprites);
//
// 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::()?;
if version != 7 {
bail!("Unsupported MAP version.");
}
// TODO: Use this to position when initializing the player.
let _start_x = data.read_i32::()?;
let _start_y = data.read_i32::()?;
let _start_z = data.read_i32::()?;
let _start_angle = data.read_i16::()? & 0x7ff;
let _start_sector = data.read_i16::()?;
// From BUILDINF.TXT:
//
// -------------------------------------------------------------
// | @@@@@@@ @@@@@@@ @@@@@@@ @@@@@@@@ @@@@@@@ @@@@@@@ @@@@@@@ |
// | @@ @@ @@ @@ @@ @@ @@ @@@ @@ |
// | @@@@@@@ @@@@@ @@ @@ @@ @@ @@@@@@@ @@@@@@@ |
// | @@ @@ @@ @@ @@ @@ @@ @@@ @@ |
// | @@@@@@@ @@@@@@@ @@@@@@@ @@ @@@@@@@ @@ @@ @@@@@@@ |
// -------------------------------------------------------------
//
// // sizeof(sectortype) = 40
// typedef struct
// {
// short wallptr, wallnum;
// long ceilingz, floorz;
// short ceilingstat, floorstat;
// short ceilingpicnum, ceilingheinum;
// signed char ceilingshade;
// char ceilingpal, ceilingxpanning, ceilingypanning;
// short floorpicnum, floorheinum;
// signed char floorshade;
// char floorpal, floorxpanning, floorypanning;
// char visibility, filler;
// short lotag, hitag, extra;
// } sectortype;
// sectortype sector[1024];
//
// wallptr - index to first wall of sector
// wallnum - number of walls in sector
// z's - z coordinate (height) at first point of sector
//
// stat's
// bit 0: 1 = parallaxing, 0 = not "P"
// bit 1: 1 = sloped, 0 = not
// bit 2: 1 = swap x&y, 0 = not "F"
// bit 3: 1 = double smooshiness "E"
// bit 4: 1 = x-flip "F"
// bit 5: 1 = y-flip "F"
// bit 6: 1 = Align texture to first wall of sector "R"
// bits 7-15: reserved
//
// picnum's - texture index into art file
// heinum's - slope value (0-parallel to floor, 4096-45 degrees)
// shade's - shade offset of ceiling/floor
// pal's - palette lookup table number (0 - use standard colors)
// panning's - used to align textures or to do texture panning
// visibility - determines how area changes shade relative to distance
// filler - useless byte to make structure aligned
// lotag, hitag, extra - These variables used by the programmer only
let mut sectors = Vec::new();
let sector_count = data.read_u16::()?;
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::()?;
let wall_count = data.read_i16::()?;
let ceiling_height = data.read_i32::()?;
let floor_height = data.read_i32::()?;
let ceiling_status = data.read_i16::()?;
let floor_status = data.read_i16::()?;
let ceiling_bitmap = data.read_i16::()?;
let ceiling_slope = data.read_i16::()?;
let ceiling_shade = data.read_i8()?;
let ceiling_palette = data.read_u8()?;
let ceiling_panning_x = data.read_u8()?;
let ceiling_panning_y = data.read_u8()?;
let floor_bitmap = data.read_i16::()?;
let floor_slope = data.read_i16::()?;
let floor_shade = data.read_i8()?;
let floor_palette = data.read_u8()?;
let floor_panning_x = data.read_u8()?;
let floor_panning_y = data.read_u8()?;
let visibility = data.read_u8()?;
let _padding = data.read_u8()?;
let lotag = data.read_i16::()?;
let hitag = data.read_i16::()?;
let extra = data.read_i16::()?;
sectors.push(Sector {
first_wall,
wall_count,
visibility,
tags: (lotag, hitag, extra),
ceiling_height,
ceiling_slope,
ceiling_status,
ceiling_bitmap,
ceiling_shade,
ceiling_palette,
ceiling_panning: (ceiling_panning_x, ceiling_panning_y),
floor_height,
floor_slope,
floor_status,
floor_bitmap,
floor_shade,
floor_palette,
floor_panning: (floor_panning_x, floor_panning_y),
});
}
// From BUILDINF.TXT:
//
// -----------------------------------------------
// | @@ @@ @@@@@@@@ @@ @@ @@@@@@@ |
// | @@ @@ @@ @@ @@ @@ @@ |
// | @@ @@ @@ @@@@@@@@ @@ @@ @@@@@@@ |
// | @@ @@@@ @@ @@ @@ @@ @@ @@ |
// | @@@ @@@@ @@ @@ @@@@@@@ @@@@@@@ @@@@@@@ |
// ----------------------------------------------|
//
// // sizeof(walltype) = 32
// typedef struct
// {
// long x, y;
// short point2, nextwall, nextsector, cstat;
// short picnum, overpicnum;
// signed char shade;
// char pal, xrepeat, yrepeat, xpanning, ypanning;
// short lotag, hitag, extra;
// } walltype;
// walltype wall[8192];
//
// x, y: Coordinate of left side of wall
// point2: Index to next wall on the right (in same sector)
// nextwall: Index to wall on other side (-1 if there is no sector)
// nextsector: Index to sector on other side (-1 if there is no sector)
// cstat:
// bit 0: 1 = Blocking wall (use with clipmove, getzrange) "B"
// bit 1: 1 = bottoms of invisible walls swapped, 0 = not "2"
// bit 2: 1 = align picture on bottom (for doors), 0 = top "O"
// bit 3: 1 = x-flipped, 0 = normal "F"
// bit 4: 1 = masking wall, 0 = not "M"
// bit 5: 1 = 1-way wall, 0 = not "1"
// bit 6: 1 = Blocking wall (use with hitscan / cliptype 1) "H"
// bit 7: 1 = Transluscence, 0 = not "T"
// bit 8: 1 = y-flipped, 0 = normal "F"
// bit 9: 1 = Transluscence reversing, 0 = normal "T"
// bits 10-15: reserved
// picnum - texture index into art file
// overpicnum - texture index into art file for masked / 1-way walls
// shade - shade offset of wall
// pal - palette lookup table number (0 - use standard colors)
// repeat's - used to change the size of pixels (stretch textures)
// pannings - used to align textures or to do texture panning
// lotag, hitag, extra - These variables used by the programmer only
let mut walls = Vec::new();
let wall_count = data.read_u16::()?;
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::()?;
let position_y = data.read_i32::()?;
let adjacent_wall_index = data.read_i16::()?;
let opposite_wall_index = data.read_i16::()?;
let into_sector_index = data.read_i16::()?;
let status = data.read_i16::()?;
let bitmap = data.read_i16::()?;
let bitmap_overlay = data.read_i16::()?;
let shade = data.read_i8()?;
let palette = data.read_u8()?;
let stretch_x = data.read_u8()?;
let stretch_y = data.read_u8()?;
let panning_x = data.read_u8()?;
let panning_y = data.read_u8()?;
let lotag = data.read_i16::()?;
let hitag = data.read_i16::()?;
let extra = data.read_i16::()?;
walls.push(Wall {
position: (position_x, position_y),
adjacent_wall_index,
opposite_wall_index,
into_sector_index,
bitmap,
bitmap_overlay,
shade,
palette,
stretch: (stretch_x, stretch_y),
panning: (panning_x, panning_y),
status,
tags: (lotag, hitag, extra),
});
}
// From BUILDINF.TXT:
//
// -------------------------------------------------------------
// | @@@@@@@ @@@@@@@ @@@@@@@ @@@@@@ @@@@@@@@ @@@@@@@ @@@@@@@ |
// | @@ @@ @@ @@ @@@ @@ @@ @@ @@ |
// | @@@@@@@ @@@@@@@ @@@@@@@ @@ @@ @@@@@ @@@@@@@ |
// | @@ @@ @@ @@ @@ @@ @@ @@ |
// | @@@@@@@ @@ @@ @@ @@@@@@ @@ @@@@@@@ @@@@@@@ |
// -------------------------------------------------------------
//
// // sizeof(spritetype) = 44
// typedef struct
// {
// long x, y, z;
// short cstat, picnum;
// signed char shade;
// char pal, clipdist, filler;
// unsigned char xrepeat, yrepeat;
// signed char xoffset, yoffset;
// short sectnum, statnum;
// short ang, owner, xvel, yvel, zvel;
// short lotag, hitag, extra;
// } spritetype;
// spritetype sprite[4096];
//
// 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"
// bits 10-14: reserved
// bit 15: 1 = Invisible sprite, 0 = not invisible
// picnum - texture index into art file
// shade - shade offset of sprite
// pal - palette lookup table number (0 - use standard colors)
// clipdist - the size of the movement clipping square (face sprites only)
// filler - useless byte to make structure aligned
// repeat's - used to change the size of pixels (stretch textures)
// offset's - used to center the animation of sprites
// sectnum - current sector of sprite
// 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
let mut sprites = Vec::new();
let sprite_count = data.read_u16::()?;
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::()?;
let position_y = data.read_i32::()?;
let position_z = data.read_i32::()?;
let sprite_status = data.read_i16::()?;
let bitmap = data.read_i16::()?;
let shade = data.read_i8()?;
let palette = data.read_u8()?;
let clip_distance = data.read_u8()?;
let _filler = data.read_u8()?;
let stretch_x = data.read_u8()?;
let stretch_y = data.read_u8()?;
let panning_x = data.read_i8()?;
let panning_y = data.read_i8()?;
let sector_index = data.read_i16::()?;
let entity_status = data.read_i16::()?;
let angle = data.read_i16::()?;
let owner = data.read_i16::()?;
let velocity_x = data.read_i16::()?;
let velocity_y = data.read_i16::()?;
let velocity_z = data.read_i16::()?;
let lotag = data.read_i16::()?;
let hitag = data.read_i16::()?;
let extra = data.read_i16::()?;
sprites.push(Sprite {
position: (position_x, position_y, position_z),
velocity: (velocity_x, velocity_y, velocity_z),
angle,
sector_index,
bitmap,
clip_distance,
shade,
palette,
stretch: (stretch_x, stretch_y),
panning: (panning_x, panning_y),
sprite_status,
entity_status,
owner,
tags: (lotag, hitag, extra),
});
}
Ok(World { sectors, walls })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[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,
];
if let Err(e) = World::from_map(&data) {
panic!("{}", e);
}
// TODO: Test the contents of the map.
}
#[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,
];
if let Ok(_) = World::from_map(&data) {
panic!("Accepted invalid header.");
}
}
}
#[derive(Debug)]
struct Sprite {
position: (i32, i32, i32),
velocity: (i16, i16, i16),
angle: i16,
sector_index: i16,
bitmap: i16,
clip_distance: u8,
shade: i8,
palette: u8,
stretch: (u8, u8),
panning: (i8, i8),
sprite_status: i16,
entity_status: i16,
owner: i16,
tags: (i16, i16, i16),
}