Skater effects on other players: costume and board trails, and contact sparks and dust

Other players' skaters and skateboards showed none of the game's skater
effects: not the trail or fire a costume or deck comes with, and not the
sparks, dust and puffs of a skater touching the world.

Costume and skateboard effects. The game gives another player's skater and
skateboard the right effect sets by itself, but builds them before their
outfit is on, so nothing showed. They are now built again half a second after
the outfit is applied. An always-on effect that a skater's item puts on the
skateboard's wheel joints (the Ghost Skater trail) follows the skater's own
copy of those joints, which only this player's skater keeps up to date: for
other players it is taken out of the skater's sets and built on their
skateboard, which has the same joints.

Contact effects. They come from a skater touching the world, which only
happens in that player's own game. Each game now captures its skater's
contacts (position, velocity, surface normal, surface material) and sends up
to three a tick in a new unreliable packet, PacketKind::effects. Other games
play each through the game's own function, so the effect is still chosen from
the surface and the speed. A contact is shown as late as that player's poses
are, only within 30 m of their skater, and only for a material the level's
table has: the game reads that table without a bound.

Server. A dedicated server relays effects packets to players within 150 m of
the sender, at most one packet a tick from each player. New setting
players.sync_effects (default true), console and admin command
"effects on|off". Off: the server relays none, and the roster tells games
neither to send their contacts nor to build other players' costume and
skateboard effects. Player-hosted lobbies relay through the host and always
share them.

The roster carries one more byte for the setting. Still protocol 45, which
has not been released.

New console command "mp effects": what has been captured and played, and why
the last contact was refused if one was.
This commit is contained in:
AyeZeeBB committed 2026-10-09 00:10:11 -05:00
1 parent 574860da72
commit b229062400
23 files changed
+763 -9

No files matched your search

