3 Commits

9 changed files with 588 additions and 23 deletions

View File

@@ -1,7 +1,12 @@
# RustyDoom # RustyDoom
A Rust version of the original DOOM from the offically released source code found at [https://github.com/id-software/doom](https://github.com/id-software/doom) A Rust version of the original DOOM base on the offically released source code found at [https://github.com/id-software/doom](https://github.com/id-software/doom)
This is more of a learning exercise than something good! This is more of a learning exercise than something good! For example, the original `z_zone.c` and `z_zone.h` will be more "Rust-like" compared to the original version.
This repository will only contain the SHAREWARE verison of the original DOOM1, but will be able to run both IWAD and PWADS. This repository will only contain the SHAREWARE verison of the original DOOM1, but will be able to run both IWAD and PWADS.
This repository will be done in two parts before a "release"
1. An initial port keeping as close to the original C code as possible, which some minor modifications where it is clear on how to convert the code to Rust
2. Rust-ify any (hopefully all) parts of the code, that is removing `unsafe` and other such things. This will also include breaking down some of the `uber` sized funtions of the original source into more managable bits

View File

@@ -8,9 +8,12 @@ use crate::m_argv;
use crate::m_argv::M_CheckParm; use crate::m_argv::M_CheckParm;
use crate::m_argv::M_GetOptionalArgumentValueByArgument; use crate::m_argv::M_GetOptionalArgumentValueByArgument;
use crate::m_argv::PARM_NOT_FOUND; use crate::m_argv::PARM_NOT_FOUND;
use crate::m_menu::M_Init;
use crate::m_misc::M_LoadDefaults; use crate::m_misc::M_LoadDefaults;
use crate::v_video::V_Init;
use crate::w_wad::{W_CheckNumForName, W_InitMultipleFiles}; use crate::w_wad::{W_CheckNumForName, W_InitMultipleFiles};
use crate::z_zone::Z_Init;
use crate::doomdef::{ use crate::doomdef::{
VERSION, VERSION,
@@ -400,19 +403,16 @@ pub fn D_DoomMain() {
} }
} }
// TODO: Implement subsytem inits
/*
println!("V_Init: allocate screens."); println!("V_Init: allocate screens.");
V_Init(); V_Init();
*/
println!("M_LoadDefaults: Load system defaults."); println!("M_LoadDefaults: Load system defaults.");
M_LoadDefaults(); M_LoadDefaults();
/*
println!("Z_init: Init zone memory allocation daemon."); println!("Z_init: Init zone memory allocation daemon.");
Z_Init(); Z_Init();
*/
println!("W_Init: Init WADfiles"); println!("W_Init: Init WADfiles");
W_InitMultipleFiles(DOOMGLOBALS::with_ref(|g| g.wadfiles.clone())); // we are loading so how cares about a borrow W_InitMultipleFiles(DOOMGLOBALS::with_ref(|g| g.wadfiles.clone())); // we are loading so how cares about a borrow
@@ -467,11 +467,12 @@ pub fn D_DoomMain() {
} }
} }
// TODO: Implement other inits
/*
println!("M_Init: Init miscellaneous info."); println!("M_Init: Init miscellaneous info.");
M_Init(); M_Init();
// TODO: Implement other inits
/*
println!("R_Init: Init DOOM refresh daemon - ") println!("R_Init: Init DOOM refresh daemon - ")
R_Init(); R_Init();

View File

@@ -1,7 +1,35 @@
use std::cell::RefCell; use std::cell::RefCell;
/// DOOM version
pub const VERSION:i32 = 110; pub const VERSION:i32 = 110;
pub const D_DEVSTR: &str = "Development mode ON.";
/// BASE_WIDTH
/// For resize of screen, at start of game.
/// It will not work dynamically, see visplanes. TODO: Investigate what this means.
pub const BASE_WIDTH: i32 = 320;
/// Screen scale multiplier?
/// Original source comment:
/// It is educational but futile to change this
/// scaling e.g. to 2. Drawing of status bar,
/// menues etc. is tied to the scale implied
/// by the graphics.
pub const SCREEN_MUL: i32 = 1;
/// Inverse of the aspect ratio
pub const INV_ASPECT_RATIO: f32 = 0.625; // 0.75, ideally according to the original source
/// SCREENWIDTH
/// = SCREEN_MUL*BASE_WIDTH //320
pub const SCREENWIDTH: i32 = 320;
/// SCREENHEIGHT
/// (int)(SCREEN_MUL*BASE_WIDTH*INV_ASPECT_RATIO) //200
pub const SCREENHEIGHT:i32 = 200;
/// The maximum number of players, multiplayer/networking.
pub const MAXPLAYERS: i32 = 4;
/// The number of state updates (ticks) to be done per second /// The number of state updates (ticks) to be done per second
pub const TICRATE: i32 = 35; pub const TICRATE: i32 = 35;

10
src/doomtype/mod.rs Normal file
View File

@@ -0,0 +1,10 @@
const MAXCHAR: i8 = i8::MAX;
const MAXSHORT: i16 = i16::MAX;
const MAXINT: i32 = i32::MAX;
const MAXLONG: i32 = i32::MAX;
// Minimum values
const MINCHAR: i8 = i8::MIN;
const MINSHORT: i16 = i16::MIN;
const MININT: i32 = i32::MIN;
const MINLONG: i32 = i32::MIN;

320
src/m_menu/mod.rs Normal file
View File

@@ -0,0 +1,320 @@
// Menu Widget stuff i.e episode selection etc.
// TODO: Implement d_event.handle
use std::ptr::addr_of_mut;
use crate::doomdef::{DOOMGLOBALS, GameMode};
#[repr(C)]
#[derive(Copy, Clone)]
#[allow(non_snake_case)]
struct menuitem_t {
status: i32,
name: [u8; 10],
routine: Option<fn(choice: i32)>,
// hotkey in menu
alpha_key: u8,
}
#[repr(C)]
#[derive(Copy, Clone)]
#[allow(non_snake_case)]
struct menu_t {
/// Number of items in the menu
numitems: i32,
/// Pointer to the previous menu
prevMenu: *mut menu_t,
/// The menu items
menuItems: *mut menuitem_t,
/// draw routine
routine: Option<fn()>,
/// X of the menu
x: i32,
/// y of the menu
y: i32,
/// The last item the user was on in the menu
lastOn: i32
}
/// TEMP function
#[allow(non_snake_case)]
fn VoidRoutine() {
}
#[allow(non_snake_case)]
fn VoidRoutineWithChoice(choice: i32) {
let _ = choice;
}
// Main Menu
#[allow(non_snake_case)]
mod MainMenu {
use super::*;
/// Enumeration of the main menu options
#[allow(non_camel_case_types)]
pub(super) enum main_e {
newgame = 0,
options,
loadgame,
savegame,
readthis,
quitdoom,
main_end
}
/// Main Menu items
#[allow(non_upper_case_globals)]
pub(super) static mut MainMenu: [menuitem_t; 6] = [
menuitem_t {
status: 1,
name: *b"M_NGAME\0\0\0",
routine: Some(VoidRoutineWithChoice),
alpha_key: b'n'
},
menuitem_t {
status: 1,
name: *b"M_OPTION\0\0",
routine: Some(VoidRoutineWithChoice),
alpha_key: b'o'
},
menuitem_t {
status: 1,
name: *b"M_LOADG\0\0\0",
routine: Some(VoidRoutineWithChoice),
alpha_key: b'l'
},
menuitem_t {
status: 1,
name: *b"M_SAVEG\0\0\0",
routine: Some(VoidRoutineWithChoice),
alpha_key: b's'
},
menuitem_t {
status: 1,
name: *b"M_RDTHIS\0\0",
routine: Some(VoidRoutineWithChoice),
alpha_key: b'r'
},
menuitem_t {
status: 1,
name: *b"M_QUITG\0\0\0",
routine: Some(VoidRoutineWithChoice),
alpha_key: b'q'
}
];
/// Default MainMenu
#[allow(non_upper_case_globals)]
pub(super) static mut MainDef: menu_t = menu_t {
numitems: main_e::main_end as i32,
prevMenu: std::ptr::null_mut(),
menuItems: &raw mut MainMenu as *mut menuitem_t, // Use &raw mut instead of .as_mut_ptr() to avoid illegal static mut references (Feels dirty)
routine: Some(VoidRoutine),
x: 97,
y: 64,
lastOn: 0,
};
}
// New Game menu
#[allow(non_snake_case)]
mod NewGameMenu {
use super::*;
/// Enumeration of the New Game options
#[allow(non_camel_case_types)]
pub(super) enum newgame_e {
killthings,
toorough,
hurtme,
violence,
nightmare,
newg_end
}
/// New Game Menu items
#[allow(non_upper_case_globals)]
pub(super) static mut NewGameMenu: [menuitem_t; 5] = [
menuitem_t {
status: 1,
name: *b"M_JKILL\0\0\0",
routine: Some(VoidRoutineWithChoice),
alpha_key: b'i'
},
menuitem_t {
status: 1,
name: *b"M_ROUGH\0\0\0",
routine: Some(VoidRoutineWithChoice),
alpha_key: b'h'
},
menuitem_t {
status: 1,
name: *b"M_HURT\0\0\0\0",
routine: Some(VoidRoutineWithChoice),
alpha_key: b'h'
},
menuitem_t {
status: 1,
name: *b"M_ULTRA\0\0\0",
routine: Some(VoidRoutineWithChoice),
alpha_key: b'u'
},
menuitem_t {
status: 1,
name: *b"M_NMARE\0\0\0",
routine: Some(VoidRoutineWithChoice),
alpha_key: b'n'
}
];
#[allow(non_upper_case_globals)]
pub(super) static mut NewDef: menu_t = menu_t {
numitems: newgame_e::newg_end as i32,
prevMenu: std::ptr::null_mut(), // TODO: Add EpiDef
menuItems: &raw mut NewGameMenu as *mut menuitem_t, // Use &raw mut instead of .as_mut_ptr() to avoid illegal static mut references (Feels dirty)
routine: Some(VoidRoutine),
x: 48,
y: 63,
lastOn: newgame_e::hurtme as i32,
};
}
// TODO: add OptionsMenu
#[allow(non_snake_case)]
mod ReadThis1 {
use super::*;
/// Enumeration of the main menu options
#[allow(non_camel_case_types)]
pub(super) enum read_e {
rdthisempty1 = 0,
read1_end,
}
/// Read1 Menu items
#[allow(non_upper_case_globals)]
pub(super) static mut ReadMenu1: [menuitem_t; 1] = [
menuitem_t {
status: 1,
name: *b"\0\0\0\0\0\0\0\0\0\0",
routine: Some(VoidRoutineWithChoice),
alpha_key: b'0'
}
];
/// Default MainMenu
#[allow(non_upper_case_globals)]
pub(super) static mut Read1Def: menu_t = menu_t {
numitems: read_e::rdthisempty1 as i32,
prevMenu: addr_of_mut!(MainMenu::MainDef),
menuItems: &raw mut ReadMenu1 as *mut menuitem_t, // Use &raw mut instead of .as_mut_ptr() to avoid illegal static mut references (Feels dirty)
routine: Some(VoidRoutine),
x: 280,
y: 185,
lastOn: 0,
};
}
// TODO: The rest of this hell!
#[allow(non_upper_case_globals)]
static mut currentMenu: *mut menu_t = std::ptr::null_mut();
#[allow(non_upper_case_globals)]
static mut menuactive: bool = false;
#[allow(non_upper_case_globals)]
static mut itemOn: i32 = 0;
#[allow(non_upper_case_globals)]
static mut whichSkull: i32 = 0;
#[allow(non_upper_case_globals)]
static mut skullAnimCounter: i32 = 0;
#[allow(non_upper_case_globals)]
static mut screenSize: i32 = 0;
#[allow(non_upper_case_globals)]
static mut screenBlocks: i32 = 0;
#[allow(non_upper_case_globals)]
static mut messageToPrint: i32 = 0;
#[allow(non_upper_case_globals)]
static mut messageString: *mut u8 = std::ptr::null_mut();
#[allow(non_upper_case_globals)]
static mut messageLastMenuActive: i32 = 0;
#[allow(non_upper_case_globals)]
static mut quickSaveSlot: i32 = -1;
#[allow(non_snake_case)]
pub fn M_Ticket() {
}
#[allow(non_snake_case)]
pub fn M_Drawer() {
}
#[allow(non_snake_case)]
pub fn M_Init() {
unsafe {
currentMenu = addr_of_mut!(MainMenu::MainDef);
menuactive = false;
itemOn = (*currentMenu).lastOn;
whichSkull = 0;
skullAnimCounter = 10;
screenSize = screenBlocks - 3;
messageToPrint = 0;
messageString = std::ptr::null_mut();
messageLastMenuActive = menuactive as i32;
quickSaveSlot = -1;
match DOOMGLOBALS::with_ref(|g| g.gamemode) {
GameMode::Commercial => {
/*
Original source code comment:
// This is used because DOOM 2 had only one HELP
// page. I use CREDIT as second page now, but
// kept this hack for educational purposes.
*/
MainMenu::MainMenu[MainMenu::main_e::readthis as usize] = MainMenu::MainMenu[MainMenu::main_e::quitdoom as usize];
MainMenu::MainDef.numitems -=1;
MainMenu::MainDef.y += 8;
NewGameMenu::NewDef.prevMenu = addr_of_mut!(MainMenu::MainDef);
ReadThis1::Read1Def.routine = Some(VoidRoutine);
ReadThis1::Read1Def.x = 330;
ReadThis1::Read1Def.y = 165;
ReadThis1::ReadMenu1[ReadThis1::read_e::rdthisempty1 as usize].routine = Some(VoidRoutineWithChoice);
},
GameMode::Shareware | GameMode::Registered => {
// TODO EpiDef.numitems--
}
_ => {
// do nothing :)
}
}
}
}
#[allow(non_snake_case)]
pub fn M_StartControlPanel() {
}

