Files
ReSkate/Extension/Multiplayer/console_commands.cpp
T
AyeZeeBB 7ee82b2771 Player distance and nametag settings; far players sent less and more coarsely; profiler addresses
Game
- Player distance (Settings > Interface, "player-distance"): how far
  away another player is still built as a skater, 50 to 1000 m, default
  120. Past it they keep their nametag or dot. All the way up shows
  everyone. A player already shown stays until 15% further out, so one
  skating along the edge is not built and taken down over and over. The
  game ran a full skater for every player on the server wherever they
  were, which is most of a frame on a full one
- Nametags are always ReSkate's own. The "ReSkate nametags" toggle, the
  "Nametag style" button and the nametag-style command are gone; the
  game's own nametags are never shown
- Nametag distance ("nametag-distance"): how far away a name still
  shows before it becomes a dot, 10 to 500 m. Default 120, was a fixed
  150, so by default the name and the skater go at the same distance
- Nametag dots ("nametag-dots"): off hides the dots for far players and
  the ones at the screen's edge for players off screen
- Friends' nametags only ("nametags-friends"): only Steam friends get a
  name or dot; chat bubbles still show for everyone

Server
- A player out of sight of another (half as far again as the low rate
  starts, 255 m by default) is sent to them once a second instead of
  five times. On a full server most players are out of each other's
  sight, and at five a second each they were most of what was sent
- Rotations are rounded before a pose is passed on, more coarsely the
  further away the recipient is: 0.04 degrees within the full rate's
  reach, 0.16 within the half rate's, a third of a degree past that,
  about 5 degrees out of sight. The pose is still an ordinary one. A
  bone that turned by less than a step is the same bytes as before, so
  the difference from the reference leaves it out, and the low bits of
  the ones that did turn pack away. By distance only: a player the
  crowd limit slowed keeps the fine rotations
- A recipient crossing from one precision to another is sent a whole
  state of that player, since a difference from a reference of another
  precision would carry every bone again
- "net" shows the size of the poses sent at each precision

Profiler
- A function in ReSkate.dll is reported by its address when the DLL has
  no symbols. A release build named every one of ours as the nearest of
  three exports, so a report could not say which was slow; the
  release's symbols name the address afterwards
2026-10-07 16:43:18 -05:00

255 lines
16 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 &registry) {
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{"player-distance", "How far away another player still gets a skater, in metres (50-1000; 1000: every player). Past it: a nametag or dot"},
Command{"nametag-distance", "How far away a player's name still shows, in metres (10-500); past it they are a dot"},
Command{"nametag-dots", "Show far and off-screen players as dots (on, off, toggle)"},
Command{"nametags-friends", "Only your Steam friends have nametags (on, off, 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) == "chat-bubbles" || std::string_view(c.name) == "chat-bubbles-own" ||
std::string_view(c.name) == "nametag-dots" || std::string_view(c.name) == "nametags-friends" ||
std::string_view(c.name) == "score-check")
args.push_back(argument("choice"));
if (std::string_view(c.name) == "chat-bubbles-distance" || std::string_view(c.name) == "nametag-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) == "player-distance")
args.push_back(argument("metres"));
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