- With events.tricks a plugin hears the local skater's tricks: trick_started (where), trick_landed
(points, multiplier, where it started and landed, how long it took) and line_broken (a slam or
a wipeout); reskate.score.current() is the score HUD's line, multiplier and whether a trick is
going on. Without the permission the events cannot be registered.
- The score HUD is read into the trick watch (Extension/Skater/score_hud.h, trick_watch.h) on the
client tick only while a running plugin has events.tricks; a new level starts a new line.
plugins::start takes the image base for it.
- reskate-test has a tricks module (skipped until a trick lands) and ptest trick, which waits 30 s
for one and prints what its events said; reskate-hostile has trick-loop.
Not yet checked against real tricks in game.
- skater.read: reskate.skater.get() (position, heading, speed, vertical speed, airborne, air
time) and position, heading, speed and on_ground on their own; nil without a skater. From the
trainer's telemetry. The heading is radians, -pi to pi: 0 faces +Z, pi/2 faces +X, and a higher
heading is turned to the skater's left (checked in game).
- skater.teleport: reskate.skater.teleport(at, heading?) through teleport_local_skater_heading;
false and why when the host has turned teleporting off (as the console's tp), there is no
skater, or it is asked more than twice a second. Positions must be finite and within 100 km.
- world.raycast: reskate.world.raycast(from, to) is where the line first meets the world's
collision (local_surface_hit), or nil; 64 a tick, 1000 m at most.
- The host passes the game to plugins through Environment, so the tests fake a skater and flat
ground. reskate-test has a skater module (skipped without a skater; the harness counts skips)
and ptest tp; reskate-hostile has teleport-spam and raycast-flood.
Checked in game: teleporting, the two-a-second cap and the raycast cap. ptest skater's heading
check failed there (0 to 2 pi from the trainer) and is fixed, not yet rerun in game.
The console answered every command missing from ReSkate's own list itself, as unknown, so a
plugin's command (ptest) never reached the plugins on the game thread. Now only ReSkate's local
commands run there; anything else is queued, and the game thread tries the plugins first and
answers unknown as before when none has it. Checked in game: ptest all, 45 passed, 0 failed.
- contrib/plugins/reskate-test calls every plugin API and says what worked: ptest all, ptest
<module>, ptest list. Its 45 checks cover the base set, timers, the level, package files, JSON,
require, the console, storage and the sandbox. It is the example to copy for a plugin.
- contrib/plugins/reskate-hostile attacks the sandbox, one attack per command (hostile list):
loops, memory and buffer bombs, caught errors, error and log floods, timers, recursion, an
unload that never ends, and 17 ways out. Each must end stopped or refused.
- dingosdk_plugin_tests runs both from the repository: every reskate-test check passes, and every
hostile attack is stopped or refused with nothing escaping.
- Taken from Domination (PR 279) for the plugin API to use: the trick watch (when a trick starts,
lands and what it scored, or a slam breaks it; Domination's spots left out) in
Extension/Skater/trick_watch.h with its tests, the score HUD reader in score_hud.h, and
teleport_local_skater_heading, whose hunks are the PR's own. Neither is wired to anything yet.
A mod with a reskate-plugin.json is a plugin: Luau scripts that run in their own VM, with only
the reskate API ReSkate registers for them. Luau 0.741 is vendored in External/luau (the VM and
compiler only: no native code generation, no loadstring, io or C modules).
- Limits: 64 MB of memory, 50 ms a call, 2 ms a tick on average (over it for 3 s stops it),
more than 5 errors in 10 s stops it, 50 log lines a second. A plugin over a limit cannot catch
it in a pcall and carry on.
- API: reskate.log, on (load, unload, tick, level_loaded, level_unloading), timers, the level,
its own package's files and JSON, require inside the package; console.command and print
("console"); storage get, set, add and keys ("storage"), 1 MB in
%LOCALAPPDATA%\ReSkate\Plugins\<mod>\storage.json.
- Found every 2 s among the mods mods.json leaves on: switching a mod, or changing a plugin's
files, loads, unloads or reloads it without a restart. A package holding a program is refused.
- plugin list, commands, reload, off and on in the console. A plugin cannot take a name of
ReSkate's own commands.
- Tests: DINGOSDK_BUILD_PLUGIN_TESTS, including plugins written to break the sandbox.
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.
The device-removal watcher always runs, and the fatal handler notes a DXGI stop in gpu-breadcrumbs.txt. Crash reports carry a gpu.breadcrumbs tag (protocol 5).
The launcher still reads the list at start and looks up each mod's latest
version, moves in a copy installed from Thunderstore and takes away retired
mods, but it downloads nothing by itself. Each card on the OFFICIAL tab says
NOT INSTALLED, INSTALLED, UPDATE AVAILABLE, DOWNLOADING or UPDATING, with an
INSTALL or UPDATE TO vX button (official_mods::install, on a thread of its
own), OPEN FOLDER once installed, and the On / Off switch beside its pills.
The banner counts how many are installed. Play waits for an install under
way.
Board Break and Road Rash start only when their official mod is installed
and on (mods::official_in_use), and say in the log which it is not; the
menus leave them out otherwise, as when turned off.
The live test (RESKATE_OFFICIAL_LIVE) checks that nothing downloads until
asked, then installs each.
The game's info sign shows on the first of the notice's three reserved
lines, where the mod's icon covered it. The icon, tag, name and details
now take the two lines under it, the icon 40 instead of 46 so it still
ends on the card's bottom text margin.
The OFFICIAL tab has an on/off switch on each mod's card, with an OFF pill
while it is off. The list of those turned off is
Mods/.reskate-official/official-off.txt, one folder name a line, which the
launcher's sync never removes. A mod turned off stays downloaded and kept
up to date, but official_packs() leaves it out, so nothing merges it, no
deck comes from it and the in-game Mods page does not list it.
Board Break and Road Rash do not start while their official mod is off,
and say so in the log. The menu model carries that as `off`, and the side
rail, the addon tabs and tiles, the home tiles, the pause menu's settings
and the native tools menu leave them out without a gap. category_tabs
takes a list built at run time for that.
Tests: an official mod turned off stays in place and is left out, comes
back when turned on, and only a plain folder name is written.
A model made for another player's drone crashed the game a few seconds
later in the per-frame update of its shader sections (Skate.exe+0x505b04b,
2.1.0): 28 of 29 reports there came right after one was made. Real
players' drones show as markers and are heard as before until that is
found; the pretend one from dronemodeltest remote still gets its model.
The warning sign on a mod item's description notice was cleared by writing
an empty IconOverride. The game resolves that override again later in its
UI update and crashed on the empty one (Skate.exe+0x3e897fb, a lookup that
finds nothing): about a quarter of 2.1.0's crash reports, all of them after
the sign was cleared.
The notice now gets the game's own info sign (img_generic_info_64), set the
way the favorites star is. When that texture is not loaded the warning sign
stays and it is looked for again two seconds later; an empty override is
never written. Texture assets are matched by name ignoring case.
The window listed one ADDONS row that opened an overview of tiles, and showed the addons under it
only while that page was open. EXTRAS now lists Drone, Board Break, Road Rash, Hall of Meat, Grind
Exits and Music as rows of their own, always; a window left on the overview (switched from full
screen on ALL ADDONS) shows Drone. Every row of the rail is as tall as fits it without scrolling,
from 28 to 36. Full screen keeps its ADDONS page and ALL ADDONS tab.
The shader expression registry registers a database from a queued job, and builds its per-key item
lists from the database's lookup without a bounds check. A database destroyed before its job ran,
or a lookup index past its items, left the registry holding pointers that crash it later
(Skate.exe+0x3f7cf33, +0x3f7ceed, +0x20960b in Sentry). The guard skips a registration for a
database already destroyed and points an out-of-range index at a valid item of the same key,
logging each.
On a dedicated server with object scaling off, a loaded park's scaled objects were resized anyway
(and after a refused resize the pose poll saved 1x over their scale), and a scaled paste, set or
undo ended in a resize the server would not take, so the edit waited on it. The placements now
know whether this game may resize (set by the session from the server's setting and the role, the
host and roles that may always build exempt, allowed outside a session): where it may not, objects
stay 1x in the world, nothing waits on a resize, and the layout keeps their scale for when scaling
is allowed again. A transaction test pastes a scaled object with scaling off.
A light slam had a flat 25% chance to break a board (about 50% worn), a bail with no recorded
force counted as light, and any board could give way on a timer while just riding. Now a slam
under 60% of the breaking force never breaks it and the chance rises smoothly to certain (a new
board: never under 6, half the time at 8, always from 10); a bail of unknown force never breaks it;
and the riding timer is gone: a board that is done (85% worn) gives way on a hard landing instead,
from a big drop as it becomes done down to a curb's when worn out. The player is told when the
board becomes beat and done, and each break's cause is logged at info level.
A rebuild builds one segment per key and each needed every key's slot, so a long replay built
them once per segment: the game's rebuild is hooked too and the slots built for its first segment
are kept for the rest of it (per Behaviour and Subject). The segment player is no longer hooked
(it only counted plays for keysmooth status, every frame). The smoothness is a slider beside the
switch in the overlay (sent when let go, as each change rebuilds the open replay's camera) and
Keyframe smoothness (100, 75, 50, 25%) in the pause menu and Mod Options > Visuals. The startup
line logs on the runtime channel.
A mod's item (or one of the game's own a mod changes) keeps three empty description lines, so
the game's item card, which grows upward from a fixed bottom edge, always has room in the same
place; the overlay draws the mod there: its icon (decoded off the render thread into a fourth
texture slot, the last 32 kept), a MOD or CHANGED BY MOD tag, its name, author, version and how
many items it has. The card is placed as the game places its own, measured at many window sizes:
by the smaller of its scales, a 21:9 frame centred on wider screens (the search bar follows it
too). The warning sign the game's focus script gives any description is taken off that notice
each tick while it shows the reserved lines.
The merge's item list records each mod's version and folder (an older list falls back to the mods
loaded at launch). The item grids now run every client tick instead of at the 500 ms
customization pace, follow the highlight between their 40 ms passes, and let go of a grid as soon
as its screen closes, so the card, the search bar and favorites keep up with the player.
The merge keeps the items the store-copy check reads in every mod and writes them to
Mods/.reskate/reskate-items.json (highest priority first, so an item two mods ship is the one
whose copy the game uses); a live merge keeps the entries of the mods it did not read again, and a
patch built before the file existed is built again once. The game reads it when it builds its
item catalogue: an item a mod adds says "From: <mod> by <author>" under its name, one of the
game's own that a mod changes "Changed by <mod>", unless it has a description of its own.
The pretend drone goes into the beacon as a received one would, so other players' drone models
and markers can be checked without a second player. A drone flying 3 s without its model now logs
what is waited for (the engine, the local skater, or the thread's job context) instead of waiting
silently.
The game plays the replay camera's framing keys as cubic Hermite segments whose speed changes at
every key (the acceleration jumps there, and it brakes where a channel slows or turns back), so a
free-camera shot surges and settles around each key. keysmooth on rewrites each segment as the game
builds it, so that the game's own player plays the natural cubic spline through every key: speed
and acceleration carry on through the keys, and every key is still met exactly, at its time.
- keysmooth on|off (off by default), keysmooth amount 0..1 (1 by default; 0 is the game's curve)
and keysmooth status; kept in %LOCALAPPDATA%\ReSkate\KeySmooth.settings. Only the free camera
(Behaviour 1) is smoothed; the other camera modes keep the game's curve.
- A switch in Mod Options > Visuals and in the overlay's Skater > Freecam.
- The curve apart from the game, with tests (DINGOSDK_BUILD_REPLAY_KEYFRAMES_TESTS); the hooked
functions are fingerprinted, and a build that differs gets no hook.
- contrib/keyframe-smoothing.md: what it does, how, and what was seen in game.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ReSkate page of the pause menu listed the maps as rows of names on two tabs. They are now one
MAPS tab of cards in the page's own style, each with its map's picture (an official map's loading
screen, a custom map's mod icon, from the same pictures as the ReSkate menu's map grid) and its
name: the game's maps, then the custom ones under their own heading. The one pointed at says
LOAD (or why not), the D-pad moves across the grid, and a map that cannot be loaded now is dimmed.
Tutorial Island is left out, and the note on why travel is unavailable sits beside the title.
With what was made (heap type and flags, size, format, flags) and the game code that asked, at
most 40 times. Small buffers and textures took 120 to 170 ms each, two threads at the same moment,
which showed the 5 s stall was the GPU or driver held up by something outside the game, not the
size of what the game made.
Scrolling shirts onto Yudi-Yuddispack's philippleinshirt_ap crashed the game (Skate.exe+0x3dc07ed):
one of the preset's set parameters has a type of 0x0c07010304040502 (null in another crash). The
game type-checks every set parameter when it next applies a preset to a material, reading the
type unchecked, and catching that crash inside addEsShaderPreset was too late: the preset was on
the material already and the next update crashed on it.
addEsShaderPreset's guard now checks the preset first, as that type check reads it: the TypeInfo,
its data, a word at +4. A preset with a parameter whose type cannot be read is left out as a
preset that is not loaded is (the item shows without it), and named in the log once with that
parameter's key. Many of the game's own types are built at run time, so the test is that the
type reads, not that it lies in the game's image.
With "unlock all cosmetics" (and objects) on, the default, ReSkate wrote every installed item into
the profile as owned and never removed one, so every mod a player ever tried stayed in the save:
8765 cosmetics and 520 objects in one profile, past the limit that left a player with no items.
What is owned is now worked out from what is installed (installed_item_owned): a paid store copy
never, every other item while the kind's unlock option is on, and with it off what the profile
saved. The catalog given to the game, the owned list, saved outfits, the player card, the Build
object browser and the park editor all ask it. Nothing per item is saved any more, and a profile
with an option on drops the rows earlier versions saved, once, on the next start.
profile_migration_tests checks that drop on a copy of a real save: the rows stay with the options
off, are gone after a restart with them on, and nothing else in the profile changes.
The profile keeps every cosmetic it has seen, a removed mod's too, and refused to save past
8192 of them (or of Build objects). A player who had tried enough mods passed that, every save
was refused, and the cosmetic catalog failed with it: no items in the customization menu and
no playcard. The saved limit is now 65536 (saved_item_limit), a bound against a damaged file;
the game's own catalog of installed items keeps its 8192.
Also: one item the catalog cannot hold (two items with one identity hash, a record the game
will not read) is left out and named in the log instead of failing the whole catalog, and a
catalog that still fails logs why.
The ReSkate menu now has two styles, switched with View / F11 or the buttons in its top bar
and saved with the profile (`ui fullscreen`):
- Window: the mouse menu as before, with a side rail of pages in groups.
- Full screen: made for a controller, in the pause menu's look. Pages across the top (LB / RB),
each page's sections as the ONLINE page's tabs (LT / RT), button prompts at the bottom, the
pause menu's sounds, and the pause menu's own Multiplayer pages as ONLINE.
The pages are regrouped: HOME (quick switches and a tile for each page), MAP, BUILD, SKATER
(freecam, first person, cheats), SANDBOX (once Trainer), ADDONS (drone, Board Break, Road
Rash, Hall of Meat, grind exits, music), ONLINE, PROGRESS, MODS, SETTINGS, DEVELOPER.
Full screen, every shared control is drawn for a pad: switches and option rows changed with
left / right one step a press, sliders as steppers that repeat when held, rows of buttons,
and pages that scroll with the right stick. Search boxes and typed values stay in the window.
A pad press takes the menu's focus back and hides the pointer until the mouse moves.
Also: map pictures in the map picker (official loading screens, a mod's icon) with a search
box in the window, Splash and Tutorial Island left out of it; number boxes beside the
window's sliders; the binds in groups, with an action bind to open the menu (`menu`); a menu
size for each style (`ui scalefull`); the time of day stepping from the one last asked for.
They load from Mods/.reskate-official, outside the Mods folder's list, so
the page took the ones in effect for mods whose folder was gone: "Folder
deleted / APPLY TO UNLOAD", a change pending, and an Apply that reloaded
the level for nothing.
- Each official mod has a tile of its own after the installed mods:
OFFICIAL while in effect, SET ASIDE while Board Break has another deck
in the game, APPLY TO LOAD for one put in place since the game started.
No Enable/Disable: the launcher keeps them.
- live_mods::preview leaves built-in and official content out of what
applying would unload.
In a level, the cracks, the board's metal and the body's hits play through the game's own
positioned sound where the skater is, as the drone's motor does, so the game's mix and echo apply
and a break heard from afar sounds far off. Several still play over each other.
- Each wave file is also made into the 16 kHz mono that sound takes, as it loads (8, 16, 24, 32
bit and float, any rate and channels, averaged down so a crack's highs do not alias).
- A tick on the client thread, beside the drone sounds, streams them in at the skater's root.
- Outside a level, or until the game's sound is ready, they play straight to the speakers as
before.
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.
- Riding back and forth in one place (a halfpipe, a bowl, laps of a spot) is filmed from a spot
off to the side, high enough for the airs, instead of chasing each turn; riding off or no turn
back for a few seconds goes back to following.
- It looks along where it flies and slows in time to stop short of a wall, sliding along it;
short probes push it off anything close and keep it from climbing into a ceiling. A spot behind
the skater that is inside a wall comes in to this side of it.
- It keeps its clearance over the ground under it and just ahead of it, climbing as fast as a rise
needs, and only sinks slowly near the ground whatever its spot asks.
- FOLLOW ME has Follow distance (3-12 m) and Aggressiveness (0-1, how quickly the pilot reacts,
how hard it leans, closes gaps and turns); the middle is the tuning it had. Also dronetune
followdistance / followaggression.
- Tests: a halfpipe session, turning round at a wall, a slope and a spot under the ground, and the
settings' middle being the usual tuning.
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.
An autopilot flies the real flight model behind the skater while they skate: it
works the sticks a pilot would (angle mode with height hold), reacts a moment late,
eases toward the lie of the ground instead of every ollie, closes gaps no faster than
it can slow down gently, and climbs a little over what blocks the shot. Its camera
sits on a tilt gimbal so leaning to speed up or brake keeps the skater in frame.
Started from FOLLOW ME on the DRONE page or the dronefollow command; it hides the
game's interface like Start flying does. Tests cover straight, turns, hard stops,
ollies and catching up.
The icon is about half its old size and is drawn just above the drone's
model, with the distance and name stacked above it, so the marker no
longer covers the model.
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.
Gamepad: the drone flies with a gamepad by default (Xbox or PlayStation,
read directly; with none plugged in, the keyboard and mouse fly it), Mode
2. Its throttle stick springs back and its middle is hover power, so a
resting stick no longer means 50% throttle; the sticks are scaled down
(60%, which in angle mode slows turning only), softened in the middle and
given a round dead zone, and the right stick is inverted by default (up
tips the nose up; padinvert 0 for a radio's way). A arms and disarms, Menu
puts the drone back; while the ReSkate menu is open the pad is the menu's
and the drone holds still. An RC radio is still a choice.
Height hold is a toggle (off by default): the middle of a springing
throttle holds the height, adding throttle as the drone leans; partway it
climbs at the climb speed (20 m/s), and full stick is always full or no
throttle. Angle (self-levelling) mode, leaning 55 degrees, joins acro.
Flight: thrust grows with motor output to the power 1.5, so the hover
throttle sets thrust to weight (22% is about 10:1); the motors spool up
and down and the turn rates catch up with the sticks in about 30 ms.
Drag is quadratic and larger across the props, set by the top speed
setting so flat out settles near it (no more hard cap); inflow, ground
effect and prop wash. Defaults: gravity 9.81, camera tilt 0; the drone
starts level.
Crashes: collisions sweep to the surface met, find its normal with two
more rays, and slide along it; hard hits bounce and tumble the drone,
and one at the crash speed (7 m/s) cuts the motors until it is armed
again, with a warning and a broken-up picture.
- overlay.h: the gamepad reader is declared in the overlay namespace,
where it is defined; nothing could call it before.
- Tests (drone_flight_tests): hover and height hold, full stick past the
hold, thrust, motor and rate lag, top speed, angle mode, wall crash and
re-arm, crashes off, a gentle landing, and the gamepad's sticks.
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.