View File

@@ -1,20 +1,10 @@
use std::ptr::addr_of_mut; use std::ptr::addr_of_mut;
use crate::d_strings; use crate::d_strings;
use crate::doomtype;
use crate::hu_stuff::get_chat_macro_ptr; use crate::hu_stuff::get_chat_macro_ptr;
use crate::m_argv::{M_CheckParm, M_GetOptionalArgumentValueByArgument, PARM_NOT_FOUND}; use crate::m_argv::{M_CheckParm, M_GetOptionalArgumentValueByArgument, PARM_NOT_FOUND};
const MAXCHAR: i8 = i8::MAX;
const MAXSHORT: i16 = i16::MAX;
const MAXINT: i32 = i32::MAX;
const MAXLONG: i32 = i32::MAX;
// Minimum values
const MINCHAR: i8 = i8::MIN;
const MINSHORT: i16 = i16::MIN;
const MININT: i32 = i32::MIN;
const MINLONG: i32 = i32::MIN;
// static mut variable stuff // static mut variable stuff
@@ -280,7 +270,6 @@ pub fn M_WriteFile(name: &str, source: std::fs::File, length: i64) {
} }
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub fn M_ReadFile(name: &str, buffer: Vec<u8>) { pub fn M_ReadFile(name: &str, buffer: Vec<u8>) {
} }

