path::string() uses the ANSI code page and throws on a character it lacks (a Cyrillic folder on
an English Windows). The remaining places it was used now go through path_utf8:
- Mods: a deck folder or a mod folder named outside the code page no longer leaves the Board Break
decks empty, or the Mods folder ignored altogether.
- Game and mod archives: a file under Win32 with such a name is passed over instead of stopping
the read.
- pose-dump no longer ends the multiplayer session when the game folder is not ASCII.
- The item browser's status line and a Board Break warning no longer fail on such a path.
- Launcher: system fonts are named to ImGui in UTF-8, which it reads.
In a level the pilot's drone is played with the other players' drones through the
game's positioned voice sound, just under its camera, so it gets the game's mixing,
reverb and obstruction. The Windows output only plays it where the game's sound is
not there to take it.
Each remote drone's motor is synthesised and played through the game's positioned voice sound,
the one proximity chat uses, so it is placed in the world and muffled behind walls. The throttle
is worked out from how the drone accelerates, so the drone packet and protocol stay the same.
Full volume only within 1.5 m, heard out to 120 m; the DRONE page's sound volume sets the level.
A model is spawned at each other player's flying drone and moved every frame. It is the drone's
own model (FPV Drone by PinaPipe, CC BY 4.0, credited on the DRONE page), built by ReSkate Studio
into a pack that ReSkate.dll carries and unpacks into Mods/.reskate-builtin before the merge.
Without the pack the game's traffic cone stands in. dronemodeltest spawns one in front of you.
A whole pose from a dedicated server opened a stream under any of its
65,536 numbers, and each stream kept its last 32 poses. Streams were only
dropped once their player had left, so a sender that opened one under
every number for a player who was there (itself) made the receiver keep
millions of poses. Sound streams were open the same way.
- pose_batch::open_stream: a player's new stream ends their old one, and
no more than 512 streams are held (twice the player limit); both the
pose and sound receivers open streams only through it.
- A stream is opened only for a player in the session. A refused whole
pose is not acked, so a real server sends it again; sound is sent whole
every couple of seconds.
- Tests: the reported flood leaves one stream for one player and 512 for
65,535; the same for sound, and only admitted players open a stream.
- mesh_tests: HookBatch stubbed, which the session uses since the freeze
fixes; the test had stopped linking.
Each bone's scale was held to the limit (a server's bone-scale, 2x by
default; 8x on every client), but a bone shows at its own size times its
parents' and the whole skater's. A modified client resized each bone of
a chain to the limit: four neck bones at 2x showed a neck 16 times as
long, within every per-bone check.
- skater_bone_parents.h: the parent of each of the skater rig's 386 bones,
read from the game's data (animbase_default_renderskeleton), which the
pose's skater bones follow.
- limit_bone_scale then holds each bone to what its parents leave of the
limit: the size it shows stays within 1/limit to limit. A single resized
bone (a bigger head) is unchanged; bones past the rig's have no parent
and keep the per-bone limit.
- Servers and clients already hold every pose to their limit with it, so
the combined limit follows each server's bone-scale setting, and every
client's 8x where a server allows more.
- Tests: a neck chain at 2x each shows at 2x; a resized skater's bones are
held to what its own size leaves them.
Players reported the game hanging for about a second while skating. A
profile showed the memory line in the log walking every heap
(HeapSummary) on the client thread, 0.5 to 2 s every two minutes. An
audit of everything that runs on the game's update and render threads
found more of the same, and Sentry's top crashes had causes of ours.
Freezes:
- Memory log line: no heap walk or thread snapshot; the GPU part and the
write happen on a thread of their own.
- Profile database: synchronous=NORMAL in WAL mode. A save is still kept
when the game crashes; every play event no longer waits for the disk
inside the game's hooks.
- ReSkate's own preferences and values (menu clicks, session sliders,
Discord) are saved by a background writer in order; reads see a value
until it is saved.
- Input: the foreground window and the keyboard are sampled on a thread
of their own (GetAncestor and GetAsyncKeyState stalled the client tick
40-50 ms when another program held Windows' lock); flight input uses
the sample; ClipCursor at most twice a second while a menu has the
pointer.
- Hooks installed while the game runs go in one transaction per place
(HookBatch): throwdowns, remote players, the party, Hall of Meat.
- Off the update thread: the host's map package lookup (one table read in
the background, not the Mods folder per offer), the trainer's map file
and settings save, Board Break's settings save, mod music playlists and
their covers, the Spotify station covers.
- Off the render thread: the Modding page's and the deck card's Mods
folder reads. The overlay's three startup waits share five seconds.
- Less work per frame: Steam names reused while one a frame is asked
again, 2 friends a tick, steam_api64.dll checked once per process,
cosmetic names read in place rather than a system call per character,
an outfit load no longer copies the inventory per slot, the throwdown
type scan runs at background priority over resident memory, the Board
Break deck label is kept a minute.
Crashes:
- Render-resource pool: the metadata fields are written to the state
owner only. The other renderer is 0x2daa00 bytes, and +0x11aae28 is far
past its end (store_field crash; heap corruption in every release).
- Other players' skateboard effects: a stale skateboard component is no
longer followed, and the joint lookup is walked with a bound first (an
endless loop in reused memory ended in GameStall).
- Launcher: Oodle decodes into the buffer it asks for with corrupt data
possible, and its assertions no longer stop the process (heap corruption
in the store-copy scan; a breakpoint in oo2core).
Timings (Send performance timings):
- A frame of half a second or more in a level is sent as game.freeze:
where the time went (ReSkate's tick, the game's frame, or outside it),
whether the renderer kept presenting, and ReSkate's busiest zones.
- Closing the game sends the level session it was in.
Music after a connection drop:
- Spotify device: an account Spotify could not be reached with is kept and
tried again (3 s doubling to a minute); a session that drops is taken
up again. Only an account Spotify refuses is asked for anew.
- YouTube playlist pictures that could not be fetched are tried again,
from 30 s to 10 minutes apart.
Follow-up to #225. The session still waits 30 minutes in all for a map
(checking, the two-minute question, download, install, apply, join), and
servers and lobbies still let a player go at 30 minutes. But a download
that has begun is no longer cut off at 27 minutes: a large map on a slow
connection never came at all that way.
- Once the download has begun, the session's deadline is the session's
alone. The download goes on (up to its own four hours, as before #225),
installs and is applied, and the player is told to join again; no
rejoin is tried past the deadline, where the server would turn them
away. "Map still downloading" says so when the session stops waiting.
- Before the download, the deadline still ends the check and the question.
Declining or cancelling still stops it and removes the partial file.
- A download whose session has gone is not cancelled by joining another
session; a new map need is turned away until it is done.
- The 27-minute transfer budget and its completion reserve are gone from
map_fetch_policy.h, with their tests; the map download tests cover the
new behaviour.
- The HTTPS timeout test compiles with warnings as errors (a signed and
unsigned comparison), and get_text's stale-file removal is indented again.
- README: map downloads described as they now are.
Boards wear: hard landings, hits onto rails and ledges, grinding and
slams wear the board of the deck in the game, kept per deck. From a fifth
worn it steers lazier and its trucks go soft (the game's own physics
tuning, outside multiplayer sessions only); a worn board breaks from less
and, worn out, can give way under the rider. A board that broke is a new
one once it is whole again, and the game's own board wear is cleared with
it.
A boardslide, lipslide or darkslide that comes down too hard onto a rail
can snap the board, and the rider goes down with it: No Bail can now be
asked for a wipeout, which goes in as a cause before the selector picks.
Sounds play through XAudio2: a crack by the size of the break, metal when
the board broke on metal (the material comes from the game's contact
effects; which are metal is marked in play or listed by a pack), and the
body's hits in a bail by how hard they land.
The settings are a card on the overlay's Skater page, two rows in Mod
Options and console verbs, kept in BoardBreak.settings. A deck may name
itself and set its wheels wider apart in a deck.ini; Old School and Pig
join the decks the feature knows.
mp map-markers, mp puppet-savings, throwdown-hud and throwdown-offboard take an optional
argument and, without one, say what they are set to. Each read values[0] unchecked, and an
optional argument that is not given adds no value, so typed by itself each indexed an empty
vector. They check now, as mp collision and challenge-invite beside them do.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2.0.8's dedicated server was released with a world-layers.json scanned from
a game Steam had updated: 263 layers, where every player's game has 261. A
server that shares its layers (world_layer_sync) sends one choice for each,
the player's game turned the message down as unreadable, and nobody could
join such a server ("Join code, sender identity, or multiplayer protocol
did not match", and "Disconnected." in the server's log).
- supported_build::world_layer_count (261): how many layers the supported
build's levels have.
- Server: a world-layers.json of another count is not used; the log says
why and layer sync stays off, where before every player was turned away.
- --export-world-layers refuses a game folder whose count differs, so a
release cannot be built from an updated game again.
- Player's game: a layer list of another length is left unread and every
layer stays as the player has it; the rest of the message is taken and
they join.
Music screen: large playlists no longer empty it, and two logs say why things fail
Music screen
- The screen's list was turned down whole, and came up empty (the game's own
Featured row too), once it held more than 1,024 songs: a few large YouTube
Music, Spotify or local playlists went over that. The most is now 2,000
songs and 16,384 places in playlists (music_ui_most_songs,
music_ui_most_places); 2,000 because the screen's own table of songs is
read only up to 2,048.
- The rows of the player's playlists are counted as they are registered and
held under that: with no room left, the rest of a playlist is left out of
its list (it still plays) and the screen is never empty for it.
- A row whose "artist - title" is longer than the screen takes is left out;
one such row turned the whole list down as well.
- Said in the log: how many songs were left unlisted, and, should the list
still be turned down, what it held.
Logs
- Spotify device: librespot's own warnings and errors are written at the
warning level again; only the rest of what it says stays at debug. A
player whose Spotify connects and stays silent had nothing to send.
- Multiplayer: a message turned down with "Join code, sender identity, or
multiplayer protocol did not match" now says which of the three it was,
its size and first bytes, and whether it came from a dedicated server.
An FPV drone flown from the free camera: real quad physics (thrust,
gravity, drag, Betaflight "Actual" rates), collisions with the world, and
a goggle-style display. Open it with `dronemenu` (or the drone window's
Start flying).
Flying
- Keyboard and mouse, gamepads, or USB RC radios (EdgeTX etc. in Joystick
mode). "Set up controller" learns each stick and switch by asking for
one at a time, reversed axes and springing gamepad throttles included.
While a controller flies the drone, the game is kept off it, as
freecam's controller mode does.
- Presets (Tinywhoop, Toothpick, 5 inch), arm/disarm, back to takeoff,
and every setting live through `dronetune`, saved in the profile.
- Optional range: the picture breaks up and is lost with distance from
the takeoff point; past it, failsafe cuts the motors.
- OSD, on-screen sticks, an analogue video look, synthesised motor sound.
- Goggles: the pilot's skater wears eyewear from the game's catalog while
flying, and gets their own back on landing (Road Rash aware).
- Start flying hides the game UI, Stop flying brings it back.
Multiplayer
- New packet `drone` (36): where a player's drone is and which way it
faces, 15 times a second, unreliable, relayed like effects. Markers
glide between updates. Protocol version 52.
- FLYING DRONE above the name of a player who is flying.
- Players choose whether to share their drone and whether to see others'.
- Dedicated servers: `sync_drones` (players section) and `drones on|off`.
Off: no drone packets are relayed, and players are told in the roster.
Changes to existing code
- `flyspeed` takes any speed from 0.6 to 1500, not only the seven listed
(the drone's presets and top speed slider use speeds of their own).
- Overlay: begin/end_input_access, so the drone's controller read sees
the device while the game is kept off it.
- Profile: local_surface_hit, the park editor's ray from one point to
another, for the drone's collisions.
Tests: Skater/Test/drone_radio_tests (controller setup), and the drone
packet and roster rule in Multiplayer/Test/protocol_tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MArBL6UbYZPsp9rMnq2Ks5
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.
Objects other players place are spawned locally, so the game took every one
for the local player's own: all were solid, and their card offered the
owner's buttons and named the wrong player.
- Other players' objects are ghosts the skater passes through, as in the
game's own sessions. Each player's row in the session menu has an
"Objects: solid / ghost" switch, remembered by Steam ID; a dedicated
server's own objects (its parks) are solid unless switched off.
`mp objects-solid <SteamID64|server> on|off` does the same.
- The card's own Disable and Enable on one object stand over that player's
setting until the setting is changed.
- The card on an object someone else placed no longer offers Edit or
Delete: the game's "is this mine" and "may this be edited" checks answer
for who really placed it. It offers Disable instead.
- The card names the owner of the object it is for, read from the card's
own script, instead of the owner of the object nearest the skater.
- A dedicated server has a player card of its own, "Server", shown on what
it placed.
Server: the Mods folder is kept when the configuration is reloaded. A park
mod was reported as not being in the Mods folder after a reload.
A line that starts with "/" is a command, a whisper or party chat, so typing
one no longer shows dots to the whole server. Dots already showing are cleared
when the line becomes one.
A "stopped typing" notice was relayed every time it arrived, while "typing"
was held to one per 200 ms, so a modified client could flood everyone through
the host. A lobby's host and a dedicated server now pass a stop on only when
that player's dots are up, which is at most once per accepted "typing".
Crash dumps now go to the project's own Sentry server. The launcher's
out-of-process helper is unchanged; only where it posts and what it expects
back are:
- The default endpoint is the Sentry project's minidump URL. Report
attributes travel as Sentry tags, with the release as reskate@<version>.
- An upload counts as accepted when the server answers with an event ID.
- "backtrace" is "crash_report" throughout: the namespace, files, CMake
targets, DINGOSDK_CRASH_REPORT_URL, DINGOSDK_BUILD_CRASH_REPORT_TESTS and
the RESKATE_CRASH_REPORT_URL override.
Performance timings are new, and off unless "Send performance timings" is
ticked in the launcher's settings (RESKATE_TRACING=1). A small tracer
(Engine/Core/Debug/trace.h) posts each finished step to Sentry as a
transaction from a background thread; the launcher and the game it starts
share one trace. Timed: the update check, the Steam download, Thunderstore
installs, the mod merge, starting the game, the launch up to the first
playable level, each level load, joining a session, fetching the identity
lists, and the time spent in each level with its average frame rate and
long frames. Turning crash reports off turns timings off too.
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.
A 30 s profile on a 77-player server had four frames of 20 to 30 ms against a
usual 7.8 ms, from two things:
- The Park Editor read every mod's manifest every five seconds to list park
mods, on the game's thread and whether or not its list was showing (the
switch for that is never set): about 11 ms each time. The look every few
seconds now runs on a thread of its own and is taken up when it is done;
the one after making, renaming or removing a park is still at once.
- Connecting to a player asked Steam for their name, and waited on the Steam
client for 7 to 10 ms each time a player came into reach. The link takes
the name already read, if any: it only labels the link in diagnostics.
Profiler: a release ReSkate.dll has no symbols, and its functions were all
named after the nearest export. The check for that looked at the symbol's
size, but DbgHelp gives an export a size up to the next one. It now asks
whether the module's own symbols were loaded, and writes ReSkate.dll+0x<offset>
when they were not, to be named from the release's symbols afterwards
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.
A slam can break the local skater's board at the nose, in the middle or at
the tail. Half the time the grip tape holds the pieces together and the board
folds or an end hangs; otherwise a piece comes off, flies and lies where it
lands. The board is whole again in hand, on riding and on a respawn.
The pieces are four stretches of the deck in a content mod's copy of the
game's one deck model, each on a spare bone of the board's rig. The boards'
render pose hook only moves bones, because the replay records where a bone is
and not its size: a broken board's deck goes away and its stretches go to
where the pieces lie, and a whole board's stretches go away.
The deck the player chose loads first and other mods that bring a deck model
are set aside for the run, without touching mods.json. Another deck is chosen
while the game runs, through a live apply of the mods.
contrib/board-break.md says how it works and how a deck pack is made.
- 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 dedicated server's admins see /park-mod list, add, remove and clear with
their usage in the chat's "/" menu, beside the server's other admin commands
- No Tab completion for the park names: the roster does not carry them; the
park/unpark votes already show with their choices as custom votes
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.
/tp refused anyone without a skater built here ("is not in the world
right now"). Since skaters are only built within the player distance
(120 m by default), that left only players already nearby: a player far
from everyone could teleport to nobody. The Social menu's teleport failed
for the same players, as the game was only told where a player was while
their skater was built.
- /tp goes to where the player was last heard to be, skater or not. Poses
keep arriving for far players (their name or dot is placed by them), so
distance no longer matters. Only a player no pose has placed yet is
refused.
- The game's party position is kept up to date for far players too, for
the Social menu's teleport.
No protocol or server change. /tp is not tested in game; the Social menu
path follows from the code and is not tested either. Spectate from the
Social menu may now accept far players and show the place without a
skater.
Memory
- The entity and pending-update pages are four times the game's own size
(524,288 slots), down from eight times. At eight, the game kept a
3.5 GB block for them that was fully in RAM on custom maps; the largest
map load seen needs about 150,000 slots. The render-instance pool and
spatial buckets stay at eight times.
- The log has a "Memory:" line when the game's memory, the players in the
session or the skaters built change: committed, in RAM, players, skaters
built, player distance and map.
- Console: `perf memory` lists the game's memory by block size.
`perf memory mark` remembers it, and the next `perf memory` says what
was added since. `perf memory trace` also records who asked for each
block until `perf memory stop`. `perf memory big` lists the largest
blocks with how much of each is in RAM and a sample of what is in them.
RESKATE_MEMORY_TRACE=<megabytes|all> starts the trace with the game.
Replay crash guard
- The game's replay recorder crashes when its stream has no room for the
next chunk: it writes to the chunk it was not given (Skate.exe+0x48efa34,
crash reports of 2026-10-10). The function now runs under a handler
that, for that one fault alone, returns as if no chunk was ended, and
says so in the log. Any other fault still goes to the crash reporter.
The skip path has not been triggered in testing.
Launcher
- `-map <name>` (also --map, -map=<name>) loads that map as soon as the
game is up, named as the console's `load` names one. With --no-gui and
no other options the launch still uses the saved launcher settings.
Pause menu
- The Multiplayer and Mod Options pages step aside while the game's side
menu, or a screen opened from it such as Settings, is over them. They
drew over both before.
- A button still held when a page comes back is not a press on it: the
Back that closed the side menu no longer dismissed the page as well.
Multiplayer cosmetics
- A player whose mods add or remove cosmetic slots (stickers, say) no
longer has the whole outfit refused. Their items go to this game's slots
by slot: a slot they lack stays empty, an item in a slot this game
lacks is left out, and the log says how many of each.
Map downloader
- A host or dedicated server now tells joiners which Thunderstore package
its map comes from (the Mods folder name plus manifest.json).
- A player without the map gets a card: icon, name, author, version and
description, with DOWNLOAD and CANCEL. It works with mouse, keyboard and
controller.
- Only packages listed as Maps in the ReSkate community are offered, and
never deprecated, rejected or NSFW ones.
- On yes the package is downloaded, installed into Mods and applied as a
live mod, then the session loads the map. A small card on the right
shows the progress, so the player can keep skating.
- The player stays connected while it downloads: the host gives a player
who says they are fetching the map up to 45 minutes instead of the
usual load time.
- When the server changes map with the player in it, the card says so.
- Protocol version 47: map package on map_offer and world_state, and a
"fetching the map" flag on map_request and world_ready.
Joining
- The pause-menu server browser shows a CONNECTING card over the page
while a join is under way, and a COULDN'T CONNECT card with the reason
when it does not get in.
- The session and voice tabs no longer show before the player is in.
- Leaving without the map (declined or cancelled download) no longer
counts towards the failed-join limit, on servers and player hosts.
Input
- A card that asks something holds the game's input until the buttons
are released.
- Steam's overlay hooks XInput in front of ReSkate and answers the game
itself, so a controller reached the game behind menus. ReSkate now
hooks that hook as well.
Mod merging
- Mods are read on several threads, at launch and for live merges. A live
merge runs below the game's priority.
- The paid-cosmetics check reads the game's files and the mods on several
threads.
- A live merge reuses the paid-cosmetics verdicts it already has and
reports its progress and timings.
Launcher
- NSFW mods and their categories are not shown in the mod browser.
Player list. A player on the developer, staff or homie list sees each
player's Steam ID in the overlay's PLAYERS list for the session they are
in, after "Host" / "Guest" and the distance, on their own row too. A click
copies it. The lists are the backend's, the same ones the tags come from;
every client in a session already knew the IDs, so this changes who is
shown them and nothing else.
Custom scripts. Nothing in scripts/Custom is read or run any more, whatever
the launcher's "Loose files" setting says: nothing used them, and a mod was
using the folder to load a DLL into the game (package.loadlib from its
init.lua). The loader is kept behind one switch (custom_scripts_enabled in
custom_script_loader.cpp) for the day they come back.
For that day, the loader now also takes Lua's ways of running native code
away before the first custom script runs: package.loadlib, require through
the two C searchers and package.cpath, os.execute and io.popen. If that
does not hold, no custom script runs.
The overlay's Mods page no longer has the INSTALLED / SCRIPTS tabs; it is
the installed mods. Editing the game's own Lua and config through loose
files is unchanged.
The game shows on the player's Discord profile where they are skating.
It talks to the Discord app on the same PC over its local pipe, with no
SDK; without Discord running nothing happens.
- Solo: "On <map>", "Solo skating", and how long they have been in.
- In a session: "On <map>", "Server: <name>" or "Lobby: <name>", the
player count, and how long since they joined. A session with a
password shows "Private server" or "Private lobby" and no name.
Joining: a status carries the session's join code when anyone may walk
in (no password, not full, a dedicated server or a publicly listed
lobby). Discord shows that as "Ask to Join"; the game answers yes for
the player at once, and the other player's game is handed the code and
joins. If their game is not running, Discord starts the ReSkate
launcher, which the game registers for under the player's own
discord-<application id> URL scheme.
Switches: "Discord status" in the launcher's settings (off starts the
game with --no-discord, and the game never opens Discord's pipe), and
the same in the in-game Multiplayer settings, kept in the profile.
The model gains join_code: the code of the session a player is in as
host or guest (invite was a host's only).
A dedicated server with word_warnings set to 0 passed such messages on.
It now never does: at 0 the message is dropped, its player is told it
was not sent, and nobody is warned or kicked. The team's lists are read
on every server for this, including one with use_global_bans off, which
still lets globally banned players in.
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.
Host bans: the backend's new banned_hosts list (players the team has
stopped from hosting lobbies) is read with the other identity lists.
A listed player's own game refuses to host and stops a lobby already
running. Since that is theirs to change, everyone else's game does the
rest: the lobby browser leaves their lobbies out, and the game will not
join one by code or stay in one. They can still play on servers and in
other players' lobbies. The browser now also leaves out lobbies hosted
by globally banned players.
Server log: "Direct connection asked for by ..." is written once a
minute per player, with a count of the repeats. A player waiting for a
place on a full server asks again about every second, and each ask was
a line.