use std::env; use std::fs; use std::io::{self, Write}; use std::path::{self}; use std::process; use crate::m_argv; use crate::m_argv::M_CheckParm; use crate::m_argv::M_GetOptionalArgumentValueByArgument; use crate::m_argv::PARM_NOT_FOUND; use crate::w_wad::{W_CheckNumForName, W_InitMultipleFiles}; use crate::doomdef::{ VERSION, // D_DEVSTR, GameMode, Skill, DOOMGLOBALS }; fn flush_and_wait() { let _ = io::stdout().flush(); // Replaces C's getchar() by waiting for the Enter key let mut buffer = String::new(); let _ = io::stdin().read_line(&mut buffer); } #[allow(non_snake_case)] pub fn D_AddFile(file: &str) { DOOMGLOBALS::with_mut(|g| { g.wadfiles.push(file.to_string()); }); } #[allow(non_snake_case)] pub fn D_ProcessFileParameters(p_file: i32) { let args = m_argv::M_GetAll(); let pos = p_file as usize; // Find the index of "-file" // Look at everything after the index for &arg in args.iter().skip(pos + 1) { if arg.starts_with('-') { break; // Stop if we hit another flag like -debug } D_AddFile(arg); } } /// Reads a Response file for additional command line arguments /// A response file is a text file that may store additional command line parameters. /// The file may have any name that is valid to the system, optionally with an extension. /// The parameters are typed as in the command line (-episode 2, for example), /// but one per line, where up to 100 lines may be used. /// The additional parameters may be disabled for later use by placing a vertical bar /// (the | character) between the prefixing dash (-) and the rest of the parameter name. #[allow(non_snake_case)] fn FindResponseFile(args: &mut Vec) -> Result<(), String> { const MAX_ARGVS: usize = 100; let mut i = 0; while i < args.len() { let arg = &args[i]; if arg.len() > 0 && arg.as_bytes()[0] == b'@' { let f_name = &arg[1..]; // remove @ let f_content: String; match fs::read_to_string(f_name) { Ok(content) => f_content = content, Err(e) => { eprintln!("Found response file {}! (Error reading: {})", f_name, e); std::process::exit(1); } } eprintln!("Found response file {}!", f_name); let f_args: Vec = f_content .split_whitespace() .map(|s| s.to_string()) .collect(); args.remove(i); for (offset, new_arg) in f_args.into_iter().enumerate() { args.insert(i+offset, new_arg); } if args.len() > MAX_ARGVS { eprintln!("Warning: Recieved {} total arguments, exceeding original MAX_ARGSV limit {}", args.len(), MAX_ARGVS) } println!("{} command-line args:", args.len()); for arg in args { println!("\t{}", arg); } break; } i +=1; } Ok(()) } #[allow(non_snake_case)] fn IdentifyVersion() -> Result<(), String> { if M_CheckParm("-shdev") != PARM_NOT_FOUND { eprintln!("-shddev param not implemented in this port"); eprintln!("Usage: RustyDoom "); process::exit(1); } if M_CheckParm("-regdev") != PARM_NOT_FOUND { eprintln!("-regdev param not implemented in this port"); eprintln!("Usage: RustyDoom "); process::exit(1); } if M_CheckParm("-comdev") != PARM_NOT_FOUND { eprintln!("-comdev param not implemented in this port"); eprintln!("Usage: RustyDoom "); process::exit(1); } let iwad_arg = m_argv::M_GetPositionalArgv() .unwrap_or_else(|| { eprintln!("Usage: RustyDoom "); process::exit(1); }); let Ok(iwad_path) = path::absolute(std::path::PathBuf::from(iwad_arg)) else { eprintln!("FATAL: Could not resolve the IWAD path: {}", iwad_arg); process::exit(1); }; if !iwad_path.is_file() { eprintln!("FATAL: IWAD file not found: {}", iwad_path.display()); process::exit(1); } let filename = iwad_path .file_name() .and_then(|f| f.to_str()) .unwrap_or_else(|| { eprintln!("FATAL: Unable to get WAD filename from path: {:?}", iwad_path); std::process::exit(1); }) .to_ascii_lowercase(); DOOMGLOBALS::with_mut(|g| { g.language = 0; g.gamemode = match filename.as_str() { "doom1.wad" => GameMode::Shareware, "doom.wad" => GameMode::Registered, "doomu.wad" | "ultimate.wad" => { GameMode::Retail } "doom2.wad" => GameMode::Commercial, "doom2f.wad" => { g.language = 1; GameMode::Commercial } "tnt.wad" => GameMode::Commercial, "plutonia.wad" => GameMode::Commercial, _ => GameMode::Indetermined, }; }); D_AddFile(&iwad_arg); Ok(()) } // Converts the given string into a valid [u8; 128] array fn to_fixed_bytes(input: &str) -> [u8; 128] { let mut b = [0u8; 128]; let src = input.as_bytes(); let len = src.len().min(128); b[..len].copy_from_slice(&src[..len]); b } #[allow(non_snake_case)] pub fn D_DoomMain() { // get cli args from env // Original doom uses a static module argc, argv // Collects arguments into a Vector of Strings // Process args let mut args: Vec = env::args().collect(); let _ = FindResponseFile(&mut args); m_argv::init(args); let _ = IdentifyVersion(); // setbuf (stdout, NULL); // TODO: Investigate this DOOMGLOBALS::with_mut(|g| { g.modifiedgame = false; g.nomonsters = M_CheckParm("-nomonsters") != 0; g.respawnparm = M_CheckParm("-respawn") != 0; g.fastparm = M_CheckParm("-fastparm") != 0; g.devparm = M_CheckParm("-devparam") != 0; g.deathmatch = if M_CheckParm("-altdeath") != 0 { 2 } else if M_CheckParm("-deathmatch") != 0 { 1 } else { 0 }; // TODO: fix the original issue to handle TnT + Plutonia g.title = match g.gamemode { GameMode::Retail => to_fixed_bytes(&format!(" The Ultimate DOOM Startup v{}.{} ", VERSION / 100, VERSION % 100)), GameMode::Shareware => to_fixed_bytes(&format!(" DOOM Shareware Startup v{}.{} ", VERSION / 100, VERSION % 100)), GameMode::Registered => to_fixed_bytes(&format!(" DOOM Registered Startup v{}.{} ", VERSION / 100, VERSION % 100)), GameMode::Commercial => to_fixed_bytes(&format!(" DOOM 2: Hell on Earth v{}.{} ", VERSION / 100, VERSION % 100)), GameMode::Indetermined => to_fixed_bytes(&format!(" Public DOOM - v{}.{} ", VERSION / 100, VERSION % 100)), }; println!("{}", String::from_utf8_lossy(&g.title)); if g.devparm { eprintln!("devparm not implemented in this port"); eprintln!("Usage: RustyDoom "); process::exit(1); } }); // Check and parse turbo options if applicable if M_CheckParm("-turbo") != PARM_NOT_FOUND { let mut scale :i32 = 200; // TODO: Resolve externs (int should be fixed_t) or FixedPoint in our case // extern int forwardmove[2]; // extern int sidemove[2]; if let Some(arg_str) = M_GetOptionalArgumentValueByArgument("-turbo") { if let Ok(parsed_val) = arg_str.parse() { scale = parsed_val; } else { } } scale = scale.clamp(10, 400); println!("turbo scale: {}%", scale); // TODO: Resolve externs //forwardmove[0] = forwardmove[0]*scale/100; //forwardmove[1] = forwardmove[1]*scale/100; //sidemove[0] = sidemove[0]*scale/100; //sidemove[1] = sidemove[1]*scale/100; } if M_CheckParm ("-wart") != PARM_NOT_FOUND { eprintln!("-wart is not implemented in this port"); eprintln!("Usage: RustyDoom "); process::exit(1); } let mut p = M_CheckParm("-file");{ if p != PARM_NOT_FOUND { // Params after -file are wadfile/lump names // Continue collecting until end of params or another `-` preceded param DOOMGLOBALS::with_mut(|g|{ g.modifiedgame = true; }); D_ProcessFileParameters(p); } } // Playing a dmeo? p = M_CheckParm("-playdemo"); if p != PARM_NOT_FOUND { p = M_CheckParm("-timedemo"); } if p != PARM_NOT_FOUND && p < m_argv::M_GetArgC() - 1 { if let Some(demo_file) = m_argv::M_GetOptionalArgumentValueByIndex(p+1) { D_AddFile(demo_file); println!("Playing demo {}.lmp.\n", demo_file); } } // get the skill, episode and map from the parms if given DOOMGLOBALS::with_mut(|g| { g.startskill = Skill::sk_medium as i32; g.startepisode = 1; g.startmap = 1; g.autostart = false; }); p = M_CheckParm("-skill"); if p != PARM_NOT_FOUND && p < m_argv::M_GetArgC() - 1 { if let Some(skill_val) = m_argv::M_GetOptionalArgumentValueByIndex(p + 1) { if let Some(first_char) = skill_val.chars().next() { let start_skill = (first_char as i32).wrapping_sub('1' as i32); DOOMGLOBALS::with_mut(|g| { g.startskill = start_skill; }) } } } p = M_CheckParm("-episode"); if p != PARM_NOT_FOUND && p < m_argv::M_GetArgC() - 1 { if let Some(episode_val) = m_argv::M_GetOptionalArgumentValueByIndex(p + 1) { if let Some(first_char) = episode_val.chars().next() { let start_episode = (first_char as i32).wrapping_sub('0' as i32); DOOMGLOBALS::with_mut(|g| { g.startepisode = start_episode; g.startmap = 1; }) } } } p = M_CheckParm("-timer"); let is_deathmath = DOOMGLOBALS::with_ref(|g| g.deathmatch); if p != PARM_NOT_FOUND && p < m_argv::M_GetArgC() - 1 && is_deathmath > 0 { if let Some(timer_val) = m_argv::M_GetOptionalArgumentValueByIndex(p + 1) { match timer_val.parse::() { Ok(time) => { if time > 1 { println!("Levels will end after {} minutes", time) } else { println!("Levels will end after {} minute", time) } }, Err(_) => { eprintln!("value for -timer is not a valid number of minutes"); eprintln!("Usage: RustyDoom "); std::process::exit(1); } }; } } p = M_CheckParm("-avg"); if p != PARM_NOT_FOUND && p < m_argv::M_GetArgC() - 1 && is_deathmath > 0 { println!("Austin Virtual Gaming: Levels will end after 20 minutes\n"); } p = M_CheckParm("-warp"); if p != PARM_NOT_FOUND && p < m_argv::M_GetArgC() - 1 { let g_mode = DOOMGLOBALS::with_ref(|g| g.gamemode); if g_mode == GameMode::Commercial { if let Some(gamemode_val) = m_argv::M_GetOptionalArgumentValueByIndex(p + 1) { match gamemode_val.parse::() { Ok(mapstart) => { DOOMGLOBALS::with_mut(|g| { g.startmap = mapstart; g.autostart = true; }) }, Err(_) => { eprintln!("value for -warp is not a valid map number"); eprintln!("Usage: RustyDoom "); std::process::exit(1); } }; } } else { if let Some(episode_val) = m_argv::M_GetOptionalArgumentValueByIndex(p + 1) { if let Some(map_val) = m_argv::M_GetOptionalArgumentValueByIndex(p + 2) { match (episode_val.parse::(), map_val.parse::()) { (Ok(startepisode), Ok(startmap)) => { DOOMGLOBALS::with_mut(|g| { g.startepisode = startepisode; g.startmap = startmap; g.autostart = true; } ); } (Ok(_), Err(_)) => { eprintln!("value for starting map in -warp is not a valid number"); eprintln!("Usage: RustyDoom "); std::process::exit(1); } (Err(_), Ok(_)) => { eprintln!("value for starting episode in -warp is not a valid number"); eprintln!("Usage: RustyDoom "); std::process::exit(1); } (Err(_), Err(_)) => { eprintln!("Values for starting episode and map in -warp are not a valid numbers"); eprintln!("Usage: RustyDoom "); std::process::exit(1) } } }; } } } // TODO: Implement subsytem inits /* println!("V_Init: allocate screens."); V_Init(); println!("M_LoadDefaults: Load system defaults.") M_LoadDefaults(); println!("Z_init: Init zone memory allocation daemon."); Z_Init(); */ println!("W_Init: Init WADfiles"); W_InitMultipleFiles(DOOMGLOBALS::with_ref(|g| g.wadfiles.clone())); // we are loading so how cares about a borrow let is_modified = DOOMGLOBALS::with_ref(|g| g.modifiedgame); if is_modified { let iwads_lumps_to_check: Vec<&str> = vec!["e2m1","e2m2","e2m3","e2m4","e2m5","e2m6","e2m7","e2m8","e2m9", "e3m1","e3m3","e3m3","e3m4","e3m5","e3m6","e3m7","e3m8","e3m9", "dphoof","bfgga0","heada1","cybra1","spida1d1"]; let g_mode = DOOMGLOBALS::with_ref(|g| g.gamemode); if g_mode == GameMode::Shareware { // I_ERROR? } if g_mode == GameMode::Registered { let missing_registered_lump = iwads_lumps_to_check .iter() .any(|lump_name| W_CheckNumForName(lump_name) == -1); if missing_registered_lump { // TODO: Implement I_Error // I_Error("\nThis is not the registered version."); } } } // Display the modifed game header if is_modified { print!("===========================================================================\n\ ATTENTION: This version of DOOM has been modified. If you would like to\n\ get a copy of the original game, call 1-800-IDGAMES or see the readme file.\n\ You will not receive technical support for modified games.\n\ press enter to continue\n\ ===========================================================================\n" ); flush_and_wait(); } match DOOMGLOBALS::with_ref(|g| g.gamemode) { GameMode::Shareware | GameMode::Indetermined => { print!("===========================================================================\n\ Shareware!\n\ ===========================================================================\n") } GameMode::Registered | GameMode::Retail | GameMode::Commercial => { print!("===========================================================================\n\ Commercial product - do not distribute!\n\ Please report software piracy to the SPA: 1-800-388-PIR8\n\ ===========================================================================\n") } } // TODO: Implement other inits /* println!("M_Init: Init miscellaneous info."); M_Init(); println!("R_Init: Init DOOM refresh daemon - ") R_Init(); println!("P_Init: Init Playloop state."); P_Init(); println!("I_Init: Setting up machine state."); I_Init() println!("D_CheckNetGame: Checking network game status."); D_CheckNetGame() println!("S_Init: Setting up sound."); S_Init (snd_SfxVolume /* *8 */, snd_MusicVolume /* *8*/ ); printf ("HU_Init: Setting up heads up display.\n"); HU_Init (); printf ("ST_Init: Init status bar.\n"); ST_Init (); */ // Check for a driver that wants intermission stats p = M_CheckParm("-statcopy"); if p != PARM_NOT_FOUND && p < m_argv::M_GetArgC() - 1 { // TODO: implement for statistics driver /*extern void* statcopy; statcopy = (void*)atoi(myargv[p+1]); printf ("External statistics registered.\n");*/ } // Start the approiate gaem based on parms p = M_CheckParm("-record"); if p != PARM_NOT_FOUND && p < m_argv::M_GetArgC() - 1 { // TODO: Implement G_RecordDemo //G_RecordDemo(m_argv::M_GetOptionalArgumentValueByIndex(p+1)); DOOMGLOBALS::with_mut(|g| g.autostart = true); } p = M_CheckParm("-playdemo"); if p != PARM_NOT_FOUND && p < m_argv::M_GetArgC() - 1 { // TODO: Implement G_DeferedPlayDemo // G_DeferedPlayDemo (myargv[p+1]); // D_DoomLoop (); // never returns } p = M_CheckParm("-timedemo"); if p != PARM_NOT_FOUND && p < m_argv::M_GetArgC() - 1 { // TODO: Implement G_TimeDemo // G_TimeDemo (myargv[p+1]); // D_DoomLoop (); // never returns } p = M_CheckParm("-loadgame"); if p != PARM_NOT_FOUND && p < m_argv::M_GetArgC() - 1 { // TODO: Implement loadgame param G_LoadGame // G_TimeDemo (myargv[p+1]); // D_DoomLoop (); // never returns if M_CheckParm("-cdrom") != PARM_NOT_FOUND { // sprintf(file, "c:\\doomdata\\"SAVEGAMENAME"%c.dsg",myargv[p+1][0]); } else { // sprintf(file, SAVEGAMENAME"%c.dsg",myargv[p+1][0]); } // G_LoagGame(file) } p = M_CheckParm("-ga_loadgame"); if p != PARM_NOT_FOUND && p < m_argv::M_GetArgC() - 1 { } // let g_action = DOOMGLOBALS::with_ref(|g| g.gameaction); // TODO: Impement Game actions // if g_action != ga_loadgame { // let autostart = DOOMGLOBALS::with_ref(|g| g.autostart); // let netgame = DOOMGLOBALS::with_ref(|G| G.) // } // D_DoomLoop() }