mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-23 08:45:53 +02:00
After 10 bails the leg of the side that took more of them shows road dust and a graze, its arm one bail later; at 14 the marks are bigger and the other side starts; at 20 they are raw wounds and the face is bruised. A mark never gets smaller until the player heals. The worst wounds bleed, or are dry and scabbed with the blood switch off. The marks are cosmetic items of a content mod (tattoo items for the limbs, moles items for the face, named RoadRash_<Set>_[<design>_]<level>). Road Rash puts them into the local skater's recipe with the game's own recipe copy, after the validation the multiplayer code runs on a peer's outfit. Nothing is written to the outfit or the profile's skater, and without the item pack nothing shows and nothing is changed. A bail is the physics state selector choosing Wipeout (No Bail's watch). Its side comes from the ragdoll's contacts through Hall of Meat's reader: impact and slide of the arm and leg bodies that touch something other than the skater's own board, summed per side while the body is a ragdoll. Outfit menus. The game feeds a skater's items either from the player's outfit or from a recipe of the skater's own: after the recipe copy the skater in the world no longer gets what the player changes in the menus. So the skater is given back to the game while a game menu is up, with nothing to show, on healing, when a switch changes and when switched off: the four words of the item component the copy had changed go back to what they were and the component is told its feed changed, and the game puts the outfit's own items back and feeds the skater again. The marks go back on, over the outfit as it is by then, a second after the menu has gone. Whose recipe a skater wears is read from the skater at every look: a respawn hands over the same skater, items and all, as if it were new. The player's own tattoos and moles stay unless roadrashtattoos is on, and come back the same way. The recipe handed to the copy names no item, only hashes, like the game's own for the local skater: the game changes that skater's items where they lie, and a name left beside one would stop every later read of the recipe, the multiplayer code's too. Console: roadrash, roadrashblood, roadrashtattoos, roadrash heal, roadrash bails <count>. Menus: Road Rash, Road Rash blood and Heal Road Rash beside Hall of Meat (Mod Options > Player, the overlay's skater page, the native tools view). The three choices are saved with the profile. No new hooks. The rules (side of a fall, levels per count, item names, decal placement, comparing parameters the game has rounded) are in road_rash_model.h with their own test target, DINGOSDK_BUILD_ROAD_RASH_TESTS. Notes and what is not tried yet: contrib/road-rash.md.
234 lines
10 KiB
C++
234 lines
10 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 ®istry) {
|
|
auto topic = argument("command", Type::text, true);
|
|
topic.rest = true;
|
|
topic.complete = [®istry](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 = [®istry](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 = [®istry](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 = [®istry](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 = [®istry](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_trainer_commands(*result);
|
|
register_hall_of_meat_commands(*result);
|
|
register_road_rash_commands(*result);
|
|
return result;
|
|
}();
|
|
return *registry;
|
|
}
|
|
} // namespace dingosdk::console
|