Files
ReSkate/Extension/Console/commands.cpp
T
Fourth Studio bd628abc76 Board Break: a hard slam can snap the board
A slam can break the local skater's board at the nose, in the middle or at
the tail. Half the time the grip tape holds the pieces together and the board
folds or an end hangs; otherwise a piece comes off, flies and lies where it
lands. The board is whole again in hand, on riding and on a respawn.

The pieces are four stretches of the deck in a content mod's copy of the
game's one deck model, each on a spare bone of the board's rig. The boards'
render pose hook only moves bones, because the replay records where a bone is
and not its size: a broken board's deck goes away and its stretches go to
where the pieces lie, and a whole board's stretches go away.

The deck the player chose loads first and other mods that bring a deck model
are set aside for the run, without touching mods.json. Another deck is chosen
while the game runs, through a live apply of the mods.

contrib/board-break.md says how it works and how a deck pack is made.
2026-10-11 20:07:00 +02:00

236 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 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);
if (!entry) {
bool found = false;
for (const auto &item : registry.entries())
if (ascii_case_insensitive_starts_with(item.name, name + " ")) {
out(Commands::usage(item) + (item.description.empty() ? "" : " - " + item.description));
found = true;
}
if (!found)
out("error: Unknown help topic.");
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);
};
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);
return result;
}();
return *registry;
}
} // namespace dingosdk::console