og-odamex/common/c_cvars.cpp
2026-07-31 18:30:10 -05:00

860 lines
18 KiB
C++

// Emacs style mode select -*- C++ -*-
//-----------------------------------------------------------------------------
//
// $Id$
//
// Copyright (C) 1998-2006 by Randy Heit (ZDoom).
// 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:
// Command-line variables
//
//-----------------------------------------------------------------------------
#include "odamex.h"
#include <cmath>
#include <exception>
#include "cmdlib.h"
#include "c_console.h"
#include "c_dispatch.h"
#include "m_alloc.h"
#include "d_netinf.h"
#include "i_system.h"
bool cvar_t::m_DoNoSet = false;
bool cvar_t::m_UseCallback = false;
// denis - all this class does is delete the cvars during its static destruction
class ad_t {
public:
cvar_t *&GetCVars() { static cvar_t *CVars; return CVars; }
ad_t() {}
~ad_t()
{
cvar_t *cvar = GetCVars();
while (cvar)
{
cvar_t *next = cvar->GetNext();
if(cvar->m_Flags & CVAR_AUTO)
delete cvar;
cvar = next;
}
}
} ad;
cvar_t* GetFirstCvar(void)
{
return ad.GetCVars();
}
int cvar_defflags;
cvar_t::cvar_t(const char* var_name, const char* def, const char* help, cvartype_t type,
uint32_t flags, float minval, float maxval)
{
InitSelf(var_name, def, help, type, flags, NULL, minval, maxval);
}
cvar_t::cvar_t(const char* var_name, const char* def, const char* help, cvartype_t type,
uint32_t flags, void (*callback)(cvar_t &), float minval, float maxval)
{
InitSelf(var_name, def, help, type, flags, callback, minval, maxval);
}
void cvar_t::InitSelf(const char* var_name, const char* def, const char* help, cvartype_t type,
uint32_t var_flags, void (*callback)(cvar_t &), float minval, float maxval)
{
cvar_t* dummy;
cvar_t* var = FindCVar(var_name, &dummy);
m_Callback = callback;
m_String = "";
m_Value = 0.0f;
m_Flags = 0;
m_LatchedString = "";
m_HelpText = help;
m_Type = type;
if (var_flags & CVAR_NOENABLEDISABLE)
{
m_MinValue = minval;
m_MaxValue = maxval;
}
else
{
m_MinValue = 0.0f;
m_MaxValue = 1.0f;
}
if (def)
m_Default = def;
else
m_Default = "";
if (var_name)
{
C_AddTabCommand(var_name);
m_Name = var_name;
m_Next = ad.GetCVars();
ad.GetCVars() = this;
}
else
m_Name = "";
if (var)
{
ForceSet(var->m_String.c_str());
if (var->m_Flags & CVAR_AUTO)
delete var;
else
var->~cvar_t();
}
else if (def)
ForceSet(def);
m_Flags = var_flags | CVAR_ISDEFAULT;
}
cvar_t::~cvar_t ()
{
if (m_Name.length())
{
cvar_t *var, *dummy = NULL;
var = FindCVar (m_Name.c_str(), &dummy);
if (var == this)
{
if (dummy)
dummy->m_Next = m_Next;
else
ad.GetCVars() = m_Next;
}
}
}
void cvar_t::ForceSet(const char* valstr)
{
// [SL] 2013-04-16 - Latched CVARs do not change values until the next map.
// Servers and single-player games should abide by this behavior but
// multiplayer clients should just do what the server tells them.
if (m_Flags & CVAR_LATCH && serverside &&
(gamestate == GS_LEVEL || gamestate == GS_INTERMISSION))
{
m_Flags |= CVAR_MODIFIED;
if (valstr)
m_LatchedString = valstr;
else
m_LatchedString.clear();
}
else
{
m_Flags |= CVAR_MODIFIED;
bool numerical_value = IsRealNum(valstr);
bool integral_type = m_Type == CVARTYPE_BOOL || m_Type == CVARTYPE_BYTE ||
m_Type == CVARTYPE_WORD || m_Type == CVARTYPE_INT;
bool floating_type = m_Type == CVARTYPE_FLOAT;
float valf = numerical_value ? atof(valstr) : 0.0f;
// perform rounding to nearest integer for integral types
if (integral_type)
valf = std::round(valf);
valf = std::clamp(valf, m_MinValue, m_MaxValue);
if (numerical_value || integral_type || floating_type)
{
// generate m_String based on the clamped valf value
m_String = fmt::format("{:g}", valf);
}
else
{
// just set m_String to valstr
if (valstr)
m_String = valstr;
else
m_String.clear();
}
m_Value = valf;
if (m_UseCallback)
Callback();
if (m_Flags & CVAR_USERINFO)
D_UserInfoChanged(this);
if (m_Flags & CVAR_SERVERINFO)
D_SendServerInfoChange(this, m_String.c_str());
}
m_Flags &= ~CVAR_ISDEFAULT;
}
void cvar_t::ForceSet(float val)
{
char string[32];
snprintf(string, 32, "%g", val);
ForceSet(string);
}
void cvar_t::Set (const char *val)
{
if (!(m_Flags & CVAR_NOSET) || !m_DoNoSet)
ForceSet (val);
}
void cvar_t::Set (float val)
{
if (!(m_Flags & CVAR_NOSET) || !m_DoNoSet)
ForceSet (val);
}
void cvar_t::SetDefault (const char *val)
{
if(val)
m_Default = val;
else
m_Default = "";
if (m_Flags & CVAR_ISDEFAULT)
{
Set (val);
m_Flags |= CVAR_ISDEFAULT;
}
}
void cvar_t::RestoreDefault ()
{
Set(m_Default.c_str());
m_Flags |= CVAR_ISDEFAULT;
}
//
// cvar_t::Transfer
//
// Copies the value from one cvar to another and then removes the source cvar
//
void cvar_t::Transfer(const char *fromname, const char *toname)
{
cvar_t *from, *to, *dummy;
from = FindCVar(fromname, &dummy);
to = FindCVar(toname, &dummy);
if (from && to)
{
to->ForceSet(from->m_Value);
to->ForceSet(from->m_String.c_str());
// remove the old cvar
cvar_t *cur = ad.GetCVars();
while (cur->m_Next != from)
cur = cur->m_Next;
cur->m_Next = from->m_Next;
}
}
cvar_t *cvar_t::cvar_set (const char *var_name, const char *val)
{
cvar_t *var, *dummy;
if ( (var = FindCVar (var_name, &dummy)) )
var->Set (val);
return var;
}
cvar_t *cvar_t::cvar_forceset (const char *var_name, const char *val)
{
cvar_t *var, *dummy;
if ( (var = FindCVar (var_name, &dummy)) )
var->ForceSet (val);
return var;
}
void cvar_t::EnableNoSet ()
{
m_DoNoSet = true;
}
void cvar_t::EnableCallbacks ()
{
m_UseCallback = true;
cvar_t *cvar = ad.GetCVars();
while (cvar)
{
cvar->Callback ();
cvar = cvar->m_Next;
}
}
void cvar_t::FilterCompactCVars (std::vector<cvar_t *> &cvars, uint32_t filter)
{
cvar_t *cvar = ad.GetCVars();
while (cvar)
{
if (cvar->m_Flags & filter)
cvars.push_back(cvar);
cvar = cvar->m_Next;
}
std::sort(cvars.begin(), cvars.end(), [](const cvar_t* a, const cvar_t* b){ return a->name().compare(b->name()); });
}
// Uses snprintf's return value (number of chars written) to advance
// a pointer of an array of chars to write out a packed byte array
// of cvars, subtracting the base array size from the total after
// each advancement.
void cvar_t::C_WriteCVars (byte **demo_p, uint32_t filter, size_t array_size, bool compact)
{
if (array_size <= 0)
return;
byte *ptr = *demo_p;
int chars;
if (compact)
{
std::vector<cvar_t *> cvars;
chars = snprintf((char*)ptr, array_size, "\\\\%ux", (unsigned int)filter);
ptr += chars;
array_size -= chars;
FilterCompactCVars(cvars, filter);
for (const cvar_t* cvar : cvars)
{
if (array_size <= 0)
{
PrintFmt(PRINT_WARNING, "Warning: Saved Cvars exceed {} bytes, no more cvars will be written.\n", array_size);
return;
}
chars = snprintf ((char *)ptr, array_size, "\\%s", cvar->cstring());
ptr += chars;
array_size -= chars;
}
}
else
{
cvar_t *cvar = ad.GetCVars();
while (cvar)
{
if (cvar->m_Flags & filter)
{
if (array_size <= 0)
{
PrintFmt(PRINT_WARNING, "Saved Cvars exceed {} bytes, no more "
"cvars will be written.\n",
array_size);
return;
}
chars = snprintf((char*)ptr, array_size, "\\%s\\%s",
cvar->name().c_str(), cvar->cstring());
ptr += chars;
array_size -= chars;
}
cvar = cvar->m_Next;
}
}
*demo_p = ptr + 1;
}
void cvar_t::C_ReadCVars (byte **demo_p)
{
char *ptr = *((char **)demo_p);
char *breakpt;
if (*ptr++ != '\\')
return;
if (*ptr == '\\')
{ // compact mode
std::vector<cvar_t *> cvars;
uint32_t filter;
ptr++;
breakpt = strchr (ptr, '\\');
*breakpt = 0;
filter = strtoul (ptr, NULL, 16);
*breakpt = '\\';
ptr = breakpt + 1;
FilterCompactCVars (cvars, filter);
for (cvar_t* cvar : cvars)
{
breakpt = strchr (ptr, '\\');
if (breakpt)
*breakpt = 0;
cvar->Set (ptr);
if (breakpt)
{
*breakpt = '\\';
ptr = breakpt + 1;
}
else
break;
}
}
else
{
char *value;
while ( (breakpt = strchr (ptr, '\\')) )
{
*breakpt = 0;
value = breakpt + 1;
if ( (breakpt = strchr (value, '\\')) )
*breakpt = 0;
cvar_set (ptr, value);
*(value - 1) = '\\';
if (breakpt)
{
*breakpt = '\\';
ptr = breakpt + 1;
}
else
{
break;
}
}
}
*demo_p += strlen (*((char **)demo_p)) + 1;
}
static struct backup_s
{
std::string name, string;
} CVarBackups[MAX_BACKUPCVARS];
static int numbackedup = 0;
//
// C_BackupCVars
//
// Backup cvars for restoration later. Called before connecting to a server
// or a demo starts playing to save all cvars which could be changed while
// by the server or by playing a demo.
// [SL] bitflag can be used to filter which cvars are set to default.
// The default value for bitflag is 0xFFFFFFFF, which effectively disables
// the filtering.
//
void cvar_t::C_BackupCVars (unsigned int bitflag)
{
struct backup_s *backup = CVarBackups;
cvar_t *cvar = ad.GetCVars();
while (cvar)
{
if (cvar->m_Flags & bitflag)
{
if (backup == &CVarBackups[MAX_BACKUPCVARS])
I_Error("C_BackupDemoCVars: Too many cvars to save ({})", MAX_BACKUPCVARS);
backup->name = cvar->m_Name;
backup->string = cvar->m_String;
backup++;
}
cvar = cvar->m_Next;
}
numbackedup = backup - CVarBackups;
}
void cvar_t::C_RestoreCVars (void)
{
struct backup_s *backup = CVarBackups;
int i;
for (i = numbackedup; i; i--, backup++)
{
cvar_set (backup->name.c_str(), backup->string.c_str());
backup->name = backup->string = "";
}
numbackedup = 0;
UnlatchCVars();
}
cvar_t *cvar_t::FindCVar (std::string_view var_name, cvar_t **prev)
{
cvar_t *var;
if (var_name.empty())
return nullptr;
var = ad.GetCVars();
*prev = nullptr;
while (var)
{
if (iequals(var->m_Name, var_name))
break;
*prev = var;
var = var->m_Next;
}
return var;
}
void cvar_t::UnlatchCVars (void)
{
cvar_t *var;
var = ad.GetCVars();
while (var)
{
if (var->m_Flags & (CVAR_MODIFIED | CVAR_LATCH))
{
unsigned oldflags = var->m_Flags & ~CVAR_MODIFIED;
var->m_Flags &= ~(CVAR_LATCH);
if (var->m_LatchedString.length())
{
var->Set (var->m_LatchedString.c_str());
var->m_LatchedString = "";
}
var->m_Flags = oldflags;
}
var = var->m_Next;
}
}
//
// C_SetCvarsToDefault
//
// Initialize cvars to default values after they are created.
// [SL] bitflag can be used to filter which cvars are set to default.
// The default value for bitflag is 0xFFFFFFFF, which effectively disables
// the filtering.
//
void cvar_t::C_SetCVarsToDefaults (unsigned int bitflag)
{
cvar_t *cvar = ad.GetCVars();
while (cvar)
{
if (cvar->m_Flags & bitflag)
{
if (cvar->m_Default.length())
cvar->Set (cvar->m_Default.c_str());
}
cvar = cvar->m_Next;
}
}
void cvar_t::C_ArchiveCVars (void *f)
{
cvar_t *cvar = ad.GetCVars();
while (cvar)
{
if ((baseapp == client && (cvar->m_Flags & CVAR_CLIENTARCHIVE))
|| (baseapp == server && (cvar->m_Flags & CVAR_SERVERARCHIVE)))
{
fmt::print((FILE *)f, "// {}\n", cvar->helptext());
fmt::print((FILE *)f, "set {} {}\n\n", C_QuoteString(cvar->name()), C_QuoteString(cvar->str()));
}
cvar = cvar->m_Next;
}
}
void cvar_t::cvarlist()
{
cvar_t *var = ad.GetCVars();
int count = 0;
while (var)
{
unsigned flags = var->m_Flags;
count++;
PrintFmt(PRINT_HIGH, "{:c}{:c}{:c}{:c} {} \"{}\"\n",
flags & CVAR_ARCHIVE ? 'A' :
flags & CVAR_CLIENTARCHIVE ? 'C' :
flags & CVAR_SERVERARCHIVE ? 'S' : ' ',
flags & CVAR_USERINFO ? 'U' : ' ',
flags & CVAR_SERVERINFO ? 'S' : ' ',
flags & CVAR_NOSET ? '-' :
flags & CVAR_LATCH ? 'L' :
flags & CVAR_UNSETTABLE ? '*' : ' ',
var->name(),
var->str());
var = var->m_Next;
}
PrintFmt(PRINT_HIGH, "{} cvars\n", count);
}
static std::string C_GetValueString(const cvar_t* var)
{
if (!var)
return "unset";
if (var->flags() & CVAR_NOENABLEDISABLE)
return '"' + var->str() + '"';
if (atof(var->cstring()) == 0.0f)
return "disabled";
else
return "enabled";
}
static std::string C_GetLatchedValueString(const cvar_t* var)
{
if (!var)
return "unset";
if (!(var->flags() & CVAR_LATCH))
return C_GetValueString(var);
if (var->flags() & CVAR_NOENABLEDISABLE)
{
return fmt::sprintf("\"%s\"", var->latched());
}
if (atof(var->latched()) == 0.0f)
return "disabled";
else
return "enabled";
}
BEGIN_COMMAND (set)
{
if (argc != 3)
{
PrintFmt(PRINT_HIGH, "usage: set <variable> <value>\n");
}
else
{
cvar_t *var, *prev;
var = cvar_t::FindCVar (argv[1], &prev);
if (!var)
{
const std::string description = "Unsupported in Odamex v" + std::string(NiceVersion());
var = new cvar_t(argv[1], argv[2], description.c_str(), CVARTYPE_NONE,
CVAR_NOENABLEDISABLE | // If we got here due to LoadDefaults, make sure we save the value back out as-is.
CVAR_AUTO |
CVAR_UNSETTABLE |
cvar_defflags);
}
if (var->flags() & CVAR_NOSET)
{
PrintFmt(PRINT_HIGH, "{} is write protected.\n", argv[1]);
return;
}
else if (multiplayer && baseapp == client && (var->flags() & CVAR_SERVERINFO))
{
PrintFmt(PRINT_HIGH, "{} is under server control.\n", argv[1]);
return;
}
// [Russell] - Allow the user to specify either 'enable' and 'disable',
// this will get converted to either 1 or 0
// [AM] Introduce zdoom-standard "true" and "false"
if (!(var->flags() & CVAR_NOENABLEDISABLE))
{
if (strcmp("enabled", argv[2]) == 0 ||
strcmp("true", argv[2]) == 0)
{
argv[2] = (char *)"1";
}
else if (strcmp("disabled", argv[2]) == 0 ||
strcmp("false", argv[2]) == 0)
{
argv[2] = (char *)"0";
}
}
if (var->flags() & CVAR_LATCH)
{
// if new value is different from current value and latched value
if (strcmp(var->cstring(), argv[2]) && strcmp(var->latched(), argv[2]) && gamestate == GS_LEVEL)
PrintFmt(PRINT_HIGH, "{} will be changed for next game.\n", argv[1]);
}
var->Set(argv[2]);
}
}
END_COMMAND (set)
BEGIN_COMMAND (get)
{
cvar_t *prev;
cvar_t *var;
if (argc < 2)
{
PrintFmt(PRINT_HIGH, "usage: get <variable>\n");
return;
}
var = cvar_t::FindCVar (argv[1], &prev);
if (var)
{
// [AM] Determine whose control the cvar is under
std::string control;
if (multiplayer && baseapp == client && (var->flags() & CVAR_SERVERINFO))
control = " (server)";
// [Russell] - Don't make the user feel inadequate, tell
// them its either enabled, disabled or its other value
PrintFmt(PRINT_HIGH, "\"{}\" is {}{}.\n",
var->name(), C_GetValueString(var), control);
if (var->flags() & CVAR_LATCH && var->flags() & CVAR_MODIFIED)
PrintFmt(PRINT_HIGH, "\"{}\" will be changed to {}.\n",
var->name(), C_GetLatchedValueString(var));
}
else
{
PrintFmt(PRINT_HIGH, "\"{}\" is unset.\n", argv[1]);
}
}
END_COMMAND (get)
BEGIN_COMMAND (toggle)
{
cvar_t *prev;
cvar_t *var;
if (argc < 2)
{
PrintFmt(PRINT_HIGH, "usage: toggle <variable>\n");
return;
}
var = cvar_t::FindCVar (argv[1], &prev);
if (!var)
{
PrintFmt(PRINT_HIGH, "\"{}\" is unset.\n", argv[1]);
}
else if (var->flags() & CVAR_NOENABLEDISABLE)
{
PrintFmt(PRINT_HIGH, "\"{}\" cannot be toggled.\n", argv[1]);
}
else if (var->flags() & CVAR_NOSET)
{
PrintFmt(PRINT_HIGH, "{} is write protected.\n", argv[1]);
return;
}
else if (multiplayer && baseapp == client && (var->flags() & CVAR_SERVERINFO))
{
PrintFmt(PRINT_HIGH, "{} is under server control.\n", argv[1]);
return;
}
else
{
if (var->flags() & CVAR_LATCH && var->flags() & CVAR_MODIFIED)
var->Set(!atof(var->latched()));
else
var->Set(!var->value());
// [Russell] - Don't make the user feel inadequate, tell
// them its either enabled, disabled or its other value
PrintFmt(PRINT_HIGH, "\"{}\" is {}.\n",
var->name(), C_GetValueString(var));
if (var->flags() & CVAR_LATCH && var->flags() & CVAR_MODIFIED)
PrintFmt(PRINT_HIGH, "\"{}\" will be changed to {}.\n",
var->name(), C_GetLatchedValueString(var));
}
}
END_COMMAND (toggle)
BEGIN_COMMAND (cvarlist)
{
cvar_t::cvarlist();
}
END_COMMAND (cvarlist)
BEGIN_COMMAND (help)
{
cvar_t *prev;
cvar_t *var;
if (argc < 2)
{
PrintFmt(PRINT_HIGH, "usage: help <variable>\n");
return;
}
var = cvar_t::FindCVar (argv[1], &prev);
if (!var)
{
PrintFmt(PRINT_HIGH, "\"{}\" is unset.\n", argv[1]);
return;
}
PrintFmt(PRINT_HIGH, "Help: {} - {}\n", var->name(), var->helptext());
}
END_COMMAND (help)
BEGIN_COMMAND(errorout)
{
I_Error("errorout was run from the console");
}
END_COMMAND(errorout)
BEGIN_COMMAND(fatalout)
{
I_FatalError("fatalout was run from the console");
}
END_COMMAND(fatalout)
BEGIN_COMMAND(exceptout)
{
std::string crashma = "What's crashma?";
fmt::sprintf("%crashma game, lmao.\n", crashma.at(std::string::npos));
}
END_COMMAND(exceptout)
#if defined _WIN32
BEGIN_COMMAND(crashout)
{
std::terminate();
}
END_COMMAND(crashout)
#elif defined UNIX
#include <signal.h>
#include <unistd.h>
BEGIN_COMMAND(crashout)
{
kill(getpid(), SIGSEGV);
}
END_COMMAND(crashout)
#endif
VERSION_CONTROL (c_cvars_cpp, "$Id$")