#pragma once #include "console_core.h" #include #include #include #include #include #include #include #include #include #include #include namespace dingosdk::console { enum class Group { console, movement, gameplay, world, graphics, progression, objects, engine }; enum class Kind { action, variable }; enum class Execution { local, game_thread }; enum class Type { text, boolean, integer, unsigned_integer, number }; using Value = std::variant; using Values = std::vector; struct Output { std::function write; std::function clear; std::function history; void operator()(const std::string &text) const { if (write) write(text); } }; struct State { bool available = true; std::optional value; std::string reason; std::string detail; bool overridden = false; }; std::string lower(std::string_view text); bool equal(std::string_view a, std::string_view b); const char *group_name(Group group) noexcept; std::string value_text(const Value &value); std::optional parse_value(Type type, std::string_view text); struct CompletionToken { std::string text; int begin = 0, end = 0; bool quoted = false; }; std::vector completion_tokens(std::string_view input, int cursor); struct Suggestion { std::string text, name, description, usage; Group group = Group::console; Kind kind = Kind::action; State state; bool argument = false; }; template class Registry { public: struct Argument { std::string name; Type type = Type::text; bool optional = false, rest = false; std::vector choices; std::optional minimum, maximum; std::function(const Context &, std::span)> complete; }; struct Entry { std::string name, description; Group group = Group::console; Kind kind = Kind::action; Execution execution = Execution::game_thread; bool echo_input = true; std::vector arguments; std::vector aliases; std::function inspect; std::function run; std::function reset; }; Registry() = default; Registry(const Registry &) = delete; Registry &operator=(const Registry &) = delete; void add(Entry entry) { const auto parsed = parse_console_command(entry.name); if (!parsed || parsed.arguments.empty()) throw std::logic_error("Invalid command name"); bool optional = false; for (std::size_t i = 0; i < entry.arguments.size(); ++i) { const auto &arg = entry.arguments[i]; if (arg.name.empty() || (optional && !arg.optional) || (arg.rest && (i + 1 != entry.arguments.size() || arg.type != Type::text))) throw std::logic_error("Invalid command argument schema: " + entry.name); optional = arg.optional; } if (entry.kind == Kind::variable && (entry.arguments.size() != 1 || !entry.inspect)) throw std::logic_error("Variables require one typed argument and a state reader"); std::vector names{entry.name}; names.insert(names.end(), entry.aliases.begin(), entry.aliases.end()); std::map unique; for (const auto &name : names) { const auto key = lower(name); if (!parse_console_command(name) || name.empty() || lookup_.contains(key) || !unique.emplace(key, true).second) throw std::logic_error("Duplicate/invalid command: " + name); } const auto index = entries_.size(); entries_.push_back(std::move(entry)); for (const auto &name : names) lookup_.emplace(lower(name), index); } const std::vector &entries() const noexcept { return entries_; } const Entry *find(std::string_view name) const { const auto it = lookup_.find(lower(name)); return it == lookup_.end() ? nullptr : &entries_[it->second]; } // The other entries whose name or an alias goes on from a name by more words: // `perf hud` and `perf status` under `perf`. std::vector under(std::string_view name) const { const auto prefix = std::string(name) + ' '; const auto *self = find(name); // (`log` is also `log status`) std::vector result; for (const auto &entry : entries_) { if (&entry == self) continue; bool below = ascii_case_insensitive_starts_with(entry.name, prefix); for (const auto &alias : entry.aliases) below = below || ascii_case_insensitive_starts_with(alias, prefix); if (below) result.push_back(&entry); } return result; } static std::string usage(const Entry &entry) { std::string result = entry.name; for (const auto &arg : entry.arguments) { const bool optional = arg.optional || entry.kind == Kind::variable; result += optional ? " [" : " <"; result += arg.name; if (arg.rest) result += "..."; result += optional ? "]" : ">"; } return result; } State state(const Entry &entry, const Context &context) const { return entry.inspect ? entry.inspect(context) : State{}; } std::string describe(const Entry &entry, const Context &context) const { const auto current = state(entry, context); std::string result = entry.name; if (current.value) result += " [" + *current.value + "]"; if (!entry.description.empty()) result += " - " + entry.description; if (!current.available) result += " | unavailable: " + current.reason; if (!current.detail.empty()) result += " | " + current.detail; return result; } bool echoes(std::span words) const { const auto [entry, consumed] = resolve(words); return !entry || entry->echo_input || consumed != words.size(); } bool local(std::span words, const Context &context) const { const auto [entry, consumed] = resolve(words); return !entry || entry->execution == Execution::local || (entry->kind == Kind::variable && consumed == words.size()) || !state(*entry, context).available; } bool execute(std::string_view text, const Context &context, const Output &output) const { const auto parsed = parse_console_command(text); if (!parsed || parsed.arguments.empty()) { output("error: Invalid command text; check quoting and length."); return false; } return execute(parsed.arguments, context, output); } bool execute(std::span words, const Context &context, const Output &output) const { const auto [entry, consumed] = resolve(words); if (!entry) { output("error: Unknown command. Use help or Tab completion."); return false; } const auto current = state(*entry, context); if (entry->kind == Kind::variable && consumed == words.size()) { output(describe(*entry, context)); return true; } if (!current.available) { output("error: " + entry->name + " is unavailable: " + current.reason); return false; } const auto args = words.subspan(consumed); Values values; std::size_t index = 0; for (const auto &arg : entry->arguments) { if (index == args.size()) { if (arg.optional) break; output("error: Missing " + arg.name + ". Usage: " + usage(*entry)); return false; } std::string token = args[index++]; if (arg.rest) while (index < args.size()) { token += ' '; token += args[index++]; } // "toggle" on an on/off switch: the other of what it is now. if (arg.type == Type::boolean && equal(token, "toggle")) token = current.value && *current.value == "1" ? "0" : "1"; auto value = parse_value(arg.type, token); if (!value) { output("error: Invalid " + arg.name + ": '" + token + "'. Usage: " + usage(*entry)); return false; } if (!arg.choices.empty()) { const auto match = std::find_if(arg.choices.begin(), arg.choices.end(), [&](const auto &choice) { if (arg.type == Type::text) return equal(choice, token); const auto parsed_choice = parse_value(arg.type, choice); return parsed_choice && *parsed_choice == *value; }); if (match == arg.choices.end()) { std::string choices; for (const auto &choice : arg.choices) { if (!choices.empty()) choices += ", "; choices += choice; } output("error: " + arg.name + " must be one of: " + choices); return false; } value = parse_value(arg.type, *match); } if (arg.minimum || arg.maximum) { const auto numeric = std::visit( [](const auto &v) -> double { using T = std::decay_t; if constexpr (std::is_arithmetic_v) return static_cast(v); else return 0; }, *value); if ((arg.minimum && numeric < *arg.minimum) || (arg.maximum && numeric > *arg.maximum)) { output("error: " + arg.name + " is outside the allowed range. Use help " + entry->name); return false; } } values.push_back(std::move(*value)); } if (index != args.size()) { // `perf hdu`: neither an argument of perf nor a command under it. output("error: Too many arguments. Usage: " + usage(*entry) + (under(entry->name).empty() ? "" : ". Use help " + entry->name + " for the commands under it.")); return false; } if (!entry->run) { output("error: " + entry->name + " is read-only."); return false; } entry->run(context, values, output); return true; } // words includes the active, possibly empty token; completion only reads Context. std::vector complete(std::span words, const Context &context) const { std::vector result; std::set emitted; if (words.empty()) return result; const auto prefix = words.back(); const auto prior = words.first(words.size() - 1); auto add = [&](std::string text, const Entry &entry, bool argument) { if (!ascii_case_insensitive_starts_with(text, prefix)) return; if (!emitted.insert(lower(text)).second) return; result.push_back({std::move(text), entry.name, entry.description, usage(entry), entry.group, entry.kind, state(entry, context), argument}); }; for (const auto &item : entries_) { std::vector routes{item.name}; // Multiword aliases also supply the menu command routes. for (const auto &alias : item.aliases) if (alias.find(' ') != std::string::npos) routes.push_back(alias); for (const auto &route : routes) { const auto path = parse_console_command(route).arguments; if (prior.size() >= path.size()) continue; bool matches = true; for (std::size_t i = 0; i < prior.size(); ++i) matches = matches && equal(prior[i], path[i]); if (!matches) continue; const auto before = result.size(); add(path[prior.size()], item, false); if (result.size() == before) continue; auto &suggestion = result.back(); if (prior.size() + 1 < path.size()) { suggestion.name = suggestion.text; suggestion.description = "Browse " + suggestion.text + " commands"; suggestion.usage = route; suggestion.kind = Kind::action; suggestion.state = {}; } else suggestion.name = route; } } const auto [entry, consumed] = resolve(prior); if (entry) { const auto arg_index = prior.size() - consumed; if (arg_index < entry->arguments.size()) { const auto &arg = entry->arguments[arg_index]; auto candidates = arg.complete ? arg.complete(context, prior.subspan(consumed)) : arg.choices; if (candidates.empty() && arg.type == Type::boolean) candidates = {"0", "1"}; for (const auto &candidate : candidates) add(candidate, *entry, true); } } std::sort(result.begin(), result.end(), [](const auto &a, const auto &b) { return lower(a.text) < lower(b.text); }); if (result.size() > console_max_autocomplete_matches) result.resize(console_max_autocomplete_matches); return result; } private: std::pair resolve(std::span words) const { const Entry *entry = nullptr; std::size_t consumed = 0; std::string path; for (std::size_t i = 0; i < words.size(); ++i) { if (i) path += ' '; path += words[i]; if (const auto *found = find(path)) { entry = found; consumed = i + 1; } } return {entry, consumed}; } std::vector entries_; std::map lookup_; }; } // namespace dingosdk::console