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.
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.
Shader tables
- Reverts a61f82c ("Shader crash fix"). Since 2.0.8 a mod's own shader
programs were copied into the merged table, and the tables that cosmetic
bundles carry were merged across mods. Each such bundle then held other
mods' programs, and the game built them as the bundle loaded, before the
files with their compiled variants were there. That was 2.0.8's most
reported crash (Skate.exe+0x3f86da6, some forty machines in ten minutes).
2.0.9's guard passed such a program over, and the same players then
crashed a little later in cosmetic loading (+0x3f7bb65, +0x16fe1a6,
+0x3dc07ed).
- Whatever crash a61f82c fixed comes back with this for the mods that
needed it.
- The guard stays.
Music
- The game's read of one song for its scorer (0x14f23a0) is replaced by the
same read with the check it lacks: a place past the end of the playlist
asset's songs has no song, which the game already answers with 0. 2.0.9's
check in music_select_next_hook looked at what the queue held before the
game was asked and did not see these; the game asked for places such as
0xd570 and crashed reading them (7 reports on 6 machines).
- Said in the log the first eight times.
Not confirmed in game: neither change has been run by a player who was
crashing.
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
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.
The Favorites order fix gave Favorites to any channel that asked for
playlist 0. That is what the music player asks for when Favorites is
chosen, but nothing said another channel could not ask for 0 and mean
something else. The channels the music player's own queues pick songs
for are now remembered as they are seen, and only those are given
Favorites for 0; on any other channel the request goes through as it
came. The channel numbers are still the game's: none is written down.
A channel's binding was logged once per change, except for a channel
past the end of the table, which was logged on every call. Those are no
longer logged at all. The binding line now says whether the channel is
one of the music player's.
The hook on the game's candidate check is removed, with its address: it
changed nothing and only logged.
In the skater's item grids (Apparel, Accessories, Board, Gestures) F favorites
the highlighted item, - (or Ctrl+F) opens a search box that filters as you
type, and X steps a filter (All, Favorites, Mods, Game items). Favorites sit
right after the worn item, carry the game's star where the tile shows its
rarity icon, and are kept in ReSkate.favorites.json beside Skate.exe.
The grids stay the game's own: their tile rows are reordered and filtered
through the native model functions, as the music screen's Mods shelf does.
Nothing is hooked for it and no file of the game or of a mod is changed.
See contrib/native-item-browser.md for how it works and what is not tried yet.
This is the Item Favorites mod done natively, so it no longer needs a DLL
loaded through a custom script.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
With the game's Camera height control on (Gameplay > Advanced Camera),
R3 on the board switches between the high and the low camera by writing
the opposite of the UseHighCam option. A new switch in SKATER > CAMERA >
FIRST PERSON, "First person in the camera cycle" (off by default), makes
it high -> low -> first person -> high.
- The existing native option hook passes UseHighCam writes to a
listener, so the settings code does not depend on the skater code
- Low -> first person keeps the low camera and turns first person on;
first person -> high turns it off and keeps the high camera
- First person is switched through the menu toggle's own action on the
next client tick, so the player's first-person settings apply as
they are; it only starts on the board, outside Freecam, Noclip and
the Park Editor
- Saved as FirstPerson.CameraCycle; Reset arm settings keeps it
- Add a ctest (first_person_cycle) for the cycle's steps
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Binds for first person, the HUD, voice chat, time of day, No Bail, challenges, nametags and dots
Eight more actions can be put on a keyboard key, a chord or a controller
combo, in the binds list beside the others. None is bound until the player
binds it.
- First person, Hide HUD, Voice chat on / off, Next time of day, No Bail,
Show / hide challenges, Player nametags, Player dots.
- Each bind runs the console switch for it, so it follows the same rules as
typing it: No Bail still answers to a server that has it off, and the time
of day goes to the server for an admin where the server sets it.
- They are one table (action_binds): a bind is its name in the profile, the
name "bind <name>" takes, its label and its command.
- "bind firstperson|hidehud|voicechat|tod|nobail|challenges|nametags|
nametagdots <n>" sets one from the console; 0 clears it.
- They are not read while the menu, the console or the chat box is open.
Console, which the binds use and anyone can type:
- Every on/off switch takes "toggle" ("nobail toggle", "hideui toggle").
- "firstperson 0|1", which was only in the menu.
- "tod next": the time of day after the current one.
- "mp voice-chat on|off|toggle": the player's own voice chat, leaving the
rest of their voice settings; it says which way it went in their chat.
Votes on a dedicated server show as a card above chat instead of being
followed through chat lines.
- The card has what is voted on, the Yes and No counts, how many are needed,
a bar of how far Yes has got, and the seconds left. When the vote ends it
shows Passed, Failed or Cancelled for four seconds.
- Yes and No are two buttons, labelled with the player's binds. The answer
given is highlighted and can be changed. They can be clicked whenever the
cursor is free (chat, menu or console open).
- Two new binds, "Vote yes" and "Vote no", in the binds list: F1 and F2
unless changed, to keys or controller combos like the others. They are
read only while a vote is running, and not while typing.
- /yes and /no still work and show on the card. The player a kick vote is
about sees the card and has no vote in it.
- Console: "mp vote yes|no", "bind voteyes <n>", "bind voteno <n>".
Server:
- The roster carries the running vote (id, kind, tally, seconds left,
starter, target, label) and the one just finished with how it ended.
- A chat line is no longer sent for each change of the tally; the lines for
a vote starting and ending stay.
Fix: a dedicated server's roster may list more players than its limit.
The extra slots for reserved players and admins (33 of 32) made a roster the
protocol refused to encode, so with one in use the server logged
"Server error" every pass and stopped part-way through each one until
somebody left. Lobbies hosted by a player are still held to their size.
PRACTICE > TELEPORT now shows the waypoint the player placed on the
game's pause map, with a "Go to map waypoint" button. The console gets
`trainer waypoint [info]` and `trainer ground <x> <z>`.
Reading the waypoint (trainer_waypoint.cpp)
- It is a point of interest in the map's registry, the one ReSkate's
party markers register into (addr::native_party::map_manager), with
kind DingoMapPOIType_Waypoint (3) and its world matrix at @0.
- The registry is walked only while the TELEPORT card is drawn (the
page notes it, like the class list) or the command runs, every
250 ms at most. A player who never opens the card pays nothing.
- A registry the map has filled without a waypoint means it was
removed; an empty or unreadable one keeps the last waypoint seen. A
new level forgets it.
Landing on the surface (trainer_landing.h)
- The waypoint's height is not the ground: teleporting to it dropped
the skater through the map. local_ground_height() casts the park
editor's downward ray (editor::cast_surface) through the client
physics world and takes the topmost hit.
- The first ray starts at 1500 or 500 above the hint. A custom map
can sit higher than that (a waypoint at Y 12 over ground at Y 1939
was seen in game), so a miss is cast again from 20000.
- If collision there is still streaming in, the teleport goes to the
hint's height and a 12 s watch puts the skater on the surface when
it arrives (at most 6 corrections; the teleport is re-sent up to 3
times if it never arrives).
- Teleports still go through go_to(), so they follow the host's
noclip / teleport permission in a session.
The decisions (ray spans, the watch, remember/forget) have no game in
them and are tested by trainer_landing (DINGOSDK_BUILD_TRAINER_TESTS).
README and CHANGELOG updated.
Create independent anchored and tile-list models before native widget binding.
Populate Mods from Featured rows with mod playlist IDs while retaining native
presentation and actions. Leave existing playlist contexts and cosmetic
ownership unchanged.
Document the native UI model flow, verified loader entry points, typed
publication, and inspection workflow for future modders.
Skater slot, menu and throwdown fixes; MY MODS rework; lobby parties
Saved skaters (Extension/Customization, Extension/Skater)
- The selected slot, and slots before it, came up as the standard
skater after a start or map change. The game asks for a slot's saved
outfit once per level, and that happened before the cosmetics catalog
could be read. The first selection now waits for the catalog (60 s at
most), and later map changes check against the catalog already read.
- The retry from abdda77 is removed: the record it wrote to is a
temporary on the game's stack, so it could never work.
Pause menu (Extension/UI/NativeMenu)
- ReSkate's entries stopped working after enough map changes: each
level change took new slots in a fixed table of 1024 for the same
buttons. A button now keeps its slot, and a full table hands out the
slot that has gone longest unused.
Throwdowns (Extension/Throwdowns, Runtime/runtime_init.cpp)
- Multiplayer crashed when someone placed a throwdown after a map
change. The event types are freed with the level but were cached for
the process. They are now cleared when a level is left, checked
before use, and looked for again in each level.
MY MODS (Launcher/gui_mods.cpp, gui.cpp)
- Search, a filter and an order; a switch per mod; a menu per row.
- Multi-select with tick boxes, Shift-click and Ctrl+A, and a bar to
enable, disable, move, update or uninstall everything ticked.
Lobby parties (Extension/Multiplayer/Session)
- A lobby is no longer one party. Its host keeps parties the way a
dedicated server does: invite, accept, join, leave, kick, promote,
open and close, with "/p" party chat. The "Lobby party" switch is
gone. Protocol version unchanged (38).
Mod merge (Engine/Vfs), follow-ups to e715b9b
- Files rewritten into the merged patch are tracked by position, so
another mod's added assets no longer shift the lookup.
- Two mods adding different assets under one name is now noted, with
whose copy the maps get.
- mod_merge_unshift_tests compiles with the launcher tests on.
Mod archives (Engine/Vfs/mod_merge*)
- A free archive index no longer has to sit at or below the highest index
the layout already declares. The engine keeps archives in a table keyed
by layer, install chunk and a 16-bit index, with no range to stay
inside; what it cannot survive is a reference to an archive the layout
never declared. Indices are taken lowest first, per package directory.
- So a mod that collides takes an index of its own and its file is hard
linked rather than concatenated, and no two mods share one archive's
4 GB of addressable bytes. Sharing is left for a mod added while the
game runs, which has no declared index to take, and for a directory
with no index left.
- A placement addresses its payload with a 32-bit offset, so both append
paths now refuse to cross 4 GB instead of wrapping round and pointing
the game at some other payload: CasStore::write fails with the archive
that is full, and the live-apply path checks archive 1's size plus the
mod's before appending and asks for a restart, which gives the mod an
archive of its own anyway.
Injection through a hooked LoadLibraryW (Engine/Core/Platform)
- A player could not launch at all: "Child LoadLibraryW entry bytes match
neither Windows' kernel32 file nor this process". Anti-virus and
overlays hook the entry with a jump to a per-process trampoline, so the
bytes differ in the launcher and in the child and neither comparison
could match. The guard already allowed the same hook when both
processes carried it identically, so it refused on an accident of how
the hook was written rather than on intent.
- inline_jump_length recognises the forms a hooking engine writes (rel32
jmp or call, push/ret, jmp [rip], mov rax/jmp rax, short jmp). When one
is present and every byte after it still matches the clean kernel32
file, the launch goes ahead: the body is Windows' own, and the provider
image already matched on timestamp, checksum and size. A wholesale
replacement is still refused, and now says so.
- The failure names the modules loaded into Skate that came from neither
Windows nor the game folder, instead of guessing at "security
software". An accepted hook is logged as a warning with the same list.
Launching (Launcher/launch.cpp)
- Exporting loose scripts is a convenience, and a folder that would not
take it was failing the whole launch: fs::create_directories threw and
nothing caught it. The export is skipped, RESKATE_LOOSE_FILES goes to
0 so the runtime does not look for files that are not there, and Skate
starts. Someone whose install is under Program Files can play; only
editing loose scripts is lost.
Failures worth reading (Launcher/problem.h)
- A hooked LoadLibraryW, a refused write and ERROR_ELEVATION_REQUIRED
each explain themselves now. Windows reports the last two in the user's
own language -- "Zugriff verweigert", "La operación solicitada requiere
elevación" -- so the rules key on the operation names and the error
number, which are never translated.
Saved skaters (Extension/Customization, Extension/Profile)
- Outfits stopped saving and every slot showed the standard skater. The
load hook marks a preset blocked on entry and clears it only on a full
success, so a load refused for a catalog that was not ready yet left
the preset blocked for the session and every later save was refused as
previous_load_rejected. The game asks once per level load and does not
ask again, so one lost race cost a skater until a restart. Logs show
the catalog arriving three seconds after the refused load, at startup
and again on a map switch 17 minutes later.
- The record is remembered in CosmeticRuntime::pending_loads and applied
by retry_pending_cosmetic_loads from the customization pump, which
already waits for refresh_cosmetic_catalog. The preset stays blocked
until a load really succeeds, so nothing is written over the outfit on
disk.
- Those records belong to the loadout manager's array, which can be
replaced in between, so each pointer is checked against the live array
exactly as a save checks one -- manager current, begin/end sane, 0x48
stride, at most 10 records, in range and aligned. A record that no
longer belongs is dropped as retry_record_moved rather than written
through.
Changing a setting in the game's menu did not stick. The camera went
back to high on leaving the menu, grind assist and lean exit stayed at
20 / on, the compass and scoring ticker kept their old state, and the
Experimental tab's "reset to default" returned to whatever was set at
launch.
b763631 gave the settings hooks per-thread caches of saved values that
look a value up again only when profile::Store::changes() advances.
Only whole-profile commits advanced it. Every single save (a setting,
an option, an outfit) goes through Store::Update::commit(), which did
not, so the caches answered with launch-time values for the rest of
the session. native_setting_get also wrote the cached value back over
the new one when the two differed. The saves themselves reached the
database, so profiles are intact.
- Update::commit() advances the counter through Store::note_change(),
after value_ holds the new values and before the lock is released.
Store::commit() and the constructor use it too.
- A cache hit needs the whole key text, not the key's address and first
8 bytes. Many keys share those bytes (egf_enable..., egf_exp_...,
CRAS_HasSeen...Tab), so a reused address could answer for another
setting. Still no heap string, store lock or map walk on a hit.
- An outfit that is not loaded or not saved is logged as a warning with
its reason and preset (local_cosmetic_loadout_rejected). It was a
debug event, so a player's log never said why an outfit was refused.
Routine cases stay at debug.
- dingosdk_profile_change_tests (with the park editor tests): each kind
of save advances changes(), an unchanged save does not, and saved
values keep their types across a restart.
Mesh streaming pool (fixes loading crashes on older GPUs)
- 1.0.0 forced MeshStreaming.PoolSize to 3.5 GiB on every card. An RX 580
crashed while loading with DXGI_ERROR_DEVICE_RESET, and the oversized pool
is the likely cause. The pool is now a quarter of the largest hardware
adapter's dedicated video memory, capped at 3.5 GiB; cards of 6 GB or less
keep the game's own 520 MB. The card, its memory and the chosen size are
logged once at startup.
Remote players (busy servers)
- Transform interpolate is inline plain arithmetic: 22 -> 5.8 ns per joint.
It was 4% of a busy server's client frame, since every player within
200 m has the whole skeleton interpolated every frame.
- Players out of the camera's view and more than 8 m away are sampled at
20 Hz, like far players. Their position still updates every frame for
collision, sound and nametags, and a player coming into view is sampled
that frame.
Expression hooks (run for every expression the game executes)
- No profiler zones in the expression hooks: taking a sample switched them
on, inflating the very samples that measured the hooks.
- The throwdown lab pump does one sorted lookup of the running graph and
skips everything else for graphs it doesn't watch (graph_throttle gets
throttled_graph for this).
Settings and memory reads
- Script gameplay-setting and native-setting gets are served from small
per-thread caches, keyed on the asset/key address, its first 8 name
bytes and a new profile::Store::changes() counter, instead of a store
lookup each call.
- Per-frame reads in the client tick, native settings and population
controls use memory::peek instead of ReadProcessMemory.
- Script error logging probes each argument pointer once.
Launcher
- "Close the launcher when Skate starts" now hides the launcher and exits
when Skate.exe does, instead of exiting as the splash opens. Outside a
Steam library, Skate's protection closed the game (TerminateProcess 0x32)
when the launcher exited during startup; players confirmed turning the
setting off avoided it.
- STATUS tile warns when Steam isn't open or signed in that PLAY starts
offline as Unknown Player (hidden when Offline mode is chosen or while
launching).
- Setting description updated to match.
Startup interventions (20260929)
- exit_return 0x87755f2 -> 0x87fe7fb: the last LEA RSP,[RSP]; JMP stub
before the PUSHFQ / LOCK CMPXCHG routine, as 0x87367cf was in 20260908,
and the return address player logs show. Only enforced once a frame site
is armed; this build's protector has no equivalent rep stosb frame clear.
Client frame cost (profiled 2026-10-02)
- Transform interpolate is inline plain arithmetic (~1% of an MP frame).
- Script gameplay-setting and native-setting gets are served from small
per-thread caches keyed on asset/key address, name prefix and a new
profile::Store::changes() counter, instead of a store lookup each call.
- Hot-path reads in client tick, native settings and population controls
use memory::peek instead of ReadProcessMemory.
- Script error logging probes each argument pointer once.