mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-24 00:35:59 +02:00
Plugins register modes (card, replaces, order, flag, players) and publish documents through reskate.throwdowns.ui: the setup page, the waiting card, HUD slots themed on the game's HUD_<theme>_* widgets, the match panel, flags and match restrictions. SmokeyDawg58's native plumbing (throwdown_menu.cpp and its .inl files) renders them and posts throwdown_ui events back. kit::flag is the one native flag registration. General services added for it: reskate.files, reskate.ui.dialog, reskate.skater push/hold/bone_cam, reskate.camera.pad_flight, reskate.physics competition, reskate.session.world, reskate.tricks feed/gesture, and world entities. His ten modes are now Lua in his plugin: the rules, referee and wire (td/rules, referee, wire), and every page and HUD document (td/ui*.lua). Extension/Modes and reskate.modes are gone, along with every race and 1-UP branch in the SDK. The plugin test drives each mode through kit events. A native setup page is never written to while it closes into flag placement: its rows are not republished, the stock page is put back once the exit is over, and the stock Players slider is left alone while a plugin flag is out. This fixes the crashes on the Deathrace button and on 1-UP's Confirm.
596 lines
25 KiB
C++
596 lines
25 KiB
C++
#include "plugin_host.h"
|
|
#include <cctype>
|
|
#include "plugin_api.h"
|
|
#include "plugin_wire.h"
|
|
#include "Engine/Core/Console/console_core.h"
|
|
#include "Extension/Throwdowns/throwdown_kit.h"
|
|
#include <algorithm>
|
|
#include <format>
|
|
#include <utility>
|
|
|
|
namespace dingosdk::plugins {
|
|
namespace {
|
|
std::string lowercase(std::string text) {
|
|
for (auto& c : text) if (c >= 'A' && c <= 'Z') c = static_cast<char>(c + 32);
|
|
return text;
|
|
}
|
|
}
|
|
|
|
Host::Host(Environment& environment, Side side, Limits limits, std::string suffix)
|
|
: environment_(environment), side_(side), limits_(limits), suffix_(std::move(suffix)) {}
|
|
|
|
Host::~Host() { shutdown(); }
|
|
|
|
Host::Entry* Host::find(std::string_view id) {
|
|
// An owner's id, as the game was given it, names its entry too.
|
|
if (!suffix_.empty() && id.ends_with(suffix_)) id.remove_suffix(suffix_.size());
|
|
const auto found = entries_.find(id);
|
|
return found == entries_.end() ? nullptr : &found->second;
|
|
}
|
|
|
|
void Host::load(Entry& entry) {
|
|
entry.failure.clear();
|
|
entry.plugin.reset();
|
|
const auto& id = entry.candidate.id;
|
|
if (!entry.candidate.refusal.empty()) {
|
|
entry.failure = entry.candidate.refusal;
|
|
environment_.log(LogLevel::warn, std::format("Plugin {} was not loaded: {}", id, entry.failure));
|
|
return;
|
|
}
|
|
Manifest manifest;
|
|
try {
|
|
std::string error;
|
|
const auto text = read_package_file(entry.candidate.directory / manifest_file, maximum_manifest_bytes, error);
|
|
if (!error.empty()) throw std::runtime_error(std::string(manifest_file) + " " + error);
|
|
manifest = parse_manifest(text);
|
|
} catch (const std::exception& error) {
|
|
entry.failure = error.what();
|
|
environment_.log(LogLevel::warn, std::format("Plugin {} was not loaded: {}", id, entry.failure));
|
|
return;
|
|
}
|
|
const bool here = manifest.side == Side::both || manifest.side == side_;
|
|
if (!here) {
|
|
entry.failure = std::format("it is a {} plugin", side_name(manifest.side));
|
|
environment_.log(LogLevel::info, std::format("Plugin {} was not loaded here: {}", id, entry.failure));
|
|
return;
|
|
}
|
|
std::string permissions;
|
|
for (const auto& permission : manifest.permissions) permissions += (permissions.empty() ? "" : ", ") + permission;
|
|
Services& services = *this;
|
|
entry.plugin = std::make_unique<Plugin>(id + suffix_, entry.candidate.directory, std::move(manifest), services, limits_);
|
|
// Its messages go under the name its manifest.json gives it, the same on every player's game
|
|
// whatever its folder is called; the folder's name when it has none.
|
|
std::string channel = id;
|
|
try {
|
|
std::string error;
|
|
const auto text = read_package_file(entry.candidate.directory / "manifest.json", maximum_manifest_bytes, error);
|
|
if (error.empty()) {
|
|
const auto json = Json::parse(text, {maximum_manifest_bytes, 8, 4096});
|
|
if (json.is_object() && json.contains("name") && json.at("name").is_string() && !json.at("name").string().empty())
|
|
channel = json.at("name").string();
|
|
}
|
|
} catch (const std::exception&) {}
|
|
entry.plugin->set_channel(channel);
|
|
if (!entry.plugin->load()) {
|
|
entry.failure = entry.plugin->reason();
|
|
entry.plugin.reset();
|
|
release_commands(id); // its own log says why it stopped
|
|
return;
|
|
}
|
|
environment_.log(LogLevel::info, std::format("Plugin {} loaded ({}).", id, permissions.empty() ? "no permissions" : permissions));
|
|
}
|
|
|
|
void Host::unload(Entry& entry) {
|
|
if (entry.plugin) {
|
|
entry.plugin->unload();
|
|
entry.plugin.reset();
|
|
}
|
|
release_commands(entry.candidate.id + suffix_);
|
|
environment_.game().release(entry.candidate.id + suffix_);
|
|
throwdowns::kit::release(entry.candidate.id + suffix_);
|
|
}
|
|
|
|
void Host::collect(Entry& entry) {
|
|
if (!entry.plugin || entry.plugin->state() != State::stopped) return;
|
|
entry.failure = entry.plugin->reason();
|
|
entry.plugin->close_if_stopped();
|
|
entry.plugin.reset();
|
|
release_commands(entry.candidate.id + suffix_);
|
|
environment_.game().release(entry.candidate.id + suffix_);
|
|
throwdowns::kit::release(entry.candidate.id + suffix_);
|
|
}
|
|
|
|
void Host::release_commands(std::string_view id) {
|
|
std::erase_if(commands_, [&](const auto& item) { return item.second == id; });
|
|
}
|
|
|
|
void Host::apply(const std::vector<Candidate>& found) {
|
|
std::map<std::string, const Candidate*, std::less<>> wanted;
|
|
for (const auto& candidate : found) wanted.emplace(candidate.id, &candidate);
|
|
for (auto it = entries_.begin(); it != entries_.end();) {
|
|
if (wanted.contains(it->first)) {
|
|
++it;
|
|
continue;
|
|
}
|
|
if (it->second.plugin) environment_.log(LogLevel::info, std::format("Plugin {} unloaded: its mod was switched off or removed.", it->first));
|
|
unload(it->second);
|
|
it = entries_.erase(it);
|
|
}
|
|
for (const auto& [id, candidate] : wanted) {
|
|
auto* entry = find(id);
|
|
if (!entry) {
|
|
entry = &entries_.emplace(id, Entry{*candidate, {}, {}, false}).first->second;
|
|
load(*entry);
|
|
continue;
|
|
}
|
|
if (entry->candidate == *candidate) continue;
|
|
entry->candidate = *candidate;
|
|
if (entry->held) continue;
|
|
environment_.log(LogLevel::info, std::format("Plugin {} reloading: its files changed.", id));
|
|
unload(*entry);
|
|
load(*entry);
|
|
}
|
|
}
|
|
|
|
void Host::tick(double dt_ms) {
|
|
const double now = environment_.now_ms();
|
|
const bool save = now >= next_save_;
|
|
if (save) next_save_ = now + 2000;
|
|
follow_session();
|
|
deliver_messages();
|
|
deliver_events();
|
|
deliver_http();
|
|
follow_game();
|
|
for (auto& [id, entry] : entries_) {
|
|
if (!entry.plugin) continue;
|
|
entry.plugin->tick(dt_ms);
|
|
collect(entry);
|
|
if (save && entry.plugin) entry.plugin->save_storage();
|
|
}
|
|
draw_and_build();
|
|
}
|
|
|
|
void Host::follow_session() {
|
|
auto now = environment_.session();
|
|
if (!now.active) now.players.clear();
|
|
std::vector<SessionPlayer> joined, left;
|
|
const auto has = [](const std::vector<SessionPlayer>& list, std::uint64_t id) {
|
|
return std::any_of(list.begin(), list.end(), [&](const auto& p) { return p.id == id; });
|
|
};
|
|
for (const auto& player : now.players)
|
|
if (!has(session_.players, player.id)) joined.push_back(player);
|
|
for (const auto& player : session_.players)
|
|
if (!has(now.players, player.id)) left.push_back(player);
|
|
session_ = std::move(now);
|
|
if (joined.empty() && left.empty()) return;
|
|
for (auto& [id, entry] : entries_) {
|
|
if (!entry.plugin) continue;
|
|
entry.plugin->session_changed(joined, left);
|
|
collect(entry);
|
|
}
|
|
}
|
|
|
|
void Host::deliver_messages() {
|
|
std::uint64_t me = 0;
|
|
for (const auto& player : session_.players)
|
|
if (player.me) me = player.id;
|
|
const bool server = side_ == Side::server;
|
|
for (const auto& received : environment_.take_messages()) {
|
|
const auto message = wire::decode(received.bytes);
|
|
// A server copy hears what is for everyone or for the authority; a client copy, what is
|
|
// for everyone or for it, and the authority's words only from the authority (the session
|
|
// checks who sent those).
|
|
if (!message || (server ? message->to > wire::authority && message->to != me : message->to && message->to != me) ||
|
|
(server && message->authority))
|
|
continue;
|
|
SessionPlayer from{received.from};
|
|
for (const auto& player : session_.players)
|
|
if (player.id == received.from) from = player;
|
|
if (message->authority) {
|
|
from.server = true;
|
|
if (from.name.empty()) from.name = "Server";
|
|
}
|
|
for (auto& [id, entry] : entries_) {
|
|
if (!entry.plugin || entry.plugin->channel() != message->channel) continue;
|
|
entry.plugin->receive(from, message->payload);
|
|
collect(entry);
|
|
}
|
|
}
|
|
}
|
|
|
|
Host::Entry* Host::running(std::string_view name) {
|
|
const auto wanted = lowercase(std::string(name));
|
|
for (auto& [id, entry] : entries_)
|
|
if (entry.plugin && entry.plugin->state() == State::running &&
|
|
(lowercase(id) == wanted || lowercase(entry.plugin->channel_name()) == wanted))
|
|
return &entry;
|
|
return nullptr;
|
|
}
|
|
|
|
std::string Host::call_plugin(std::string_view caller, std::string_view target, std::string_view function, const Json& arguments,
|
|
Json& result) {
|
|
auto* entry = running(target);
|
|
if (!entry) return std::format("there is no running plugin {}", target);
|
|
if (entry->candidate.id + suffix_ == caller) return "a plugin calls its own functions directly";
|
|
auto error = entry->plugin->call_exposed(std::string(function), arguments, result);
|
|
// (It is closed if that stopped it; the plugin that called carries on.)
|
|
if (entry->plugin->state() == State::stopped) collect(*entry);
|
|
return error;
|
|
}
|
|
|
|
std::string Host::exposed(std::string_view target, std::vector<std::string>& names) {
|
|
auto* entry = running(target);
|
|
if (!entry) return std::format("there is no running plugin {}", target);
|
|
names = entry->plugin->exposed_names();
|
|
return {};
|
|
}
|
|
|
|
bool Host::emit(std::string_view source, std::string_view event, Json data) {
|
|
if (events_.size() >= 1024) return false;
|
|
events_.push_back({std::string(source), std::string(event), std::move(data)});
|
|
return true;
|
|
}
|
|
|
|
void Host::follow_game() {
|
|
auto& game = environment_.game();
|
|
const auto wants = [&](std::string_view permission) {
|
|
return std::any_of(entries_.begin(), entries_.end(),
|
|
[&](const auto& item) { return item.second.plugin && item.second.plugin->has(permission); });
|
|
};
|
|
const auto each = [&](const auto& tell) {
|
|
for (auto& [id, entry] : entries_) {
|
|
if (!entry.plugin) continue;
|
|
tell(*entry.plugin, id);
|
|
collect(entry);
|
|
}
|
|
};
|
|
if (wants("events.skater")) {
|
|
const auto now = game.physics_state();
|
|
const auto name = [](std::optional<std::uint32_t> s) { return s ? skater_state_name(*s) : "none"; };
|
|
if (std::string_view(name(now)) != name(skater_state_)) {
|
|
const auto previous = name(skater_state_);
|
|
each([&](Plugin& plugin, const std::string&) { plugin.skater_changed(name(now), previous); });
|
|
}
|
|
skater_state_ = now;
|
|
const auto wipeouts = game.wipeouts();
|
|
if (wipeouts_known_ && wipeouts > wipeouts_) {
|
|
const auto slam = game.last_slam();
|
|
each([&](Plugin& plugin, const std::string&) { plugin.bailed(wipeouts, slam); });
|
|
}
|
|
wipeouts_ = wipeouts;
|
|
wipeouts_known_ = true;
|
|
auto news = game.take_skater_news();
|
|
for (const auto& l : news.landings) {
|
|
auto data = Json::object();
|
|
data["down_speed"] = l.down_speed;
|
|
data["speed"] = l.speed;
|
|
data["onto"] = l.rail ? "rail" : "ground";
|
|
each([&](Plugin& plugin, const std::string&) { plugin.game_event("board_landed", "events.skater", data); });
|
|
}
|
|
for (const auto& h : news.hits) {
|
|
auto data = Json::object();
|
|
data["bone"] = h.bone;
|
|
data["impact"] = h.impact;
|
|
data["vehicle"] = h.vehicle;
|
|
data["board"] = h.board;
|
|
each([&](Plugin& plugin, const std::string&) { plugin.game_event("body_hit", "events.skater", data); });
|
|
}
|
|
for (const auto& f : news.falls) {
|
|
auto data = Json::object();
|
|
data["seconds"] = f.seconds;
|
|
data["left"] = f.left;
|
|
data["right"] = f.right;
|
|
auto bones = Json::object();
|
|
for (const auto& b : f.bones) {
|
|
auto bone = Json::object();
|
|
bone["impact"] = b.impact;
|
|
bone["slide"] = b.slide;
|
|
bones[b.name] = std::move(bone);
|
|
}
|
|
data["bones"] = std::move(bones);
|
|
each([&](Plugin& plugin, const std::string&) { plugin.game_event("bail_end", "events.skater", data); });
|
|
}
|
|
}
|
|
if (wants("events.physics"))
|
|
for (const auto& step : game.take_physics_steps()) {
|
|
auto data = Json::object();
|
|
data["at"] = step.at;
|
|
data["seconds"] = step.seconds;
|
|
data["wipeout"] = step.wipeout;
|
|
data["airborne"] = step.airborne;
|
|
if (step.ragdoll) data["ragdoll"] = *step.ragdoll;
|
|
if (step.velocity) data["velocity"] = Json::array({(*step.velocity)[0], (*step.velocity)[1], (*step.velocity)[2]});
|
|
if (step.spin) data["spin"] = *step.spin;
|
|
auto bodies = Json::object();
|
|
for (const auto& b : step.bodies) {
|
|
auto body = Json::object();
|
|
body["impact"] = b.impact;
|
|
body["slide"] = b.slide;
|
|
body["board"] = b.board;
|
|
body["vehicle"] = b.vehicle;
|
|
body["world"] = b.world;
|
|
bodies[b.name] = std::move(body);
|
|
}
|
|
data["bodies"] = std::move(bodies);
|
|
each([&](Plugin& plugin, const std::string&) { plugin.game_event("physics_step", "events.physics", data); });
|
|
}
|
|
if (wants("skater.control"))
|
|
each([&](Plugin& plugin, const std::string& id) {
|
|
for (const auto& pop : game.take_grind_pops(id)) {
|
|
auto data = Json::object();
|
|
data["applied"] = pop.applied;
|
|
data["reason"] = pop.reason;
|
|
if (!pop.basis.empty()) data["basis"] = pop.basis;
|
|
data["side"] = pop.toward > 0 ? "left" : pop.toward < 0 ? "right" : "";
|
|
data["before"] = pop.before;
|
|
data["after"] = pop.after;
|
|
plugin.game_event("grind_exit", "events.skater", data);
|
|
}
|
|
});
|
|
if (wants("chat.read")) {
|
|
auto lines = game.chat_since(chat_seen_);
|
|
// Lines from before the first plugin listened are not news.
|
|
if (!chat_started_) {
|
|
chat_started_ = true;
|
|
for (const auto& line : lines) chat_seen_ = std::max(chat_seen_, line.sequence);
|
|
lines.clear();
|
|
}
|
|
if (lines.size() > 20) lines.erase(lines.begin(), lines.end() - 20);
|
|
for (const auto& line : lines) {
|
|
chat_seen_ = std::max(chat_seen_, line.sequence);
|
|
each([&](Plugin& plugin, const std::string&) { plugin.chat_line(line); });
|
|
}
|
|
}
|
|
const double now = environment_.now_ms();
|
|
if (wants("music") && now >= next_track_) {
|
|
next_track_ = now + 1000;
|
|
const auto track = game.track();
|
|
if (track.available && track.revision != track_revision_) {
|
|
track_revision_ = track.revision;
|
|
each([&](Plugin& plugin, const std::string&) { plugin.track_changed(track); });
|
|
}
|
|
}
|
|
// The trick feed (tricks.feed): what the trick list wrote, to plugins that read tricks.
|
|
if (wants("events.tricks"))
|
|
if (const auto notes = game.take_trick_notes(); !notes.empty()) {
|
|
auto list = Json::array();
|
|
for (const auto& n : notes) list.push_back(Json{{"entry", std::to_string(n.entry)}, {"kind", std::string(1, n.kind)}, {"text", n.text}});
|
|
auto data = Json::object();
|
|
data["notes"] = std::move(list);
|
|
each([&](Plugin& plugin, const std::string&) { plugin.game_event("trick_notes", "events.tricks", data); });
|
|
}
|
|
if (wants("throwdowns")) {
|
|
each([&](Plugin& plugin, const std::string&) {
|
|
// What the native Throwdowns UI reports of the plugin's modes (throwdown_kit.h), each
|
|
// mode by the plugin's own id.
|
|
const auto prefix = plugin.id() + "/";
|
|
for (auto& e : throwdowns::kit::take(plugin.id())) {
|
|
auto data = Json::object();
|
|
data["action"] = std::move(e.action);
|
|
data["mode"] = e.mode.starts_with(prefix) ? e.mode.substr(prefix.size()) : std::move(e.mode);
|
|
auto args = Json::array();
|
|
for (auto& a : e.args) args.push_back(std::move(a));
|
|
data["args"] = std::move(args);
|
|
plugin.game_event("throwdown_ui", "throwdowns", data);
|
|
}
|
|
});
|
|
const bool active = game.throwdowns().active;
|
|
if (active != throwdown_active_) {
|
|
throwdown_active_ = active;
|
|
each([&](Plugin& plugin, const std::string&) { plugin.throwdown_phase(active ? "started" : "ended"); });
|
|
}
|
|
}
|
|
for (const auto& [plugin, action] : game.take_presses())
|
|
if (auto* entry = find(plugin); entry && entry->plugin) {
|
|
entry->plugin->action(action);
|
|
collect(*entry);
|
|
}
|
|
}
|
|
|
|
void Host::deliver_http() {
|
|
for (const auto& response : environment_.take_http_responses()) {
|
|
auto* entry = find(response.plugin);
|
|
if (!entry || !entry->plugin) continue; // unloaded since: its answer goes nowhere
|
|
entry->plugin->http_done(response);
|
|
collect(*entry);
|
|
}
|
|
}
|
|
|
|
void Host::deliver_events() {
|
|
const auto events = std::exchange(events_, {});
|
|
for (const auto& emitted : events) {
|
|
std::string channel = emitted.source;
|
|
if (const auto* source = find(emitted.source); source && source->plugin) channel = source->plugin->channel_name();
|
|
for (auto& [id, entry] : entries_) {
|
|
if (!entry.plugin || id + suffix_ == emitted.source) continue;
|
|
entry.plugin->plugin_event(emitted.source, channel, emitted.event, emitted.data);
|
|
collect(entry);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool Host::send(std::uint32_t channel, std::uint64_t to, std::string_view payload) {
|
|
if (!session_.active) return false;
|
|
if (side_ == Side::server && to == wire::authority) return false; // it is the authority
|
|
return environment_.send_message(wire::encode(channel, to, payload, side_ == Side::server));
|
|
}
|
|
|
|
void Host::draw_and_build() {
|
|
std::vector<plugin_ui::Layer> layers;
|
|
for (auto& [id, entry] : entries_) {
|
|
if (!entry.plugin) continue;
|
|
entry.plugin->draw();
|
|
collect(entry);
|
|
if (entry.plugin) {
|
|
auto shapes = entry.plugin->take_shapes();
|
|
if (!shapes.empty()) layers.push_back({id, std::move(shapes)});
|
|
}
|
|
}
|
|
environment_.publish_layers(std::move(layers));
|
|
for (auto& action : environment_.take_actions()) {
|
|
// Esc on a full-screen screen closes it at once; the plugin hears screen_closed.
|
|
if (action.widget == plugin_ui::close_widget) {
|
|
if (auto* entry = find(action.plugin); entry && entry->plugin) {
|
|
entry->plugin->close_screen();
|
|
collect(*entry);
|
|
}
|
|
continue;
|
|
}
|
|
if (actions_.size() < 256) actions_.push_back(std::move(action));
|
|
}
|
|
// Pages and screens are built ten times a second: a plugin's page (and its panels) only while the
|
|
// menu shows it, a screen while its plugin has it open.
|
|
const double now = environment_.now_ms();
|
|
if (now < next_panels_) return;
|
|
next_panels_ = now + 100;
|
|
const bool panels_shown = environment_.panels_shown();
|
|
const auto shown = environment_.shown_page();
|
|
std::vector<plugin_ui::Panel> panels;
|
|
std::vector<plugin_ui::PageEntry> pages;
|
|
for (auto& [id, entry] : entries_)
|
|
if (entry.plugin)
|
|
if (auto name = entry.plugin->page_name(); !name.empty()) pages.push_back({id, std::move(name)});
|
|
// By name, as the rail lists them, whatever their case.
|
|
std::sort(pages.begin(), pages.end(), [](const auto& a, const auto& b) {
|
|
return std::lexicographical_compare(a.title.begin(), a.title.end(), b.title.begin(), b.title.end(),
|
|
[](unsigned char x, unsigned char y) { return std::tolower(x) < std::tolower(y); });
|
|
});
|
|
if (pages != pages_) {
|
|
pages_ = pages;
|
|
environment_.publish_pages(std::move(pages));
|
|
}
|
|
for (auto& [id, entry] : entries_) {
|
|
if (!entry.plugin) continue;
|
|
std::vector<plugin_ui::Action> mine;
|
|
for (const auto& action : actions_)
|
|
if (action.plugin == id) mine.push_back(action);
|
|
if (auto screen = entry.plugin->build_screen(mine)) panels.push_back(std::move(*screen));
|
|
collect(entry);
|
|
if (!panels_shown || !entry.plugin || (!shown.empty() && shown != id)) continue;
|
|
auto built = entry.plugin->build_panels(mine);
|
|
collect(entry);
|
|
for (auto& panel : built) panels.push_back(std::move(panel));
|
|
}
|
|
actions_.clear();
|
|
environment_.publish_panels(std::move(panels));
|
|
}
|
|
|
|
void Host::set_level(std::string level) {
|
|
if (level == level_) return;
|
|
const auto previous = std::exchange(level_, std::move(level));
|
|
for (auto& [id, entry] : entries_) {
|
|
if (!entry.plugin) continue;
|
|
entry.plugin->level_changed(previous, level_);
|
|
collect(entry);
|
|
}
|
|
}
|
|
|
|
void Host::trick(const TrickNews& news) {
|
|
for (auto& [id, entry] : entries_) {
|
|
if (!entry.plugin || !entry.plugin->has("events.tricks")) continue;
|
|
entry.plugin->trick(news);
|
|
collect(entry);
|
|
}
|
|
}
|
|
|
|
bool Host::wants_tricks() const {
|
|
return std::any_of(entries_.begin(), entries_.end(),
|
|
[](const auto& item) { return item.second.plugin && item.second.plugin->has("events.tricks"); });
|
|
}
|
|
|
|
bool Host::run_console(std::string_view text) {
|
|
if (commands_.empty()) return false;
|
|
const auto parsed = parse_console_command(text);
|
|
if (!parsed || parsed.arguments.empty()) return false;
|
|
const auto& words = parsed.arguments;
|
|
// The longest command name the text starts with.
|
|
for (std::size_t count = std::min<std::size_t>(3, words.size()); count > 0; --count) {
|
|
std::string name;
|
|
for (std::size_t i = 0; i < count; ++i) name += (i ? " " : "") + lowercase(words[i]);
|
|
const auto found = commands_.find(name);
|
|
if (found == commands_.end()) continue;
|
|
auto* entry = find(found->second);
|
|
if (!entry || !entry->plugin) return false;
|
|
entry->plugin->run_command(name, {words.begin() + static_cast<std::ptrdiff_t>(count), words.end()});
|
|
collect(*entry);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
std::vector<Status> Host::status() const {
|
|
std::vector<Status> result;
|
|
for (const auto& [id, entry] : entries_) {
|
|
Status status{id};
|
|
if (entry.plugin) {
|
|
status.state = "running";
|
|
status.memory = entry.plugin->memory();
|
|
status.tick_ms = entry.plugin->tick_ms();
|
|
status.commands = entry.plugin->commands();
|
|
} else if (entry.held) {
|
|
status.state = "switched off";
|
|
status.reason = "plugin on " + id + suffix_ + " starts it again";
|
|
} else {
|
|
status.state = entry.failure.starts_with("it is a ") ? "not loaded" : "stopped";
|
|
status.reason = entry.failure;
|
|
}
|
|
result.push_back(std::move(status));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::string Host::reload(std::string_view id) {
|
|
auto* entry = find(id);
|
|
if (!entry) return std::format("error: There is no plugin {}.", id);
|
|
entry->held = false;
|
|
unload(*entry);
|
|
load(*entry);
|
|
return entry->plugin ? std::format("Plugin {} reloaded.", id) : std::format("error: Plugin {} did not start: {}", id, entry->failure);
|
|
}
|
|
|
|
std::string Host::switch_off(std::string_view id) {
|
|
auto* entry = find(id);
|
|
if (!entry) return std::format("error: There is no plugin {}.", id);
|
|
entry->held = true;
|
|
unload(*entry);
|
|
return std::format("Plugin {} switched off until plugin on {}, or the next launch.", std::string(id) + suffix_, std::string(id) + suffix_);
|
|
}
|
|
|
|
std::string Host::switch_on(std::string_view id) {
|
|
auto* entry = find(id);
|
|
if (!entry) return std::format("error: There is no plugin {}.", id);
|
|
if (entry->plugin) return std::format("Plugin {} is running.", id);
|
|
entry->held = false;
|
|
load(*entry);
|
|
return entry->plugin ? std::format("Plugin {} started.", id) : std::format("error: Plugin {} did not start: {}", id, entry->failure);
|
|
}
|
|
|
|
void Host::shutdown() {
|
|
for (auto& [id, entry] : entries_) unload(entry);
|
|
entries_.clear();
|
|
}
|
|
|
|
void Host::log(std::string_view plugin, LogLevel level, std::string_view text) {
|
|
environment_.log(level, std::format("[{}] {}", plugin, text));
|
|
}
|
|
void Host::console(std::string_view text) { environment_.console(text); }
|
|
double Host::now_ms() { return environment_.now_ms(); }
|
|
std::string Host::level() { return level_; }
|
|
std::filesystem::path Host::storage_file(std::string_view plugin) {
|
|
return environment_.storage_root() / std::filesystem::path(std::string(plugin)) / "storage.json";
|
|
}
|
|
|
|
std::string Host::claim_command(std::string_view plugin, std::string_view name) {
|
|
const auto first = name.substr(0, name.find(' '));
|
|
if (first == "plugin" || first == "help" || environment_.builtin_command(first))
|
|
return std::format("ReSkate has a console command \"{}\": pick another name", first);
|
|
for (const auto& [taken, owner] : commands_) {
|
|
if (owner == plugin) continue;
|
|
const bool overlaps = taken == name || name.starts_with(taken + " ") || taken.starts_with(std::string(name) + " ");
|
|
if (overlaps) return std::format("the plugin {} has the console command \"{}\"", owner, taken);
|
|
}
|
|
commands_.emplace(std::string(name), std::string(plugin));
|
|
return {};
|
|
}
|
|
}
|