/* Copyright (C) 1996-2001 Id Software, Inc. Copyright (C) 2002-2009 John Fitzgibbons and others Copyright (C) 2010-2014 QuakeSpasm developers 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. 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ // cvar.c -- dynamic variable tracking #include "quakedef.hpp" #include #include #include #include #include #include std::map CLIENT_VARIABLES{}; //============================================================================== // // USER COMMANDS // //============================================================================== /* ============ Cvar_List_f -- johnfitz ============ */ void Cvar_List_f() { convar *cvar; const char *partial; int len, count; if (command::argc() > 1) { partial = command::argv(1)->c_str(); len = std::strlen(partial); } else { partial = NULL; len = 0; } count = 0; for (const auto &[name, var]: CLIENT_VARIABLES) { if (partial && std::strncmp(partial, name.c_str(), len) != 0) { continue; } console::safe_print("%s%s %s \"%s\"\n", var->flags.archive ? "*" : " ", var->flags.notify ? "s" : " ", name.c_str(), var->string); count++; } console::safe_print("%i cvars", count); if (partial) { console::safe_print(" beginning with \"%s\"", partial); } console::safe_print("\n"); } /* ============ Cvar_Inc_f -- johnfitz ============ */ void Cvar_Inc_f(void) { switch (command::argc()) { default: case 1: console::info("inc [amount] : increment cvar\n"); break; case 2: convar::set_value(*command::argv(1), convar::variable_value(*command::argv(1)).value_or(0.0) + 1); break; case 3: convar::set_value(*command::argv(1), convar::variable_value(*command::argv(1)).value_or(0.0) + std::atof(command::argv(2)->c_str())); break; } } /* ============ Cvar_Toggle_f -- johnfitz ============ */ void Cvar_Toggle_f() { switch (command::argc()) { default: case 1: console::info("toggle : toggle cvar\n"); break; case 2: if (convar::variable_value(*command::argv(1)).value_or(0.0) != 0.0) convar::set(*command::argv(1), "0"); else convar::set(*command::argv(1), "1"); break; } } /* ============ Cvar_Cycle_f -- johnfitz ============ */ void Cvar_Cycle_f() { int i; if (command::argc() < 3) { console::info("cycle : cycle cvar through a list of values\n"); return; } //loop through the args until you find one that matches the current cvar value. //yes, this will get stuck on a list that contains the same value twice. //it's not worth dealing with, and i'm not even sure it can be dealt with. for (i = 2; i < command::argc(); i++) { //zero is assumed to be a string, even though it could actually be zero. The worst case //is that the first time you call this command, it won't match on zero when it should, but after that, //it will be comparing strings that all had the same source (the user) so it will work. if (std::atof(command::argv(i)->c_str()) == 0) { if (!strcmp(command::argv(i)->c_str(), convar::variable_string(*command::argv(1)).value_or("").c_str())) break; } else { if (std::atof(command::argv(i)->c_str()) == convar::variable_value(*command::argv(1)).value_or(0.0)) break; } } if (i == command::argc()) convar::set(*command::argv(1), *command::argv(2)); // no match else if (i + 1 == command::argc()) convar::set(*command::argv(1), *command::argv(2)); // matched last value in list else convar::set(*command::argv(1), *command::argv(i + 1)); // matched earlier in list } /* ============ Cvar_Reset_f -- johnfitz ============ */ void Cvar_Reset_f() { switch (command::argc()) { default: case 1: console::info("reset : reset cvar to default\n"); break; case 2: convar::reset(*command::argv(1)); break; } } /* ============ Cvar_ResetAll_f -- johnfitz ============ */ void Cvar_ResetAll_f(void) { convar *var; for (auto name: std::views::keys(CLIENT_VARIABLES)) convar::reset(name); } /* ============ Cvar_ResetCfg_f -- QuakeSpasm ============ */ void Cvar_ResetCfg_f() { for (const auto &var: std::views::values(CLIENT_VARIABLES)) if (var->flags.archive) convar::reset(var->name); } //============================================================================== // // INIT // //============================================================================== /* ============ Cvar_Init -- johnfitz ============ */ void Cvar_Init() { command::add("cvarlist", Cvar_List_f); command::add("toggle", Cvar_Toggle_f); command::add("cycle", Cvar_Cycle_f); command::add("inc", Cvar_Inc_f); command::add("reset", Cvar_Reset_f); command::add("resetall", Cvar_ResetAll_f); command::add("resetcfg", Cvar_ResetCfg_f); } std::optional convar::find_var(const std::string_view &desired) { for (auto &[name, var]: CLIENT_VARIABLES) { if (name == desired) return var; } return std::nullopt; } std::optional convar::find_var_after(const std::optional &prev_name, const std::optional with_flags) { auto iter = CLIENT_VARIABLES.begin(); if (prev_name.has_value()) { iter = CLIENT_VARIABLES.find(prev_name.value()); if (iter == CLIENT_VARIABLES.end()) return std::nullopt; ++iter; } // search for the next cvar matching the needed flags while (iter != CLIENT_VARIABLES.end()) { if (!with_flags.has_value() || iter->second->flags.matches_mask(with_flags.value())) break; ++iter; } if (iter != CLIENT_VARIABLES.end()) { return iter->second; } return std::nullopt; } void convar::lock_var(const std::string_view &name) { if (const auto var = find_var(name); var.has_value()) var.value()->flags.locked = true; } void convar::unlock_var(const std::string_view &name) { if (const auto var = find_var(name); var.has_value()) var.value()->flags.locked = false; } void convar::unlock_all() { for (const auto &var: std::views::values(CLIENT_VARIABLES)) { var->flags.locked = false; } } /** * Gets a floating-point representation of the `string` value of the `convar` matching `name`. * * @param name The name of the variable to retreive * @return The value of the variable matching `name`, or `std::nullopt` if a variable with * that name cannot be found. */ std::optional convar::variable_value(const std::string_view &name) { const auto var = find_var(name); if (!var.has_value()) return std::nullopt; return std::strtof(var.value()->string, nullptr); } /** * Gets the `string` value of the `convar` matching `name`. * * @param name The name of the variable to retreive * @return The value of the variable matching `name`, or `std::nullopt` if a variable with * that name cannot be found. */ std::optional convar::variable_string(const std::string_view &name) { const auto var = find_var(name); if (!var.has_value()) return std::nullopt; return var.value()->string; } /** * Autocompletes the name of a `convar` starting with `partial`. * * @param partial Partial name of a variable to retrieve * @return The full name of the first variable beginning with `partial`, or `std::nullopt` if * no matching variable is found. */ std::optional convar::complete_variable(const std::string_view &partial) { if (!partial.empty()) return std::nullopt; // check functions for (const auto &name: CLIENT_VARIABLES | std::views::keys) { if (name.starts_with(name)) return name; } return std::nullopt; } /** * Resets a `convar`s `string` to its `default_string` value. * * @param name Name of the convar to reset */ void convar::reset(const std::string &name) { if (const auto var = find_var(name); !var.has_value()) console::info("variable \"%s\" not found\n", name.c_str()); else var.value()->set(var.value()->default_string); } void convar::set(const std::string &new_value) { if (this->flags.rom || this->flags.locked) return; if (!(this->flags.registered)) return; if (!this->string) this->string = Z_Strdup(new_value.c_str()); else { if (!strcmp(this->string, new_value.c_str())) return; // no change this->flags.changed = true; const std::size_t len = new_value.length(); if (len != std::strlen(this->string)) { Z_Free((void *) this->string); this->string = (char *) Z_Malloc(len + 1); } memcpy((char *) this->string, new_value.c_str(), len + 1); } this->value = std::atof(this->string); //johnfitz -- save initial value for "reset" command if (!this->default_string) this->default_string = Z_Strdup(this->string); //johnfitz -- during initialization, update default too else if (!host_initialized) { // Sys_Printf("changing default of %s: %s -> %s\n", // this->name, this->default_string, this->string); Z_Free((void *) this->default_string); this->default_string = Z_Strdup(this->string); } //johnfitz if (this->callback) this->callback(this); } void convar::set_value(const float new_value) { std::string val; if (new_value == static_cast(static_cast(new_value))) val = std::format("{:d}", static_cast(new_value)); else { val = std::format("{:f}", new_value); // kill trailing zeroes val.erase(val.find_last_not_of('0') + 1, std::string::npos); } this->set(val); } /* ============ Cvar_Set ============ */ void convar::set(const std::string &name, const std::string &value) { const auto var = find_var(name); if (!var.has_value()) { // there is an error in C code if this happens console::info("Cvar_Set: variable %s not found\n", name.c_str()); return; } var.value()->set(value); } /* ============ convar::set_value ============ */ void convar::set_value(const std::string &name, const float new_value) { std::string val; if (new_value == static_cast(static_cast(new_value))) val = std::format("{:d}", static_cast(new_value)); else { val = std::format("{:f}", new_value); // kill trailing zeroes val.erase(val.find_last_not_of('0') + 1, std::string::npos); } set(name, val); } /* ============ Cvar_SetROM ============ */ void convar::set_rom(const std::string_view &name, const std::string value) { if (const auto var = find_var(name); var.has_value()) { var.value()->flags.rom = false; var.value()->set(value); var.value()->flags.rom = true; } } void convar::set_value_rom(const std::string_view &name, const float value) { if (const auto var = find_var(name); var.has_value()) { var.value()->flags.rom = false; var.value()->set_value(value); var.value()->flags.rom = true; } } convar_view convar::get_variables() { return std::ranges::views::values(CLIENT_VARIABLES); } convar::convar(const std::string_view name, const char *str) : convar(name, str, {}) { } convar::convar(const std::string_view name, const char *str, const cvar_flags flags) { this->name = name; this->string = str; this->flags = flags; } /** * Adds a freestanding variable to the variable list. */ void convar::inscribe() { char value[512]; convar *cursor, *prev; //johnfitz -- sorted list insert // first check to see if it has already been defined if (find_var(this->name)) { console::info("Can't register variable %s, already defined\n", this->name.c_str()); return; } // check for overlap with a command if (command::exists(this->name)) { console::info("Cvar_RegisterVariable: %s is a command\n", this->name.c_str()); return; } CLIENT_VARIABLES.emplace(std::string(this->name), this); //johnfitz this->flags.registered = true; // copy the value off, because future sets will Z_Free it std::strncpy(value, this->string, sizeof(value)); this->string = nullptr; this->default_string = nullptr; if (!this->flags.callback) this->callback = nullptr; // set it through the function to be consistent const bool set_rom = (this->flags.rom); this->flags.rom = false; this->set(value); if (set_rom) this->flags.rom = true; } /** * Set a callback function for the variable * @param func Callback to be used (or `nullptr` to disable to callback). */ void convar::set_callback(const cvarcallback_t func) { this->callback = func; this->flags.callback = func != nullptr; } /* ============ Cvar_Command Handles variable inspection and changing from the console ============ */ bool Cvar_Command() { // check variables auto var = convar::find_var(command::argv(0).value_or("")); if (!var.has_value()) return false; // perform a variable print or set if (command::argc() == 1) { console::info("\"%s\" is \"%s\"\n", var.value()->name.c_str(), var.value()->string); return true; } convar::set(var.value()->name, *command::argv(1)); return true; } /* ============ Cvar_WriteVariables Writes lines containing "set variable value" for all variables with the archive flag set to true. ============ */ void Cvar_WriteVariables(FILE *f) { for (const auto &[name, var]: CLIENT_VARIABLES) { if (var->flags.archive) fprintf(f, "%s \"%s\"\n", name.c_str(), var->string); } }