raze-gles/source/core/gamecontrol.cpp
Christoph Oelckers 1e12bca0a6 - route World Tour's texts through the string table by adding an indirection option
This way they can be translated into all languages without duplicating all relevant content.
The original text files are no longer being used.
2020-09-26 10:08:48 +02:00

1777 lines
46 KiB
C++

//-------------------------------------------------------------------------
/*
Copyright (C) 2019 Christoph Oelckers
This 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.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
//-------------------------------------------------------------------------
#include <stdexcept>
#include "gamecontrol.h"
#include "tarray.h"
#include "zstring.h"
#include "name.h"
#include "sc_man.h"
#include "c_cvars.h"
#include "gameconfigfile.h"
#include "gamecvars.h"
#include "build.h"
#include "inputstate.h"
#include "m_argv.h"
#include "rts.h"
#include "printf.h"
#include "c_bind.h"
#include "v_font.h"
#include "c_console.h"
#include "c_dispatch.h"
#include "i_specialpaths.h"
#include "raze_music.h"
#include "statistics.h"
#include "menu.h"
#include "gstrings.h"
#include "quotemgr.h"
#include "mapinfo.h"
#include "raze_sound.h"
#include "i_system.h"
#include "inputstate.h"
#include "v_video.h"
#include "st_start.h"
#include "s_music.h"
#include "i_video.h"
#include "v_text.h"
#include "resourcefile.h"
#include "c_dispatch.h"
#include "glbackend/glbackend.h"
#include "engineerrors.h"
#include "mmulti.h"
#include "gamestate.h"
#include "gstrings.h"
#include "texturemanager.h"
#include "i_interface.h"
#include "x86.h"
#include "startupinfo.h"
#include "mapinfo.h"
#include "menustate.h"
#include "screenjob.h"
#include "statusbar.h"
#include "uiinput.h"
#include "d_net.h"
#include "automap.h"
CVAR(Bool, autoloadlights, true, CVAR_ARCHIVE|CVAR_GLOBALCONFIG)
CVAR(Bool, autoloadbrightmaps, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
CUSTOM_CVAR(String, language, "auto", CVAR_ARCHIVE | CVAR_NOINITCALL | CVAR_GLOBALCONFIG)
{
GStrings.UpdateLanguage(self);
}
CUSTOM_CVAR(Int, mouse_capturemode, 1, CVAR_GLOBALCONFIG | CVAR_ARCHIVE)
{
if (self < 0)
{
self = 0;
}
else if (self > 2)
{
self = 2;
}
}
// The last remains of sdlayer.cpp
GameInterface* gi;
int myconnectindex, numplayers;
int connecthead, connectpoint2[MAXMULTIPLAYERS];
auto vsnprintfptr = vsnprintf; // This is an inline in Visual Studio but we need an address for it to satisfy the MinGW compiled libraries.
int lastTic;
extern bool pauseext;
cycle_t thinktime, actortime, gameupdatetime, drawtime;
gamestate_t gamestate = GS_STARTUP;
gameaction_t gameaction = ga_nothing;
// gameaction state
MapRecord* g_nextmap;
int g_nextskill;
FILE* hashfile;
FStartupInfo GameStartupInfo;
FMemArena dump; // this is for memory blocks than cannot be deallocated without some huge effort. Put them in here so that they do not register on shutdown.
InputState inputState;
int ShowStartupWindow(TArray<GrpEntry> &);
FString GetGameFronUserFiles();
void InitFileSystem(TArray<GrpEntry>&);
void I_SetWindowTitle(const char* caption);
void S_ParseSndInfo();
void I_DetectOS(void);
void LoadScripts();
void MainLoop();
bool AppActive = true;
FString currentGame;
FString LumpFilter;
TMap<FName, int32_t> NameToTileIndex; // for assigning names to tiles. The menu accesses this list. By default it gets everything from the dynamic tile map in Duke Nukem and Redneck Rampage.
// Todo: Add additional definition file for the other games or textures not in that list so that the menu does not have to rely on indices.
CVAR(Bool, queryiwad, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG);
CVAR(String, defaultiwad, "", CVAR_ARCHIVE | CVAR_GLOBALCONFIG);
CVAR(Bool, disableautoload, false, CVAR_ARCHIVE | CVAR_NOINITCALL | CVAR_GLOBALCONFIG)
//CVAR(Bool, autoloadbrightmaps, false, CVAR_ARCHIVE | CVAR_NOINITCALL | CVAR_GLOBALCONFIG) // hopefully this is an option for later
//CVAR(Bool, autoloadlights, false, CVAR_ARCHIVE | CVAR_NOINITCALL | CVAR_GLOBALCONFIG)
extern int hud_size_max;
int paused;
bool pausedWithKey;
CUSTOM_CVAR(Int, cl_gender, 0, CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
{
if (self < 0 || self > 3) self = 0;
}
int StrTable_GetGender()
{
return cl_gender;
}
bool validFilter(const char* str);
static StringtableCallbacks stblcb =
{
validFilter,
StrTable_GetGender
};
extern int chatmodeon;
bool System_WantGuiCapture()
{
bool wantCapt;
if (menuactive == MENU_Off)
{
wantCapt = ConsoleState == c_down || ConsoleState == c_falling || chatmodeon;
}
else
{
wantCapt = (menuactive == MENU_On || menuactive == MENU_OnNoPause);
}
return wantCapt;
}
bool System_WantLeftButton()
{
return false;// (gamestate == GS_MENUSCREEN || gamestate == GS_TITLELEVEL);
}
bool System_NetGame()
{
return false; // fixme later. For now there is no netgame support.
}
bool System_WantNativeMouse()
{
return false;
}
static bool System_CaptureModeInGame()
{
return true;
}
static bool System_DisableTextureFilter()
{
return hw_useindexedcolortextures;
}
static IntRect System_GetSceneRect()
{
// Special handling so the view with a visible status bar displays properly
int height = windowxy2.y - windowxy1.y + 1, width = windowxy2.x - windowxy1.x + 1;
IntRect mSceneViewport;
mSceneViewport.left = windowxy1.x;
mSceneViewport.top = windowxy1.y;
mSceneViewport.width = width;
mSceneViewport.height = height;
return mSceneViewport;
}
//==========================================================================
//
// DoomSpecificInfo
//
// Called by the crash logger to get application-specific information.
//
//==========================================================================
void System_CrashInfo(char* buffer, size_t bufflen, const char *lfstr)
{
const char* arg;
char* const buffend = buffer + bufflen - 2; // -2 for CRLF at end
int i;
buffer += mysnprintf(buffer, buffend - buffer, GAMENAME " version %s (%s)", GetVersionString(), GetGitHash());
buffer += snprintf(buffer, buffend - buffer, "%sCommand line:", lfstr);
for (i = 0; i < Args->NumArgs(); ++i)
{
buffer += snprintf(buffer, buffend - buffer, " %s", Args->GetArg(i));
}
for (i = 0; (arg = fileSystem.GetResourceFileName(i)) != NULL; ++i)
{
buffer += mysnprintf(buffer, buffend - buffer, "%sFile %d: %s", lfstr, i, arg);
}
buffer += mysnprintf(buffer, buffend - buffer, "%s", lfstr);
*buffer = 0;
}
//==========================================================================
//
//
//
//==========================================================================
UserConfig userConfig;
void UserConfig::ProcessOptions()
{
// -help etc are omitted
// -cfg / -setupfile refer to Build style config which are not supported.
if (Args->CheckParm("-cfg") || Args->CheckParm("-setupfile"))
{
Printf("Build-format config files not supported and will be ignored\n");
}
auto v = Args->CheckValue("-addon");
if (v)
{
auto val = strtol(v, nullptr, 0);
static const char* const addons[] = { "DUKE3D.GRP", "DUKEDC.GRP", "NWINTER.GRP", "VACATION.GRP" };
if (val >= 0 && val < 4) gamegrp = addons[val];
else Printf("%s: Unknown Addon\n", v);
}
else if (Args->CheckParm("-nam"))
{
gamegrp = "NAM.GRP";
}
else if (Args->CheckParm("-napalm"))
{
gamegrp = "NAPALM.GRP";
}
else if (Args->CheckParm("-ww2gi"))
{
gamegrp = "WW2GI.GRP";
}
// Set up all needed content for these two mod which feature a very messy distribution.
// As an alternative they can be zipped up - the launcher will be able to detect and set up such versions automatically.
else if (Args->CheckParm("-route66"))
{
gamegrp = "REDNECK.GRP";
DefaultCon = "GAME66.CON";
const char* argv[] = { "tilesa66.art" , "tilesb66.art" };
AddArt.reset(new FArgs(2, argv));
toBeDeleted.Push("turd66.anm*turdmov.anm");
toBeDeleted.Push("turd66.voc*turdmov.voc");
toBeDeleted.Push("end66.anm*rr_outro.anm");
toBeDeleted.Push("end66.voc*rr_outro.voc");
}
else if (Args->CheckParm("-cryptic"))
{
gamegrp = "BLOOD.RFF";
DefaultCon = "CRYPTIC.INI";
const char* argv[] = { "cpart07.ar_" , "cpart15.ar_" };
AddArt.reset(new FArgs(2, argv));
}
v = Args->CheckValue("-gamegrp");
if (v)
{
gamegrp = v;
}
else
{
// This is to enable the use of Doom launchers. that are limited to -iwad for specifying the game's main resource.
v = Args->CheckValue("-iwad");
if (v)
{
gamegrp = v;
}
}
Args->CollectFiles("-rts", ".rts");
auto rts = Args->CheckValue("-rts");
if (rts) RTS_Init(rts);
Args->CollectFiles("-map", ".map");
CommandMap = Args->CheckValue("-map");
static const char* defs[] = { "-def", "-h", nullptr };
Args->CollectFiles("-def", defs, ".def");
DefaultDef = Args->CheckValue("-def");
if (DefaultCon.IsEmpty())
{
static const char* cons[] = { "-con", "-x", nullptr };
Args->CollectFiles("-con", cons, ".con");
DefaultCon = Args->CheckValue("-con");
if (DefaultCon.IsEmpty()) DefaultCon = Args->CheckValue("-ini");
}
static const char* demos[] = { "-playback", "-d", "-demo", nullptr };
Args->CollectFiles("-demo", demos, ".dmo");
CommandDemo = Args->CheckValue("-demo");
static const char* names[] = { "-pname", "-name", nullptr };
Args->CollectFiles("-name", names, ".---"); // this shouldn't collect any file names at all so use a nonsense extension
CommandName = Args->CheckValue("-name");
static const char* nomos[] = { "-nomonsters", "-nodudes", "-nocreatures", nullptr };
Args->CollectFiles("-nomonsters", nomos, ".---"); // this shouldn't collect any file names at all so use a nonsense extension
nomonsters = Args->CheckParm("-nomonsters");
static const char* acons[] = { "-addcon", "-mx", nullptr };
Args->CollectFiles("-addcon", acons, ".con");
AddCons.reset(Args->GatherFiles("-addcon"));
static const char* adefs[] = { "-adddef", "-mh", nullptr };
Args->CollectFiles("-adddef", adefs, ".def");
AddDefs.reset(Args->GatherFiles("-adddef"));
Args->CollectFiles("-art", ".art");
AddArt.reset(Args->GatherFiles("-art"));
nologo = Args->CheckParm("-nologo") || Args->CheckParm("-quick");
nosound = Args->CheckParm("-nosfx") || Args->CheckParm("-nosound");
if (Args->CheckParm("-setup")) queryiwad = 1;
else if (Args->CheckParm("-nosetup")) queryiwad = 0;
if (Args->CheckParm("-file"))
{
// For file loading there's two modes:
// If -file is given, all content will be processed in order and the legacy options be ignored entirely.
//This allows mixing directories and GRP files in arbitrary order.
Args->CollectFiles("-file", NULL);
AddFiles.reset(Args->GatherFiles("-file"));
}
else
{
// Trying to emulate Build. This means to treat RFF files as lowest priority, then all GRPs and then all directories.
// This is only for people depending on lauchers. Since the semantics are so crappy it is strongly recommended to
// use -file instead which gives the user full control over the order in which things are added.
// For single mods this is no problem but don't even think about loading more stuff consistently...
static const char* grps[] = { "-g", "-grp", nullptr };
static const char* dirs[] = { "-game_dir", "-j", nullptr };
static const char* rffs[] = { "-rff", "-snd", nullptr };
static const char* twostep[] = { "-rff", "-grp", nullptr };
// Abuse the inner workings to get the files into proper order. This is not 100% accurate but should work fine for everything that doesn't intentionally fuck things up.
Args->CollectFiles("-rff", rffs, ".rff");
Args->CollectFiles("-grp", grps, nullptr);
Args->CollectFiles("-grp", twostep, nullptr); // The two previous calls have already brought the content in order so collecting it again gives us one list with everything.
AddFilesPre.reset(Args->GatherFiles("-grp"));
Args->CollectFiles("-game_dir", dirs, nullptr);
AddFiles.reset(Args->GatherFiles("-game_dir"));
}
if (Args->CheckParm("-showcoords") || Args->CheckParm("-w"))
{
C_DoCommand("stat coord");
}
}
//==========================================================================
//
//
//
//==========================================================================
void CheckUserMap()
{
if (userConfig.CommandMap.IsEmpty()) return;
FString startupMap = userConfig.CommandMap;
if (startupMap.IndexOfAny("/\\") < 0) startupMap.Insert(0, "/");
DefaultExtension(startupMap, ".map");
startupMap.Substitute("\\", "/");
NormalizeFileName(startupMap);
if (fileSystem.FileExists(startupMap))
{
Printf("Using level: \"%s\".\n", startupMap.GetChars());
}
else
{
Printf("Level \"%s\" not found.\n", startupMap.GetChars());
startupMap = "";
}
userConfig.CommandMap = startupMap;
}
//==========================================================================
//
//
//
//==========================================================================
namespace Duke3d
{
::GameInterface* CreateInterface();
}
namespace Blood
{
::GameInterface* CreateInterface();
}
namespace ShadowWarrior
{
::GameInterface* CreateInterface();
}
namespace Powerslave
{
::GameInterface* CreateInterface();
}
extern int MinFPSRate; // this is a bit messy.
void CheckFrontend(int flags)
{
if (flags & GAMEFLAG_BLOOD)
{
gi = Blood::CreateInterface();
}
else if (flags & GAMEFLAG_SW)
{
MinFPSRate = 40;
gi = ShadowWarrior::CreateInterface();
}
else if (flags & GAMEFLAG_PSEXHUMED)
{
gi = Powerslave::CreateInterface();
}
else
{
gi = Duke3d::CreateInterface();
}
}
void I_StartupJoysticks();
void I_ShutdownInput();
int RunGame();
int GameMain()
{
int r;
static SystemCallbacks syscb =
{
System_WantGuiCapture,
System_WantLeftButton,
System_NetGame,
System_WantNativeMouse,
System_CaptureModeInGame,
nullptr,
nullptr,
nullptr,
System_DisableTextureFilter,
nullptr,
System_GetSceneRect,
};
sysCallbacks = &syscb;
try
{
r = RunGame();
}
catch (const CExitEvent & exit)
{
// Just let the rest of the function execute.
r = exit.Reason();
}
catch (const std::exception & err)
{
// shut down critical systems before showing a message box.
I_ShowFatalError(err.what());
r = -1;
}
DeleteScreenJob();
M_ClearMenus(true);
if (gi)
{
gi->FreeGameData(); // Must be done before taking down any subsystems.
}
S_StopMusic(true);
if (soundEngine) delete soundEngine;
soundEngine = nullptr;
I_CloseSound();
I_ShutdownInput();
G_SaveConfig();
C_DeinitConsole();
V_ClearFonts();
vox_deinit();
TexMan.DeleteAll();
TileFiles.CloseAll(); // delete the texture data before shutting down graphics.
GLInterface.Deinit();
I_ShutdownGraphics();
M_DeinitMenus();
engineUnInit();
if (gi)
{
delete gi;
gi = nullptr;
}
DeleteStartupScreen();
PClass::StaticShutdown();
if (Args) delete Args;
return r;
}
//==========================================================================
//
//
//
//==========================================================================
void SetDefaultStrings()
{
if ((g_gameType & GAMEFLAG_DUKE) && fileSystem.FindFile("E4L1.MAP") < 0)
{
// Pre-Atomic releases do not define this.
gVolumeNames[0] = "$L.A. Meltdown";
gVolumeNames[1] = "$Lunar Apocalypse";
gVolumeNames[2] = "$Shrapnel City";
if (g_gameType & GAMEFLAG_SHAREWARE) gVolumeNames[3] = "$The Birth";
gSkillNames[0] = "$Piece of Cake";
gSkillNames[1] = "$Let's Rock";
gSkillNames[2] = "$Come get Some";
gSkillNames[3] = "$Damn I'm Good";
}
// Blood hard codes its skill names, so we have to define them manually.
if (g_gameType & GAMEFLAG_BLOOD)
{
gSkillNames[0] = "$STILL KICKING";
gSkillNames[1] = "$PINK ON THE INSIDE";
gSkillNames[2] = "$LIGHTLY BROILED";
gSkillNames[3] = "$WELL DONE";
gSkillNames[4] = "$EXTRA CRISPY";
}
//Set a few quotes which are used for common handling of a few status messages
quoteMgr.InitializeQuote(23, "$MESSAGES: ON");
quoteMgr.InitializeQuote(24, "$MESSAGES: OFF");
quoteMgr.InitializeQuote(83, "$FOLLOW MODE OFF");
quoteMgr.InitializeQuote(84, "$FOLLOW MODE ON");
quoteMgr.InitializeQuote(85, "$AUTORUNOFF");
quoteMgr.InitializeQuote(86, "$AUTORUNON");
}
//==========================================================================
//
//
//
//==========================================================================
static TArray<GrpEntry> SetupGame()
{
// Startup dialog must be presented here so that everything can be set up before reading the keybinds.
auto groups = GrpScan();
if (groups.Size() == 0)
{
// Abort if no game data found.
G_SaveConfig();
I_Error("Unable to find any game data. Please verify your settings.");
}
decltype(groups) usedgroups;
int groupno = -1;
// If the user has specified a file name, let's see if we know it.
//
FString game = GetGameFronUserFiles();
if (userConfig.gamegrp.IsEmpty())
{
userConfig.gamegrp = game;
}
if (userConfig.gamegrp.Len())
{
FString gamegrplower = "/" + userConfig.gamegrp.MakeLower();
int g = 0;
for (auto& grp : groups)
{
auto grplower = grp.FileName.MakeLower();
grplower.Substitute("\\", "/");
if (grplower.LastIndexOf(gamegrplower) == grplower.Len() - gamegrplower.Len())
{
groupno = g;
break;
}
g++;
}
}
if (groupno == -1)
{
int pick = 0;
// We got more than one so present the IWAD selection box.
if (groups.Size() > 1)
{
// Locate the user's prefered IWAD, if it was found.
if (defaultiwad[0] != '\0')
{
for (unsigned i = 0; i < groups.Size(); ++i)
{
FString& basename = groups[i].FileName;
if (stricmp(basename, defaultiwad) == 0)
{
pick = i;
break;
}
}
}
if (groups.Size() > 1)
{
TArray<WadStuff> wads;
for (auto& found : groups)
{
WadStuff stuff;
stuff.Name = found.FileInfo.name;
stuff.Path = ExtractFileBase(found.FileName);
wads.Push(stuff);
}
pick = I_PickIWad(&wads[0], (int)wads.Size(), queryiwad, pick);
if (pick >= 0)
{
// The newly selected IWAD becomes the new default
defaultiwad = groups[pick].FileName;
}
groupno = pick;
}
}
else if (groups.Size() == 1)
{
groupno = 0;
}
}
if (groupno == -1) return TArray<GrpEntry>();
auto& group = groups[groupno];
if (GameStartupInfo.Name.IsNotEmpty()) I_SetWindowTitle(GameStartupInfo.Name);
else I_SetWindowTitle(group.FileInfo.name);
// Now filter out the data we actually need and delete the rest.
usedgroups.Push(group);
auto crc = group.FileInfo.dependencyCRC;
if (crc != 0) for (auto& dep : groups)
{
if (dep.FileInfo.CRC == crc)
{
usedgroups.Insert(0, dep); // Order from least dependent to most dependent, which is the loading order of data.
}
}
groups.Reset();
FString selectedScript;
FString selectedDef;
for (auto& ugroup : usedgroups)
{
// For CONs the command line has priority, aside from that, the last one wins. For Blood this handles INIs - the rules are the same.
if (ugroup.FileInfo.scriptname.IsNotEmpty()) selectedScript = ugroup.FileInfo.scriptname;
if (ugroup.FileInfo.defname.IsNotEmpty()) selectedDef = ugroup.FileInfo.defname;
// CVAR has priority. This also overwrites the global variable each time. Init here is lazy so this is ok.
if (ugroup.FileInfo.rtsname.IsNotEmpty() && **rtsname == 0) RTS_Init(ugroup.FileInfo.rtsname);
// For the game filter the last non-empty one wins.
if (ugroup.FileInfo.gamefilter.IsNotEmpty()) LumpFilter = ugroup.FileInfo.gamefilter;
g_gameType |= ugroup.FileInfo.flags;
}
if (userConfig.DefaultCon.IsEmpty()) userConfig.DefaultCon = selectedScript;
if (userConfig.DefaultDef.IsEmpty()) userConfig.DefaultDef = selectedDef;
// This can only happen with a custom game that does not define any filter.
// In this case take the display name and strip all whitespace and invaliid path characters from it.
if (LumpFilter.IsEmpty())
{
LumpFilter = usedgroups.Last().FileInfo.name;
LumpFilter.StripChars(".:/\\<>?\"*| \t\r\n");
}
currentGame = LumpFilter;
currentGame.Truncate(currentGame.IndexOf("."));
CheckFrontend(g_gameType);
return usedgroups;
}
//==========================================================================
//
//
//
//==========================================================================
void InitLanguages()
{
GStrings.LoadStrings(language);
}
//==========================================================================
//
//
//
//==========================================================================
int RunGame()
{
// Set up the console before anything else so that it can receive text.
C_InitConsole(1024, 768, true);
// +logfile gets checked too late to catch the full startup log in the logfile so do some extra check for it here.
FString logfile = Args->TakeValue("+logfile");
// As long as this engine is still in prerelease mode let's always write a log file.
if (logfile.IsEmpty()) logfile.Format("%s" GAMENAMELOWERCASE ".log", M_GetDocumentsPath().GetChars());
if (logfile.IsNotEmpty())
{
execLogfile(logfile);
}
I_DetectOS();
userConfig.ProcessOptions();
G_LoadConfig();
auto usedgroups = SetupGame();
InitFileSystem(usedgroups);
if (usedgroups.Size() == 0) return 0;
// Handle CVARs with game specific defaults here.
if (g_gameType & GAMEFLAG_BLOOD)
{
mus_redbook.SetGenericRepDefault(false, CVAR_Bool); // Blood should default to CD Audio off - all other games must default to on.
am_showlabel.SetGenericRepDefault(true, CVAR_Bool);
}
if (g_gameType & GAMEFLAG_SW)
{
cl_weaponswitch.SetGenericRepDefault(1, CVAR_Int);
if (cl_weaponswitch > 1) cl_weaponswitch = 1;
}
if (g_gameType & (GAMEFLAG_BLOOD|GAMEFLAG_RR))
{
am_nameontop.SetGenericRepDefault(true, CVAR_Bool); // Blood and RR show the map name on the top of the screen by default.
}
G_ReadConfig(currentGame);
V_InitFontColors();
InitLanguages();
CheckCPUID(&CPU);
CalculateCPUSpeed();
auto ci = DumpCPUInfo(&CPU);
Printf("%s", ci.GetChars());
V_InitScreenSize();
V_InitScreen();
StartScreen = FStartupScreen::CreateInstance(100);
TArray<FString> addArt;
for (auto& grp : usedgroups)
{
for (auto& art : grp.FileInfo.loadart)
{
addArt.Push(art);
}
}
if (userConfig.AddArt) for (auto& art : *userConfig.AddArt)
{
addArt.Push(art);
}
TileFiles.AddArt(addArt);
inputState.ClearAllInput();
if (!GameConfig->IsInitialized())
{
CONFIG_ReadCombatMacros();
}
if (userConfig.CommandName.IsNotEmpty())
{
playername = userConfig.CommandName;
}
GameTicRate = 30;
CheckUserMap();
GPalette.Init(MAXPALOOKUPS + 2); // one slot for each translation, plus a separate one for the base palettes and the internal one
TexMan.Init([]() {}, [](BuildInfo &) {});
V_InitFonts();
TileFiles.Init();
sfx_empty = fileSystem.FindFile("engine/dsempty.lmp"); // this must be done outside the sound code because it's initialized late.
I_InitSound();
Mus_InitMusic();
S_ParseSndInfo();
InitStatistics();
LoadScripts();
SetDefaultStrings();
if (Args->CheckParm("-sounddebug"))
C_DoCommand("stat sounddebug");
if (enginePreInit())
{
I_FatalError("There was a problem initializing the Build engine: %s\n", engineerrstr);
}
auto exec = C_ParseCmdLineParams(nullptr);
if (exec) exec->ExecCommands();
SetupGameButtons();
gi->app_init();
M_Init();
if (!(paletteloaded & PALETTE_MAIN))
I_FatalError("No palette found.");
V_LoadTranslations(); // loading the translations must be delayed until the palettes have been fully set up.
lookups.postLoadTables();
TileFiles.PostLoadSetup();
videoInit();
D_CheckNetGame();
MainLoop();
return 0; // this is never reached. MainLoop only exits via exception.
}
//---------------------------------------------------------------------------
//
// The one and only main loop in the entire engine. Yay!
//
//---------------------------------------------------------------------------
void TickSubsystems()
{
// run these on an independent timer until we got something working for the games.
static const uint64_t tickInterval = 1'000'000'000 / 30;
static uint64_t nexttick = 0;
auto nowtick = I_nsTime();
if (nexttick == 0) nexttick = nowtick;
int cnt = 0;
while (nexttick <= nowtick && cnt < 5)
{
nexttick += tickInterval;
C_Ticker();
M_Ticker();
C_RunDelayedCommands();
cnt++;
}
// If this took too long the engine was most likely suspended so recalibrate the timer.
// Perfect precision is not needed here.
if (cnt == 5) nexttick = nowtick + tickInterval;
}
void updatePauseStatus()
{
// This must go through the network in multiplayer games.
if (M_Active() || System_WantGuiCapture())
{
paused = 1;
}
else if (!M_Active() || !System_WantGuiCapture())
{
if (!pausedWithKey)
{
paused = 0;
}
if (sendPause)
{
sendPause = false;
paused = pausedWithKey ? 0 : 2;
pausedWithKey = !!paused;
}
}
paused ? S_PauseSound(!pausedWithKey, !paused) : S_ResumeSound(paused);
}
//==========================================================================
//
//
//
//==========================================================================
void PolymostProcessVoxels(void);
void videoInit()
{
lookups.postLoadLookups();
V_Init2();
videoSetGameMode(vid_fullscreen, screen->GetWidth(), screen->GetHeight(), 32, 1);
PolymostProcessVoxels();
GLInterface.Init(screen->GetWidth());
screen->BeginFrame();
screen->SetTextureFilterMode();
setViewport(hud_size);
}
void G_FatalEngineError(void)
{
I_FatalError("There was a problem initializing the engine: %s\n\nThe application will now close.", engineerrstr);
}
//==========================================================================
//
//
//
//==========================================================================
CVAR(String, combatmacro0, "", CVAR_ARCHIVE | CVAR_USERINFO)
CVAR(String, combatmacro1, "", CVAR_ARCHIVE | CVAR_USERINFO)
CVAR(String, combatmacro2, "", CVAR_ARCHIVE | CVAR_USERINFO)
CVAR(String, combatmacro3, "", CVAR_ARCHIVE | CVAR_USERINFO)
CVAR(String, combatmacro4, "", CVAR_ARCHIVE | CVAR_USERINFO)
CVAR(String, combatmacro5, "", CVAR_ARCHIVE | CVAR_USERINFO)
CVAR(String, combatmacro6, "", CVAR_ARCHIVE | CVAR_USERINFO)
CVAR(String, combatmacro7, "", CVAR_ARCHIVE | CVAR_USERINFO)
CVAR(String, combatmacro8, "", CVAR_ARCHIVE | CVAR_USERINFO)
CVAR(String, combatmacro9, "", CVAR_ARCHIVE | CVAR_USERINFO)
FStringCVar* const CombatMacros[] = { &combatmacro0, &combatmacro1, &combatmacro2, &combatmacro3, &combatmacro4, &combatmacro5, &combatmacro6, &combatmacro7, &combatmacro8, &combatmacro9};
void CONFIG_ReadCombatMacros()
{
FScanner sc;
try
{
sc.Open("engine/combatmacros.txt");
for (auto s : CombatMacros)
{
sc.MustGetToken(TK_StringConst);
UCVarValue val;
val.String = sc.String;
s->SetGenericRepDefault(val, CVAR_String);
}
}
catch (const CRecoverableError &)
{
// We do not want this to error out. Just ignore if it fails.
}
}
//==========================================================================
//
//
//
//==========================================================================
CCMD(snd_reset)
{
Mus_Stop();
if (soundEngine) soundEngine->Reset();
Mus_ResumeSaved();
}
//==========================================================================
//
// S_PauseSound
//
// Stop music and sound effects, during game PAUSE.
//
//==========================================================================
void S_PauseSound (bool notmusic, bool notsfx)
{
if (!notmusic)
{
S_PauseMusic();
}
if (!notsfx)
{
soundEngine->SetPaused(true);
GSnd->SetSfxPaused (true, 0);
}
}
//==========================================================================
//
// S_ResumeSound
//
// Resume music and sound effects, after game PAUSE.
//
//==========================================================================
void S_ResumeSound (bool notsfx)
{
S_ResumeMusic();
if (!notsfx)
{
soundEngine->SetPaused(false);
GSnd->SetSfxPaused (false, 0);
}
}
//==========================================================================
//
// S_SetSoundPaused
//
// Called with state non-zero when the app is active, zero when it isn't.
//
//==========================================================================
void S_SetSoundPaused(int state)
{
if (state)
{
if (paused == 0)
{
S_ResumeSound(true);
if (GSnd != nullptr)
{
GSnd->SetInactive(SoundRenderer::INACTIVE_Active);
}
}
}
else
{
if (paused == 0)
{
S_PauseSound(false, true);
if (GSnd != nullptr)
{
GSnd->SetInactive(SoundRenderer::INACTIVE_Complete);
}
}
}
#if 0
if (!netgame
#if 0 //def _DEBUG
&& !demoplayback
#endif
)
{
pauseext = !state;
}
#endif
}
FString G_GetDemoPath()
{
FString path = M_GetDemoPath();
path << LumpFilter << '/';
CreatePath(path);
return path;
}
CCMD(printinterface)
{
Printf("Current interface is %s\n", gi->Name());
}
CCMD (togglemsg)
{
FBaseCVar *var, *prev;
UCVarValue val;
if (argv.argc() > 1)
{
if ( (var = FindCVar (argv[1], &prev)) )
{
var->MarkUnsafe();
val = var->GetGenericRep (CVAR_Bool);
val.Bool = !val.Bool;
var->SetGenericRep (val, CVAR_Bool);
const char *statestr = argv.argc() <= 2? "*" : argv[2];
if (*statestr == '*')
{
Printf(PRINT_MEDIUM|PRINT_NOTIFY, "\"%s\" = \"%s\"\n", var->GetName(), val.Bool ? "true" : "false");
}
else
{
int state = (int)strtoll(argv[2], nullptr, 0);
if (state != 0)
{
// Order of Duke's quote string varies, some have on first, some off, so use the sign of the parameter to decide.
// Positive means Off/On, negative means On/Off
int quote = state > 0? state + val.Bool : -(state + val.Bool);
auto text = quoteMgr.GetQuote(quote);
if (text) Printf(PRINT_MEDIUM|PRINT_NOTIFY, "%s\n", text);
}
}
}
}
}
bool OkForLocalization(FTextureID texnum, const char* substitute)
{
return false;
}
// Mainly a dummy.
CCMD(taunt)
{
if (argv.argc() > 2)
{
int taunt = atoi(argv[1]);
int mode = atoi(argv[2]);
// In a ZDoom-style protocol this should be sent:
// Net_WriteByte(DEM_TAUNT);
// Net_WriteByte(taunt);
// Net_WriteByte(mode);
if (mode == 1)
{
// todo:
//gi->PlayTaunt(taunt);
// Duke:
// startrts(taunt, 1)
// Blood:
// sndStartSample(4400 + taunt, 128, 1, 0);
// SW:
// PlaySoundRTS(taunt);
// Exhumed does not implement RTS, should be like Duke
//
}
Printf(PRINT_NOTIFY, "%s", **CombatMacros[taunt - 1]);
}
}
//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------
void GameInterface::FreeLevelData()
{
// Make sure that there is no more level to toy around with.
initspritelists();
numsectors = numwalls = 0;
currentLevel = nullptr;
}
//---------------------------------------------------------------------------
//
// Load crosshair definitions
//
//---------------------------------------------------------------------------
FGameTexture* CrosshairImage;
int CrosshairNum;
CVAR(Int, crosshair, 0, CVAR_ARCHIVE)
CVAR(Color, crosshaircolor, 0xff0000, CVAR_ARCHIVE);
CVAR(Int, crosshairhealth, 2, CVAR_ARCHIVE);
void ST_LoadCrosshair(int num, bool alwaysload)
{
char name[16];
if (!alwaysload && CrosshairNum == num && CrosshairImage != NULL)
{ // No change.
return;
}
if (num == 0)
{
CrosshairNum = 0;
CrosshairImage = NULL;
return;
}
if (num < 0)
{
num = -num;
}
mysnprintf(name, countof(name), "XHAIRB%d", num);
FTextureID texid = TexMan.CheckForTexture(name, ETextureType::MiscPatch, FTextureManager::TEXMAN_TryAny | FTextureManager::TEXMAN_ShortNameOnly);
if (!texid.isValid())
{
mysnprintf(name, countof(name), "XHAIRB1");
texid = TexMan.CheckForTexture(name, ETextureType::MiscPatch, FTextureManager::TEXMAN_TryAny | FTextureManager::TEXMAN_ShortNameOnly);
if (!texid.isValid())
{
texid = TexMan.CheckForTexture("XHAIRS1", ETextureType::MiscPatch, FTextureManager::TEXMAN_TryAny | FTextureManager::TEXMAN_ShortNameOnly);
}
}
CrosshairNum = num;
CrosshairImage = TexMan.GetGameTexture(texid);
}
//---------------------------------------------------------------------------
//
// DrawCrosshair
//
//---------------------------------------------------------------------------
void DrawGenericCrosshair(int num, int phealth, double xdelta)
{
uint32_t color;
double size;
int w, h;
ST_LoadCrosshair(num, false);
// Don't draw the crosshair if there is none
if (CrosshairImage == NULL)
{
return;
}
float crosshairscale = cl_crosshairscale * 0.005;
if (crosshairscale > 0.0f)
{
size = twod->GetHeight() * crosshairscale / 200.;
}
else
{
size = 1.;
}
w = int(CrosshairImage->GetDisplayWidth() * size);
h = int(CrosshairImage->GetDisplayHeight() * size);
if (crosshairhealth == 1)
{
// "Standard" crosshair health (green-red)
int health = phealth;
if (health >= 85)
{
color = 0x00ff00;
}
else
{
int red, green;
health -= 25;
if (health < 0)
{
health = 0;
}
if (health < 30)
{
red = 255;
green = health * 255 / 30;
}
else
{
red = (60 - health) * 255 / 30;
green = 255;
}
color = (red << 16) | (green << 8);
}
}
else if (crosshairhealth == 2)
{
// "Enhanced" crosshair health (blue-green-yellow-red)
int health = clamp(phealth, 0, 200);
float rr, gg, bb;
float saturation = health < 150 ? 1.f : 1.f - (health - 150) / 100.f;
HSVtoRGB(&rr, &gg, &bb, health * 1.2f, saturation, 1);
int red = int(rr * 255);
int green = int(gg * 255);
int blue = int(bb * 255);
color = (red << 16) | (green << 8) | blue;
}
else
{
color = crosshaircolor;
}
DrawTexture(twod, CrosshairImage,
(windowxy1.x + windowxy2.x) / 2 + xdelta * (windowxy2.y - windowxy1.y) / 240.,
(windowxy1.y + windowxy2.y) / 2,
DTA_DestWidth, w,
DTA_DestHeight, h,
DTA_AlphaChannel, true,
DTA_FillColor, color & 0xFFFFFF,
TAG_DONE);
}
void DrawCrosshair(int deftile, int health, double xdelta, double ydelta, double scale, PalEntry color)
{
int type = -1;
if (automapMode == am_off && cl_crosshair)
{
if (deftile < MAXTILES && crosshair == 0)
{
auto tile = tileGetTexture(deftile);
if (tile)
{
double crosshair_scale = cl_crosshairscale * .01 * scale;
DrawTexture(twod, tile, 160 + xdelta, 100 + ydelta, DTA_Color, color,
DTA_FullscreenScale, FSMode_Fit320x200, DTA_ScaleX, crosshair_scale, DTA_ScaleY, crosshair_scale, DTA_CenterOffsetRel, true,
DTA_ViewportX, windowxy1.x, DTA_ViewportY, windowxy1.y, DTA_ViewportWidth, windowxy2.x - windowxy1.x + 1, DTA_ViewportHeight, windowxy2.y - windowxy1.y + 1, TAG_DONE);
return;
}
}
DrawGenericCrosshair(crosshair == 0 ? 2 : *crosshair, health, xdelta);
}
}
//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------
void LoadDefinitions()
{
loaddefinitionsfile("engine/engine.def"); // Internal stuff that is required.
const char* defsfile = G_DefFile();
cycle_t deftimer;
deftimer.Reset();
deftimer.Clock();
if (!loaddefinitionsfile(defsfile, true))
{
deftimer.Unclock();
Printf(PRINT_NONOTIFY, "Definitions file \"%s\" loaded in %.3f ms.\n", defsfile, deftimer.TimeMS());
}
userConfig.AddDefs.reset();
// load the widescreen replacements last so that they do not clobber the CRC for the original items so that mod-side replacement are picked up.
if (fileSystem.FindFile("engine/widescreen.def") >= 0 && !Args->CheckParm("-nowidescreen"))
{
loaddefinitionsfile("engine/widescreen.def");
}
}
//---------------------------------------------------------------------------
//
// code fron gameexec/conrun
//
//---------------------------------------------------------------------------
int getincangle(int a, int na)
{
a &= 2047;
na &= 2047;
if(abs(a-na) < 1024)
return (na-a);
else
{
if(na > 1024) na -= 2048;
if(a > 1024) a -= 2048;
na -= 2048;
a -= 2048;
return (na-a);
}
}
fixed_t getincangleq16(fixed_t a, fixed_t na)
{
a &= 0x7FFFFFF;
na &= 0x7FFFFFF;
if(abs(a-na) < IntToFixed(1024))
return (na-a);
else
{
if(na > IntToFixed(1024)) na -= IntToFixed(2048);
if(a > IntToFixed(1024)) a -= IntToFixed(2048);
na -= IntToFixed(2048);
a -= IntToFixed(2048);
return (na-a);
}
}
//---------------------------------------------------------------------------
//
// Player's movement function, called from game's ticker or from gi->GetInput() as required.
//
//---------------------------------------------------------------------------
void processMovement(InputPacket* currInput, InputPacket* inputBuffer, ControlInfo* const hidInput, double const scaleAdjust, short const drink_amt, bool const allowstrafe, double const turnscale)
{
// set up variables
int const running = !!(inputBuffer->actions & SB_RUN);
int const keymove = gi->playerKeyMove() << running;
int const mousevelscale = g_gameType & GAMEFLAG_BLOOD ? 32 : 4;
int const cntrlvelscale = g_gameType & GAMEFLAG_PSEXHUMED ? 8 : 1;
// process mouse and initial controller input.
if (buttonMap.ButtonDown(gamefunc_Strafe) && allowstrafe)
currInput->svel -= xs_CRoundToInt((hidInput->mousex * mousevelscale) + (scaleAdjust * (hidInput->dyaw / 60) * keymove * cntrlvelscale));
else
currInput->q16avel += FloatToFixed(hidInput->mousex + (scaleAdjust * hidInput->dyaw));
if (!(inputBuffer->actions & SB_AIMMODE))
currInput->q16horz -= FloatToFixed(hidInput->mousey);
else
currInput->fvel -= xs_CRoundToInt(hidInput->mousey * mousevelscale * 2);
if (in_mouseflip)
currInput->q16horz = -currInput->q16horz;
// process remaining controller input.
currInput->q16horz -= FloatToFixed(scaleAdjust * hidInput->dpitch);
currInput->svel -= xs_CRoundToInt(scaleAdjust * hidInput->dx * keymove * cntrlvelscale);
currInput->fvel -= xs_CRoundToInt(scaleAdjust * hidInput->dz * keymove * cntrlvelscale);
// process keyboard turning keys.
if (buttonMap.ButtonDown(gamefunc_Strafe) && allowstrafe)
{
if (abs(inputBuffer->svel) < keymove)
{
if (buttonMap.ButtonDown(gamefunc_Turn_Left))
currInput->svel += keymove;
if (buttonMap.ButtonDown(gamefunc_Turn_Right))
currInput->svel -= keymove;
}
}
else
{
static double turnheldtime;
int const turnheldamt = 120 / GameTicRate;
double const turboturntime = 590. / GameTicRate;
double turnamount = ((running ? 43375. / 27. : 867.5) / GameTicRate) * turnscale;
double preambleturn = turnamount / (347. / 92.);
// allow Exhumed to use its legacy values given the drastic difference from the other games.
if ((g_gameType & GAMEFLAG_PSEXHUMED) && cl_exhumedoldturn)
{
turnamount = running ? 12 : 8;
preambleturn = turnamount;
}
if (buttonMap.ButtonDown(gamefunc_Turn_Left) || (buttonMap.ButtonDown(gamefunc_Strafe_Left) && !allowstrafe))
{
turnheldtime += scaleAdjust * turnheldamt;
currInput->q16avel -= FloatToFixed(scaleAdjust * (turnheldtime >= turboturntime ? turnamount : preambleturn));
}
else if (buttonMap.ButtonDown(gamefunc_Turn_Right) || (buttonMap.ButtonDown(gamefunc_Strafe_Right) && !allowstrafe))
{
turnheldtime += scaleAdjust * turnheldamt;
currInput->q16avel += FloatToFixed(scaleAdjust * (turnheldtime >= turboturntime ? turnamount : preambleturn));
}
else
{
turnheldtime = 0;
}
}
// process keyboard forward/side velocity keys.
if (abs(inputBuffer->svel) < keymove)
{
if (buttonMap.ButtonDown(gamefunc_Strafe_Left) && allowstrafe)
currInput->svel += keymove;
if (buttonMap.ButtonDown(gamefunc_Strafe_Right) && allowstrafe)
currInput->svel -= keymove;
}
if (abs(inputBuffer->fvel) < keymove)
{
if (isRR() && drink_amt >= 66 && drink_amt <= 87)
{
if (buttonMap.ButtonDown(gamefunc_Move_Forward))
{
currInput->fvel += keymove;
if (drink_amt & 1)
currInput->svel += keymove;
else
currInput->svel -= keymove;
}
if (buttonMap.ButtonDown(gamefunc_Move_Backward))
{
currInput->fvel -= keymove;
if (drink_amt & 1)
currInput->svel -= keymove;
else
currInput->svel += keymove;
}
}
else
{
if (buttonMap.ButtonDown(gamefunc_Move_Forward))
currInput->fvel += keymove;
if (buttonMap.ButtonDown(gamefunc_Move_Backward))
currInput->fvel -= keymove;
}
}
// add collected input to game's local input accumulation packet.
inputBuffer->fvel = clamp(inputBuffer->fvel + currInput->fvel, -keymove, keymove);
inputBuffer->svel = clamp(inputBuffer->svel + currInput->svel, -keymove, keymove);
inputBuffer->q16avel += currInput->q16avel;
inputBuffer->q16horz += currInput->q16horz;
}
//---------------------------------------------------------------------------
//
// Player's horizon function, called from game's ticker or from gi->GetInput() as required.
//
//---------------------------------------------------------------------------
void sethorizon(fixed_t* q16horiz, fixed_t const q16horz, ESyncBits* actions, double const scaleAdjust)
{
// Calculate adjustment as true pitch (Fixed point math really sucks...)
double horizAngle = atan2(*q16horiz - IntToFixed(100), IntToFixed(128)) * (512. / pi::pi());
if (q16horz)
{
*actions &= ~SB_CENTERVIEW;
horizAngle = clamp(horizAngle + FixedToFloat(q16horz), -180, 180);
}
// this is the locked type
if (*actions & (SB_AIM_UP|SB_AIM_DOWN))
{
*actions &= ~SB_CENTERVIEW;
double const amount = 250. / GameTicRate;
if (*actions & SB_AIM_DOWN)
horizAngle -= scaleAdjust * amount;
if (*actions & SB_AIM_UP)
horizAngle += scaleAdjust * amount;
}
// this is the unlocked type
if (*actions & (SB_LOOK_UP|SB_LOOK_DOWN))
{
*actions |= SB_CENTERVIEW;
double const amount = 500. / GameTicRate;
if (*actions & SB_LOOK_DOWN)
horizAngle -= scaleAdjust * amount;
if (*actions & SB_LOOK_UP)
horizAngle += scaleAdjust * amount;
}
// convert back to Build's horizon
*q16horiz = IntToFixed(100) + xs_CRoundToInt(IntToFixed(128) * tan(horizAngle * (pi::pi() / 512.)));
// return to center if conditions met.
if ((*actions & SB_CENTERVIEW) && !(*actions & (SB_LOOK_UP|SB_LOOK_DOWN)))
{
if (*q16horiz < FloatToFixed(99.75) || *q16horiz > FloatToFixed(100.25))
{
// move *q16horiz back to 100
*q16horiz += xs_CRoundToInt(scaleAdjust * (((1000. / GameTicRate) * FRACUNIT) - (*q16horiz * (10. / GameTicRate))));
}
else
{
// not looking anymore because *q16horiz is back at 100
*q16horiz = IntToFixed(100);
*actions &= ~SB_CENTERVIEW;
}
}
// clamp before returning
*q16horiz = clamp(*q16horiz, gi->playerHorizMin(), gi->playerHorizMax());
}
//---------------------------------------------------------------------------
//
// Player's angle function, called from game's ticker or from gi->GetInput() as required.
//
//---------------------------------------------------------------------------
void applylook(fixed_t* q16ang, fixed_t* q16look_ang, fixed_t* q16rotscrnang, fixed_t* spin, fixed_t const q16avel, ESyncBits* actions, double const scaleAdjust, bool const crouching)
{
// return q16rotscrnang to 0 and set to 0 if less than a quarter of a FRACUNIT (16384)
*q16rotscrnang -= xs_CRoundToInt(scaleAdjust * (*q16rotscrnang * (15. / GameTicRate)));
if (abs(*q16rotscrnang) < (FRACUNIT >> 2)) *q16rotscrnang = 0;
// return q16look_ang to 0 and set to 0 if less than a quarter of a FRACUNIT (16384)
*q16look_ang -= xs_CRoundToInt(scaleAdjust * (*q16look_ang * (7.5 / GameTicRate)));
if (abs(*q16look_ang) < (FRACUNIT >> 2)) *q16look_ang = 0;
if (*actions & SB_LOOK_LEFT)
{
// start looking left
*q16look_ang -= FloatToFixed(scaleAdjust * (4560. / GameTicRate));
*q16rotscrnang += FloatToFixed(scaleAdjust * (720. / GameTicRate));
}
if (*actions & SB_LOOK_RIGHT)
{
// start looking right
*q16look_ang += FloatToFixed(scaleAdjust * (4560. / GameTicRate));
*q16rotscrnang -= FloatToFixed(scaleAdjust * (720. / GameTicRate));
}
if (*actions & SB_TURNAROUND)
{
if (*spin == 0)
{
// currently not spinning, so start a spin
*spin = IntToFixed(-1024);
}
*actions &= ~SB_TURNAROUND;
}
if (*spin < 0)
{
// return spin to 0
fixed_t add = FloatToFixed(scaleAdjust * ((!crouching ? 3840. : 1920.) / GameTicRate));
*spin += add;
if (*spin > 0)
{
// Don't overshoot our target. With variable factor this is possible.
add -= *spin;
*spin = 0;
}
*q16ang += add;
}
if (q16avel)
{
// add player's input
*q16ang = (*q16ang + q16avel) & 0x7FFFFFF;
}
}
//---------------------------------------------------------------------------
//
// Player's ticrate helper functions.
//
//---------------------------------------------------------------------------
void playerAddAngle(fixed_t* q16ang, double* helper, double adjustment)
{
if (!cl_syncinput)
{
*helper += adjustment;
}
else
{
*q16ang = (*q16ang + FloatToFixed(adjustment)) & 0x7FFFFFF;
}
}
void playerSetAngle(fixed_t* q16ang, fixed_t* helper, double adjustment)
{
if (!cl_syncinput)
{
// Add slight offset if adjustment is coming in as absolute 0.
if (adjustment == 0) adjustment += (1. / (FRACUNIT >> 1));
*helper = *q16ang + getincangleq16(*q16ang, FloatToFixed(adjustment));
}
else
{
*q16ang = FloatToFixed(adjustment);
}
}
void playerAddHoriz(fixed_t* q16horiz, double* helper, double adjustment)
{
if (!cl_syncinput)
{
*helper += adjustment;
}
else
{
*q16horiz += FloatToFixed(adjustment);
}
}
void playerSetHoriz(fixed_t* q16horiz, fixed_t* helper, double adjustment)
{
if (!cl_syncinput)
{
// Add slight offset if adjustment is coming in as absolute 0.
if (adjustment == 0) adjustment += (1. / (FRACUNIT >> 1));
*helper = FloatToFixed(adjustment);
}
else
{
*q16horiz = FloatToFixed(adjustment);
}
}
//---------------------------------------------------------------------------
//
// Player's ticrate helper processor.
//
//---------------------------------------------------------------------------
void playerProcessHelpers(fixed_t* q16ang, double* angAdjust, fixed_t* angTarget, fixed_t* q16horiz, double* horizAdjust, fixed_t* horizTarget, double const scaleAdjust)
{
// Process angle amendments from the game's ticker.
if (*angTarget)
{
fixed_t angDelta = getincangleq16(*q16ang, *angTarget);
*q16ang = (*q16ang + xs_CRoundToInt(scaleAdjust * angDelta));
if (abs(*q16ang - *angTarget) < FRACUNIT)
{
*q16ang = *angTarget;
*angTarget = 0;
}
}
else if (*angAdjust)
{
*q16ang = (*q16ang + FloatToFixed(scaleAdjust * *angAdjust)) & 0x7FFFFFF;
}
// Process horizon amendments from the game's ticker.
if (*horizTarget)
{
fixed_t horizDelta = *horizTarget - *q16horiz;
*q16horiz += xs_CRoundToInt(scaleAdjust * horizDelta);
if (abs(*q16horiz - *horizTarget) < FRACUNIT)
{
*q16horiz = *horizTarget;
*horizTarget = 0;
}
}
else if (*horizAdjust)
{
*q16horiz += FloatToFixed(scaleAdjust * *horizAdjust);
}
}