The promotion loop went through the edits in the order they were absorbed, lowest priority first,
and kept the first usable graph, so a lower pack's graph won and the higher pack's private rows
were dropped. It now looks from the highest priority down. The archive test covers two custom packs
with their own bytecode and different second programs (it runs with a game root, as the other
archive checks do).
Mods: preserve bundle-owned shader graphs alongside stock material aliases
Conflict with #363 resolved by keeping both: its native variant addition tests and the shader
selection tests in mod_merge_added_assets_tests.cpp, run from one main().
Plugins: support declared colour inputs on mod-owned board materials
Conflicts with #363 resolved by keeping both: reconcile_variant_additions beside
active_mod_sources in mod_catalog.h, and both test targets in cmake/Apps.cmake.
merge_chunk_metadata gave such a chunk a record with no fields. The game's reader (0x41af080) takes
a record without "h64" as a "g" record and, with no "g" either, reads its guid through null
(Skate.exe+0x15b2c34: computerK-Fingerboard adds its grip texture to a game deck bundle whose list
has no record for it). The record is now an owner hash no resource has, with an empty meta, which
the game passes over.
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.
Resolved where main's trainer meets the plugin branch:
- Board Break, the drone, Road Rash, Hall of Meat and Grind Exits are plugins here, so their
built-in code stays deleted: the trainer no longer hands Board Break's wear values back (the
plugin does not tune physics), its QUICK SETTINGS list no longer names Grind Exits or Board
Break, and the drone's debug action and menu are not brought back.
- The trainer's pump power, gravity, revert boost and transition help run beside the plugins'
pushes and velocities on each physics step; both sides' new declarations are kept.
- The game's own pad reads go through game_pad_read (the gamepad filter, Dark Pop's D-pad hiding,
the grind sticks) and then the plugin branch's menu and hidden-button handling.
- The trainer's forced grind settings are noted as the stance like any other value.
Plugins' files from before Mods/.reskate_plugin_data were still in Mods/.reskate/data, and the
launcher's merge deletes Mods/.reskate before the game ever looks for a plugin's folder: a saved
Deathrace route went at the next launch that rebuilt the merge. mods::keep_plugin_data moves each
plugin's old folder to Mods/.reskate_plugin_data first (whole, or the files a folder there lacks,
its own kept), at the start of every merge; the game still runs it once for a merge that kept its
patch. Checked in the build stamp test.
A mod's reskate-plugin.json can declare "variants" {folder, order, displaces}: each <folder>/<name>/
is a variant, the one chosen (else the first of its order) loads, and every other mod whose layout
lists a displaced bundle is set aside for the run. Choices are kept in
%LOCALAPPDATA%\ReSkate\variants.json. Plugins read and choose their own mod's variants with
reskate.package.variants and reskate.package.choose (permission "variants"), and fit every board
for a deck that is not the game's own with reskate.board.fit {plates, axle}.
The board-break plugin, which ships inside the Board Break deck pack, declares the pack's decks as
variants, picks the deck on its own page and with `boardbreak deck`, reads each deck.ini and fits
the boards. Gone from the SDK: breakable_deck, the deck catalog, the MODS page's deck card, the
boarddeck command and the Board_Break_<Name> folder naming.
reskate-plugin.json's "resident" list names the assets a mod's bundles keep (world objects and HUD
widgets the game picks at run time, and their shader block depots), so they survive another mod
replacing that bundle. Scanning now uses lexically_relative, so a mod folder that is a junction no
longer yields ../ paths, and every output path is checked to be inside the merge output. Chunk
metadata audit failures are fixed; the full merge audit passes.
reskate.throwdowns.mode{...} adds a mode to the game's own Throwdowns page: a card on
the picker, the real Spot Battle setup page relabelled with the mode's name, logo,
description, players, timer and rows (sliders, choices, buttons), and the native start
flag. The plugin hears throwdown_mode events (opened, setting, button, placed with the
flag's position and heading, start, leave, cancelled) and runs the mode itself;
release_flag takes the flag down. The flag's queue never starts a Spot Battle: its
waiting card's Start goes to the plugin, MaxPlayers is held at 10 and it is never
relayed as a drop. After SmokeyDawg58's one_up_menu and one_up_placement
(reskate-custom-mods), generalised over a mode registry.
The page is his 4-column logo grid whether or not a plugin has a mode. Its widgets and
grid records, and the game assets it uses that only some levels load, ship in
ReSkate.dll as the throwdown_menu built-in pack, taken from his asset mod's FBMOD
(dingosdk_ebx_pick trims the asset list in place). A mode's logo is a texture path in
its own mod, so no mod edits the shared list. The FPV drone is no longer a built-in pack.
Native menu data: text initialization, change-only writes, retained array publication,
owned-model rollback. Tests: registry and setup helpers, the plugin API, a test plugin
with two modes.
MY MODS: a table with an animated drag for the load order, a conflicts bar with Check conflicts, and a Resolve popup whose choices are kept. The check runs at startup and after changes; conflicts no longer show as problems, block Play, or hold mod files during an update.
Conflicts: only game EBX that cannot be combined, or Lua scripts with different versions; colourways and same-name additions are not conflicts.
Official mods: installed in Mods/<Namespace-Name> and turned on or off through mods.json, so they show in MY MODS; the old .reskate-official folder is moved over.
Merge: a scan cache (Mods/.reskate-scan.txt) keeps each unchanged mod's repair, mesh and texture scans, so big maps are not re-read every merge.
Launcher: an announcement from the backend shows under STATUS, with HIDE.
Many free object-dropper objects were treated as store items and hidden from
the Object Browser and the park editor (pole jams, road signs, bleachers, A
frames, quarter pipes, bowl pieces, ...).
- Items whose record names a neighbourhood rank unlock (field 11) are open,
even when no rank record lists them as a reward (20 objects, 6 cosmetics).
- Build-kit objects that no record offers, sells or grants are open (169).
Objects with any offer or bundle grant, and brand collabs (field 24), stay
reserved: 42 of 439 on build 24855063. Cosmetics are unchanged.
- Object Browser categories use their first object's thumbnail as the tab
icon instead of the game's empty placeholder.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
combine() recorded every source's copy of every EBX and resource asset, so that conflicting edits could be combined later. Map mods carry near-complete copies of the game's root-level bundles, so most of those copies are the game's own bytes. A mod's copy equal to the game's first copy of the asset, the copy edits are combined onto, is no longer recorded; the game's copy, recorded first, stands for it. A contribution points at its source's folder instead of copying the path. The merge report counts the copies left out.
The tests read the merged patch through CasStore and list_bundle, report a missing archive as a failure, and cover a mod archive numbered like a placeholder.
The music mod's song names its audio chunk; each map that takes the song carries the chunk too, and every chunk entry in the patch points at bytes inside its archive.
A carried asset was written again for every superbundle that carried it: 1.4 GB of 2.2 GB with 96 mods, which left no room for a live apply. Each one is now written once per install chunk, when its first superbundle settles.
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.
The renderer gives each frame 131,072 shader-visible descriptors past a persistent 120,000, in
heaps of 333,333. Dense custom maps (the Los Santos City sublevels, San Andreas) draw more than
that in a frame, and the renderer wrote past the end of the heap: shaders read garbage
descriptors, which faulted or hung the GPU (page faults at nonsense addresses, in draws, texture
fetches and ray tracing alike). ReSkate now patches the device setup at boot so each heap is
D3D12's 1,000,000 and a frame may use 524,288; heaps are 644,288 descriptors. LS-B2, LS-B3 and
San Andreas (210,000 a frame at its peak) now hold.
Also in this change:
- Memory: perf memory owners/heap/gpu/report, a ledger of who commits the game's RAM; the
render-resource pool no longer allocates its 115 MB twice; the EadpErrorsData arena is
capped at 32 MiB (EA's error reporting is blocked anyway).
- Crash reports: tagged with the map, destination, graphics card and driver version (protocol
v3), so Sentry can group GPU hangs by map and card.
- GPU diagnostics (--gpu-diagnostics): a fuller DRED report, the device-removal and video
memory watches, a large-draw watch, a record of which game code made and freed each GPU heap
and resource, checks of every GPU address bound, and a meter of each descriptor heap's use.
- World pools: peak use of entity pages, render instances and spatial blocks in the log.
- dingosdk_mod_mesh_check: checks every MeshSet a mod ships against its geometry; the MeshSet
reader now knows static meshes' layout.
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.
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.
As in skate. 3: when the local skater bails, the bones it hurt show over
the world through the body, yellow where a hit bruised one and red where
one broke it, with the bail's Meat card in the bottom left corner in the
place of skate.'s own HUD there: its time, hits (head hits double,
vehicles half again), broken bones, road rash, airtime, fall, top speed
and rotations, each scoring, and the map's best. A bone breaking pulses
the screen's edges red and, in single player, slows the game down a
moment. Off until switched on, as in skate.: in skate.'s menu (Custom
Stuff > Player) or with `hallofmeat 0|1`, saved with the profile.
Everything it draws is skate.'s own, read from the installed game: its
skeleton mesh (the Dem Bones costume) posed with the renderer's own
skinning matrices, its scoring icons, the THRASHER wordmark and its UI
brush strokes. Nothing is shipped.
How it works: No Bail already hooks the skeleton response, which runs
once a physics step right after the body contacts; it now hands that step
on, with the local skater it follows, but only while Hall of Meat is on.
Each step reads what every ragdoll body hit and how hard, and whether the
skater is a ragdoll; the bail runs on the game's own clock so slow motion
measures what the body did. The feature's own hooks, on the renderer's
skinned draw packets and main view and on the UI model's write, are
prepared at startup and switched on only with the feature, and off again
once the HUD corner and the game's speed are given back: switched off, the
game never calls them and the physics step pays nothing. The skeleton mesh
is read the first time it is on; only the images are read at startup,
since the overlay builds its atlas once. The renderer's picture of the
skater is read only while a bail shows. The menu's switch reads the
overlay model like the others. Every address is in the build table with a
fingerprint checked before use. Extension/HallOfMeat/README.md describes
the parts; its tests build with DINGOSDK_BUILD_HALL_OF_MEAT_TESTS.
The slow motion changes SimulationTime's rate, frame cap and time scale as
a session override and steps the local skater along, since skate. sets a
skater's step only when it builds its core. It runs the same at every
break, but never faster than the game ran: with the trainer's game speed
below 1 it only takes the part that is slower still. Afterwards it writes
back what it found, each setting only while it still holds what the slow
motion wrote, so the trainer's speed comes back as it was; a setting
changed from elsewhere meanwhile is left as it is and ends that slow
motion. named_setting_value reads a setting's value for it.
Tuned in play: a bruise from 7 m/s and a break from 12 m/s along the
contact normal (raised twice: a hard fall on flat ground broke bones), a
break worth ten of the lightest hits (a heavy bail's hits and breaks
outweighed the rest of the card) and a fall half of skate. 3's rate.
Fix: the overlay now draws in the back buffer's pixels, not the window's,
so it lines up on displays Windows scales (1920x1080 borderless on 4K).
The skater's render hooks, its skeleton and its body maps were first
found by gBGYo's ReSkate fork (github.com/gBGYo/ReSkate).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Mod merge: rebuild a merged bundle's chunk metadata the way the game
writes it (one record per chunk, in chunk GUID order), so costumes
from different mods built on the same game costume keep their
textures and meshes
- Merge audit checks that layout, and there is a new unit test for it
- Server names: only letters, numbers, spaces and - _ [ ] ( ),
1 to 64 characters; the server browser hides servers with other names
- New Centrix tag from the backend's "centrix" list: blue and white
name, badge and item animation, ranked after Dev and before Creator
Merge: carry added assets with a changed list, combine in-place values, list repeats once
Three merge bugs found by auditing 104 installed mods:
- A changed list (the music playlist) reached every bundle copy that holds
it, but the songs a mod added beside it only reached copies of the bundle
the mod added them to. The game keeps the playlist in eight bundles, so
the other seven named songs they did not hold. Added assets now follow
the change into whichever bundle copy takes it.
- An entry naming an asset that lost its name to another mod's asset is
now dropped from the list, both in the merged bundle and in the list
other mods' copies are given.
- Two mods changing values in place in one asset (lighting) kept one mod's
copy whole and silently dropped the other's. Plain values are now
combined, the higher-priority mod winning where both changed one. An
edit that also moved a reference or resized a list is taken whole or not
at all, and the merge notes whose changes are missing.
- A reference two mods both append to a list is listed once.
- Mods that add copies of store items under new names are no longer
loaded. The launcher, mod manager and game say why.
- A chat line passed on by the host only shows a Dev/Creator/Homie
badge when the player's own copy of it arrives too, so a host
can't put one on a line that player never wrote.
When several mods change the same game asset (the master music playlist,
for one), only the first mod's edit was read for the imports it names, so
only that mod's added songs, waves and chunks were carried into the other
mods' copies of the bundle (a map's level superbundle). Every later mod's
additions went missing on those levels while the game's own levels kept
them, which showed up as songs vanishing from the playlist on custom maps.
Read the imports of every changed asset, and merge a later mod's edit into
the stored change so the combined document's imports -- and the chunks
they lead to -- are followed too. Add a regression test with two music
mods whose songs must both reach a map's copy of the bundle.
- Read Dev / Creator / Homie Steam IDs from api.reskate.dev instead of the hardcoded list
- Creator (red) and Homie (gold) tags and hoodie/board colours; Admin tag is now pink
- Global bans: lobby hosts and dedicated servers turn banned players away
("global_bans": false in ReSkateServer.json opts a server out)
- SPECIAL menu page for listed players to hide their own tag and items
- Fix hoodie and board keeping the animated colour after the animation turns off
- Multiplayer protocol 39 -> 40