View File

@@ -1,10 +1,14 @@
mod d_main; mod d_main;
mod d_strings; mod d_strings;
mod doomdef; mod doomdef;
mod doomtype;
mod hu_stuff; mod hu_stuff;
mod m_argv; mod m_argv;
mod m_menu;
mod m_misc; mod m_misc;
mod v_video;
mod w_wad; mod w_wad;
mod z_zone;
mod math; mod math;

43
src/v_video/mod.rs Normal file
View File

@@ -0,0 +1,43 @@
use crate::doomdef::{SCREENHEIGHT, SCREENWIDTH};
use crate::doomtype;
pub const CENTERY: i32 = SCREENHEIGHT / 2;
// static var stuff
#[allow(non_upper_case_globals)]
static mut screens: [[u8; (SCREENWIDTH*SCREENHEIGHT) as usize]; 5] = [[0u8; (SCREENWIDTH*SCREENHEIGHT) as usize]; 5];
#[allow(non_snake_case)]
pub fn V_Init() {
// Do nothing :)
// the memory is already allocated
}
#[allow(non_snake_case)]
pub fn V_CopyRect(srcx: i32, srcy: i32, srcscrn: i32, width: i32, height: i32, destx: i32, desty: i32, destscrn: i32) {
}
/*
TODO: Implement "r_data"
#[allow(non_snake_case)]
pub fn V_DrawPatch(x: i32, y: i32, scrn: i32, patch: Vec<patch_t>) {
}
#[allow(non_snake_case)]
pub fn V_DrawPatchDirect(x: i32, y: i32, scrn: i32, patch: Vec<patch_t>) {
}
*/
#[allow(non_snake_case)]
pub fn V_GetBlock(x: i32, y: i32, scrn: i32, width: i32, height: i32, dest: Vec<u8>) {
}
#[allow(non_snake_case)]
pub fn V_MarkRect(x: i32, y: i32, width: i32, height: i32) {
}

