Files
KalmarvandClaude Opus 5.5 99e79f123a Console: "help perf" lists the perf commands, and a wrong word after a command says where to look
help <command> showed only that command when it was also the first word of others: help
perf showed the HUD toggle and not perf hud, perf status, perf sample and the rest, and the
same held for help log, music, reset, boardbreak and roadrash. help listed a family only
when no command had the family's name. It now lists them after the command's own details,
by name or by alias (Registry::under).
"perf hdu" answered "Too many arguments. Usage: perf"; when the command has others under
it, the error now ends "Use help perf for the commands under it."

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:36:32 -06:00

240 lines
11 KiB
C++

#include "commands.h"
#include "Engine/Core/Log/logging.h"
#include "Engine/Core/Platform/path_text.h"
#include <memory>
namespace dingosdk::console {
Argument argument(std::string name, Type type, bool optional) {
Argument result;
result.name = std::move(name);
result.type = type;
result.optional = optional;
return result;
}
Entry action(std::string name, std::string description, Group group, std::vector<Argument> args) {
Entry entry;
entry.name = std::move(name);
entry.description = std::move(description);
entry.group = group;
entry.arguments = std::move(args);
return entry;
}
Entry variable(std::string name, std::string description, Group group, Argument arg) {
auto entry = action(std::move(name), std::move(description), group, {std::move(arg)});
entry.kind = Kind::variable;
return entry;
}
State boolean_state(bool available, bool value, std::string reason, std::string detail) {
return {available, available ? std::optional<std::string>(value ? "1" : "0") : std::nullopt, std::move(reason),
std::move(detail), false};
}
namespace {
void register_console_commands(Commands &registry) {
auto topic = argument("command", Type::text, true);
topic.rest = true;
topic.complete = [&registry](const Model &, auto) {
std::vector<std::string> names;
for (const auto &e : registry.entries())
names.push_back(e.name);
return names;
};
auto help = action("help", "Show command syntax, values, restrictions, and availability", Group::console, {topic});
help.execution = Execution::local;
help.run = [&registry](const Model &model, const Values &args, const Output &out) {
if (args.empty()) {
out("Enter a variable by itself to read it, or follow it with a value: "
"noclip 1, nobail 0, flyspeed 60.");
out("Use commands [prefix] to list entries, help <command> for details and the commands under it "
"(help perf, help mp, help trainer), and Tab to complete names and arguments.");
out("Groups: Movement, Gameplay, World, Graphics, Progression, Objects, "
"Engine, Console.");
return;
}
const auto &name = std::get<std::string>(args[0]);
const auto *entry = registry.find(name);
const auto below = registry.under(entry ? entry->name : name);
const auto list_below = [&below, &out] {
for (const auto *item : below)
out(Commands::usage(*item) + (item->description.empty() ? "" : " - " + item->description));
};
if (!entry) {
if (below.empty())
out("error: Unknown help topic.");
list_below();
return;
}
out(Commands::usage(*entry));
out(registry.describe(*entry, model));
out(std::string("Group: ") + group_name(entry->group));
for (const auto &arg : entry->arguments) {
if (!arg.choices.empty()) {
std::string choices = arg.name + ": ";
for (const auto &choice : arg.choices) {
if (choices.back() != ' ')
choices += ", ";
choices += choice;
}
out(choices);
}
if (arg.minimum || arg.maximum)
out(arg.name + " range: " + (arg.minimum ? value_text(*arg.minimum) : "unbounded") + " to " +
(arg.maximum ? value_text(*arg.maximum) : "unbounded"));
}
if (entry->reset)
out("Restore: reset " + entry->name);
// `perf` is a command of its own and the first word of `perf hud`, `perf status` and the rest.
if (!below.empty())
out(entry->name + " commands:");
list_below();
};
registry.add(std::move(help));
auto list = action("commands", "List commands and variables, optionally filtered by prefix", Group::console,
{argument("prefix", Type::text, true)});
list.execution = Execution::local;
list.run = [&registry](const Model &model, const Values &args, const Output &out) {
const auto prefix = args.empty() ? std::string{} : std::get<std::string>(args[0]);
std::vector<const Entry *> rows;
for (const auto &e : registry.entries())
if (ascii_case_insensitive_starts_with(e.name, prefix))
rows.push_back(&e);
std::sort(rows.begin(), rows.end(), [](auto *a, auto *b) { return lower(a->name) < lower(b->name); });
for (const auto *e : rows)
out(registry.describe(*e, model));
out(std::to_string(rows.size()) + " matching entries.");
};
registry.add(std::move(list));
for (const auto &name : {"clear", "history"}) {
auto entry =
action(name, equal(name, "clear") ? "Clear console scrollback" : "Show recent commands", Group::console);
entry.execution = Execution::local;
entry.echo_input = !equal(name, "clear");
entry.run = [clear = equal(name, "clear")](const Model &, const Values &, const Output &out) {
const auto &callback = clear ? out.clear : out.history;
if (callback)
callback();
else
out("This command belongs to the overlay console.");
};
registry.add(std::move(entry));
}
auto text = argument("text", Type::text, true);
text.rest = true;
auto echo = action("echo", "Print text to the console", Group::console, {text});
echo.execution = Execution::local;
echo.run = [](const Model &, const Values &args, const Output &out) {
out(args.empty() ? "" : std::get<std::string>(args[0]));
};
registry.add(std::move(echo));
auto status = action("status", "Show current game state", Group::console);
status.execution = Execution::local;
status.run = [](const Model &model, const Values &, const Output &out) {
out("state: " + model.state);
out("freecam=" + value_text(model.debug.free_camera) + " noclip=" + value_text(model.debug.noclip) +
" nobail=" + value_text(model.debug.no_bail) +
" flyspeed=" + value_text(static_cast<double>(model.debug.camera_speed)));
if (!model.offline.status.empty())
out(model.offline.status);
};
registry.add(std::move(status));
auto reset_arg = argument("command");
reset_arg.rest = true;
reset_arg.complete = [&registry](const Model &, auto) {
std::vector<std::string> names{"all", "movement", "features", "engine"};
for (const auto &e : registry.entries())
if (e.reset)
names.push_back(e.name);
return names;
};
auto reset = action("reset", "Restore a variable or a group of session overrides", Group::console, {reset_arg});
reset.run = [&registry](const Model &model, const Values &args, const Output &out) {
const auto &name = std::get<std::string>(args[0]);
const auto *entry = registry.find(name);
if (!entry || !entry->reset) {
out("error: This entry has no restore operation. Use help reset.");
return;
}
entry->reset(model, out);
};
registry.add(std::move(reset));
for (const auto &name : {"movement", "features", "engine", "all"}) {
auto entry = action(std::string("reset ") + name, "Restore the selected runtime overrides", Group::console);
entry.run = [scope = std::string(name)](const Model &model, const Values &, const Output &out) {
// Restore native fields before queuing other changes; the runtime
// reservation intentionally rejects competing setting transactions.
if (scope == "engine" || scope == "all") {
out(request_reset_named_settings());
if (scope == "engine")
for (const auto &v : model.offline.variables)
if (v.override_active)
request_engine_variable(v.name, true, false);
}
if (scope == "movement" || scope == "all")
request_debug(overlay::DebugAction::restore_debug);
if (scope == "features" || scope == "all")
request_feature(overlay::OfflineFeatureGroup::restore_all, false);
};
if (equal(name, "movement"))
entry.aliases = {"reset debug"};
registry.add(std::move(entry));
}
auto log = action("log", "Show logging level and output path", Group::console);
log.execution = Execution::local;
log.aliases = {"log status"};
log.run = [](const Model &, const Values &, const Output &out) {
const auto s = logging::status();
out(std::string("Logging level: ") + std::string(logging::name(s.level)));
out(path_utf8(s.directory / L"ReSkate.log"));
};
registry.add(std::move(log));
auto level = argument("level");
level.choices = {"trace", "debug", "info", "warning", "error", "critical", "off"};
auto loglevel = variable("loglevel", "Set the shared logging level", Group::console, level);
loglevel.execution = Execution::local;
loglevel.aliases = {"log level"};
loglevel.inspect = [](const Model &) {
return State{true, std::string(logging::name(logging::status().level)), {}, {}, false};
};
loglevel.run = [](const Model &, const Values &args, const Output &out) {
const auto &text = std::get<std::string>(args[0]);
const auto parsed = logging::parse_level(text);
if (parsed) {
logging::set_level(*parsed);
out("Logging level: " + text);
}
};
registry.add(std::move(loglevel));
auto flush = action("log flush", "Flush the log file", Group::console);
flush.execution = Execution::local;
flush.run = [](const Model &, const Values &, const Output &out) {
logging::flush();
out("Log flushed.");
};
registry.add(std::move(flush));
}
} // namespace
const Commands &game_commands() {
static const auto registry = [] {
auto result = std::make_unique<Commands>();
register_console_commands(*result);
register_movement_commands(*result);
register_ai_commands(*result);
register_settings_commands(*result);
register_world_commands(*result);
register_graphics_commands(*result);
register_progression_commands(*result);
register_object_commands(*result);
register_park_editor_commands(*result);
register_multiplayer_commands(*result);
register_perf_commands(*result);
register_item_commands(*result);
register_trainer_commands(*result);
register_hall_of_meat_commands(*result);
register_road_rash_commands(*result);
register_board_break_commands(*result);
register_grind_exit_commands(*result);
return result;
}();
return *registry;
}
} // namespace dingosdk::console