#![allow(clippy::indexing_slicing)]
use alloc::vec::Vec;
use crate::game_data::MOBJINFO;
use crate::map::MapData;
use crate::math::*;
use crate::render::sprites::ThingRef;
use crate::render::Renderer;
use crate::world::HasPose;
use crate::rules::{GameRules, PlayerAction, WorldAction};
use crate::specials;
use crate::texture::TextureData;
use crate::wad::Wad;
use crate::world::{Entity, EntityId, EntityType, PeerId, Pose, SectorState, World};
pub struct DoomEngine<R: GameRules> {
pub(crate) rules: R,
pub(crate) world: World,
pub(crate) map: MapData,
pub(crate) textures: TextureData,
pub(crate) renderer: Renderer,
thing_refs: Vec<ThingRef>,
}
impl<R: GameRules> DoomEngine<R> {
#[inline]
pub fn rules(&self) -> &R {
&self.rules
}
#[inline]
pub fn world(&self) -> &World {
&self.world
}
#[inline]
pub fn world_mut(&mut self) -> &mut World {
&mut self.world
}
#[inline]
pub fn map(&self) -> &MapData {
&self.map
}
#[inline]
pub fn map_mut(&mut self) -> &mut MapData {
&mut self.map
}
#[inline]
pub fn textures(&self) -> &TextureData {
&self.textures
}
#[inline]
pub fn renderer(&self) -> &Renderer {
&self.renderer
}
#[inline]
pub fn renderer_mut(&mut self) -> &mut Renderer {
&mut self.renderer
}
#[inline]
pub fn render_parts_mut(&mut self) -> (&mut Renderer, &MapData, &TextureData) {
(&mut self.renderer, &self.map, &self.textures)
}
#[inline]
pub fn level_complete(&self) -> bool {
self.world.level_exit != crate::world::LevelExit::None
}
#[inline]
pub fn level_exit(&self) -> crate::world::LevelExit {
self.world.level_exit
}
pub fn spawn_entity(&mut self, kind: EntityType, pose: Pose) -> Option<EntityId> {
let info = kind.info()?;
let (fz, cz) = crate::physics::find_sector_heights(&self.map, pose.x, pose.y);
let mut entity = Entity {
entity_type: kind,
x: pose.x,
y: pose.y,
z: fz,
angle: pose.angle,
floor_z: fz,
ceiling_z: cz,
radius: info.radius,
height: info.height,
health: info.spawnhealth,
flags: info.flags(),
reaction_time: info.reactiontime,
..Entity::default()
};
if let Some(st) = info.spawnstate.get() {
entity.sprite = st.sprite;
entity.frame = st.frame;
entity.state = info.spawnstate;
entity.tics = st.tics;
}
Some(self.world.spawn(entity))
}
pub fn spawn_player(&mut self, peer: PeerId, pose: Pose) -> Option<EntityId> {
let eid = self.spawn_entity(EntityType(0), pose)?;
self.world.set_controller(peer, eid);
Some(eid)
}
pub fn respawn(&mut self, eid: EntityId, pose: Pose) -> Option<()> {
let kind = self.world.get(eid)?.entity_type;
let info = kind.info()?;
let (fz, cz) = crate::physics::find_sector_heights(&self.map, pose.x, pose.y);
let e = self.world.get_mut(eid)?;
e.x = pose.x;
e.y = pose.y;
e.z = fz;
e.floor_z = fz;
e.ceiling_z = cz;
e.angle = pose.angle;
e.momx = 0;
e.momy = 0;
e.momz = 0;
e.health = info.spawnhealth;
e.flags = info.flags();
if let Some(st) = info.spawnstate.get() {
e.sprite = st.sprite;
e.frame = st.frame;
e.state = info.spawnstate;
e.tics = st.tics;
}
Some(())
}
}
#[derive(Debug)]
pub enum DoomError {
WadError(crate::wad::WadError),
MapError(crate::map::MapError),
TextureError(crate::texture::TextureError),
NoPlayerStart,
}
impl core::fmt::Display for DoomError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::WadError(e) => write!(f, "WAD parse error: {e}"),
Self::MapError(e) => write!(f, "map load error: {e}"),
Self::TextureError(e) => write!(f, "texture load error: {e}"),
Self::NoPlayerStart => write!(f, "map has no PLAYER1_START thing"),
}
}
}
impl core::error::Error for DoomError {
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
match self {
Self::WadError(e) => Some(e),
Self::MapError(e) => Some(e),
Self::TextureError(e) => Some(e),
Self::NoPlayerStart => None,
}
}
}
impl<R: GameRules> DoomEngine<R> {
pub fn new_with_rules(wad_data: &[u8], map_name: &str, rules: R) -> Result<Self, DoomError> {
let wad = Wad::parse(wad_data).map_err(DoomError::WadError)?;
let mut map = MapData::load(&wad, map_name).map_err(DoomError::MapError)?;
let textures = TextureData::load(&wad).map_err(DoomError::TextureError)?;
map.resolve_textures(&textures);
let mut renderer = Renderer::new();
let sky_name = *b"F_SKY1\0\0";
renderer.sky_flat_num = textures.flat_num_for_name(&sky_name);
let sky_tex_name = *b"SKY1\0\0\0\0";
renderer.sky_texture_num = textures.texture_num_for_name(&sky_tex_name);
let world = world_from_map(&map);
Ok(Self {
rules,
world,
map,
textures,
renderer,
thing_refs: Vec::new(),
})
}
pub fn load_map(&mut self, wad_data: &[u8], map_name: &str) -> Result<(), DoomError> {
let wad = Wad::parse(wad_data).map_err(DoomError::WadError)?;
let mut map = MapData::load(&wad, map_name).map_err(DoomError::MapError)?;
map.resolve_textures(&self.textures);
self.world = world_from_map(&map);
self.map = map;
self.thing_refs.clear();
Ok(())
}
pub fn spawn_map_things(&mut self, local_peer: PeerId) {
use crate::types::doomednum as dn;
let things = self.map.things.to_vec();
for thing in &things {
let pose = Pose::from_map_thing(thing);
if thing.type_num == dn::PLAYER1_START {
self.spawn_player(local_peer, pose);
continue;
}
if (dn::PLAYER2_START..=dn::PLAYER4_START).contains(&thing.type_num) {
continue;
}
if let Some(kind) = doomednum_to_type(thing.type_num as i32) {
self.spawn_entity(kind, pose);
}
}
}
pub fn simulate(
&mut self,
player_actions: &[(PeerId, PlayerAction)],
world_actions: &[WorldAction],
) {
self.rules
.tick(&mut self.world, &self.map, player_actions, world_actions);
specials::update_specials(&mut self.world.specials, &mut self.map.sectors);
for (i, sector) in self.map.sectors.iter().enumerate() {
if i < self.world.sectors.len() {
self.world.sectors[i].floor_height = sector.floor_height;
self.world.sectors[i].ceiling_height = sector.ceiling_height;
self.world.sectors[i].light_level = sector.light_level;
}
}
let ids = self.world.take_entity_ids();
for &id in &ids {
let is_player = self.world.is_controlled(id);
if let Some(entity) = self.world.get_mut(id) {
let (fz, cz) = crate::physics::find_sector_heights(&self.map, entity.x, entity.y);
if !is_player && (entity.z == entity.floor_z || entity.z < fz) {
entity.z = fz;
}
entity.floor_z = fz;
entity.ceiling_z = cz;
}
}
self.world.return_id_scratch(ids);
}
pub fn tick(
&mut self,
player_actions: &[(PeerId, PlayerAction)],
world_actions: &[WorldAction],
) {
self.simulate(player_actions, world_actions);
self.render_for(PeerId(0));
}
pub fn tick_single(&mut self, peer: PeerId, input: PlayerAction) {
self.tick(&[(peer, input)], &[]);
}
fn player_view(&self, peer: PeerId) -> Pose {
let Some(eid) = self.world.controlled_by(peer) else {
return Pose::default();
};
let Some(e) = self.world.get(eid) else {
return Pose::default();
};
let Some(ps) = self.world.player_state(eid) else {
return e.pose();
};
if e.health <= 0 {
let mut v = e.pose();
v.z = e.z + 6 * FRACUNIT;
return v;
}
let bob_angle = (FINEANGLES / 20 * self.world.tick as usize) & FINEMASK;
let view_bob = fixed_mul(ps.bob / 2, finesine(bob_angle));
let mut v = e.pose();
v.z = e.z + 41 * FRACUNIT + view_bob;
v
}
pub fn render_for(&mut self, peer: PeerId) {
let view_entity = self.world.controlled_by(peer);
let view = self.player_view(peer);
self.renderer
.render_player_view(&self.map, &self.textures, &view);
self.thing_refs.clear();
self.thing_refs.extend(
self.world
.iter()
.filter(|e| Some(e.id) != view_entity)
.map(|e| ThingRef {
x: e.x,
y: e.y,
z: e.z,
angle: e.angle,
sprite_num: e.sprite.0 as usize,
frame: e.frame as usize,
flags: e.flags.bits(),
mobj_type: e.entity_type.0,
light_level: sector_light_at(&self.map, e.x, e.y),
}),
);
self.renderer.draw_things(&self.thing_refs, &self.textures);
self.renderer.draw_masked_walls(&self.map, &self.textures);
let alive = view_entity
.and_then(|eid| self.world.get(eid))
.is_some_and(|e| e.health > 0);
if alive
&& let Some(ps) = view_entity.and_then(|eid| self.world.player_state(eid))
{
self.renderer
.draw_weapon_psp(ps.psp_state, ps.psp_sx, ps.psp_sy, &self.textures);
if !ps.flash_state.is_null() {
self.renderer
.draw_weapon_psp(ps.flash_state, ps.psp_sx, ps.psp_sy, &self.textures);
}
}
if let Some(eid) = view_entity
&& let Some(e) = self.world.get(eid)
{
let (armor, ammo, weapon) = self
.world
.player_state(eid)
.map(|ps| (ps.armor_points, ps.ammo, ps.ready_weapon))
.unwrap_or((0, [0; 4], crate::types::WeaponType::Pistol));
self.renderer
.draw_hud(crate::render::HudFrame::from_stats(e.health, armor, ammo, weapon));
}
self.renderer.indexed_to_rgba(&self.textures);
}
pub fn framebuffer(&self) -> &[u8] {
&self.renderer.rgba
}
pub fn semantic_buffer(&self) -> &[u8] {
&self.renderer.semantic
}
pub fn depth_buffer(&self) -> &[Fixed] {
&self.renderer.depth
}
pub fn buffer_size(&self) -> (usize, usize) {
(SCREENWIDTH, SCREENHEIGHT)
}
}
pub type ClassicEngine = DoomEngine<crate::classic::ClassicDoomRules>;
impl ClassicEngine {
pub fn new(wad_data: &[u8], map_name: &str) -> Result<Self, DoomError> {
let mut engine =
Self::new_with_rules(wad_data, map_name, crate::classic::ClassicDoomRules)?;
engine.spawn_map_things(PeerId(0));
Ok(engine)
}
}
fn world_from_map(map: &MapData) -> World {
let sectors = map
.sectors
.iter()
.map(|s| SectorState {
floor_height: s.floor_height,
ceiling_height: s.ceiling_height,
light_level: s.light_level,
special: s.special,
})
.collect();
let mut world = World::new(sectors);
world.specials = specials::spawn_specials(map);
world
}
fn sector_light_at(map: &MapData, x: Fixed, y: Fixed) -> i16 {
let ssect = crate::physics::find_subsector(map, x, y);
if ssect < map.subsectors.len() {
map.sectors[map.subsectors[ssect].sector as usize].light_level
} else {
160
}
}
fn doomednum_to_type(doomednum: i32) -> Option<EntityType> {
MOBJINFO
.iter()
.enumerate()
.find(|(_, info)| info.doomednum == doomednum)
.map(|(i, _)| EntityType(i as u16))
}