mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-18 00:27:06 +02:00
A player's recent chat lines show as speech bubbles above their skater: newest nearest the head, older lines stacking upward, each popping in with a slight overshoot before it fades. Emotes render as their images, a message that is nothing but a few emotes shows them jumbo, and a message longer than a handful of lines is cut with an ellipsis so one line can never wall the screen off. Local display options live under Settings > Interface > Multiplayer (on/off, own messages, distance, duration and how many recent lines stack) and the console gains mp chat-bubbles, chat-bubbles-own, chat-bubbles-distance, chat-bubbles-duration and chat-bubbles-history. The chat panel and the bubbles now share one rich-text module (chat_rich), and a filtered message keeps its emote names: the bad-word mask is applied, but emote tokens are put back from the original before they are parsed.
249 lines
15 KiB
C++
249 lines
15 KiB
C++
#include "Extension/Console/commands.h"
|
|
#include "Extension/Multiplayer/Session/session.h"
|
|
#include "Extension/Multiplayer/Hud/native_party.h"
|
|
#include "Extension/Multiplayer/Remote/native_skater.h"
|
|
#include "Extension/Multiplayer/Remote/remote_collision.h"
|
|
#include "Extension/Throwdowns/throwdown_lab.h"
|
|
#include "Extension/Throwdowns/throwdown_debug_text.h"
|
|
#include "Extension/Throwdowns/throwdown_relay.h"
|
|
#include "Extension/UI/NativeMenu/native_menu_dump.h"
|
|
#include "Extension/Assets/live_mods.h"
|
|
#include <algorithm>
|
|
#include <array>
|
|
|
|
namespace dingosdk::console {
|
|
void register_multiplayer_commands(Commands ®istry) {
|
|
struct Command {
|
|
const char *name;
|
|
const char *description;
|
|
};
|
|
for (const auto &c : {Command{"host", "Host a Steam lobby for 2-32 players"},
|
|
Command{"join", "Join using the host's complete session code"},
|
|
Command{"browse", "Refresh the public Steam lobby browser"},
|
|
Command{"join-lobby", "Join a lobby selected in the server browser"},
|
|
Command{"echo", "Test a delayed remote skater locally"},
|
|
Command{"stop", "Disconnect and remove the remote skater"},
|
|
Command{"chat", "Send a text message to everyone in the session (T opens the chat box)"},
|
|
Command{"server", "Send a command to the dedicated server you are an admin of (try: help)"},
|
|
Command{"tp", "Teleport to a player (name start) or to x y z"},
|
|
Command{"party", "Your party in a session: invite|join|kick|promote <player>, "
|
|
"accept|decline [player], leave, open, close, status"},
|
|
Command{"tpall", "Host or server admin: teleport everyone to you"},
|
|
Command{"tphere", "Host or server admin: teleport one player to you"},
|
|
Command{"nametags", "Show or hide player nametags (on, off, toggle)"},
|
|
Command{"nametag-style", "ReSkate nametags or the game's own (reskate, game, toggle)"},
|
|
Command{"chat-bubbles", "Show or hide chat bubbles above skaters (on, off, toggle)"},
|
|
Command{"chat-bubbles-own", "Also show your own chat messages above your skater (on, off, toggle)"},
|
|
Command{"chat-bubbles-distance", "How far away a player may be and still show a chat bubble, in metres (5-500)"},
|
|
Command{"chat-bubbles-duration", "How many seconds a chat bubble stays before it fades (1-30)"},
|
|
Command{"chat-bubbles-history", "How many recent messages stack above a skater (1-8)"},
|
|
Command{"score-check", "Host: keep players whose mods change scoring or physics out of throwdowns and "
|
|
"coop challenges (on, off, toggle; on by default)"},
|
|
Command{"retry", "Retry remote skater creation after an error"},
|
|
Command{"test", "Test a Steam socket-pair round trip"},
|
|
Command{"status", "Show multiplayer connection and animation status"}}) {
|
|
std::vector<Argument> args;
|
|
if (std::string_view(c.name) == "join")
|
|
args.push_back(argument("join_code"));
|
|
if (std::string_view(c.name) == "host") {
|
|
args.push_back(argument("visibility", Type::text, true));
|
|
args.push_back(argument("players", Type::text, true));
|
|
}
|
|
if (std::string_view(c.name) == "join-lobby")
|
|
args.push_back(argument("lobby_id"));
|
|
if (std::string_view(c.name) == "nametags" || std::string_view(c.name) == "nametag-style" ||
|
|
std::string_view(c.name) == "chat-bubbles" || std::string_view(c.name) == "chat-bubbles-own" ||
|
|
std::string_view(c.name) == "score-check")
|
|
args.push_back(argument("choice"));
|
|
if (std::string_view(c.name) == "chat-bubbles-distance")
|
|
args.push_back(argument("metres"));
|
|
if (std::string_view(c.name) == "chat-bubbles-duration")
|
|
args.push_back(argument("seconds"));
|
|
if (std::string_view(c.name) == "chat-bubbles-history")
|
|
args.push_back(argument("lines"));
|
|
if (std::string_view(c.name) == "chat" || std::string_view(c.name) == "server" || std::string_view(c.name) == "party" ||
|
|
std::string_view(c.name) == "tp" || std::string_view(c.name) == "tphere") {
|
|
auto message = argument("message");
|
|
message.rest = true;
|
|
args.push_back(std::move(message));
|
|
}
|
|
auto entry = action(std::string("mp ") + c.name, c.description, Group::gameplay, std::move(args));
|
|
entry.aliases = {std::string("multiplayer.") + c.name};
|
|
entry.run = [name = std::string(c.name)](const Model &, const Values &values, const Output &out) {
|
|
std::string arguments;
|
|
for (const auto &value : values) {
|
|
const auto &part = std::get<std::string>(value);
|
|
if (part.empty())
|
|
continue;
|
|
if (!arguments.empty())
|
|
arguments += ' ';
|
|
arguments += part;
|
|
}
|
|
out(multiplayer::command(name, arguments));
|
|
};
|
|
registry.add(std::move(entry));
|
|
}
|
|
auto markers_choice = argument("on|off", Type::text, true);
|
|
auto markers = action("mp map-markers",
|
|
"Named, selectable markers for your party members on the pause map, like the live game's "
|
|
"(default). Off, or players outside your party: the game's plain player dots",
|
|
Group::gameplay, {std::move(markers_choice)});
|
|
markers.run = [](const Model &, const Values &values, const Output &out) {
|
|
const auto &choice = std::get<std::string>(values[0]);
|
|
if (choice == "on" || choice == "off") multiplayer::set_native_party_map_markers(choice == "on");
|
|
else if (!choice.empty()) {
|
|
out("error: Choose on or off.");
|
|
return;
|
|
}
|
|
out(multiplayer::native_party_map_markers() ? "Map player markers: named and selectable."
|
|
: "Map player markers: the game's plain player dots.");
|
|
};
|
|
registry.add(std::move(markers));
|
|
// Other players as solid capsules for your skater (remote_collision.cpp).
|
|
auto collision_choice = argument("on|off", Type::text, true);
|
|
collision_choice.complete = [](const Model &, auto) { return std::vector<std::string>{"on", "off"}; };
|
|
auto collision = action("mp collision",
|
|
"Player collision: show whether other players are solid for your skater, or switch "
|
|
"ReSkate's collision capsules on or off. Both players need the game's Enable Party "
|
|
"Collision setting (shown while the playercollision feature is on)",
|
|
Group::gameplay, {std::move(collision_choice)});
|
|
collision.run = [](const Model &, const Values &values, const Output &out) {
|
|
const auto &choice = values.empty() ? std::string{} : std::get<std::string>(values[0]);
|
|
if (!choice.empty()) {
|
|
if (choice != "on" && choice != "off") {
|
|
out("error: Choose on or off.");
|
|
return;
|
|
}
|
|
multiplayer::set_remote_collision_enabled(choice == "on");
|
|
}
|
|
out(multiplayer::remote_collision_status());
|
|
};
|
|
registry.add(std::move(collision));
|
|
// The native work far players' skaters skip (puppet_cost.cpp), each switchable.
|
|
auto saving_name = argument("saving", Type::text, true);
|
|
saving_name.complete = [](const Model &, auto) {
|
|
return std::vector<std::string>{"driven-placement", "ecs-retry", "far-components"};
|
|
};
|
|
auto saving_choice = argument("on|off", Type::text, true);
|
|
auto savings = action("mp puppet-savings",
|
|
"Far players' skaters: show the savings, or switch one (driven-placement, ecs-retry, "
|
|
"far-components) on or off",
|
|
Group::gameplay, {std::move(saving_name), std::move(saving_choice)});
|
|
savings.run = [](const Model &, const Values &values, const Output &out) {
|
|
using multiplayer::PuppetSaving;
|
|
const auto &name = std::get<std::string>(values[0]);
|
|
const auto &choice = values.size() > 1 ? std::get<std::string>(values[1]) : std::string{};
|
|
if (!name.empty()) {
|
|
static const std::array<std::pair<std::string_view, PuppetSaving>, 3> names{{
|
|
{"driven-placement", PuppetSaving::driven_placement}, {"ecs-retry", PuppetSaving::ecs_retry},
|
|
{"far-components", PuppetSaving::far_components}}};
|
|
const auto found = std::find_if(names.begin(), names.end(), [&](const auto &n) { return n.first == name; });
|
|
if (found == names.end()) {
|
|
out("error: Unknown saving. Choose driven-placement, ecs-retry or far-components.");
|
|
return;
|
|
}
|
|
if (choice != "on" && choice != "off") {
|
|
out("error: Choose on or off.");
|
|
return;
|
|
}
|
|
multiplayer::set_puppet_saving(found->second, choice == "on");
|
|
}
|
|
out(multiplayer::puppet_saving_status());
|
|
};
|
|
registry.add(std::move(savings));
|
|
auto hud_choice = argument("reskate|game", Type::text, true);
|
|
hud_choice.complete = [](const Model &, auto) { return std::vector<std::string>{"reskate", "game"}; };
|
|
auto hud = action("throwdown-hud",
|
|
"The S.K.A.T.E. throwdown HUD: ReSkate's (default) or the game's own debug text",
|
|
Group::gameplay, {std::move(hud_choice)});
|
|
hud.run = [](const Model &, const Values &values, const Output &out) {
|
|
const auto &choice = std::get<std::string>(values[0]);
|
|
if (choice == "reskate" || choice == "game") multiplayer::set_skate_hud_reskate(choice == "reskate");
|
|
else if (!choice.empty()) {
|
|
out("error: Choose reskate or game.");
|
|
return;
|
|
}
|
|
out(multiplayer::skate_hud_reskate() ? "S.K.A.T.E. HUD: ReSkate's." : "S.K.A.T.E. HUD: the game's debug text.");
|
|
};
|
|
registry.add(std::move(hud));
|
|
auto offboard_choice = argument("on|off", Type::text, true);
|
|
auto offboard = action("throwdown-offboard",
|
|
"S.K.A.T.E.: wait for your turn on foot, off the board (default on)",
|
|
Group::gameplay, {std::move(offboard_choice)});
|
|
offboard.run = [](const Model &, const Values &values, const Output &out) {
|
|
const auto &choice = std::get<std::string>(values[0]);
|
|
if (choice == "on" || choice == "off") multiplayer::set_throwdown_offboard(choice == "on");
|
|
else if (!choice.empty()) {
|
|
out("error: Choose on or off.");
|
|
return;
|
|
}
|
|
out(multiplayer::throwdown_offboard_enabled() ? "S.K.A.T.E.: waiting players stand off their boards."
|
|
: "S.K.A.T.E.: waiting players keep skating.");
|
|
};
|
|
registry.add(std::move(offboard));
|
|
auto invite_choice = argument("on|off", Type::text, true);
|
|
invite_choice.complete = [](const Model &, auto) { return std::vector<std::string>{"on", "off"}; };
|
|
auto invite = action("challenge-invite",
|
|
"Coop challenges: starting a challenge invites party members near you, and you join theirs (default on)",
|
|
Group::gameplay, {std::move(invite_choice)});
|
|
invite.run = [](const Model &, const Values &values, const Output &out) {
|
|
const auto &choice = values.empty() ? std::string{} : std::get<std::string>(values[0]);
|
|
if (choice == "on" || choice == "off") multiplayer::set_challenge_invites(choice == "on");
|
|
else if (!choice.empty()) {
|
|
out("error: Choose on or off.");
|
|
return;
|
|
}
|
|
out(multiplayer::challenge_invites() ? "Coop challenges: on (nearby party members join each other's challenges)."
|
|
: "Coop challenges: off (every challenge is your own).");
|
|
};
|
|
registry.add(std::move(invite));
|
|
// Research aid for multiplayer throwdowns (analysis/throwdowns-native-mp.md).
|
|
auto lab_args = argument("verb_and_arguments", Type::text, true);
|
|
lab_args.rest = true;
|
|
lab_args.complete = [](const Model &, auto) {
|
|
return std::vector<std::string>{"status", "ai", "spawn", "join", "start", "destroy", "mmid", "trace", "endturn", "score", "mirror", "attempt", "relay"};
|
|
};
|
|
auto lab = action("throwdown", "Throwdown lab: add virtual participants, spawn/start/destroy queues (research aid)",
|
|
Group::console, {std::move(lab_args)});
|
|
lab.run = [](const Model &model, const Values &values, const Output &out) {
|
|
std::optional<std::array<float, 3>> skater;
|
|
if (model.debug.skater_position_valid) skater = model.debug.skater_position;
|
|
out(multiplayer::throwdown_lab_command(values.empty() ? std::string{} : std::get<std::string>(values[0]), skater));
|
|
};
|
|
registry.add(std::move(lab));
|
|
// Research aid for multiplayer coop challenges (analysis/coop-challenges-mp.md).
|
|
auto challenge_args = argument("verb_and_arguments", Type::text, true);
|
|
challenge_args.rest = true;
|
|
challenge_args.complete = [](const Model &, auto) {
|
|
return std::vector<std::string>{"status", "virtual", "attempt", "clear"};
|
|
};
|
|
auto challenge = action("challenge", "Coop challenge lab: start a challenge with virtual participants, replay your attempt as one (research aid)",
|
|
Group::console, {std::move(challenge_args)});
|
|
challenge.run = [](const Model &, const Values &values, const Output &out) {
|
|
out(multiplayer::challenge_lab_command(values.empty() ? std::string{} : std::get<std::string>(values[0])));
|
|
};
|
|
registry.add(std::move(challenge));
|
|
// Research aid: "all", "roots", "0x<schema>", or text every printed root must contain.
|
|
auto what = argument("what", Type::text, true);
|
|
what.complete = [](const Model &, auto) { return std::vector<std::string>{"all", "roots"}; };
|
|
auto delay = argument("delay_seconds", Type::unsigned_integer, true);
|
|
delay.maximum = 600;
|
|
auto dump = action("ui dump", "Write the live native UI model tree to the game's logs folder (research aid)",
|
|
Group::console, {std::move(what), std::move(delay)});
|
|
dump.run = [](const Model &, const Values &values, const Output &out) {
|
|
const auto text = values.empty() ? std::string{} : std::get<std::string>(values[0]);
|
|
const auto seconds = values.size() > 1 ? std::get<std::uint64_t>(values[1]) : 0;
|
|
out(multiplayer::request_ui_dump(text, static_cast<unsigned>(seconds)));
|
|
};
|
|
registry.add(std::move(dump));
|
|
auto then = argument("reload", Type::text, true);
|
|
auto apply = action("mods apply", "Apply Mods/ and mods.json now; takes effect at the next level load, "
|
|
"or straight away with 'reload'", Group::console, {std::move(then)});
|
|
apply.run = [](const Model &, const Values &values, const Output &out) {
|
|
const auto reload = !values.empty() && _stricmp(std::get<std::string>(values[0]).c_str(), "reload") == 0;
|
|
out(live_mods::apply(reload));
|
|
};
|
|
registry.add(std::move(apply));
|
|
}
|
|
} // namespace dingosdk::console
|