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

@@ -585,6 +585,9 @@ void object_codec() {
const auto listed = decode(encode(served));
check(listed && listed->members.size() == 34 && listed->capacity == 32, "A server's roster past its limit failed to round-trip");
check(listed && listed->vote == served.vote, "A server's vote failed to round-trip");
served.sync_effects = false;
const auto plain = decode(encode(served));
check(plain && !plain->sync_effects && listed && listed->sync_effects, "The roster's skater effects rule failed to round-trip");
served.object_scaling = false;
const auto fixed = decode(encode(served));
check(fixed && !fixed->object_scaling && listed && listed->object_scaling, "The roster's object scaling rule failed to round-trip");
@@ -889,6 +892,33 @@ void dedicated_server_codec() {
tuning.tuning.assign(max_physics_tuning + 1, 0);
check(reject(tuning), "Oversized physics tuning encoded");
Packet extras;
// Skater effects: a player's contacts with the world, a few to a packet.
{
Packet fx;
fx.kind = PacketKind::effects; fx.session = 9; fx.epoch = 10; fx.map = 11; fx.source = 12;
dingosdk::multiplayer::Impact slide;
slide.position = {594.5f, 199.15f, 1070.1f};
slide.velocity = {16.97f, -8.44f, 0.f};
slide.normal = {0.f, 1.f, 0.f};
slide.material = 49;
fx.impacts = {dingosdk::multiplayer::wire_impact(slide), dingosdk::multiplayer::wire_impact(slide)};
const auto back = decode(encode(fx));
check(back && back->kind == PacketKind::effects && back->impacts == fx.impacts, "Skater effects failed to round-trip");
fx.impacts.assign(dingosdk::multiplayer::max_impacts + 1, slide);
bool refused{};
try { encode(fx); } catch (const std::invalid_argument &) { refused = true; }
check(refused, "More contacts than a packet carries were encoded");
fx.impacts = {slide};
fx.impacts[0].velocity = {60.f, 0.f, 0.f};
refused = false;
try { encode(fx); } catch (const std::invalid_argument &) { refused = true; }
check(refused, "A contact faster than the game allows was encoded");
fx.impacts[0] = slide;
fx.impacts[0].material = 0x2000;
refused = false;
try { encode(fx); } catch (const std::invalid_argument &) { refused = true; }
check(refused, "A material past the game's table was encoded");
}
extras.kind = PacketKind::physics_extras; extras.session = 9; extras.epoch = 10; extras.map = 11; extras.source = player;
check(decode(encode(extras)) && decode(encode(extras))->kind == PacketKind::physics_extras && decode(encode(extras))->extras.empty(),
"Empty physics extras (the game's own) lost");