A server lists Thunderstore packages in plugins.required ("Owner-Name", or "Owner-Name-1.2.3"
for at least that version, at most 16). It checks each on Thunderstore as it starts and leaves
out one not listed as a plugin, and sends the rest with its map offer (protocol 55).
A joining player's game checks them before the server's map loads. Those missing, switched off
or too old go on one card with the map when that is missing too, so the player is asked once.
GET ALL downloads and installs each, switches on any that were off, runs one live merge when
any brings game data, and joins; CANCEL leaves. The game fetches a package only when
Thunderstore lists it as a plugin and it installs as one (a reskate-plugin.json naming Luau
code), whatever a server asks; anything else is taken out again, so a cosmetic can never be
required.
map_download handles a list of packages instead of one map, and the Thunderstore chooser takes
the category it needs. A map alone keeps its card as it was.
Hide and Seek, tag, spectating and ghost runs need one player to be
gone for some others, which no plugin could do.
reskate.server.hide(target, viewers, {skater, name, voice, sound}) and
reskate.server.unhide(target, viewers?) do it per viewer; viewers is a
player, a list of players or "all" (everyone on now but the target).
Wholly (the default) the target leaves those viewers' rosters, so their
games take the skater, its name tag and its objects away as for a player
who left, and the server sends them nothing of the target: poses, sound,
voice, outfit, effects and objects. No protocol change: the roster per
viewer is the one the cards and polls use, and the server's introduce
already holds a player back until shown. Shown again, the target is
introduced anew (outfit, then whole poses, then all its objects).
Without the skater (skater = false), only the voice and/or the sound is
held back and the player stays. Name and skater go together: a game has
no skater without its player, and hiding one without the other would
need a protocol change.
Refused while the target shares a party or a throwdown with one of the
viewers, whose game would lose a party member or a player mid-way; while
hidden, the two cannot come into one party. A plugin that stops shows
everyone it hid; a player who leaves is dropped from every list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYabE7FCPgj3SaWeF92EML
A server copy on a dedicated server had no chat, an empty session.world() and no way to change the
map or the time of day, though the server does each with its own commands. Now:
- chat (chat.read): each player's line as the server passes it on, the server's own lines to
everyone, and a /command the server does not know itself, which goes to the plugins instead of
"Unknown command" while one hears chat, so a game mode takes its own commands;
- session.world(): the server's world and the name of its map;
- travel.levels() and travel.load(): the server's maps, and a map change as the console's map does
(checked, saved and logged);
- world.set_time(): the time of day for everyone, as tod does, when the server has its world
layers and layer-sync is on.
The names custom chat commands may not take now include every server command: lock, unlock, locks,
throwdown-locks, words, bone-reach and poll-run were missing, so a custom command could hide them.
Plugins: server cards and polls, mods.content on dedicated servers, world_to_screen bounds
Conflicts in the plugin and server plugin tests, where #348's role tests and this PR's tests were
added in the same places: both kept.
The server took a player as away unless their skater moved or they chatted, talked or changed
objects, so a pilot of the FPV drone plugin, whose skater stands still, was removed by the away
limit. The game now notes when the player works the controls (keys, mouse, a gamepad's buttons,
sticks and triggers, a joystick a plugin reads) and tells the server every 20 s while they do,
with a new presence packet (protocol 54). The server counts it as activity, once in 10 s at most.
A server plugin could only reach players through chat lines (say, tell).
A mode that asks players something (which track next, join the next
round) had to read chat commands, and could not show a card.
reskate.server.card(who, text, seconds) and reskate.server.poll(who,
question, answers, {seconds}) put a card or a poll on the screens of a
player, a list of players or "all", in the slot the roster already has
for the server's announcement and vote, so games need nothing new and
the protocol is unchanged. Each player's is their own and shows over the
server's until it ends, then the server's shows again. A poll has one id
for everyone it asks; a player's answer ("/2", as the game sends it)
closes it for them and comes back to the plugin that asked as a
poll_answer event; a poll that runs out, is replaced by a newer one for
that player, is closed with reskate.server.poll_close, or whose player
leaves comes back as poll_closed with why. The plugin counts the answers.
Both need the "server" permission and a server copy on a dedicated
server; a lobby's host has no roster slot for them. Answering a plugin's
poll is not a server vote, so it does not ask the player's role for
"votes". A plugin that stops takes its cards and polls with it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYabE7FCPgj3SaWeF92EML
A server plugin with mods.read could call reskate.mods.content, but the
server's game had no content of its own, so it got nil and "not
available". A mode that keeps its data in mods (a race reading start
and finish from a park mod) could only read it in the players' games.
The server now answers from every mod folder in its Mods, by name, as it
finds its maps there; it has no load order or mods.json. A hidden folder
(.reskate_plugin_data) and names a plugin could not ask for are left out,
and a mod's title is its manifest.json's "name".
The listing and reading moved from plugin_game_runtime.cpp into
plugin_content.cpp, which the game and the server share, so both keep
the same limits: the folders the manifest's "content" names, 1000 files,
four folders deep, 1 MB a file, nothing outside the mod's folder. The
game's side is unchanged apart from calling it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYabE7FCPgj3SaWeF92EML
A server plugin could not see the roles of a dedicated server (data/roles.json), so a plugin that
wanted only moderators to use some of its commands had to keep its own list of SteamIDs. In a
server copy with the server permission, a player table now has role: a read-only {name, rank,
staff}, the role the player has now (the one given them, or the server's default), read from the
server each time. staff is whether the role gives any of the server's commands. It is nil for a
player with no role, for the server itself and on a lobby's host. Each read counts as a server call.
dingosdk_server_plugin_tests reads a given role, the default role and a role given while the
player is on, and checks the table is read-only and a plugin without server is refused;
dingosdk_plugin_tests checks the API on a server copy, and that a player's copy and a plugin
without server cannot read it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hs7xxJDf6SDcQUaXnzmtL5
A dedicated server's park mods could only be spawned from its console, an admin's chat or a vote,
so a server plugin could not bring out the park its game is played in. A new permission,
server.parks, gives a server copy reskate.server.parks(), which lists the park mods set up with
whether each fits the map running now and is spawned (its objects and minutes left), and
reskate.server.park_spawn(name, minutes?) and park_remove(name), which return ok or false and why.
They go through park-mod add and remove, so the park limits, the map check and the chat lines stay
the server's. Their "[parks]" lines are logged "by plugin <id>". A name is a park mod's own (1 to 16
of a-z 0-9 - _) and minutes are 1 to 1440, or none for until removed. Reading the park files is not
part of this: a plugin brings what it needs to know about a park in its own package.
dingosdk_server_plugin_tests spawns and removes a park mod through a plugin and checks what it is
refused; dingosdk_plugin_tests checks the API on a server copy, and that a player's copy and a
plugin without server.parks cannot use it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hs7xxJDf6SDcQUaXnzmtL5
On a dedicated server reskate.level.current() was empty and level_loaded and level_unloading never
fired, so a server plugin could not tell which map ran or that it had changed. The server's
plugins are now told the map's level each tick, named as players' games name it (a destination's
level, after its "|"). When the server changes the map, they hear level_unloading for the old one
and level_loaded for the new one, as a game's plugins do; players load it after.
dingosdk_server_plugin_tests checks level.current() and both events across a map change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hs7xxJDf6SDcQUaXnzmtL5
reskate.session.skaters() returned an empty list on a dedicated server: its game for plugins did
not say where anyone was, though the host holds every player's latest pose. A server copy now gets
each player in the world with a pose: their id, the pose's root position, its heading (read as a
client reads it) and how long ago it arrived. A server plugin can now follow players itself, for
a race's checkpoints or a game of tag, instead of trusting what each player's copy reports.
Host::skater_poses() lists them for the plugins. dingosdk_server_plugin_tests drives the host with
simulated players sending poses and checks what a server plugin sees, and that a plugin without
session.skaters is still refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hs7xxJDf6SDcQUaXnzmtL5
A lock tool like noclip and boosts. The player's game enforces it: the roster
carries guest_ollies (protocol 53), and the jump hook gives a locked ollie no
upward speed. Plugins get player.permissions.ollies.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A poll put its question and answers in chat when it started, and its result when it ended, while
the poll card already shows both. Chat now says neither; the result still goes to the log, and a
cancelled poll still says why in chat.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Chat's flood control refuses a line identical to the one before it within 15 s, which also
caught /2 in a poll right after /2 in the last one. An answer to the running vote or poll is now
exempt from that one rule, on the server and in the game; the pace still holds, and the vote
still counts one answer per player.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A lock an admin sets is kept for the player's SteamID until unlocked or the server restarts, so
leaving and joining again no longer lifts it: they are locked again and told on joining. lock and
unlock take a player who is not here by SteamID64 (unlock also by the name they had), and locks
lists them as not here. A throwdown's own locks still end when the player leaves. Changing
throwdown-locks while a throwdown runs unlocks and locks just the tools that changed for its
players, saying the setting changed instead of that their throwdown is over.
A player kicked or banned only found out why from the status line of the
multiplayer menu, if they opened it.
- What the host or server says as it puts a player out ("You were
kicked...", "You are banned...", away too long, a mod or cheat it does
not allow) is now shown in the middle of the screen with an OK button,
under YOU WERE KICKED, BANNED or REMOVED. The same when they are refused
on trying to join again early. A link that is simply lost shows nothing
new.
- Answered with A or B on a controller, Enter or Esc, or a click on OK. The
game's input is held while it is up, as for the vote for the next map, so
the button that answers is not also a press in the game.
- Console: "popup test" shows it with a sample message, to try it without
being kicked.
Server: a kick by a vote has a time of its own, votes.kick_minutes, 5 by
default (1-1440, or 0 for until the server restarts); it no longer goes by
players.kick_minutes, which is now an admin's kick only and is still until
the restart by default. Console: votes kick-minutes <minutes>|restart, and
"votes" shows it. A config without the key takes the 5 minutes.
The server can take noclip (with teleporting), No Bail, the boosts and the
player's own physics tuning from one player while everyone else keeps what
the server allows: for fair throwdowns on a server that allows them for free
skating. Server only: a locked player gets a roster of their own with the
tool off (players with the same locks share one) and permissions without
"movement", so their role cannot give it back. Players without locks get
exactly the roster they got before.
- lock <player|SteamID64> <tool[,tool...]|all>, unlock <player|SteamID64>
[<tools>|all], locks: from the console, or an admin in chat. In memory
only; locks end when the player leaves. Logged as
"[lock] <name> (<SteamID64>): nobail, boosts locked by <who>".
- players.throwdown_locks (default []): tools locked for the players of a
throwdown from its start until they quit, it ends or they leave
(console: throwdown-locks <tools>|off).
- Tests: Server/Test/server_lock_tests.cpp, and the key in the config tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PreNfxwh21xyPp8EA72cUN
The Mods page has a third tab, PROFILES. A profile is a saved list of which
mods are on, in what order and at what version; every mod stays installed
once, in the one Mods folder. Loading a profile rewrites mods.json, switches
off what it does not name, and downloads from Thunderstore what it names
that is missing. Profiles live in Mods/profiles.json.
A profile travels the way r2modman's do, so the two managers read each
other's: export.r2x inside a .r2z file, and a code on Thunderstore's
profile service. Config files in an r2modman profile are ignored.
Installing or updating a Thunderstore package now brings the packages it
depends on, needed ones first, leaving out any already installed at a new
enough version. A mod pack is a package that is nothing else.
A kick, by an admin or by a passed vote, used to keep the player out until
the server restarted. "kick_minutes" in the players section of the config
(1-1440; 0, the default, is still until the restart) and the console
command "kick-time <minutes>|restart" now set how long it lasts.
The player is told how long when they are kicked, and how many minutes are
left if they try to join early. A kick keeps the time it was given, so
changing the setting is for later kicks. Bans are unchanged.
Map updates
- A map the game already has, being replaced by the host's version,
could load as soon as the new files were in Mods, while the mods
were still being applied: it ended on "This map is not loading". The
session now holds the load while a map is downloading, installing or
applying, as it waits for one that is missing.
- A player whose version of a map is later than the host's keeps it and
loads. Only an older one is replaced by the host's. Versions are
compared number by number (1.10.0 is later than 1.9.2).
Dedicated servers
- maps.latest_versions (off by default): the server asks Thunderstore
for the newest version of each of its custom maps, at start and every
maps.latest_check_minutes (5-1440, default 60), and writes it into
the mod's manifest.json. Nothing is downloaded: the server reads only
a map's level list and version, and players, who are held to the
server's version, fetch it as they join. The map being played waits
until nobody is on it. A folder that is not a Thunderstore package is
not touched, and the level list is left as it is.
- Console: maps update, maps latest on|off, maps latest every <minutes>.
Overlay
- Vote card: the pass mark is always in the middle of the bar. Green
grows over the left half up to the yes votes needed and over the
right half past them, whatever share the vote needs.
- The "Staff" tag is "ReSkate Admin", shimmering from #2500FF to
#B2F9FF.
No protocol change. Tests cover the version comparison and the
manifest write; the Thunderstore lookup was checked against the site.
The map update flow, the vote bar and the tag were not seen in game,
and the server setting has not run on a live server.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A dedicated server that rotates its maps can now ask players where to
go next instead of taking the pool's next map.
Server
- maps.rotation_vote: when the timer runs out, a vote between three
maps drawn at random from the pool, "Random map" and "Stay here".
The most votes wins, a tie is drawn, and with no votes it is the
pool's next map. It ends early once everyone has voted.
- maps.rotation_vote_seconds (10-120, default 30) and
maps.rotation_extend_minutes (0-240, default 15; 0 leaves "Stay
here" out).
- maps.rotation_announce: a card when the map has 10, 5 and 1 minutes
left.
- The vote's maps are drawn a minute ahead and sent to games
(PacketKind::next_maps), so their pictures are ready when it opens.
- /vote extend: a built-in vote that gives the map in play
rotation_extend_minutes more. On by default (votes.extend_map), and
only runs while the maps are on a timer. "extend" is now a reserved
vote name.
- Console: rotation vote on|off, rotation announce on|off,
rotation stay <minutes>|off, rotation extend [minutes],
votes extend ..., vote extend.
Game
- The vote opens as a popup in the middle of the screen: a tile per
answer with its picture, name, share of the votes and count. It
holds input as the map download card does, and works with a
controller (D-pad, A, B), the keyboard and the mouse.
- Pictures: a mod map's icon from its folder, or from Thunderstore
when it is not installed; the game's own maps show their loading
screen picture, cut to its middle square. "Random map", "Stay here"
and a map without a picture are drawn as shapes.
- Older servers and polls are unchanged: the vote is a poll, so it can
still be answered with /1, /2... in chat.
Protocol 50: games and servers both need this build.
Votes: a vote is decided by the votes cast, not by everyone on the server. It
passes when yes is its percentage of them, at the end of its time, or sooner
once the players still to vote could not change it. A kick still needs two
yes. The card's bar is yes in green from the left and no in red from the
right, with a mark where yes has to reach. No protocol change.
Map versions: a map installed at another version than the host's is no longer
loaded as it is. The download card offers the host's version (MAP VERSION,
UPDATE), which replaces the installed one and is applied while the game runs.
For that the live merge keeps each mod's fingerprint with where its archives
were placed, and places a mod again when its files have changed since: before,
a mod replaced while the game ran was read where the old one lay. When the
host's version is gone from Thunderstore, or the old one's files are in use,
the player is told and leaves the session.
Dedicated server: "maps reload" reads the Mods folder again, so a map copied
in while the server runs can be switched to at once. The server window reads a
copy of the map list.
Map names: a custom map's level is dingolevel_reskate_<map>, and "reskate" was
left in its name ("/map reskate badlands"). The map's own name is shown and
typed now; the old spelling still finds it.
as a level unloads, the game takes each mesh variation out of its registry by the
entry's mesh and reads it unchecked, and this map's meshes unload before
their databases. A guard on that one function has the read of a mesh that is
gone go on with the next entry, and logs how many were passed over.
Settings tab: a Chat Word Filter card with "Use Global Word Filter"
(use_global_word_filter), buttons that open data/words_filtered.txt and
data/words_blocked.txt, and one that has a running server read them again.
Moderation tab:
- ROLES: create, edit and delete roles (tag, tag colour, rank, default role,
inheriting from it, permissions, commands). Saving checks the roles, writes
data/roles.json and has a running server reload it. A role's name is fixed
once made, since players are given roles by name, and a save keeps who has
which role as the server has it then.
- PLAYER ROLES (was SERVER ADMINISTRATORS): giving a role picks any of the
server's roles, not only admin.
The two word files are now made empty when they are missing, without the
explanation that was written into them.
Also: the README's Chat words section sat inside the Roles section, and the
server parks test did not link without server_words.cpp.
The chat filter matched short listed words anywhere inside a longer one, so
entries like "gger", "spik", "crip" and "coon" in the backend's lists blocked
"bigger", "spike", "script" and "raccoon".
- A listed word of four or five letters now matches inside a longer word only
when the rest of it is what goes around a swear word ("motherfuckers",
"bullshit", "shithead", "shitty", "xxfuckxx"). Words of six letters or more
still match anywhere.
- An ordinary word that equals a listed phrase without its spaces
("the rapist" / "therapist") no longer matches.
- bad_words.txt is gone: the filter has no words until the backend's lists
arrive.
Dedicated servers:
- "use_global_word_filter" (default true) turns the backend's lists off for a
server. Console: words global on|off.
- data/words_filtered.txt and data/words_blocked.txt hold a server's own
words, read at start and by "words reload". Blocked words follow
word_warnings. Filtered words are sent to players (new packet, kind 33),
whose games star them for players with the chat filter on and drop them on
leaving.
Multiplayer outfits: a player whose mods add or remove skater or board slots
no longer makes everyone look like the local player. A peer's items are put
in this game's slots by slot hash; slots the peer lacks stay empty and items
in slots this game lacks are left out.
- park-mod is a command a role can list: the admin role's "*" gives it, the
default moderator role does not (spawning parks and changing the saved
setup are the owner's call); command() checks the caller's role as for
every other command, and custom votes still run it as the console
- The client's "/" menu shows the park-mod rows by role like the other
server commands there, by the row's first word (resolved in the rebased
client commit)
- README-roles.txt lists park-mod; README.txt says who may use it
- Tests: the admin role has park-mod and the moderator and player roles do
not; in game a moderator is refused and a role listing park-mod is not
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2G4APEkhHgVs59RDL3XWD
- park-mod reload <name> [<name>...] reads a spawned park's file again and
swaps its objects in place, keeping its timer; all or none, within
park_limits, and players are told the park was updated
- park-mod config add <name> | <mod folder> | <map> [| <key>] and
park-mod config remove <name> change maps.park_mods without a restart,
validated like the startup config and saved like the map pool; removing a
spawned park's entry removes the park too
- The client's admin "/" menu lists reload, config add and config remove
- README.txt and help cover the new commands; server_park_mods tests reload
(in place, timer kept, refused when not spawned or broken) and config
add/remove (saved, refusals, by an admin)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2G4APEkhHgVs59RDL3XWD
- A missing mod folder reads "The mod folder "x" is not in the Mods folder."
instead of printing where the server is installed; the reason can reach chat
through an admin's command or a vote
- server_park_mods checks the exact message
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2G4APEkhHgVs59RDL3XWD
- server_config: park_mods and park_limits are new settings with defaults,
round-trip (folders with spaces), and refuse bad names, folders, maps, keys
and limits; a missing folder is not a config error
- server_park_mods: reads the fixture parks (a shortened copy of a player's
street park, objects to skip, a missing key, broken files) and drives the host
over a fake transport: a player's game takes the park from the server's own
SteamID through routed_source and ObjectState, several parks at once, the
limits (all or none), expiry with its warning, admin and vote spawns, late
joiners, clear, and removal on a map change
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2G4APEkhHgVs59RDL3XWD
eleport <players> <player|x y z>, from the console or for a role with
the command: one player (a name's start or a SteamID64) or up to 64
SteamID64s joined by commas, to a player or a place. Several stand in
tpall's ring round the target; one sent to a place lands on it. Those
not connected or not in the world yet are skipped and named, and each
use is logged. tpall and tphere share the ring code. The name is
reserved, and it is in help, the chat list, README.txt and
README-roles.txt.
The server window merged in #170 did not compile on main. Its admin
card listed the admins a config no longer has: it lists the players
given a role, and gives or takes the admin role with the role command.
The "autostart" setting it reads is added to the config (off unless
set). A second read of Steam's debug lines is removed: they already
come with the direct notes.
Co-Authored-By: Robin Möller <moelrobi@gmail.com>
Dedicated servers: roles, with their tags, commands and permissions
A dedicated server had one list of admins who could do everything, and
everyone else could do nothing. It now has roles, kept in
data/roles.json beside the bans: each a name, the tag its players wear
in the game, the server commands it may use, what it may do besides
commands, and a rank.
A server starts with five: player (everyone given no role: chat, votes,
parties, private messages), vip (a reserved slot), moderator (kick, ban,
messages, giving out lower roles), admin (everything) and muted. The
default role is whoever was given none; a role has what the default has
as well as its own unless it says it does not inherit. A reserved slot
is a permission, so a role's players have one without being listed.
Nobody kicks, bans or changes the role of a player whose rank is theirs
or higher, or gives out a role that is; the console is above every role.
"*" is every command but role and reserved, which a role has only when
it lists them.
New commands: role <player or SteamID64> <role>|none, roles, and roles
reload. "admin add|remove" and "admins" are gone: the admins an older
config lists are moved into the admin role the first time the server
starts, and leave the config.
In the game, a player's role tag and colour show in chat and on their
nametag, and each control in the Multiplayer menu, the map, time of
day, world layers and parks follows the command it needs rather than
one admin flag, as do the commands chat offers.
Protocol 48: the roster carries each player's tag, and a new
permissions packet tells a player what their role gives them. Older
games and servers do not connect to newer ones.
Also fixes a name clash that kept the server config tests from
compiling.
A hacked game was resizing its skateboard and reporting it at another
player's position, so everyone saw an oversized board riding on them,
and with player collision on it knocked them about.
A skateboard is now held to within 10% of its size, apart from the limit
on resized body parts: on a dedicated server when its scale limit is on,
and in every player's game whatever sent the pose. Another player's
board is solid only within 3 m of its own skater; one further off is
seen and not felt.
Dedicated servers also check for a board kept on another player
(Server/board_check.h): one away from its own skater that stays beside
one other player while that player travels 8 m over at least 3 seconds.
The board is then no longer passed on to the others until it has behaved
for half a minute, and the log and the admins in the game are told who.
New setting anti_cheat.pinned_board and command board-check:
off | warn (default) | kick.
The bad-word list now comes from the ReSkate backend's Words page, in
the same /api/v1/steam-ids answer the game and servers already read.
It has two lists: filtered words, and words not allowed in chat at
all. Until the backend has any words the built-in list stays in use;
once it does, its lists replace the built-in one.
Filtered words work as before: masked for players with the chat filter
on, and not allowed in server or lobby names. Words not allowed at all
are filtered the same way, and a chat message with one is not passed
on. Its player is warned each time, and after three warnings the next
one gets them kicked.
Dedicated servers: new players setting word_warnings (default 3, 1 to
10, 0 switches the rule off). The count lasts while the server runs,
so a kicked player who joins again is kicked at their next one. Party
chat and commands are checked too, and each warning and kick is logged
under [words]. The lists are read whenever global bans or word
warnings are on.
Player lobbies: the host's game applies the same rule to its guests,
with three warnings.
- announce-to <player> <text>: the announcement card goes to that player only.
- Announcements are no longer posted in chat, only shown as the card; the
now pointless "card" setting is gone (old files' value is ignored).
- ReSkateServer.json "commands": the owner's own /commands, each with a reply
and/or server commands run as the console ({player}, {map}, {arg}), an
optional admin-only flag, and never listed in /help.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NYHTXYAUyibUbfztfcriYB
A hacked client could move its skater's bones anywhere and stretch the
skater across the map for everyone: the server limited a bone's scale
but not its place.
Server: new anti_cheat setting bone_reach_limit, default 1 metre (0.5
to 20, 0 for off; console bone-reach <0.5-20>|off). A body or board
bone further than that from its parent is pulled back before the pose
is passed on. On the game's rig these bones never move (the longest, a
thigh, is 0.44 m). The fourteen skater bones that do move by metres
(they follow the board and the body in a fall) are held to 100 m, and
the board's rig to 5 m from the board.
Client: every pose a game is shown is held to 2 m reach and 8x bone
scale, whoever sent it, which covers servers that have not updated and
sessions without a dedicated server. A client used to accept up to
100x scale.
The limits come from two recorded play sessions; the exempt bones are
picked by index for the current build's 395-bone rig. The pose codec
bench now prints each recording's bone reach, and reads recordings made
by earlier versions.
Builds on the 2.0.0 vote card. Protocol 46: a server's vote can now be a
poll, and the roster carries who may start polls, the owner's own votes
and the current announcement.
Custom votes (votes.custom): the server owner defines /vote <name> [choice]
in ReSkateServer.json. When one passes, the server runs its command as the
console; {map} is the current map and {arg} the player's choice, which must
be one of the vote's "choices", so players never put free text into a
command. They show in /help, /vote list and the "/" menu.
Polls: /poll <question> | <answer> | ... with 2 to 6 answers. A poll runs
nothing. Players answer by clicking a row on the vote card or with /1, /2...;
the card shows each answer's count, then the result for a few seconds.
votes.polls says who may start one (off, admins, everyone), poll_seconds how
long it runs, and /poll end ends it early (its starter or an admin).
More vote settings: each vote may have its own seconds, cooldown_seconds and
min_players; starter_votes_yes says whether starting a vote counts as a yes.
The "votes" admin command sets all of them.
Announcements: "announce <text>" posts a chat line and shows a card at the
top of every player's screen. The new "announcements" section holds messages
the server posts in turn every interval_minutes while players are on;
"card": false keeps them to chat. Managed with the "announcements" command.
Older configs gain the new settings with their defaults on first start.
Server/README.txt documents them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WbSRfNf5T9wX4XMMQ5kwia
A full server (about 120 players) slowed down at random until everyone lagged:
one pass a second, each taking over six seconds, 115% busy, and whole poses
going out by the thousand.
Why it stuck. After any stall the next pass had everything that arrived
meanwhile to pass on, to every player, so it took longer still. By then the
poses each update would have built on had gone stale, so poses went out whole,
ten times the size and more work again. Each pass carried the whole of the
last one's arrivals and the server never caught up.
Shedding. A pass normally reads one or two poses from each player. When more
than three of one player's are waiting, only the newest is passed on. Contact
effects are held to two packets a player a pass. Each pass then has a bounded
amount to do whatever the backlog, so the server catches up; players see the
others at a lower rate for a moment. The log says when it happens (at most
every 30 s) and `net` has a "shed while behind" count.
Threads. Building each player's update of everyone else was 80% of a pass. It
reads the other players and writes only that player's own state, so it is now
done for several players at once (worker_pool.h); the server's own counts and
the marks on whole poses are added up afterwards, and sends into the transport
are made one at a time. New setting network.threads: 0 (default) is one thread
for each processor but one, up to 8; 1 is the single thread as before. The
server says how many it uses when it starts.
Load test. Server/Test/server_load_tests.cpp drives the host with simulated
players over a transport that is not Steam's (built with the multiplayer tests
on Windows, not run by ctest). With 120 players at 20 TPS and 27 MB/s out, the
loop's share of one core went from about 50% on one thread to 26% on four and
22% on eight, and every player was sent byte for byte the same on 1, 2, 4 and
8 threads. A simulated 3 s stall shed about 7,000 poses, took one 169 ms pass
to recover and lost nobody.
The test's transport has none of Steam's send cost, so a real server's loop
sits higher than these figures. Receiving, choosing who is sent which pose and
the sends themselves still run on one thread.
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.
Server: remove players who have been away too long
A dedicated server can remove players who are away from their game.
- "afk_kick_minutes" in the config's "players" section: 1 to 1440, or 0
(the default) for never. "afk-kick <minutes>|off" from the console or an
admin; a change is saved and applies at once.
- Away is doing nothing a player at their game does: not moving or turning,
not speaking, not sending chat, and not placing, moving or removing an
object.
- A minute before the limit the player is told in chat; at the limit they
are removed with a message saying why and that they can join again. The
log has an "[afk]" line.
- Admins are never removed for it. The time starts once a player's game has
loaded the map, and starts over after a map change and when the setting is
changed.
Server: object scaling can be turned off
A dedicated server can stop players resizing the objects they place.
- "allow_object_scaling" in the config's "players" section (default true),
and "object-scaling on|off" from the console or an admin. A change is
saved and applies at once.
- Off: the server shares every player's objects at their own size, whatever
that player's game sent. Admins can still resize theirs, as with the
object limit.
- The roster tells each game. In the park editor of a player on such a
server the SCALE mode is not offered, its Y shortcut does nothing, the
scale box is disabled with a note saying why, and what is placed or edited
is its own size.
- An object resized before the setting was turned off is its own size for
everyone else straight away; its owner sees it resized until they edit it.
Server: its chat lines in colours the owner chooses; protocol 45
A dedicated server's own lines in chat (its welcome, console messages and
answers) were made violet in #129. The owner can now pick the colours.
- "chat_color": the "Server" badge and name, as "#RRGGBB" (default
"#8E5CFF", violet).
- "chat_text_color": the text of those lines (default "#D9C8FF", lavender).
- Both are in the config's "server" section; a value that is not a colour
stops the server from starting and names the setting.
- Console: chat-color <#badge> [<#text>]. Saved, and sent to everyone on
the server, so the next line uses it.
- With the defaults, chat looks as it did.
The colours travel in the roster, so this is protocol 45: servers and
games must both update. Lobbies hosted by a player are unchanged.
Fix players freezing on dedicated servers; reserved slots are extra slots; bone scale limit 2
Players stopping and jumping (1.1.6):
- The new sound messages begin with the same four bytes ("RMS1") as the
pose updates games send each other over their direct links. The game
took those pose updates for sound and discarded them, so a directly
linked player moved only when a whole state arrived.
- Game: only messages from the dedicated server are read as its sound,
pose or acknowledgement messages; anything from another player is
decoded as what it is.
- Server: poses and sound go through the server to every player, whether
or not two games have linked to each other. This fixes games still on
1.1.6 without an update, at the cost of more outbound on small servers.
Reserved slots:
- "reserved_players_slots" no longer holds slots back. Anyone joins until
max_players are on; the listed players and the admins can still join
after that, each adding one slot beyond the limit (33/32 and so on).
- An admin who is also listed counts once, and the total never exceeds
what a session can hold.
- The list holds up to 1024 players again. "reserved" in the console shows
how many extra slots there are.
Server:
- "bone_scale_limit" is 2 by default. The game's own skater height is a
bone scale, so 1 showed everyone at one height. A config that already
has 1 written in it keeps it until changed.
Server config:
- Written in sections, in reading order: server, access, maps, players,
anti_cheat, network, votes. A config from an older version, flat and
under older names, is read as it is and written back this way on first
start; only settings that are really new are reported as new.
- Renamed: no_bail, noclip, boosts, parties, voice_chat to allow_*;
global_bans to use_global_bans; welcome to welcome_message; speed_check
to speed_hack; score_check to modified_scoring; score_allow to
allowed_scoring_mods; map_pool and map_rotation_minutes to maps.pool and
maps.rotation_minutes. Console and admin commands are unchanged.
- Bans are kept in data/bans.json beside the config. A config that holds
them has them moved there; a bans file that cannot be read stops the
server from starting.
- Reserved slots are one list, "reserved_players_slots": a slot is kept
for each listed player who is not on. "reserved_slots" and the
"reserved slots <n>" command are gone; admins may take a kept slot but
none is kept for them.
- "tps" is no longer in the file: dedicated servers run at 20.
- "bone_scale_limit" is 1 by default: resized body parts are off unless a
server allows them.
- New: "pack_ms" (10), how long Steam may hold a message to pack it with
others; "finger_distance" (25); "steam_debug", which logs what Steam's
networking says, for connection faults.
Poses and skater sound between a dedicated server and its players use new
formats. Lobbies and the links between games are unchanged.
Poses (pose_codec.h, pose_batch.h):
- A pose is rounded once (rotations to 0.08 degrees, positions to a
millimetre) and sent as its differences from a pose of the same player
the receiver has acknowledged. Several players' poses share one message.
- The receiver acknowledges messages once a tick; the sender builds only on
acknowledged poses, or on the whole pose it sent reliably. Loss costs the
poses in that packet and nothing more.
- Fingers are not sent to players further than "finger_distance" (25 m).
- Games upload their own poses the same way, and the server acknowledges.
- About 300 B a pose at full rate against about 660 B before.
- The server no longer rounds rotations by distance: it saved little.
Sound (sound_codec.h):
- Each sample is sent as what changed since the one before, numbers rounded
to 1/1024. One reliable message a tick carries every skater a player can
hear. A stream is sent whole at its start and every two seconds.
- About 106 B a message against 368 B before, on a recording of real play.
Direct connections:
- "connection": "relay" (default) or "direct", with "direct_address" and
"direct_port". A direct server listens on UDP and is listed with its
address; players still sign in through Steam.
- Games connect straight to servers that offer it and fall back to the
relays after 5 s. Setting and "mp direct-connections" to turn it off.
Server:
- "bone_scale_limit" is 1 by default: resized body parts are off unless a
server allows them.
- Logs Steam's relay network status at start-up and in "net", and watches
the Steam sign-in.
- "pack_ms" (10): how long Steam may hold a message to pack it with others.
- "steam_debug": logs what Steam's networking says, for connection faults.
- "relay_everything": sends poses and sound through the server even between
games that have linked, for testing with few players.
- "net" shows pose sizes by rate, the share sent without fingers, and whole
poses sent.
Game:
- Only ReSkate's nametags are shown; the game's own are removed.
- "mp pose-dump <seconds>" records the local skater's poses and sound.
Tests: pose_codec_tests covers exact round trips, chains of differences,
held fingers, damaged input, the sender and receiver over a link that
delays, loses and reorders, and the sound format; with a recording as its
argument it prints sizes on real play.
Protocol 44: servers and games must both update.