use super::wad::WadFile;
use super::wad_types::*;
#[derive(Debug, Clone)]
pub struct DoomMap {
pub name: String,
pub vertices: Vec<Vertex>,
pub linedefs: Vec<LineDef>,
pub sidedefs: Vec<SideDef>,
pub sectors: Vec<Sector>,
pub segs: Vec<Seg>,
pub subsectors: Vec<SubSector>,
pub nodes: Vec<Node>,
pub things: Vec<Thing>,
}
#[derive(Debug, Clone, Copy)]
pub struct Vertex {
pub x: f32,
pub y: f32,
}
#[derive(Debug, Clone)]
pub struct LineDef {
pub v1: usize,
pub v2: usize,
pub flags: u16,
pub special: u16,
pub tag: u16,
pub front_sidedef: Option<usize>,
pub back_sidedef: Option<usize>,
}
impl LineDef {
#[inline]
pub fn is_two_sided(&self) -> bool {
self.flags & ML_TWOSIDED != 0
}
#[inline]
pub fn is_blocking(&self) -> bool {
self.flags & ML_BLOCKING != 0
}
pub fn front_sector(&self, sidedefs: &[SideDef]) -> Option<usize> {
self.front_sidedef.map(|s| sidedefs[s].sector)
}
pub fn back_sector(&self, sidedefs: &[SideDef]) -> Option<usize> {
self.back_sidedef.map(|s| sidedefs[s].sector)
}
}
#[derive(Debug, Clone)]
pub struct SideDef {
pub x_offset: f32,
pub y_offset: f32,
pub upper_texture: String,
pub lower_texture: String,
pub middle_texture: String,
pub sector: usize,
}
#[derive(Debug, Clone)]
pub struct Sector {
pub floor_height: f32,
pub ceiling_height: f32,
pub floor_texture: String,
pub ceiling_texture: String,
pub light_level: u16,
pub special: u16,
pub tag: u16,
}
impl Sector {
pub fn is_sky_ceiling(&self) -> bool {
self.ceiling_texture == "F_SKY1"
}
}
#[derive(Debug, Clone, Copy)]
pub struct Seg {
pub v1: usize,
pub v2: usize,
pub angle: f32,
pub linedef: usize,
pub direction: u16,
pub offset: f32,
}
#[derive(Debug, Clone, Copy)]
pub struct SubSector {
pub num_segs: u16,
pub first_seg: usize,
}
#[derive(Debug, Clone, Copy)]
pub struct Node {
pub x: f32,
pub y: f32,
pub dx: f32,
pub dy: f32,
pub bbox_right: [f32; 4],
pub bbox_left: [f32; 4],
pub right_child: NodeChild,
pub left_child: NodeChild,
}
#[derive(Debug, Clone, Copy)]
pub enum NodeChild {
Node(usize),
SubSector(usize),
}
#[derive(Debug, Clone, Copy)]
pub struct Thing {
pub x: f32,
pub y: f32,
pub angle: f32,
pub thing_type: u16,
pub flags: u16,
}
impl DoomMap {
pub fn load(wad: &WadFile, map_name: &str) -> Result<Self, super::wad::WadError> {
let map_idx = wad
.find_lump(map_name)
.ok_or_else(|| super::wad::WadError::LumpNotFound(map_name.to_string()))?;
let things_idx = wad
.find_lump_after("THINGS", map_idx)
.ok_or_else(|| super::wad::WadError::LumpNotFound("THINGS".into()))?;
let linedefs_idx = wad
.find_lump_after("LINEDEFS", map_idx)
.ok_or_else(|| super::wad::WadError::LumpNotFound("LINEDEFS".into()))?;
let sidedefs_idx = wad
.find_lump_after("SIDEDEFS", map_idx)
.ok_or_else(|| super::wad::WadError::LumpNotFound("SIDEDEFS".into()))?;
let vertexes_idx = wad
.find_lump_after("VERTEXES", map_idx)
.ok_or_else(|| super::wad::WadError::LumpNotFound("VERTEXES".into()))?;
let segs_idx = wad
.find_lump_after("SEGS", map_idx)
.ok_or_else(|| super::wad::WadError::LumpNotFound("SEGS".into()))?;
let ssectors_idx = wad
.find_lump_after("SSECTORS", map_idx)
.ok_or_else(|| super::wad::WadError::LumpNotFound("SSECTORS".into()))?;
let nodes_idx = wad
.find_lump_after("NODES", map_idx)
.ok_or_else(|| super::wad::WadError::LumpNotFound("NODES".into()))?;
let sectors_idx = wad
.find_lump_after("SECTORS", map_idx)
.ok_or_else(|| super::wad::WadError::LumpNotFound("SECTORS".into()))?;
let raw_verts = wad.parse_vertices(vertexes_idx);
let raw_linedefs = wad.parse_linedefs(linedefs_idx);
let raw_sidedefs = wad.parse_sidedefs(sidedefs_idx);
let raw_sectors = wad.parse_sectors(sectors_idx);
let raw_segs = wad.parse_segs(segs_idx);
let raw_ssectors = wad.parse_subsectors(ssectors_idx);
let raw_nodes = wad.parse_nodes(nodes_idx);
let raw_things = wad.parse_things(things_idx);
let vertices: Vec<Vertex> = raw_verts
.iter()
.map(|v| Vertex {
x: v.x as f32,
y: v.y as f32,
})
.collect();
let sidedefs: Vec<SideDef> = raw_sidedefs
.iter()
.map(|s| SideDef {
x_offset: s.x_offset as f32,
y_offset: s.y_offset as f32,
upper_texture: s.upper_name(),
lower_texture: s.lower_name(),
middle_texture: s.middle_name(),
sector: s.sector as usize,
})
.collect();
let linedefs: Vec<LineDef> = raw_linedefs
.iter()
.map(|l| LineDef {
v1: l.v1 as usize,
v2: l.v2 as usize,
flags: l.flags,
special: l.special,
tag: l.tag,
front_sidedef: if l.right_sidedef == 0xFFFF {
None
} else {
Some(l.right_sidedef as usize)
},
back_sidedef: if l.left_sidedef == 0xFFFF {
None
} else {
Some(l.left_sidedef as usize)
},
})
.collect();
let sectors: Vec<Sector> = raw_sectors
.iter()
.map(|s| Sector {
floor_height: s.floor_height as f32,
ceiling_height: s.ceiling_height as f32,
floor_texture: s.floor_name(),
ceiling_texture: s.ceiling_name(),
light_level: s.light_level,
special: s.special,
tag: s.tag,
})
.collect();
let segs: Vec<Seg> = raw_segs
.iter()
.map(|s| Seg {
v1: s.v1 as usize,
v2: s.v2 as usize,
angle: (s.angle as f32) * std::f32::consts::PI / 32768.0,
linedef: s.linedef as usize,
direction: s.direction,
offset: s.offset as f32,
})
.collect();
let subsectors: Vec<SubSector> = raw_ssectors
.iter()
.map(|s| SubSector {
num_segs: s.num_segs,
first_seg: s.first_seg as usize,
})
.collect();
let nodes: Vec<Node> = raw_nodes
.iter()
.map(|n| Node {
x: n.x as f32,
y: n.y as f32,
dx: n.dx as f32,
dy: n.dy as f32,
bbox_right: [
n.bbox_right[0] as f32,
n.bbox_right[1] as f32,
n.bbox_right[2] as f32,
n.bbox_right[3] as f32,
],
bbox_left: [
n.bbox_left[0] as f32,
n.bbox_left[1] as f32,
n.bbox_left[2] as f32,
n.bbox_left[3] as f32,
],
right_child: parse_child(n.right_child),
left_child: parse_child(n.left_child),
})
.collect();
let things: Vec<Thing> = raw_things
.iter()
.map(|t| Thing {
x: t.x as f32,
y: t.y as f32,
angle: (t.angle as f32) * std::f32::consts::PI / 180.0,
thing_type: t.thing_type,
flags: t.flags,
})
.collect();
Ok(DoomMap {
name: map_name.to_uppercase(),
vertices,
linedefs,
sidedefs,
sectors,
segs,
subsectors,
nodes,
things,
})
}
pub fn player_start(&self) -> Option<(f32, f32, f32)> {
self.things
.iter()
.find(|t| t.thing_type == THING_PLAYER1)
.map(|t| (t.x, t.y, t.angle))
}
pub fn point_in_subsector(&self, x: f32, y: f32) -> usize {
if self.nodes.is_empty() {
return 0;
}
let mut node_idx = self.nodes.len() - 1; loop {
let node = &self.nodes[node_idx];
let side = super::geometry::point_on_side(x, y, node.x, node.y, node.dx, node.dy);
let child = if side {
node.right_child
} else {
node.left_child
};
match child {
NodeChild::SubSector(ss) => return ss,
NodeChild::Node(n) => node_idx = n,
}
}
}
pub fn point_sector(&self, x: f32, y: f32) -> Option<&Sector> {
let ss_idx = self.point_in_subsector(x, y);
let ss = &self.subsectors[ss_idx];
if ss.num_segs == 0 {
return None;
}
let seg = &self.segs[ss.first_seg];
let linedef = &self.linedefs[seg.linedef];
let sidedef_idx = if seg.direction == 0 {
linedef.front_sidedef?
} else {
linedef.back_sidedef?
};
Some(&self.sectors[self.sidedefs[sidedef_idx].sector])
}
}
fn parse_child(value: u16) -> NodeChild {
if value & NF_SUBSECTOR != 0 {
NodeChild::SubSector((value & !NF_SUBSECTOR) as usize)
} else {
NodeChild::Node(value as usize)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_child_node() {
let NodeChild::Node(n) = parse_child(42) else {
unreachable!("Expected Node variant");
};
assert_eq!(n, 42);
}
#[test]
fn parse_child_subsector() {
let NodeChild::SubSector(s) = parse_child(0x8005) else {
unreachable!("Expected SubSector variant");
};
assert_eq!(s, 5);
}
#[test]
fn parse_child_node_zero() {
let NodeChild::Node(n) = parse_child(0) else {
unreachable!("Expected Node variant");
};
assert_eq!(n, 0);
}
#[test]
fn parse_child_subsector_zero() {
let NodeChild::SubSector(s) = parse_child(NF_SUBSECTOR) else {
unreachable!("Expected SubSector variant");
};
assert_eq!(s, 0);
}
#[test]
fn linedef_is_two_sided() {
let ld = LineDef {
v1: 0,
v2: 1,
flags: ML_TWOSIDED,
special: 0,
tag: 0,
front_sidedef: None,
back_sidedef: None,
};
assert!(ld.is_two_sided());
}
#[test]
fn linedef_not_two_sided() {
let ld = LineDef {
v1: 0,
v2: 1,
flags: 0,
special: 0,
tag: 0,
front_sidedef: None,
back_sidedef: None,
};
assert!(!ld.is_two_sided());
}
#[test]
fn linedef_is_blocking() {
let ld = LineDef {
v1: 0,
v2: 1,
flags: ML_BLOCKING,
special: 0,
tag: 0,
front_sidedef: None,
back_sidedef: None,
};
assert!(ld.is_blocking());
}
#[test]
fn linedef_not_blocking() {
let ld = LineDef {
v1: 0,
v2: 1,
flags: ML_TWOSIDED,
special: 0,
tag: 0,
front_sidedef: None,
back_sidedef: None,
};
assert!(!ld.is_blocking());
}
#[test]
fn linedef_front_sector_some() {
let sidedefs = vec![SideDef {
x_offset: 0.0,
y_offset: 0.0,
upper_texture: String::new(),
lower_texture: String::new(),
middle_texture: String::new(),
sector: 3,
}];
let ld = LineDef {
v1: 0,
v2: 1,
flags: 0,
special: 0,
tag: 0,
front_sidedef: Some(0),
back_sidedef: None,
};
assert_eq!(ld.front_sector(&sidedefs), Some(3));
}
#[test]
fn linedef_front_sector_none() {
let sidedefs: Vec<SideDef> = vec![];
let ld = LineDef {
v1: 0,
v2: 1,
flags: 0,
special: 0,
tag: 0,
front_sidedef: None,
back_sidedef: None,
};
assert_eq!(ld.front_sector(&sidedefs), None);
}
#[test]
fn linedef_back_sector_some() {
let sidedefs = vec![SideDef {
x_offset: 0.0,
y_offset: 0.0,
upper_texture: String::new(),
lower_texture: String::new(),
middle_texture: String::new(),
sector: 7,
}];
let ld = LineDef {
v1: 0,
v2: 1,
flags: 0,
special: 0,
tag: 0,
front_sidedef: None,
back_sidedef: Some(0),
};
assert_eq!(ld.back_sector(&sidedefs), Some(7));
}
#[test]
fn linedef_combined_flags() {
let ld = LineDef {
v1: 0,
v2: 1,
flags: ML_BLOCKING | ML_TWOSIDED,
special: 0,
tag: 0,
front_sidedef: None,
back_sidedef: None,
};
assert!(ld.is_blocking());
assert!(ld.is_two_sided());
}
#[test]
fn sector_is_sky_ceiling() {
let s = Sector {
floor_height: 0.0,
ceiling_height: 128.0,
floor_texture: String::new(),
ceiling_texture: "F_SKY1".into(),
light_level: 200,
special: 0,
tag: 0,
};
assert!(s.is_sky_ceiling());
}
#[test]
fn sector_not_sky_ceiling() {
let s = Sector {
floor_height: 0.0,
ceiling_height: 128.0,
floor_texture: String::new(),
ceiling_texture: "FLAT1".into(),
light_level: 200,
special: 0,
tag: 0,
};
assert!(!s.is_sky_ceiling());
}
#[test]
fn point_in_subsector_empty_nodes() {
let map = DoomMap {
name: "TEST".into(),
vertices: vec![],
linedefs: vec![],
sidedefs: vec![],
sectors: vec![],
segs: vec![],
subsectors: vec![],
nodes: vec![],
things: vec![],
};
assert_eq!(map.point_in_subsector(0.0, 0.0), 0);
}
#[test]
fn vertex_coords() {
let v = Vertex { x: 10.5, y: -20.3 };
assert!((v.x - 10.5).abs() < f32::EPSILON);
assert!((v.y + 20.3).abs() < f32::EPSILON);
}
#[test]
fn thing_fields() {
let t = Thing {
x: 100.0,
y: 200.0,
angle: 1.5,
thing_type: THING_PLAYER1,
flags: 0,
};
assert_eq!(t.thing_type, THING_PLAYER1);
assert!((t.angle - 1.5).abs() < f32::EPSILON);
}
#[test]
fn seg_fields() {
let s = Seg {
v1: 0,
v2: 1,
angle: std::f32::consts::PI,
linedef: 5,
direction: 0,
offset: 10.0,
};
assert_eq!(s.linedef, 5);
assert_eq!(s.direction, 0);
}
#[test]
fn subsector_fields() {
let ss = SubSector {
num_segs: 4,
first_seg: 10,
};
assert_eq!(ss.num_segs, 4);
assert_eq!(ss.first_seg, 10);
}
#[test]
fn player_start_found() {
let map = DoomMap {
name: "TEST".into(),
vertices: vec![],
linedefs: vec![],
sidedefs: vec![],
sectors: vec![],
segs: vec![],
subsectors: vec![],
nodes: vec![],
things: vec![
Thing {
x: 50.0,
y: 60.0,
angle: 0.0,
thing_type: 0,
flags: 0,
},
Thing {
x: 100.0,
y: 200.0,
angle: 1.5,
thing_type: THING_PLAYER1,
flags: 0x07,
},
],
};
let start = map.player_start();
assert!(start.is_some());
let (x, y, a) = start.unwrap();
assert!((x - 100.0).abs() < f32::EPSILON);
assert!((y - 200.0).abs() < f32::EPSILON);
assert!((a - 1.5).abs() < f32::EPSILON);
}
#[test]
fn player_start_not_found() {
let map = DoomMap {
name: "TEST".into(),
vertices: vec![],
linedefs: vec![],
sidedefs: vec![],
sectors: vec![],
segs: vec![],
subsectors: vec![],
nodes: vec![],
things: vec![Thing {
x: 50.0,
y: 60.0,
angle: 0.0,
thing_type: 0,
flags: 0,
}],
};
assert!(map.player_start().is_none());
}
#[test]
fn player_start_empty_things() {
let map = DoomMap {
name: "TEST".into(),
vertices: vec![],
linedefs: vec![],
sidedefs: vec![],
sectors: vec![],
segs: vec![],
subsectors: vec![],
nodes: vec![],
things: vec![],
};
assert!(map.player_start().is_none());
}
#[test]
fn player_start_picks_first() {
let map = DoomMap {
name: "TEST".into(),
vertices: vec![],
linedefs: vec![],
sidedefs: vec![],
sectors: vec![],
segs: vec![],
subsectors: vec![],
nodes: vec![],
things: vec![
Thing {
x: 10.0,
y: 20.0,
angle: 0.0,
thing_type: THING_PLAYER1,
flags: 0,
},
Thing {
x: 99.0,
y: 99.0,
angle: 3.0,
thing_type: THING_PLAYER1,
flags: 0,
},
],
};
let (x, y, _) = map.player_start().unwrap();
assert!(
(x - 10.0).abs() < f32::EPSILON,
"should pick first player start"
);
assert!((y - 20.0).abs() < f32::EPSILON);
}
#[test]
fn linedef_back_sector_none() {
let sidedefs: Vec<SideDef> = vec![];
let ld = LineDef {
v1: 0,
v2: 1,
flags: 0,
special: 0,
tag: 0,
front_sidedef: None,
back_sidedef: None,
};
assert_eq!(ld.back_sector(&sidedefs), None);
}
#[test]
fn linedef_both_sidedefs() {
let sidedefs = vec![
SideDef {
x_offset: 0.0,
y_offset: 0.0,
upper_texture: String::new(),
lower_texture: String::new(),
middle_texture: String::new(),
sector: 2,
},
SideDef {
x_offset: 0.0,
y_offset: 0.0,
upper_texture: String::new(),
lower_texture: String::new(),
middle_texture: String::new(),
sector: 5,
},
];
let ld = LineDef {
v1: 0,
v2: 1,
flags: ML_TWOSIDED,
special: 0,
tag: 0,
front_sidedef: Some(0),
back_sidedef: Some(1),
};
assert_eq!(ld.front_sector(&sidedefs), Some(2));
assert_eq!(ld.back_sector(&sidedefs), Some(5));
}
#[test]
fn parse_child_max_subsector() {
let NodeChild::SubSector(s) = parse_child(0xFFFF) else {
unreachable!("Expected SubSector variant");
};
assert_eq!(s, 0x7FFF);
}
#[test]
fn parse_child_max_node() {
let NodeChild::Node(n) = parse_child(0x7FFF) else {
unreachable!("Expected Node variant");
};
assert_eq!(n, 0x7FFF);
}
#[test]
fn sector_is_sky_case_sensitive() {
let s = Sector {
floor_height: 0.0,
ceiling_height: 128.0,
floor_texture: String::new(),
ceiling_texture: "f_sky1".into(),
light_level: 200,
special: 0,
tag: 0,
};
assert!(!s.is_sky_ceiling(), "should be case-sensitive");
}
#[test]
fn node_child_debug() {
let node = NodeChild::Node(42);
let ss = NodeChild::SubSector(7);
let nd = format!("{node:?}");
let sd = format!("{ss:?}");
assert!(nd.contains("Node"));
assert!(nd.contains("42"));
assert!(sd.contains("SubSector"));
assert!(sd.contains("7"));
}
#[test]
fn node_fields() {
let n = Node {
x: 10.0,
y: 20.0,
dx: 1.0,
dy: 0.0,
bbox_right: [30.0, 10.0, 10.0, 30.0],
bbox_left: [30.0, 10.0, -10.0, 10.0],
right_child: NodeChild::SubSector(0),
left_child: NodeChild::Node(1),
};
assert!((n.x - 10.0).abs() < f32::EPSILON);
assert!((n.dy - 0.0).abs() < f32::EPSILON);
assert!(matches!(n.right_child, NodeChild::SubSector(0)));
assert!(matches!(n.left_child, NodeChild::Node(1)));
}
#[test]
fn sidedef_fields() {
let s = SideDef {
x_offset: 16.0,
y_offset: -8.0,
upper_texture: "UPPER".into(),
lower_texture: "LOWER".into(),
middle_texture: "MID".into(),
sector: 3,
};
assert!((s.x_offset - 16.0).abs() < f32::EPSILON);
assert!((s.y_offset + 8.0).abs() < f32::EPSILON);
assert_eq!(s.upper_texture, "UPPER");
assert_eq!(s.lower_texture, "LOWER");
assert_eq!(s.middle_texture, "MID");
assert_eq!(s.sector, 3);
}
#[test]
fn doom_map_clone() {
let map = DoomMap {
name: "E1M1".into(),
vertices: vec![Vertex { x: 1.0, y: 2.0 }],
linedefs: vec![],
sidedefs: vec![],
sectors: vec![],
segs: vec![],
subsectors: vec![],
nodes: vec![],
things: vec![],
};
let cloned = map.clone();
assert_eq!(cloned.name, "E1M1");
assert_eq!(cloned.vertices.len(), 1);
}
#[test]
fn doom_map_debug() {
let map = DoomMap {
name: "MAP01".into(),
vertices: vec![],
linedefs: vec![],
sidedefs: vec![],
sectors: vec![],
segs: vec![],
subsectors: vec![],
nodes: vec![],
things: vec![],
};
let dbg = format!("{map:?}");
assert!(dbg.contains("DoomMap"));
assert!(dbg.contains("MAP01"));
}
#[test]
fn doom_map_name_uppercase() {
let map = DoomMap {
name: "e1m1".to_uppercase(),
vertices: vec![],
linedefs: vec![],
sidedefs: vec![],
sectors: vec![],
segs: vec![],
subsectors: vec![],
nodes: vec![],
things: vec![],
};
assert_eq!(map.name, "E1M1");
}
#[test]
fn vertex_copy_semantics() {
let v1 = Vertex { x: 1.0, y: 2.0 };
let v2 = v1; assert!((v1.x - v2.x).abs() < f32::EPSILON);
assert!((v1.y - v2.y).abs() < f32::EPSILON);
}
#[test]
fn seg_copy_semantics() {
let s1 = Seg {
v1: 0,
v2: 1,
angle: 0.5,
linedef: 3,
direction: 1,
offset: 2.0,
};
let s2 = s1; assert_eq!(s1.v1, s2.v1);
assert_eq!(s1.linedef, s2.linedef);
}
#[test]
fn thing_copy_semantics() {
let t1 = Thing {
x: 1.0,
y: 2.0,
angle: 3.0,
thing_type: 42,
flags: 7,
};
let t2 = t1; assert_eq!(t1.thing_type, t2.thing_type);
assert_eq!(t1.flags, t2.flags);
}
#[test]
fn sector_clone_and_fields() {
let s = Sector {
floor_height: -16.0,
ceiling_height: 256.0,
floor_texture: "FLOOR4_8".into(),
ceiling_texture: "CEIL3_5".into(),
light_level: 160,
special: 9,
tag: 42,
};
let cloned = s.clone();
assert!((cloned.floor_height + 16.0).abs() < f32::EPSILON);
assert!((cloned.ceiling_height - 256.0).abs() < f32::EPSILON);
assert_eq!(cloned.floor_texture, "FLOOR4_8");
assert_eq!(cloned.light_level, 160);
assert_eq!(cloned.special, 9);
assert_eq!(cloned.tag, 42);
}
}