og-odamex/common/d_main.cpp

1328 lines
34 KiB
C++

// Emacs style mode select -*- C++ -*-
//-----------------------------------------------------------------------------
//
// $Id$
//
// Copyright (C) 1993-1996 by id Software, Inc.
// Copyright (C) 2006-2026 by The Odamex Team.
//
// This program 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 2
// of the License, or (at your option) any later version.
//
// This program 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.
//
// DESCRIPTION:
// Common functions to determine game mode (shareware, registered),
// parse command line parameters, and handle wad changes.
//
//-----------------------------------------------------------------------------
#include "odamex.h"
#include <array>
#include <sstream>
#include <algorithm>
#include "win32inc.h"
#ifndef _WIN32
#include <sys/stat.h>
#endif
#ifdef UNIX
#include <unistd.h>
#include <dirent.h>
#endif
#include <stdlib.h>
#include "m_alloc.h"
#include "gstrings.h"
#include "z_zone.h"
#include "w_wad.h"
#include "m_argv.h"
#include "m_fileio.h"
#include "c_console.h"
#include "c_doc.h"
#include "i_system.h"
#include "i_time.h"
#include "g_game.h"
#include "g_spawninv.h"
#include "r_main.h"
#include "d_main.h"
#include "d_dehacked.h"
#include "s_sound.h"
#include "gi.h"
#include "w_ident.h"
#include "m_resfile.h"
#include "odainfo.h"
#include "infomap.h"
#ifdef CLIENT_APP
#include "cl_freecam.h"
#endif
OResFiles wadfiles;
OResFiles patchfiles;
OWantFiles missingfiles;
bool missingCommercialIWAD = false;
bool lastWadRebootSuccess = true;
extern bool step_mode;
bool capfps = true;
float maxfps = 35.0f;
#if defined(_WIN32)
typedef struct
{
HKEY root;
const char* path;
const char* value;
} registry_value_t;
static const char* uninstaller_string = "\\uninstl.exe /S ";
// Keys installed by the various CD editions. These are actually the
// commands to invoke the uninstaller and look like this:
//
// C:\Program Files\Path\uninstl.exe /S C:\Program Files\Path
//
// With some munging we can find where Doom was installed.
// [AM] From the persepctive of a 64-bit executable, 32-bit registry keys are
// located in a different spot.
#if _WIN64
#define SOFTWARE_KEY "Software\\Wow6432Node"
#else
#define SOFTWARE_KEY "Software"
#endif
static registry_value_t uninstall_values[] =
{
// Ultimate Doom, CD version (Depths of Doom trilogy)
{
HKEY_LOCAL_MACHINE,
SOFTWARE_KEY "\\Microsoft\\Windows\\CurrentVersion\\"
"Uninstall\\Ultimate Doom for Windows 95",
"UninstallString",
},
// Doom II, CD version (Depths of Doom trilogy)
{
HKEY_LOCAL_MACHINE,
SOFTWARE_KEY "\\Microsoft\\Windows\\CurrentVersion\\"
"Uninstall\\Doom II for Windows 95",
"UninstallString",
},
// Final Doom
{
HKEY_LOCAL_MACHINE,
SOFTWARE_KEY "\\Microsoft\\Windows\\CurrentVersion\\"
"Uninstall\\Final Doom for Windows 95",
"UninstallString",
},
// Shareware version
{
HKEY_LOCAL_MACHINE,
SOFTWARE_KEY "\\Microsoft\\Windows\\CurrentVersion\\"
"Uninstall\\Doom Shareware for Windows 95",
"UninstallString",
},
};
// Value installed by the Collector's Edition when it is installed
static registry_value_t collectors_edition_value =
{
HKEY_LOCAL_MACHINE,
SOFTWARE_KEY "\\Activision\\DOOM Collector's Edition\\v1.0",
"INSTALLPATH",
};
// Subdirectories of the above install path, where IWADs are installed.
static const char* collectors_edition_subdirs[] =
{
"Doom2",
"Final Doom",
"Ultimate Doom",
};
// Location where Steam is installed
static registry_value_t steam_install_location =
{
HKEY_LOCAL_MACHINE,
SOFTWARE_KEY "\\Valve\\Steam",
"InstallPath",
};
// Subdirs of the steam install directory where IWADs are found
static const char* steam_install_subdirs[] =
{
"steamapps\\common\\doom 2\\base",
"steamapps\\common\\Doom 2\\masterbase",
"steamapps\\common\\final doom\\base",
"steamapps\\common\\Doom 2\\finaldoombase",
"steamapps\\common\\ultimate doom\\base",
"steamapps\\common\\DOOM 3 BFG Edition\\base\\wads",
"steamapps\\common\\master levels of doom\\master\\wads", //Let Odamex find the Master Levels pwads too
"steamapps\\common\\ultimate doom\\base\\doom2", //2024 Steam re-release additions here and below
"steamapps\\common\\ultimate doom\\base\\master\\wads",
"steamapps\\common\\ultimate doom\\base\\plutonia",
"steamapps\\common\\ultimate doom\\base\\tnt",
"steamapps\\common\\ultimate doom\\rerelease",
};
static registry_value_t gog_doom_plus_doom2 =
{
HKEY_LOCAL_MACHINE,
SOFTWARE_KEY "\\GOG.com\\Games\\1413291984",
"path",
};
static registry_value_t gog_doom =
{
HKEY_LOCAL_MACHINE,
SOFTWARE_KEY "\\GOG.com\\Games\\1435827232",
"path",
};
static registry_value_t gog_doom2 =
{
HKEY_LOCAL_MACHINE,
SOFTWARE_KEY "\\GOG.com\\Games\\1435848814",
"path",
};
static registry_value_t gog_final_doom =
{
HKEY_LOCAL_MACHINE,
SOFTWARE_KEY "\\GOG.com\\Games\\1435848742",
"path",
};
static char *GetRegistryString(const registry_value_t *reg_val)
{
HKEY key = 0;
DWORD len = 0;
DWORD valtype = 0;
char* result = 0;
// Open the key (directory where the value is stored)
if (RegOpenKeyEx(reg_val->root, reg_val->path,
0, KEY_READ, &key) != ERROR_SUCCESS)
{
return NULL;
}
result = NULL;
// Find the type and length of the string, and only accept strings.
if (RegQueryValueEx(key, reg_val->value,
NULL, &valtype, NULL, &len) == ERROR_SUCCESS && valtype == REG_SZ)
{
// Allocate a buffer for the value and read the value
result = static_cast<char*>(malloc(len));
if (RegQueryValueEx(key, reg_val->value, NULL, &valtype,
(unsigned char *)result, &len) != ERROR_SUCCESS)
{
M_Free(result);
}
}
// Close the key
RegCloseKey(key);
return result;
}
#endif
//
// D_InitializeDoomObjectTables()
// [CMB] Initialize all the doom objects: MobjInfo, SprNames, SoundMap, etc.
//
void D_InitializeDoomObjectTables()
{
// [RH] Initialize items. Still only used for the give command. :-(
InitItems();
// Initialize states
states.clear();
states.insert({boomstates, ::NUMSTATES}, S_NULL);
states.insert(getOdaStates(), S_GIB0);
// Initialize mobjinfo
mobjinfo.clear();
mobjinfo.insert({doom_mobjinfo, ::NUMMOBJTYPES}, MT_PLAYER);
mobjinfo.insert(getOdaMobjinfo(), MT_GIB0);
// Initialize sprnames
sprnames.clear();
sprnames.insert({doom_sprnames, ::NUMSPRITES}, SPR_TROO);
sprnames.insert(getOdaSprNames(), SPR_GIB0);
// Initialize soundmap
SoundMap.clear();
SoundMap.insert({doom_SoundMap, ARRAY_LENGTH(doom_SoundMap)}, 0);
SoundMap.insert({odamex_SoundMap, ARRAY_LENGTH(odamex_SoundMap)}, 0x80000000);
// Initialize spawn map
D_BuildSpawnMap();
states.rebuildMap(
[](const state_t& lhs, const state_t& rhs){ return lhs.statenum < rhs.statenum; },
[](const state_t& s){ return s.statenum; }
);
mobjinfo.rebuildMap(
[](const mobjinfo_t& lhs, const mobjinfo_t& rhs){
return lhs.type < rhs.type || (lhs.type == rhs.type && lhs.doomednum < rhs.doomednum);
},
[](const mobjinfo_t& m){ return m.type; }
);
}
void D_AddSearchDir(std::vector<std::string>& dirs, const char* newdir, missing_dir_policy_t policy)
{
if (!newdir)
return;
D_AddSearchDir(dirs, std::string{newdir}, policy);
}
void D_AddSearchDir(std::vector<std::string>& dirs, std::string newdir, missing_dir_policy_t policy)
{
if (newdir.empty())
return;
M_ExpandHomeDir(newdir);
newdir = M_CleanPath(newdir);
if (M_DirectoryExists(newdir))
{
dirs.push_back(newdir);
}
else if (policy == missing_dir_policy_t::WARN ||
(policy == missing_dir_policy_t::DEV_WARN && (::developer || ::devparm)))
{
PrintFmt(PRINT_WARNING, "{}: search directory \"{}\" not found\n", __FUNCTION__, newdir);
}
}
//
// D_AddSearchDirList
// denis - Split a new directory string using the path list separator and append results to the output
//
void D_AddSearchDirList(std::vector<std::string> &dirs, const char *newdirs, missing_dir_policy_t policy)
{
if (!newdirs)
return;
D_AddSearchDirList(dirs, std::string{newdirs}, policy);
}
void D_AddSearchDirList(std::vector<std::string> &dirs, std::string newdirs, missing_dir_policy_t policy)
{
if(newdirs.empty())
return;
// search through dwd
std::stringstream ss{newdirs};
std::string segment;
while(!ss.eof())
{
std::getline(ss, segment, PATHLISTSEPCHAR);
if(!segment.length())
continue;
M_ExpandHomeDir(segment);
segment = M_CleanPath(segment);
if (M_DirectoryExists(segment))
{
dirs.push_back(segment);
}
else if (policy == missing_dir_policy_t::WARN ||
(policy == missing_dir_policy_t::DEV_WARN && (::developer || ::devparm)))
{
PrintFmt(PRINT_WARNING, "{}: search directory \"{}\" not found\n", __FUNCTION__, segment);
}
}
}
// [AM] Add platform-sepcific search directories
void D_AddPlatformSearchDirs(std::vector<std::string> &dirs)
{
#if defined(_WIN32)
// Doom 95
{
for (const auto& uninstallval : uninstall_values)
{
char* val;
char* path;
char* unstr;
val = GetRegistryString(&uninstallval);
if (val == NULL)
continue;
unstr = strstr(val, uninstaller_string);
if (unstr == NULL)
{
M_Free(val);
}
else
{
path = unstr + strlen(uninstaller_string);
const char* cpath = path;
D_AddSearchDir(dirs, cpath);
}
}
}
// Doom Collectors Edition
{
char* install_path = GetRegistryString(&collectors_edition_value);
if (install_path != NULL)
{
for (const auto& dir : collectors_edition_subdirs)
{
const std::string subpath = fmt::format("{}\\{}", install_path, dir);
D_AddSearchDir(dirs, subpath);
}
M_Free(install_path);
}
}
// Doom on Steam
{
char* install_path = GetRegistryString(&steam_install_location);
if (install_path != NULL)
{
for (const auto& dir : steam_install_subdirs)
{
const std::string subpath = fmt::format("{}\\{}", install_path, dir);
D_AddSearchDir(dirs, subpath);
}
M_Free(install_path);
}
}
// Doom on GOG
{
char* doom_plus_doom2_path = GetRegistryString(&gog_doom_plus_doom2);
if (doom_plus_doom2_path != nullptr)
{
D_AddSearchDir(dirs, doom_plus_doom2_path);
M_Free(doom_plus_doom2_path);
}
char* doom_path = GetRegistryString(&gog_doom);
if (doom_path != nullptr)
{
D_AddSearchDir(dirs, doom_path);
M_Free(doom_path);
}
char* doom2_path = GetRegistryString(&gog_doom2);
if (doom2_path != nullptr)
{
const std::string full_doom2_path = fmt::format("{}\\{}", doom2_path, "doom2");
const std::string master_levels_path = fmt::format("{}\\{}", doom2_path, "master\\wads");
D_AddSearchDir(dirs, full_doom2_path);
D_AddSearchDir(dirs, master_levels_path);
M_Free(doom2_path);
}
char* final_doom_path = GetRegistryString(&gog_final_doom);
if (final_doom_path != NULL)
{
const std::string plutonia_path = fmt::format("{}\\{}", final_doom_path, "Plutonia");
const std::string tnt_path = fmt::format("{}\\{}", final_doom_path, "TNT");
D_AddSearchDir(dirs, plutonia_path);
D_AddSearchDir(dirs, tnt_path);
M_Free(final_doom_path);
}
}
// DOS Doom via DEICE
D_AddSearchDir(dirs, "\\doom2"); // Doom II
D_AddSearchDir(dirs, "\\plutonia"); // Final Doom
D_AddSearchDir(dirs, "\\tnt");
D_AddSearchDir(dirs, "\\doom_se"); // Ultimate Doom
D_AddSearchDir(dirs, "\\doom"); // Shareware / Registered Doom
D_AddSearchDir(dirs, "\\dooms"); // Shareware versions
D_AddSearchDir(dirs, "\\doomsw");
#elif defined(UNIX)
#if defined(INSTALL_PREFIX) && defined(INSTALL_DATADIR)
D_AddSearchDir(dirs, INSTALL_PREFIX "/" INSTALL_DATADIR "/odamex", missing_dir_policy_t::DEV_WARN);
D_AddSearchDir(dirs, INSTALL_PREFIX "/" INSTALL_DATADIR "/games/odamex", missing_dir_policy_t::DEV_WARN);
#endif
// Search the maintainer-directed data directory for WADs
#if defined(ODAMEX_INSTALL_DATADIR)
D_AddSearchDir(dirs, ODAMEX_INSTALL_DATADIR, missing_dir_policy_t::DEV_WARN);
#endif
D_AddSearchDir(dirs, "/usr/share/doom", missing_dir_policy_t::DEV_WARN);
D_AddSearchDir(dirs, "/usr/share/games/doom", missing_dir_policy_t::DEV_WARN);
D_AddSearchDir(dirs, "/usr/local/share/games/doom", missing_dir_policy_t::DEV_WARN);
D_AddSearchDir(dirs, "/usr/local/share/doom", missing_dir_policy_t::DEV_WARN);
// Flatpak sandbox default directories
// (Since you need to pass envvars to a Flatpak)
D_AddSearchDir(dirs, "/run/host/usr/share/doom", missing_dir_policy_t::DEV_WARN);
D_AddSearchDir(dirs, "/run/host/usr/share/games/doom", missing_dir_policy_t::DEV_WARN);
D_AddSearchDir(dirs, "/run/host/usr/local/share/games/doom", missing_dir_policy_t::DEV_WARN);
D_AddSearchDir(dirs, "/run/host/usr/local/share/doom", missing_dir_policy_t::DEV_WARN);
#endif
}
//
// D_GetTitleString
//
// Returns the proper name of the game currently loaded into gameinfo & gamemission
//
std::string D_GetTitleString()
{
return gameinfo.titleString;
}
//
// D_PrintIWADIdentity
//
static void D_PrintIWADIdentity()
{
if (clientside)
{
PrintFmt(PRINT_HIGH, "\n\35\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36"
"\36\36\36\36\36\36\36\36\36\36\36\36\37\n");
if (gamemode == undetermined)
PrintFmt_Bold("Game mode indeterminate, no standard wad found.\n\n");
else
PrintFmt_Bold("{}\n\n", D_GetTitleString());
}
else
{
if (gamemode == undetermined)
PrintFmt(PRINT_HIGH, "Game mode indeterminate, no standard wad found.\n");
else
PrintFmt(PRINT_HIGH, "{}\n", D_GetTitleString());
}
}
/**
* @brief Load all found DEH patches, as well as all found DEHACKED lumps.
*/
void D_LoadResolvedPatches(bool reloadStrings)
{
// Load internal chex.deh if necessary
if (::gamemode == retail_chex)
{
D_DoDehPatch(nullptr, W_GetNumForName("_CHXHACK"), reloadStrings);
}
// Load external patch files first.
for (const auto& file : ::patchfiles)
{
D_DoDehPatch(&file, -1, reloadStrings);
}
// Check WAD files for lumps.
int lump = -1;
while ((lump = W_FindLump("DEHACKED", lump)) != -1)
{
D_DoDehPatch(NULL, lump, reloadStrings);
}
// Re-apply spawninv settings with our new DEH settings.
G_SetupSpawnInventory();
}
//
// D_CleanseFileName
//
// Strips a file name of path information and transforms it into uppercase
//
std::string D_CleanseFileName(const std::string &filename, const std::string &ext)
{
std::string newname(filename);
M_FixPathSep(newname);
if (ext.length())
M_AppendExtension(newname, "." + ext);
size_t slash = newname.find_last_of(PATHSEPCHAR);
if (slash != std::string::npos)
newname = newname.substr(slash + 1, newname.length() - slash);
std::transform(newname.begin(), newname.end(), newname.begin(), toupper);
return newname;
}
//
// D_FindIWAD
//
// Tries to find an IWAD from a set of known IWAD file names.
//
static bool FindIWAD(OResFile& out)
{
// Search for a pre-defined IWAD from the list above
std::vector<OString> filenames = W_GetIWADFilenames();
for (const auto& filename : filenames)
{
// Construct a file.
OWantFile wantfile;
if (!OWantFile::make(wantfile, filename, OFILE_WAD))
{
continue;
}
// Resolve the file.
if (!M_ResolveWantedFile(out, wantfile))
{
continue;
}
return W_IsIWAD(out);
}
return false;
}
/**
* @brief Load files that are assumed to be resolved, in the correct order,
* and complete.
*
* @param newwadfiles New set of WAD files.
* @param newpatchfiles New set of patch files.
*/
static void LoadResolvedFiles(const OResFiles& newwadfiles,
const OResFiles& newpatchfiles)
{
if (newwadfiles.size() < 2)
{
I_FatalError("Tried to load resources without an ODAMEX.WAD or an IWAD.");
}
::wadfiles = newwadfiles;
::patchfiles = newpatchfiles;
// Now scan the contents of the IWAD to determine which one it is
W_ConfigureGameInfo(::wadfiles.at(1));
// print info about the IWAD to the console
D_PrintIWADIdentity();
::modifiedgame = (::wadfiles.size() > 2) ||
!::patchfiles.empty(); // more than odamex.wad and IWAD?
if (::modifiedgame && (::gameinfo.flags & GI_SHAREWARE))
{
I_FatalError(
"\nYou cannot load additional WADs with the shareware version. Register!");
}
W_InitMultipleFiles(::wadfiles);
// [RH] Initialize localizable strings.
// [SL] It is necessary to load the strings here since a dehacked patch
// might change the strings
::GStrings.loadStrings(false);
P_InitMobjNameMap();
// Apply DEH patches.
D_LoadResolvedPatches();
}
/**
* @brief Print a warning that occurrs when the user has an IWAD that's a
* different version than the one we want.
*
* @param wanted The IWAD that we wanted.
* @return True if we emitted an commercial IWAD warning.
*/
static bool CommercialIWADWarning(const OWantFile& wanted)
{
const OMD5Hash& hash = wanted.getWantedMD5();
if (hash.empty())
{
// No MD5 means there is no error we can reasonably display.
return false;
}
const fileIdentifier_t* info = W_GameInfo(wanted.getWantedMD5());
if (!info)
{
// No GameInfo means that we're not dealing with a WAD we recognize.
return false;
}
if (!info->mIsCommercial)
{
// Not commercial means that we should treat the IWAD like any other
// WAD, with no special callout.
return false;
}
PrintFmt("Odamex attempted to load\n> {}.\n\n", info->mIdName);
// Try to find an IWAD file with a matching name in the user's directories.
OWantFile sameNameWant;
OWantFile::make(sameNameWant, wanted.getBasename(), OFILE_WAD);
OResFile sameNameRes;
const bool resolved = M_ResolveWantedFile(sameNameRes, sameNameWant);
if (!resolved)
{
PrintFmt(
"Odamex could not find the data file for this game in any of the locations "
"it searches for WAD files. If you know you have {} on your hard drive, you "
"can add that path to the 'waddirs' cvar so Odamex can find it.\n\n",
wanted.getBasename());
}
else
{
const fileIdentifier_t* curInfo = W_GameInfo(sameNameRes.getMD5());
if (curInfo)
{
// Found a file, but it's the wrong version.
PrintFmt("Odamex found a possible data file, but it's the wrong version.\n> "
"{}\n> {}\n\n",
curInfo->mIdName, sameNameRes.getFullpath());
}
else
{
// Found a file, but it's not recognized at all.
PrintFmt("Odamex found a possible data file, but Odamex does not recognize "
"it.\n> {}\n\n",
sameNameRes.getFullpath());
}
#ifdef _WIN32
PrintFmt(PRINT_ERROR, "You can use a tool such as Omniscient "
"<https://drinkybird.net/doom/omniscient> to patch your wad to the "
"correct version of the data file.\n");
#else
PrintFmt(PRINT_ERROR, "You can use a tool such as xdelta3 <http://xdelta.org/> paried with IWAD "
"patches located on Github <https://github.com/Doom-Utils/iwad-patches> "
"to patch your wad to the correct version of the data file.\n");
#endif
}
PrintFmt("If you do not own this game, consider purchasing it on Steam, GOG, or other "
"digital storefront.\n\n");
return true;
}
//
// D_LoadResourceFiles
//
// Performs the grunt work of loading WAD and DEH/BEX files.
// The global wadfiles and patchfiles vectors are filled with the list
// of loaded filenames and the missingfiles vector is also filled if
// applicable.
//
void D_LoadResourceFiles(const OWantFiles& newwadfiles, const OWantFiles& newpatchfiles)
{
OResFile odamex_wad;
OResFile next_iwad;
::missingfiles.clear();
::missingCommercialIWAD = false;
// Resolve wanted wads.
OResFiles resolved_wads;
resolved_wads.reserve(newwadfiles.size());
for (const auto& wantfile : newwadfiles)
{
OResFile file;
if (!M_ResolveWantedFile(file, wantfile))
{
// Give more useful information when trying to load an IWAD.
const bool isCommercial = CommercialIWADWarning(wantfile);
if (isCommercial && !::missingCommercialIWAD)
{
::missingCommercialIWAD = true;
}
::missingfiles.push_back(wantfile);
PrintFmt(PRINT_WARNING, "Could not resolve resource file \"{}\".",
wantfile.getWantedPath());
continue;
}
resolved_wads.push_back(file);
}
// Resolve wanted patches.
OResFiles resolved_patches;
resolved_patches.reserve(newpatchfiles.size());
for (const auto& wantfile : newpatchfiles)
{
OResFile file;
if (!M_ResolveWantedFile(file, wantfile))
{
::missingfiles.push_back(wantfile);
PrintFmt(PRINT_WARNING, "Could not resolve patch file \"{}\".",
wantfile.getWantedPath());
continue;
}
resolved_patches.push_back(file);
}
// ODAMEX.WAD //
if (::wadfiles.empty())
{
// If we don't have odamex.wad, resolve it now.
OWantFile want_odamex;
OWantFile::make(want_odamex, "odamex.wad", OFILE_WAD);
if (!M_ResolveWantedFile(odamex_wad, want_odamex))
{
I_FatalError("Could not resolve \"{}\". Please ensure this file is "
"someplace where Odamex can find it.\n",
want_odamex.getBasename());
}
}
else
{
// We already have odamex.wad, just make a copy of it.
odamex_wad = ::wadfiles.at(0);
}
// IWAD //
bool got_next_iwad = false;
if (resolved_wads.size() >= 1)
{
// See if the first WAD we passed was an IWAD.
const OResFile& possible_iwad = resolved_wads.at(0);
if (W_IsIWAD(possible_iwad))
{
next_iwad = possible_iwad;
got_next_iwad = true;
resolved_wads.erase(resolved_wads.begin());
if (W_IsIWADDeprecated(next_iwad))
{
PrintFmt_Bold("WARNING: IWAD {} is outdated. Please update it to the "
"latest version.\n",
next_iwad.getBasename());
}
}
}
if (!got_next_iwad && ::wadfiles.size() >= 2)
{
// Reuse the old IWAD. As an optimization, assume that the location
// of the IWAD has not changed on disk.
next_iwad = ::wadfiles.at(1);
got_next_iwad = true;
}
if (!got_next_iwad)
{
// Not provided an IWAD filename and an IWAD is not currently loaded?
// Try to find *any* IWAD using FindIWAD.
got_next_iwad = FindIWAD(next_iwad);
}
if (!got_next_iwad)
{
I_FatalError("Could not resolve an IWAD file. Please ensure at least "
"one IWAD is someplace where Odamex can find it.\n");
}
resolved_wads.insert(resolved_wads.begin(), odamex_wad);
resolved_wads.insert(resolved_wads.begin() + 1, next_iwad);
LoadResolvedFiles(resolved_wads, resolved_patches);
}
/**
* @brief Check to see if the list of WAD files and patches matches the
* currently loaded files.
*
* @detail Note that this relies on the hashes being equal, so if you want
* resources to not be reloaded, ensure the hashes are equal by the
* time they reach this spot.
*
* @param newwadfiles WAD files to check.
* @param newpatchfiles Patch files to check.
* @return True if everything checks out.
*/
static bool CheckWantedMatchesLoaded(const OWantFiles& newwadfiles,
const OWantFiles& newpatchfiles)
{
// Cheking sizes is a good first approximation.
if (newwadfiles.size() + 1 != ::wadfiles.size())
{
return false;
}
if (newpatchfiles.size() != ::patchfiles.size())
{
return false;
}
// Check WAD hashes - with an offset because you can't replace odamex.wad.
for (OWantFiles::const_iterator it = newwadfiles.begin(); it != newwadfiles.end();
++it)
{
size_t idx = it - newwadfiles.begin();
if (it->getWantedMD5() != ::wadfiles.at(idx + 1).getMD5())
{
return false;
}
}
// Check patch hashes.
for (OWantFiles::const_iterator it = newpatchfiles.begin(); it != newpatchfiles.end();
++it)
{
size_t idx = it - newpatchfiles.begin();
if (it->getWantedMD5() != ::patchfiles.at(idx).getMD5())
{
return false;
}
}
return true;
}
//
// D_DoomWadReboot
// [denis] change wads at runtime
// Returns false if there are missing files and fills the missingfiles
// vector
//
// [SL] passing an IWAD as newwadfiles[0] is now optional
// TODO: hash checking for patchfiles
//
bool D_DoomWadReboot(const OWantFiles& newwadfiles, const OWantFiles& newpatchfiles)
{
// already loaded these?
if (::lastWadRebootSuccess && CheckWantedMatchesLoaded(newwadfiles, newpatchfiles))
{
// fast track if files have not been changed
PrintFmt("Currently loaded resources match server checksums.\n\n");
return true;
}
::lastWadRebootSuccess = false;
D_Shutdown();
gamestate_t oldgamestate = ::gamestate;
::gamestate = GS_STARTUP; // prevent console from trying to use nonexistant font
// Load all the WAD and DEH/BEX files
OResFiles oldwadfiles = ::wadfiles;
OResFiles oldpatchfiles = ::patchfiles;
std::string failmsg;
#ifdef CLIENT_APP
Freecam::reset();
#endif
try
{
D_LoadResourceFiles(newwadfiles, newpatchfiles);
// get skill / episode / map from parms
startmap = (gameinfo.flags & GI_MAPxx) ? "MAP01" : "E1M1";
D_Init();
}
catch (CRecoverableError& error)
{
failmsg = error.GetMsg();
}
if (!failmsg.empty())
{
// Uh oh, loading the new resource set failed for some reason.
PrintFmt(PRINT_WARNING,
"Could not load new resource files.\n{}\nReloading previous resource "
"set...\n",
failmsg);
D_Shutdown();
std::string fatalmsg;
try
{
LoadResolvedFiles(oldwadfiles, oldpatchfiles);
// get skill / episode / map from parms
startmap = (gameinfo.flags & GI_MAPxx) ? "MAP01" : "E1M1";
D_Init();
}
catch (CRecoverableError& error)
{
// Something is seriously wrong.
fatalmsg = error.GetMsg();
}
if (!fatalmsg.empty())
{
I_FatalError("Failed to load new resource files, then ran into error when "
"loading original resource files:\n{}\n",
fatalmsg);
}
}
// preserve state
::lastWadRebootSuccess = ::missingfiles.empty();
::gamestate = oldgamestate; // GS_STARTUP would prevent netcode connecting properly
return ::missingfiles.empty() && failmsg.empty();
}
//
// AddCommandLineOptionFiles
//
// Adds the full path of all the file names following the given command line
// option parameter (eg, "-file") matching the specified extension to the
// filenames vector.
//
static void AddCommandLineOptionFiles(OWantFiles& out, const std::string& option,
ofile_t type)
{
DArgs files = Args.GatherFiles(option.c_str());
for (size_t i = 0; i < files.NumArgs(); i++)
{
OWantFile file;
if (OWantFile::make(file, files.GetArg(i), type))
out.push_back(file);
}
files.FlushArgs();
}
//
// D_AddWadCommandLineFiles
//
// Add the WAD files specified with -file.
// Call this from D_DoomMain
//
void D_AddWadCommandLineFiles(OWantFiles& out)
{
AddCommandLineOptionFiles(out, "-file", OFILE_WAD);
}
//
// D_AddDehCommandLineFiles
//
// Adds the DEH/BEX files specified with -bex or -deh.
// Call this from D_DoomMain
//
void D_AddDehCommandLineFiles(OWantFiles& out)
{
AddCommandLineOptionFiles(out, "-bex", OFILE_DEH);
AddCommandLineOptionFiles(out, "-deh", OFILE_DEH);
}
// ============================================================================
//
// Command line information dumps
//
// These are informational command line switches that simply dump
// information and exit before initializing any subsystems.
// These are allowed to run as root since they exit right after.
//
// ============================================================================
// Name the version document after the app, so a client and a server writing
// into the same directory do not clobber each other.
#ifdef CLIENT_APP
constexpr const char* VERSION_BASENAME = "odamex-version";
#elif defined(SERVER_APP)
constexpr const char* VERSION_BASENAME = "odasrv-version";
#elif defined(TEST_APP)
constexpr const char* VERSION_BASENAME = "odagtest-version";
#endif
bool C_WriteVersion(infodumpdest_t dest)
{
return EmitInfoDump(fmt::format("Odamex {}\n", NiceVersion()), VERSION_BASENAME,
".txt", dest);
}
namespace
{
constexpr infodumpdest_t D_InfoDumpDest([[maybe_unused]] infodumpdest_t dest)
{
#if defined(_WIN32) && defined(CLIENT_APP)
return infodumpdest_t::FILE;
#else
return dest;
#endif
}
struct infodump_t
{
const char* param; // the switch, including its leading dashes
bool (*handler)(infodumpdest_t); // writes the information out, false if it could not
infodumpdest_t dest; // whether to write to stdout or a file
};
const std::array InfoDumps = {
infodump_t{"--version", C_WriteVersion, infodumpdest_t::STDOUT},
infodump_t{"--cvardoc", C_WriteCvarDoc, infodumpdest_t::FILE},
infodump_t{"--cvardocjson", C_WriteCvarDocJSON, infodumpdest_t::STDOUT},
};
} // namespace
//
// D_CheckInfoDumps
//
// Checks for every information dump named on the command line, then quits if any of
// them ran.
//
void D_CheckInfoDumps()
{
bool ok = true;
for (const infodump_t& dump : InfoDumps)
{
if (Args.CheckParm(dump.param))
{
if (!dump.handler(D_InfoDumpDest(dump.dest)))
ok = false;
exit(ok ? EXIT_SUCCESS : EXIT_FAILURE);
}
}
}
// ============================================================================
//
// TaskScheduler class
//
// ============================================================================
//
// Attempts to schedule a task (indicated by the function pointer passed to
// the concrete constructor) at a specified interval. For uncapped rates, that
// interval is simply as often as possible. For capped rates, the interval is
// specified by the rate parameter.
//
class TaskScheduler
{
public:
virtual ~TaskScheduler() { }
virtual void run() = 0;
virtual dtime_t getNextTime() const = 0;
virtual float getRemainder() const = 0;
};
class UncappedTaskScheduler : public TaskScheduler
{
public:
UncappedTaskScheduler(void (*task)()) :
mTask(task)
{ }
~UncappedTaskScheduler() override { }
void run() override
{
mTask();
}
dtime_t getNextTime() const override
{
return I_GetTime();
}
float getRemainder() const override
{
return 0.0f;
}
private:
void (*mTask)();
};
class CappedTaskScheduler : public TaskScheduler
{
public:
CappedTaskScheduler(void (*task)(), float rate, int max_count) :
mTask(task), mMaxCount(max_count),
mFrameDuration(I_ConvertTimeFromMs(1000) / rate),
mAccumulator(mFrameDuration),
mPreviousFrameStartTime(I_GetTime())
{
}
~CappedTaskScheduler() override { }
void run() override
{
mFrameStartTime = I_GetTime();
mAccumulator += mFrameStartTime - mPreviousFrameStartTime;
mPreviousFrameStartTime = mFrameStartTime;
int count = mMaxCount;
while (mAccumulator >= mFrameDuration && count--)
{
mTask();
mAccumulator -= mFrameDuration;
}
}
dtime_t getNextTime() const override
{
return mFrameStartTime + mFrameDuration - mAccumulator;
}
float getRemainder() const override
{
// mAccumulator can be greater than mFrameDuration so only get the
// time remaining until the next frame
dtime_t remaining_time = mAccumulator % mFrameDuration;
return (float)(double(remaining_time) / mFrameDuration);
}
private:
void (*mTask)();
const int mMaxCount;
const dtime_t mFrameDuration;
dtime_t mAccumulator;
dtime_t mFrameStartTime;
dtime_t mPreviousFrameStartTime;
};
static std::unique_ptr<TaskScheduler> simulation_scheduler;
static std::unique_ptr<TaskScheduler> display_scheduler;
//
// D_InitTaskSchedulers
//
// Checks for external changes to the rate for the simulation and display
// tasks and instantiates the appropriate TaskSchedulers.
//
static void D_InitTaskSchedulers(void (*sim_func)(), void(*display_func)())
{
bool capped_simulation = !timingdemo;
bool capped_display = !timingdemo && capfps;
static bool previous_capped_simulation = !capped_simulation;
static bool previous_capped_display = !capped_display;
static float previous_maxfps = -1.0f;
if (capped_simulation != previous_capped_simulation)
{
previous_capped_simulation = capped_simulation;
if (capped_simulation)
simulation_scheduler = std::make_unique<CappedTaskScheduler>(sim_func, TICRATE, 4);
else
simulation_scheduler = std::make_unique<UncappedTaskScheduler>(sim_func);
}
if (capped_display != previous_capped_display || maxfps != previous_maxfps)
{
previous_capped_display = capped_display;
previous_maxfps = maxfps;
if (capped_display)
display_scheduler = std::make_unique<CappedTaskScheduler>(display_func, maxfps, 1);
else
display_scheduler = std::make_unique<UncappedTaskScheduler>(display_func);
}
}
void D_ClearTaskSchedulers()
{
simulation_scheduler.reset();
display_scheduler.reset();
}
//
// D_RunTics
//
// The core of the main game loop.
// This loop allows the game simulation timing to be decoupled from the display
// timing. If the the user selects a capped framerate and isn't using the
// -timedemo parameter, both the simulation and display functions will be called
// TICRATE times a second. If the framerate is uncapped, the simulation function
// will still be called TICRATE times a second but the display function will
// be called as often as possible. After each iteration through the loop,
// the program yields briefly to the operating system.
//
void D_RunTics(void (*sim_func)(), void(*display_func)())
{
D_InitTaskSchedulers(sim_func, display_func);
simulation_scheduler->run();
#ifdef CLIENT_APP
// Use linear interpolation for rendering entities if the display
// framerate is not synced with the simulation frequency.
// Ch0wW : if you experience a spinning effect while trying to pause the frame,
// don't forget to add your condition here.
if ((maxfps == TICRATE && capfps)
|| timingdemo || step_mode)
render_lerp_amount = FRACUNIT;
else
render_lerp_amount = simulation_scheduler->getRemainder() * FRACUNIT;
#endif
display_scheduler->run();
if (timingdemo)
return;
// Sleep until the next scheduled task.
const dtime_t simulation_wake_time = simulation_scheduler->getNextTime();
const dtime_t display_wake_time = display_scheduler->getNextTime();
const dtime_t wake_time = std::min<dtime_t>(simulation_wake_time, display_wake_time);
constexpr dtime_t max_sleep_amount = 1000LL * 1000LL; // 1ms
// Sleep in 1ms increments until the next scheduled task
for (dtime_t now = I_GetTime(); wake_time > now; now = I_GetTime())
{
const dtime_t sleep_amount = std::min<dtime_t>(max_sleep_amount, wake_time - now);
I_Sleep(sleep_amount);
}
}
VERSION_CONTROL (d_main_cpp, "$Id$")