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.
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.
Sources
- Local Files: add a folder of music from the setup page (MP3, M4A, AAC, Ogg,
FLAC, WAV; up to 200 songs a folder, 12 folders). Names and covers come from
the files' tags. Each folder is a tile in its own row.
- Online Radio: find stations by name (radio-browser.info) or add a stream's
address (MP3, AAC, .pls, .m3u; no HLS). Each station is a tile in its own
row; the track name follows what the station sends. Up to 24.
- Both are decoded by the library built into the DLL (SpotifyHelper), which
now plays files and HTTP streams as well as a program's output, reads tags
and embedded pictures, and accepts MPEG-2 ADTS from AAC stations.
Sound
- Five-band equaliser (60 Hz, 250 Hz, 1 kHz, 4 kHz, 12 kHz) and loudness
levelling, applied to every source on its way into the game.
- Loudness, the speakers option, the equaliser and levelling are now kept in
music-settings.json between sessions.
Setup page
- Remade in the mod's own colours and type, with numbered steps per source.
- Queue pinned to the right: now playing, the playlist's songs with covers,
find, and press to jump. Spotify's own queue shows once signed in.
- Cards for Local files and Online radio; equaliser and levelling under Sound.
In game
- MUSIC page in the menu: player and queue, sound settings, source status and
a button for the setup page.
- "Loading song..." notice in the corner for songs that take a moment, with a
switch to hide it. Presses made while a song is loading no longer restart it.
- The Spotify row is hidden when Spotify is not in use.
- Console: "spotify ..." is now "music ..." (music, music setup, music gain,
music through, music spotify client|login|logout).
Logs
- The music sources' working lines (traces, per-song and per-bind lines,
librespot's own) are debug only; stack captures are skipped unless debug
logging is on. Grind exits: the per-pop line is debug only too.
- A "Pace:" line when the game takes the sound faster or slower than real
time, to find why music sounds fast after tabbing back in.
Chat filter
- A run of digits is a number, not a word spelled with look-alikes: a race
time such as 2:55.0 is no longer masked.
LT / RT pressed during a grind pick the side the skater pops out to,
independently of the trick and the body's rotation (both come from the
left stick in skate.'s own controls). Only the sideways part of the
grind pop's launch is replaced, with the game's own sideways speed; the
trick, spin, pop height and along-rail speed are untouched.
- Input Mode: Toe / Heel (character relative, needs the stance) or
Left / Right of the direction of travel.
- Grab guard: a trigger used to pick the side is hidden from the game
until released, so it never starts a grab.
- Lean guard: while a claimed trigger is held in a grind, the left
stick's left/right reaches the game at the pop, so the game's lean
exit cannot drag the board into the rail.
- Off by default; switches in Mod Options > Player, the ReSkate skater
page, the tools menu and the console (grindexit ...). Kept between
sessions.
- Listener slots on existing hooks only: No Bail's physics step, the
trainer's set_trajectory detour, and the overlay's XInput capture.
No new detours. Build-locked to 20260929 through the trainer's byte
check.
Measured on build 25414733: 48 vanilla grind pops, 144 trigger-set
pops. See Extension/Skater/GRIND_EXITS.md.
Three outside sources can now be played through the game's own music
player. Each has its own row in the music screen, shown only when it is
set up, with tiles that behave like the game's playlists: track name,
cover, pause, next, and pressing a row to start that track.
How the sound gets in
- The game's song decoder is hooked. While one of these tiles is on, the
audio of the game song behind it (its "carrier") is replaced, block by
block, with the outside source's audio. The game's volume, fades and
pausing still apply.
- Rows are songs of our own registered with the game's music registry
(copies of a real song's asset with their own artist and title), so no
game song is borrowed or renamed to list a track.
Spotify
- The game is a Spotify Connect device ("ReSkate"), built on librespot
and linked into ReSkate.dll (SpotifyHelper/, Rust). Audio, track info
and controls come straight from Spotify. Needs Premium.
- Optional sign-in through the Web API lists the player's playlists and
Liked Songs as tiles with their tracks.
- librespot can no longer end the process (it does so on a free account
and on playback errors); only its thread is stopped.
YouTube Music
- Videos and playlists added by link. yt-dlp is fetched on request into
<Local AppData>\ReSkate\bin and checked against its published sums;
decoding is done in the DLL, so no ffmpeg.
- Private and age-restricted songs: cookies.txt or a browser's sign-in,
with QuickJS fetched as the JavaScript runtime yt-dlp then needs
(Node.js is used instead when installed).
External Audio
- Any app's sound, captured per process from Windows, with the app's own
output turned down while it plays through the game.
Setup page
- "spotify setup" opens http://127.0.0.1:8421/: device status, sign-ins,
YouTube links and tools, the external app, loudness. Loopback only;
changes are accepted from its own page only.
Also
- Speakers in the world play the tile's audio; grabbing one keeps the
player on their tile.
- Playlists added while the game runs are sent to the music screen live.
- MUSIC-SETUP.md: setup guide for players.
Building the Spotify device and the YouTube decoder needs cargo; without
it ReSkate.dll builds without them.
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>
The entity and pending-update pages are twice the game's own size
(262,144 slots), down from four times. Every page set the game keeps
is that many times its normal size whether a map uses the room or not,
and the largest map load seen needs about 150,000 slots.
A page is a power of two entries, found by a shift, so the steps are
two, four and eight times; there is nothing between. The 434 patch
lines are the same instructions as before, each with the number for a
page of 0x800 entries. The render-instance pool and the spatial
buckets stay at eight times, which still covers the capacity.
The game can stream 20480 textures at once. When a map and its mods
need one more, it takes a slot past the end of its table and waits for
ever on a byte that is part of a counter: a hang, then a crash in the
hang's own report (crash reports of 2026-10-11).
The limit is raised to 32764, the most a texture's 16-bit handle can
index, before the game makes its texture streaming manager:
- The manager is allocated longer (6.0 MB, was 2.3 MB) and its slot
table and per-slot floats moved behind its end. Each of the 226
instructions that hold one of those offsets or sizes gets another
number; no code is added. Its four fixed lists are pointed at longer
storage once the constructor has run.
- The renderer keeps a block of floats per slot for each of four
layers. Its object grows to match (27 instructions), and the two
places that index a layer's block as layer * 20480 become a multiply
by the new count, in the same twelve bytes.
- Three functions keep 20480 entries on their own stack. Two list
filters go through a longer list a stretch at a time, and the sort
is given scratch room of its own past that many. Up to 20480 they
run as the game wrote them.
Every instruction is checked byte for byte first, and nothing is
changed if any differs or the manager already exists. An empty
texture_limit_stock.txt beside the game leaves the limit as it is.
Players are told when they come close: a notice at 90% of the limit
in force and an error at 100%, each once until use falls below 80%.
The log records each new high in steps of 5%.
Also: when a function of the game finds its stack written over, the
game ends itself with exit code 0xc0000409 and no crash report. The
log now names that function and its callers first.
The site table (texture_streaming_sites.inc) is generated from a sweep
of the supported build; texture_streaming.h describes the layout.
Tested in game below 20480 textures only. Nothing has run above the
old limit, where the three stack guards and the longer lists first
matter. At 32764 the game still hangs; the notice is the protection.
The in-game advertising (billboards from EA's ad service, with a
render pass that measures how long each was on screen) has nothing to
serve offline and still costs frame time. Its three switches are now
set to 0 with the other performance settings:
AdTech.AdTechEnable
AdTechService.AdTechEnable
DingoAdTech.EnableAdTech
Suggested by a player who measured about 5% more fps; not measured
here. All three names are in this build's settings registry.
The game's truck model has its hangers on the trucks' bones and its
baseplates on the deck's bone. A broken board hides the real deck by moving
that bone away, so its trucks lost their baseplates.
The decks inside a deck pack bring a truck model with the baseplates on the
trucks' effectors, bones under the trucks' own that no model uses. The render
pose hook writes each effector from its truck's bone, so that the baseplate
sits on the deck of a whole board however the truck steers, and goes with the
truck when a piece carries it off. With the game's own truck model nothing is
on those bones and nothing changes.
The deck was only stepped through, one press for the next one. The overlay
now has a card BOARD BREAK DECK with a button for each deck: on the Mods page
above the installed mods, and under the feature's switch on the Skater page.
A deck that is not installed is greyed out, the one in the game is marked, and
a press asks for that deck by name: the mods are applied and the level loads
again with it.
The launcher installs one mod folder per package, and the game takes one
mod's deck at a time, so a package could only bring one deck. A mod folder
may now hold a deck in each decks/<name>/, laid out like a mod folder. When
the mods are put together each of them becomes a mod of its own for the run,
listed as <folder>#<name>: it is chosen, set aside and switched like a deck in
a folder of its own. mods.json and the mod list only know the pack, which
counts as loaded when one of its decks is.
A pack's own layout is not one of its decks, whatever the folder is named,
and is set aside like any other deck model. It is what an older ReSkate
loads from the pack: 2.0.6 knows a deck by its folder's name only, so a pack
in a folder named Board_Break_<Name> with a breakable deck as its own layout
breaks boards there with that one deck.
Guard a crash when the game describes a failed load
When a load fails, the game writes "load <address> failed with error:"
and describes each part of the load's error in turn. That list can
hold parts that were never set, and the game calls through them: six
crash reports from one player on 2.0.5 and 2.0.6 all end at
Skate.exe+0x2e3ab9c, after eight parts that read "no error".
The guard hooks that one function. Before the game describes the
list, every part that is not one of the game's own objects is emptied,
so it reads "no error" as an empty part does; the description also
runs under an exception handler. The failed load is then the game's to
deal with, as it meant to.
What the game wrote goes to the log as a warning ("The game described
the error of a load that failed"), with how many parts were emptied,
so content that does not load can be traced. The game's message names
no asset, only an address.
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
A dedicated server can be marked as a favorite: F on the chosen server, or
ADD TO FAVORITES in the panel beside the list (the same to take it away).
Favorites carry a star, are listed first whatever the sort, and have a
FAVORITES filter beside FRIENDS.
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.
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 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
- 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
- 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
- maps.park_mods binds a Park Editor park mod (Mods/<folder>/parks/<key>.park.json)
to a name and the map it was built on; maps.park_limits caps the parks and
objects spawned at once (3 and 512 by default)
- park-mod list | add <name> [<name>...] [minutes] | remove <name> [<name>...] | clear,
from the console, as /park-mod by admins, and by players through custom votes
- A spawned park is the layout of the server's own SteamID and epoch, sent like a
player's: games since 1.0.0 already show it, it counts against no player's
object limit, nobody can move or delete it, and it stays when players leave
- Objects players' games would not take (not Build Kit items, broken values) are
skipped and logged; a missing or broken park is an error line when spawning
- Optional expiry (1-1440 min) with a warning in chat a minute before; every
park goes on a map change and none survive a restart
- [parks] log lines name who did it: console, the admin, or "a vote"; a vote's
refusal reaches chat
- park-mod is reserved for custom commands; README.txt documents the settings,
commands and a park/unpark vote
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2G4APEkhHgVs59RDL3XWD