165
src/z_zone/mod.rs Normal file
View File

@@ -0,0 +1,165 @@
/// Memory allocator stuff
/// Compared to the original source, this will be some what "lean n' mean"
use std::cell::RefCell;
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
/// The PurgeTags (PUs)
/// Tags < 100 are not overwritten untill freed
pub enum PurgeTag {
/// Static the entire execution time
Static = 1,
/// Static while playing
Sound = 2,
/// static while playing
Music = 3,
/// Original source comment: anything else dave wants static
Dave = 4,
/// static until level exit
Level = 50,
/// static for special thinkers in a level
LevSpec = 51,
// Tags >= 100 are "purgable" whenever needed
PurgeLevel = 100,
Cache = 101,
}
/// Provides metadata on
struct AllocMeta {
/// The size of the allocation
size: usize,
/// The PU tag associated with the allocation
tag: i32,
/// A pointer to the caller's tracking pointer, cleared automatically upon freeing
user: *mut *mut u8,
}
thread_local! {
// We promise not to be naughty and access this from another thread :)
/// Mapping from pointers (as usize) to the correspond AllocationMeta
static REGISTRY: RefCell<HashMap<usize, AllocMeta>> = RefCell::new(HashMap::new());
}
// Helper method that we can use to access the registry
fn with_registry<F, R>(f: F) -> R
where
F: FnOnce(&mut HashMap<usize, AllocMeta>) -> R
{
REGISTRY.with(|registry| f(&mut *registry.borrow_mut()))
}
#[allow(non_snake_case)]
/// Initalized the Zone memory allocator sub-system
pub fn Z_Init() {
// do nothing, we get lazy init of the registry.
}
#[allow(non_snake_case)]
/// Allocate a contiguous chunk of zeroed memory from the zone allocator
/// # Safety
/// This function returns a raw pointer that bypasses Rust's automatic memory
/// tracking. The returned memory must be manually reclaimed using `Z_Free`.
pub unsafe fn Z_Malloc(size: usize, tag: i32, user: *mut *mut u8) -> *mut u8 {
let mut buffer = vec![0u8; size];
let ptr = buffer.as_mut_ptr();
// ensures Rust does not drop the buffer at the end of this block
std::mem::forget(buffer);
// Update the engine's tracking pointer if one was provided
if !user.is_null() {
unsafe {
*user = ptr;
}
}
// Save allocation details using the pointer address as the key
with_registry(|map| {
map.insert(ptr as usize, AllocMeta { size, tag, user });
});
ptr
}
#[allow(non_snake_case)]
/// Reclaims and frees a allocation from Z_Malloc
/// # Safety
/// The provided pointer must point to a valid allocation tracked by the registry.
/// Passing an invalid or already freed pointer can result in undefined behavior
pub unsafe fn Z_Free(ptr: *mut u8) {
if ptr.is_null() {
return;
}
let removed = with_registry(|map| map.remove(&(ptr as usize)));
if let Some(meta) = removed {
// Clear the caller's tracking pointer to prevent use-after-free
if !meta.user.is_null() {
unsafe {
*meta.user = std::ptr::null_mut();
}
}
// re-build the vector so Rust can drop it
unsafe {
let _dropped_vec = Vec::from_raw_parts(ptr, meta.size, meta.size);
}
}
}
#[allow(non_snake_case)]
/// Frees all memory allocations between the specific lifetime PU tags.
/// # Safety
/// All pointers tracked within this tag boundary are invalidated and deallocate
pub unsafe fn Z_FreeTags(low_tag: i32, high_tag: i32) {
// get all of the allocations to free
let tags_to_free: Vec<usize> = with_registry(|map| {
map.iter()
.filter(|(_, meta)| meta.tag >= low_tag && meta.tag <= high_tag)
.map(|(ptr_addr,_)| *ptr_addr)
.collect()
});
// remove each pointer returned from the map and drop the memory
for ptr_addr in tags_to_free {
let removed_ptr = with_registry(|map| map.remove(&ptr_addr));
if let Some(meta) = removed_ptr {
let p = ptr_addr as *mut u8;
unsafe {
// clear caller tracking pointer to preven use after free
if !meta.user.is_null() {
*meta.user = std::ptr::null_mut();
}
let _dropped_vec = Vec::from_raw_parts(p, meta.size, meta.size);
}
}
}
}
// Internal ChangeTag function
#[allow(non_snake_case)]
fn Z_ChangeTagInternal(ptr: *mut u8, tag: i32) {
if ptr.is_null() {
return;
}
with_registry(|map| {
if let Some(meta) = map.get_mut(&(ptr as usize)) {
meta.tag = tag;
}
})
}
/// Modifies the lifetime tag of an active allocation.
///
/// # Safety
/// The provided pointer must point to a valid allocation tracked by the registry
#[allow(non_snake_case)]
pub unsafe fn Z_ChangeTag(ptr: *mut u8, tag: i32) {
// removes the original helper macro from the original C, we don't need withcraft here
Z_ChangeTagInternal(ptr, tag);
}