mirror of
https://github.com/odamex/odamex
synced 2026-08-19 02:23:09 -04:00
488 lines
11 KiB
C++
488 lines
11 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:
|
|
// Dumps a document with a list of all cvars
|
|
//
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
|
#include "odamex.h"
|
|
|
|
#include <algorithm>
|
|
|
|
#include <json/json.h>
|
|
|
|
#include "c_dispatch.h"
|
|
#include "c_doc.h"
|
|
#include "cmdlib.h"
|
|
#include "i_system.h"
|
|
#include "m_fileio.h"
|
|
#include "version.h"
|
|
|
|
#ifdef CLIENT_APP
|
|
constexpr const char* CS_STRING = "Odamex Client";
|
|
constexpr const char* CVARDOC_BASENAME = "odamex_cvardoc";
|
|
#elif defined(SERVER_APP)
|
|
constexpr const char* CS_STRING = "Odamex Server";
|
|
constexpr const char* CVARDOC_BASENAME = "odasrv_cvardoc";
|
|
#elif defined(TEST_APP)
|
|
constexpr const char* CS_STRING = "Odamex Unit Tests";
|
|
constexpr const char* CVARDOC_BASENAME = "odagtest_cvardoc";
|
|
#endif
|
|
|
|
// A view to a list of Cvars.
|
|
typedef std::vector<cvar_t*> CvarView;
|
|
|
|
/**
|
|
* @brief Top part of an HTML document.
|
|
*
|
|
* @param out Output buffer to write to.
|
|
* @param title Title to put in the title tag.
|
|
*/
|
|
static void HTMLHeader(std::string& out, const std::string& title)
|
|
{
|
|
std::string buf;
|
|
const char* HEADER =
|
|
"<!DOCTYPE html>"
|
|
"<html>"
|
|
"<head>"
|
|
"<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">"
|
|
"<title>%s</title>"
|
|
"<style>"
|
|
"html {"
|
|
"background-color: #2c2c2c;"
|
|
"color: rgb(245, 245, 245);"
|
|
"font-family: sans-serif;"
|
|
"}"
|
|
"a {"
|
|
"color: #ffa300;"
|
|
"}"
|
|
"</style>"
|
|
"</head>"
|
|
"<body>";
|
|
out = fmt::sprintf(HEADER, title);
|
|
}
|
|
|
|
/**
|
|
* @brief Render cvar information as HTML.
|
|
*
|
|
* @param out Output buffer to write to.
|
|
* @param cvar Cvar to read.
|
|
*/
|
|
static void HTMLCvarRow(std::string& out, const cvar_t& cvar)
|
|
{
|
|
std::vector<std::string> info;
|
|
|
|
switch (cvar.type())
|
|
{
|
|
case CVARTYPE_BOOL:
|
|
info.push_back("Boolean");
|
|
break;
|
|
case CVARTYPE_BYTE:
|
|
info.push_back("Byte");
|
|
break;
|
|
case CVARTYPE_WORD:
|
|
info.push_back("Short");
|
|
break;
|
|
case CVARTYPE_INT:
|
|
info.push_back("Number");
|
|
break;
|
|
case CVARTYPE_FLOAT:
|
|
info.push_back("Float");
|
|
break;
|
|
case CVARTYPE_STRING:
|
|
info.push_back("String");
|
|
break;
|
|
default:
|
|
out = "";
|
|
return;
|
|
}
|
|
|
|
// Default and range
|
|
switch (cvar.type())
|
|
{
|
|
case CVARTYPE_BOOL: {
|
|
int val = atoi(cvar.getDefault().c_str());
|
|
info.push_back(val == 0 ? "Default: False" : "Default: True");
|
|
break;
|
|
}
|
|
case CVARTYPE_BYTE:
|
|
case CVARTYPE_WORD:
|
|
case CVARTYPE_INT: {
|
|
std::string buffer;
|
|
int val = atoi(cvar.getDefault().c_str());
|
|
buffer = fmt::sprintf("Default: %d", val);
|
|
info.push_back(buffer);
|
|
|
|
if (cvar.getMinValue() != -FLT_MAX)
|
|
{
|
|
buffer = fmt::sprintf("Min: %d", static_cast<int>(cvar.getMinValue()));
|
|
info.push_back(buffer);
|
|
}
|
|
|
|
if (cvar.getMaxValue() != FLT_MAX)
|
|
{
|
|
buffer = fmt::sprintf("Max: %d", static_cast<int>(cvar.getMaxValue()));
|
|
info.push_back(buffer);
|
|
}
|
|
|
|
break;
|
|
}
|
|
case CVARTYPE_FLOAT: {
|
|
std::string buffer;
|
|
float val = atof(cvar.getDefault().c_str());
|
|
buffer = fmt::sprintf("Default: %f", val);
|
|
info.push_back(buffer);
|
|
|
|
if (cvar.getMinValue() != -FLT_MAX)
|
|
{
|
|
buffer = fmt::sprintf("Min: %f", cvar.getMinValue());
|
|
info.push_back(buffer);
|
|
}
|
|
|
|
if (cvar.getMaxValue() != FLT_MAX)
|
|
{
|
|
buffer = fmt::sprintf("Max: %f", cvar.getMaxValue());
|
|
info.push_back(buffer);
|
|
}
|
|
|
|
break;
|
|
}
|
|
case CVARTYPE_STRING:
|
|
if (!cvar.getDefault().empty())
|
|
{
|
|
std::string buf;
|
|
buf = fmt::sprintf("Default: \"%s\"", cvar.getDefault());
|
|
info.push_back(buf);
|
|
}
|
|
break;
|
|
default:
|
|
out = "";
|
|
return;
|
|
}
|
|
|
|
if (cvar.flags() & CVAR_USERINFO)
|
|
info.push_back("Added to userinfo");
|
|
if (cvar.flags() & CVAR_SERVERINFO)
|
|
info.push_back("Servers tell clients when changed");
|
|
if (cvar.flags() & CVAR_NOSET)
|
|
info.push_back("Can't be set");
|
|
if (cvar.flags() & CVAR_LATCH)
|
|
info.push_back("Latched");
|
|
if (cvar.flags() & CVAR_UNSETTABLE)
|
|
info.push_back("Can be unset");
|
|
if (cvar.flags() & CVAR_NOENABLEDISABLE)
|
|
info.push_back("No Enable/Disable");
|
|
if (cvar.flags() & CVAR_SERVERARCHIVE)
|
|
info.push_back("Saved on the server");
|
|
if (cvar.flags() & CVAR_CLIENTARCHIVE)
|
|
info.push_back("Saved on the client");
|
|
|
|
std::string flagstr = JoinStrings(info, ", ");
|
|
|
|
const char* ROW = "<dt><code>%s</code></dt>"
|
|
"<dd>"
|
|
"<p><small><em>%s</em></small></p>"
|
|
"<p>%s</p>"
|
|
"</dd>";
|
|
out = fmt::sprintf(ROW, cvar.name(), flagstr, cvar.helptext());
|
|
}
|
|
|
|
/**
|
|
* @brief Bottom part of an HTML document.
|
|
*
|
|
* @param out Output buffer to write to.
|
|
*/
|
|
static void HTMLFooter(std::string& out)
|
|
{
|
|
out = "</body>"
|
|
"</html>";
|
|
}
|
|
|
|
/**
|
|
* @brief Return a "view" of Cvars sorted by name.
|
|
*/
|
|
static CvarView GetSortedCvarView()
|
|
{
|
|
CvarView view;
|
|
|
|
cvar_t* var = GetFirstCvar();
|
|
while (var != NULL)
|
|
{
|
|
view.push_back(var);
|
|
var = var->GetNext();
|
|
}
|
|
|
|
std::sort(view.begin(), view.end(), [](const cvar_t* a, const cvar_t* b){ return a->name().compare(b->name()) < 0; });
|
|
return view;
|
|
}
|
|
|
|
/**
|
|
* @brief Render cvar information as a JSON object.
|
|
*
|
|
* @param out Output value to write to. Set to null for cvars that have no
|
|
* representable type.
|
|
* @param cvar Cvar to read.
|
|
*/
|
|
static void JSONCvarObject(Json::Value& out, const cvar_t& cvar)
|
|
{
|
|
out = Json::Value(Json::objectValue);
|
|
|
|
// Type string and whether the cvar carries a numeric range.
|
|
const char* typestr = NULL;
|
|
bool numeric = false;
|
|
switch (cvar.type())
|
|
{
|
|
case CVARTYPE_BOOL:
|
|
typestr = "boolean";
|
|
break;
|
|
case CVARTYPE_BYTE:
|
|
case CVARTYPE_WORD:
|
|
case CVARTYPE_INT:
|
|
typestr = "integer";
|
|
numeric = true;
|
|
break;
|
|
case CVARTYPE_FLOAT:
|
|
typestr = "number";
|
|
numeric = true;
|
|
break;
|
|
case CVARTYPE_STRING:
|
|
typestr = "string";
|
|
break;
|
|
default:
|
|
out = Json::Value(Json::nullValue);
|
|
return;
|
|
}
|
|
|
|
out["name"] = cvar.name();
|
|
out["type"] = typestr;
|
|
out["helptext"] = cvar.helptext();
|
|
|
|
// Default value, typed to match the cvar.
|
|
switch (cvar.type())
|
|
{
|
|
case CVARTYPE_BOOL:
|
|
out["default"] = atoi(cvar.getDefault().c_str()) != 0;
|
|
break;
|
|
case CVARTYPE_BYTE:
|
|
case CVARTYPE_WORD:
|
|
case CVARTYPE_INT:
|
|
out["default"] = atoi(cvar.getDefault().c_str());
|
|
break;
|
|
case CVARTYPE_FLOAT:
|
|
out["default"] = atof(cvar.getDefault().c_str());
|
|
break;
|
|
case CVARTYPE_STRING:
|
|
out["default"] = cvar.getDefault();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// Min/max only for numeric cvars, and only when a bound is actually set.
|
|
if (numeric)
|
|
{
|
|
if (cvar.getMinValue() != -FLT_MAX)
|
|
{
|
|
if (cvar.type() == CVARTYPE_FLOAT)
|
|
out["min"] = cvar.getMinValue();
|
|
else
|
|
out["min"] = static_cast<int>(cvar.getMinValue());
|
|
}
|
|
|
|
if (cvar.getMaxValue() != FLT_MAX)
|
|
{
|
|
if (cvar.type() == CVARTYPE_FLOAT)
|
|
out["max"] = cvar.getMaxValue();
|
|
else
|
|
out["max"] = static_cast<int>(cvar.getMaxValue());
|
|
}
|
|
}
|
|
|
|
// Flags as an array of string names.
|
|
Json::Value flags(Json::arrayValue);
|
|
if (cvar.flags() & CVAR_USERINFO)
|
|
flags.append("USERINFO");
|
|
if (cvar.flags() & CVAR_SERVERINFO)
|
|
flags.append("SERVERINFO");
|
|
if (cvar.flags() & CVAR_NOSET)
|
|
flags.append("NOSET");
|
|
if (cvar.flags() & CVAR_LATCH)
|
|
flags.append("LATCH");
|
|
if (cvar.flags() & CVAR_UNSETTABLE)
|
|
flags.append("UNSETTABLE");
|
|
if (cvar.flags() & CVAR_NOENABLEDISABLE)
|
|
flags.append("NOENABLEDISABLE");
|
|
if (cvar.flags() & CVAR_SERVERARCHIVE)
|
|
flags.append("SERVERARCHIVE");
|
|
if (cvar.flags() & CVAR_CLIENTARCHIVE)
|
|
flags.append("CLIENTARCHIVE");
|
|
out["flags"] = flags;
|
|
}
|
|
|
|
namespace
|
|
{
|
|
|
|
/**
|
|
* @brief Render the cvar documentation as a complete HTML document.
|
|
*/
|
|
std::string BuildCvarDocHTML()
|
|
{
|
|
std::string doc;
|
|
std::string buffer;
|
|
|
|
// First the header.
|
|
std::string title;
|
|
title = fmt::sprintf("%s %s Console Variables", CS_STRING, DOTVERSIONSTR);
|
|
HTMLHeader(buffer, title);
|
|
doc += buffer;
|
|
|
|
// Then the title and initial paragraph.
|
|
const char* PREAMBLE =
|
|
"<h2>%s</h2>"
|
|
"<p>"
|
|
"These are the console variables known to the %s as of revision %s."
|
|
"</p><p>"
|
|
"In order to understand some of the documentation below, it's important to get "
|
|
"some definitions out of the way first. A Boolean is a true/false value, a Byte "
|
|
"is a number from 0-255, a Short is a number from 0-65535, and a Float is a "
|
|
"number with a decimal point in it, like 3.14."
|
|
"</p>";
|
|
|
|
doc += fmt::sprintf(PREAMBLE, title, CS_STRING, NiceVersion());
|
|
|
|
// Initial tag for cvars.
|
|
doc += "<dl>";
|
|
|
|
// Stamp out our CVars
|
|
CvarView view = GetSortedCvarView();
|
|
for (const auto& cvar : view)
|
|
{
|
|
HTMLCvarRow(buffer, *cvar);
|
|
doc += buffer;
|
|
}
|
|
|
|
// Ending tag for cvars.
|
|
doc += "</dl>";
|
|
|
|
// Lastly the footer.
|
|
HTMLFooter(buffer);
|
|
doc += buffer;
|
|
|
|
return doc;
|
|
}
|
|
|
|
/**
|
|
* @brief Render the cvar documentation as a complete JSON document.
|
|
*/
|
|
std::string BuildCvarDocJSON()
|
|
{
|
|
Json::Value root(Json::objectValue);
|
|
root["schema_version"] = 1;
|
|
root["odamex_version"] = DOTVERSIONSTR;
|
|
root["odamex_commithash"] = GitHash();
|
|
root["odamex_branchname"] = GitBranch();
|
|
|
|
Json::Value cvars(Json::arrayValue);
|
|
CvarView view = GetSortedCvarView();
|
|
for (const auto& cvar : view)
|
|
{
|
|
Json::Value obj;
|
|
JSONCvarObject(obj, *cvar);
|
|
if (obj.isNull())
|
|
continue;
|
|
cvars.append(obj);
|
|
}
|
|
root["cvars"] = cvars;
|
|
|
|
Json::StyledWriter writer;
|
|
return writer.write(root);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
/**
|
|
* @brief Emit a generated document.
|
|
*
|
|
* infodumpdest_t::FILE writes a file in the write directory.
|
|
* infodumpdest_t::STDOUT prints the document to stdout instead.
|
|
*
|
|
* @param doc Document contents to emit.
|
|
* @param basename Name to give the file, without an extension.
|
|
* @param ext Extension to give the file, including the leading dot.
|
|
* @param dest Where the document should go.
|
|
* @return False if the document could not be written.
|
|
*/
|
|
bool EmitInfoDump(const std::string& doc, const char* basename, const char* ext,
|
|
infodumpdest_t dest)
|
|
{
|
|
if (dest == infodumpdest_t::STDOUT)
|
|
{
|
|
fwrite(doc.data(), sizeof(char), doc.size(), stdout);
|
|
fflush(stdout);
|
|
return true;
|
|
}
|
|
|
|
std::string path = M_GetWriteDir();
|
|
if (!M_IsPathSep(path.back()))
|
|
{
|
|
path += PATHSEP;
|
|
}
|
|
path += basename;
|
|
path += ext;
|
|
|
|
|
|
FILE* fh = fopen(path.c_str(), "wt+");
|
|
if (fh == nullptr)
|
|
{
|
|
PrintFmt("error: Could not open \"{}\" for writing.\n", path);
|
|
return false;
|
|
}
|
|
|
|
fwrite(doc.data(), sizeof(char), doc.size(), fh);
|
|
|
|
long bytes = ftell(fh);
|
|
fclose(fh);
|
|
|
|
// Success!
|
|
PrintFmt("Wrote {} bytes to \"{}\"\n", bytes, path);
|
|
return true;
|
|
}
|
|
|
|
bool C_WriteCvarDoc(infodumpdest_t dest)
|
|
{
|
|
return EmitInfoDump(BuildCvarDocHTML(), CVARDOC_BASENAME, ".html", dest);
|
|
}
|
|
|
|
bool C_WriteCvarDocJSON(infodumpdest_t dest)
|
|
{
|
|
return EmitInfoDump(BuildCvarDocJSON(), CVARDOC_BASENAME, ".json", dest);
|
|
}
|
|
|
|
BEGIN_COMMAND(cvardoc)
|
|
{
|
|
C_WriteCvarDoc(infodumpdest_t::FILE);
|
|
}
|
|
END_COMMAND(cvardoc)
|
|
|
|
BEGIN_COMMAND(cvardocjson)
|
|
{
|
|
C_WriteCvarDocJSON(infodumpdest_t::FILE);
|
|
}
|
|
END_COMMAND(cvardocjson)
|