+7
View File
@@ -131,6 +131,12 @@ allow_object_scaling Let players place objects bigger or smaller than their
objects at their own size and turns the size controls
off in their park editor. Admins can still resize
theirs. Console: object-scaling on|off.
sync_effects Let players see each other's skater effects (default true):
sparks and dust where a skater touches the world, and
the trails and fire of costumes and skateboards. false
relays none and players' games show each other without
them, which saves a little traffic and drawing on a busy
server. Console: effects on|off.
announce_throwdowns Tell everyone in chat when a throwdown drop is placed
(default true).
@@ -277,6 +283,7 @@ and change voice, distances, placement and kicks from the Multiplayer menu.
placement everyone|admins|nobody clear-objects
objects <number>|off How many objects each player may have placed.
object-scaling on|off Whether players may resize the objects they place.
effects on|off Whether players see each other's skater effects.
noclip on|off nobail on|off boosts on|off
What players may use (admins always can).
tuning on|off Everyone on the game's own physics tuning.
+11
View File
@@ -204,6 +204,17 @@ int run() {
check(config_error(away).find("afk_kick_minutes") != std::string::npos, "An away timer over a day was accepted");
std::filesystem::remove_all(folder / "data");
}
// Skater effects: shared unless turned off, and kept in the file.
{
ServerConfig plain;
check(plain.sync_effects, "Skater effects are not shared by default");
plain.file = folder / "plain.json";
plain.sync_effects = false;
save_config(plain);
check(!load_config(plain.file).sync_effects && text(plain.file).find("\"sync_effects\": false") != std::string::npos,
"The skater effects setting was not kept");
std::filesystem::remove_all(folder / "data");
}
// Object scaling: allowed unless turned off, and kept in the file.
{
ServerConfig sized;
+8 -1
View File
@@ -16,7 +16,7 @@ constexpr std::string_view help_text =
"status | net [player] | players | say <text> | msg <player> <text> | msg-party <player> <text> | msg-admins <text> | kick <player> | ban <player or SteamID64> [name] | unban <SteamID64> | bans\n"
"map <name, e.g. San Vansterdam> | maps | name <text> | password <text|off> | welcome <text|off> | listed on|off\n"
"voice on|off | voice-range <50-1000> | distances <full> <half> <half-return> <low> | crowd <n>|off | rate <KB/s> | bone-scale <1-8>|off\n"
"placement everyone|admins|nobody | objects <number>|off | object-scaling on|off | clear-objects | noclip on|off | nobail on|off | boosts on|off | tuning on|off\n"
"placement everyone|admins|nobody | objects <number>|off | object-scaling on|off | effects on|off | clear-objects | noclip on|off | nobail on|off | boosts on|off | tuning on|off\n"
"tpall [player] | tphere <player> | votes [map|kick|tod on|off|<percent>] | vote-cancel\n"
"map-pool [add|remove <map>|clear] | rotation [<minutes>|off]\n"
"park <lot> <layout> | park random | layer-sync on|off | layer <key> default|on|off | tod <time|default>\n"
@@ -457,6 +457,13 @@ std::string Host::command(std::string_view line, std::uint64_t admin) {
return changed(*value ? "Players can resize the objects they place."
: "Placed objects are their own size for everyone. Admins can still resize theirs.");
}
if (name == "effects") {
const auto value = on_off(argument);
if (!value) return std::string("effects on|off (now ") + (config_.sync_effects ? "on" : "off") + ")";
config_.sync_effects = *value;
return changed(*value ? "Players see each other's skater effects."
: "Players no longer see each other's skater effects. Ones already showing stay until that player's skater is shown again.");
}
if (name == "votes") {
// votes | votes <map|kick|tod> on|off|<percent> | votes seconds|cooldown <n>
const auto [what_text, value_text] = split(argument);
+2
View File
@@ -105,6 +105,7 @@ Layout layout(const ServerConfig &c) {
players.set("object_placement", placement_text(c.object_placement));
players.set("object_limit", c.object_limit);
players.set("allow_object_scaling", c.object_scaling);
players.set("sync_effects", c.sync_effects);
players.set("announce_throwdowns", c.announce_throwdowns);
auto &anti_cheat = root.section("anti_cheat");
@@ -260,6 +261,7 @@ ServerConfig load_config(const std::filesystem::path &file, std::vector<std::str
: placement == "admins" || placement == "host" ? ObjectPlacement::host_only : ObjectPlacement::everyone;
c.object_limit = get("players", "object_limit", c.object_limit);
c.object_scaling = get("players", "allow_object_scaling", c.object_scaling);
c.sync_effects = get("players", "sync_effects", c.sync_effects);
c.announce_throwdowns = get("players", "announce_throwdowns", c.announce_throwdowns);
c.speed_check = get("anti_cheat", "speed_hack", c.speed_check, {"speed_check"});
+4
View File
@@ -106,6 +106,10 @@ struct ServerConfig {
// Players may place objects at another size than their own. Off: every player's objects
// are shared at their own size; admins may still resize theirs.
bool object_scaling = true;
// Players see each other's skater effects: sparks and dust where a skater touches the world,
// and the trails and fire of costumes and skateboards. Off: nothing of them is relayed and
// players' games show each other without them.
bool sync_effects = true;
// Whether players may use noclip (and teleport) / No Bail / the boosts (admins always may).
bool noclip = true, no_bail = true, boosts = true;
// Players skate with the game's own physics tuning, not copies they edited.
+14 -1
View File
@@ -367,7 +367,8 @@ void Host::broadcast(const Packet &packet, bool reliable, bool fresh, std::uint6
outgoing.reserve(guests_.size());
std::shared_ptr<const std::vector<AudioSample>> sound; // a skater's samples, shared by everyone sent them
const bool gameplay = packet.kind == PacketKind::pose || packet.kind == PacketKind::audio ||
packet.kind == PacketKind::voice || packet.kind == PacketKind::cosmetics;
packet.kind == PacketKind::voice || packet.kind == PacketKind::cosmetics ||
packet.kind == PacketKind::effects;
for (auto &[id, guest] : guests_) {
auto &p = *guest;
if (!p.handshaken || id == except || (gameplay && !p.world_ready)) continue;
@@ -387,6 +388,15 @@ void Host::broadcast(const Packet &packet, bool reliable, bool fresh, std::uint6
if (!p.unmet.empty() && (packet.kind == PacketKind::pose || packet.kind == PacketKind::audio || packet.kind == PacketKind::cosmetics) &&
p.unmet.contains(packet.source))
continue;
// A skater's effects go to those near enough to see them: within 150 m.
if (packet.kind == PacketKind::effects && source && source->latest_root && p.latest_root) {
float distance{};
for (unsigned i = 0; i < 3; ++i) {
const auto d = p.latest_root->position[i] - source->latest_root->position[i];
distance += d * d;
}
if (distance > 150.f * 150.f) continue;
}
// A skater's sound goes to those who would hear it: within the full pose rate's reach,
// and in a crowd only from the nearest (the same players sent at the full rate).
// A game stops a sound it hears nothing more of after a second.
@@ -577,6 +587,7 @@ void Host::send_roster() {
p.object_placement = config_.object_placement;
p.object_limit = config_.object_limit;
p.object_scaling = config_.object_scaling;
p.sync_effects = config_.sync_effects;
p.guest_noclip = config_.noclip;
p.guest_no_bail = config_.no_bail;
p.guest_boosts = config_.boosts;
@@ -743,6 +754,8 @@ bool Host::accept_data(Guest &source, const Packet &p) {
if (accepted) source.cosmetic_packet = encode_wire(p);
} else if (p.kind == PacketKind::audio)
accepted = source.sound_budget.accept(now_, p.audio.size()) && source.audio.push(p, now_);
else if (p.kind == PacketKind::effects)
accepted = config_.sync_effects && source.effect_budget.accept(now_);
else if (p.kind == PacketKind::pose)
accepted = source.poses.push_validated(p, now_);
// The server never plays anything back: keep only what ordering needs.
+1
View File
@@ -144,6 +144,7 @@ class Host {
VoiceBudget voice_budget;
OutfitBudget outfit_budget;
SoundBudget sound_budget;
EffectBudget effect_budget;
ChatRate chat_rate;
ChatBudget admin_budget, throwdown_budget, party_budget;
SpeedCheck speed; // how fast their game runs, from their pose timestamps