"Cannot open file for SHA-256" was all a player saw when the launcher
could not read ReSkate.dll, at startup and again right after each
download. That is an antivirus holding or blocking the file.
- A sharing violation is waited out for up to five seconds: a file that
was just written is often held by the scanner for a moment.
- Otherwise the error names the file and its folder, says to add the
folder to the antivirus exclusions where that is the likely cause, and
carries the Windows error code.
tag_text dropped a trailing lead byte even when its character was whole, so
"Skate Café" was listed as "Skate Caf". The browser shows the tag name until
the server answers a query, so hosts without a forwarded query port kept it.
Stock worlds allocate 8,200 bodies (+1 internal) from FBPhysicsWorldSize.
Studio maps give every collision part its own static body, so large maps
(e.g. a 41k-part GTA IV export) hit the FatalAssert
"bodyIndex < m_freeBodyList.capacity()" in FBPhysicsWorld::asyncAllocateBody
and crash on load.
Hook fb::FBPhysicsWorld creation (RVA 0x02606ff0) and raise the size
descriptor before it is copied into the job arguments, so every free list
and per-body array is sized consistently. BodyCount becomes 65,000 (kept
below 16 bits); the pools a static mesh body also draws from (geometry,
owner, hull/mesh, shape, aggregate/fixed shape, fixed partition,
persistent spatial index) scale by the same factor, capped at 2^20.
Contacts, joints, queries and culling tables keep their native sizes.
Both native callers build the descriptor on their own stack, so raising
it in place only affects that world. Contract bytes for the entry, the
descriptor copy, the internal-body increment and both call sites are
validated before the hook is enabled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follow-ups to the trainer (PR 37), and one multiplayer change.
Off until asked for (Extension/Trainer)
- The jump read-out and the LB+RB controller shortcuts start off. Jumps
are logged only while the read-out or telemetry recording is on, and
trick launches only when the trainer scaled them. A saved trainer.json
keeps what the player chose.
Memory search on demand (trainer.cpp, trainer_classes.cpp)
- The search for the game's tuning classes reads all writable memory.
It ran for every player after a level loaded, and up to seven times
when it found nothing. It now runs only when a class value or the flip
speed is not the game's own (the player's, or a host's they skate
with) or the EVERYTHING list is open, and at most three times a level.
- For those players it runs again on each level, which brings fresh
copies; a search from an earlier level used to count as done.
Guests under enforced physics
- A guest is locked by the session's enforce setting, from the moment
of joining (Engine/Game/Multiplayer/session_physics.h), not by whether
the enforcement status text was set yet.
- A guest's class values no longer come back after a map change.
The host's whole setup reaches its guests (protocol 39)
- Only Gameplay/SkatePhysicsTuning travelled, so a host could push,
sprint or jump further than the guests it held to the game's own.
- New packet physics_extras (30): the host's class values, trick
multipliers and auto push (trainer_session.h), sent when they change
and to players who join later, taken from the host only. A dedicated
server's are the game's own and it ignores the packet.
- Under enforcement a guest's own trick multipliers stand down for the
host's, whatever the boosts switch says. With enforcement off nothing
changes.
- A guest refuses bytes that are not an encoding, holds values to the
trainer's own ranges, and leaves out class values written for another
game build's table.
- protocol_version 38 -> 39: released versions disconnect from a peer
that sends a packet they cannot decode.
Out-of-view players are posed every frame again (Session/session.cpp)
- Players outside the camera's view and past 8 m were sampled at 20 Hz
(b763631). The game's replays record each skater as it stood every
frame and their camera looks anywhere, so those players moved at
20 Hz in replays. Removed; the distance rule (20 Hz past 200 m, 10 Hz
past 350 m) stays. This gives back the client-tick time it saved on
busy servers.
Cap outfit packets and stop dropping recipients when a relay cannot be
built. Add per-player relay limits for outfits, skater sound and voice,
and a back-off for Steam IDs that keep failing to join.
Throwdowns: only participants can write rows, turns and scores; a copy
that fails to spawn no longer blocks the others.
Throttle remote skater and skateboard re-creation, cap remote objects
per session, and show developer and friend marks only for identities
Steam vouches for. Route all connections through Steam's relays.
Dedicated server: validate map changes, clean player names, keep vote
cooldowns across reconnects, and cut ban labels on a character boundary.
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.
Presets, trick sliders and the values share one screen and one set of values, so a preset
cannot say "on" while a slider says otherwise. Every built-in preset is a dial (1 = the game's
own). Reset everything clears locks and trick sliders too. A player's grinds, pumping, climbing,
vaults and bails confirmed 56 more class values as read by the game (243 of 314).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018szL7EmApPZxh9GK4Phw6x
A mod that adds songs can ship reskate-music.json
({"schema":1,"playlists":[{"name","songs":["artist - title", ...]}]}). Each
playlist is merged into the game's music screen the way a content-cache
station is, under the id "mod:<mod folder>:<name>", with its songs matched
against the songs the game has registered; entries that match nothing are
ignored and a playlist left empty is dropped. A file of the wrong shape is
logged and skipped.
The file is read by parse_music_playlists in Engine/Vfs/mod_music, which a new
test covers (ids, which entries survive, the limits, and that a malformed
document is refused). The format is described in the README.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Song records in the content cache carry a field-10 message (10 artist, 11 title,
12 cdn:/ cover art). Read the art per song and send it as the song's
presentation field 12, resolved to the CDN rendition like a station's art, so
the game's music screen and now-playing toast show covers instead of a black
square. The catalogue test pins one song's art.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The music menu listed only the playlists the game's own music assets declare.
The licensed stations (San Van Soundtrack, 80's Mixtape, ...) live in the
content cache's music chunk: a record with the station id (field 1), its tracks
(repeated field 2, "Artist - Title") and a message with its display name and
cdn:/ artwork (field 10, parts 10 and 11). Read them into Catalogs, merge their
membership into the music catalogue (only songs the game registered, so no
unknown id is referenced), and send the name and artwork with each playlist.
The catalogue test pins two stations from the cache.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The game keeps much of its tuning in data-defined classes (push speeds,
everything on foot, dive and glide, bail speeds...). The trainer finds 24 of
them in memory by the defaults and field order the game's data ships and lists
their 314 values by name. Push speed now drives the push class's own speeds;
auto push, off-board jump height and wider trick height ranges come with it.
The Tune tab opens on a Realistic or a Fun short list. Built on ReSkate 1.0.7.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018szL7EmApPZxh9GK4Phw6x
Two fixes taken from worthlessnorms' pull request 16. The bug it was
opened for, rewritten copies read back from the mod's folder, was fixed
separately in 6fd3845.
Mod merge (Engine/Vfs/mod_merge_cas_store.cpp, mod_merge_combine.cpp)
- To combine two mods' edits the merge reads each mod's copy from the
mod's own folder, which means turning its placement in the merged
patch back into the mod's archive and offset. It took the first of the
mod's blocks in that patch archive. A mod added while the game runs
has all its archives appended to the patch's archive 1, so a copy in
its second archive was looked for in its first, the read failed, and
the asset kept the highest-priority copy.
- unshift() picks the block the offset falls in. Of two blocks that
start at the same byte the later one is used: the earlier is an empty
archive.
- Reproduced with a cosmetic mod rebuilt as two archives and added to a
running merge: cas_main_bundlereftable "could not be combined (Cannot
read ...\cas_01.cas)" before, "combined 2 edits (101 preset(s))"
after. A launch merge gives every archive an index of its own and was
not affected; 40 installed mods merge exactly as before.
- mod_merge_unshift_tests covers an archive with its own index, three
appended blocks, bytes before the first block, and an empty archive.
Tests (CMakeLists.txt, Extension/Multiplayer/Test/mesh_tests.cpp)
- multiplayer_lobbies did not link: steam_lobbies.cpp calls
contains_bad_words and the target did not link dingosdk_word_filter.
- multiplayer_mesh did not link: four functions the session code calls
had no stub. Two are the ones the pull request adds
(update_developer_hoodie, remote_skater_generation); the developer
board has since added update_developer_board and remote_board_entity.
- Both targets build and their tests pass.
Co-authored-by: worthlessnorms <110003169+worthlessnorms@users.noreply.github.com>
A cosmetic a mod installs is in no retail catalogue, so it carries no
rarity and its card shows none. Collector is the game's own sixth tier,
already styled and already worn by 395 retail items: it reads as
something apart without claiming to be Legendary, and invents nothing.
- A rarity_id from the content cache or the profile still wins, so
letting mod authors declare one later needs no change here.
- Guarded on catalogs.available: with no pack installed
reserved_cosmetic() answers false for everything, and every retail item
would have turned Collector.
- Build-kit objects share this message and are not cosmetics, so they are
left alone.
With several cosmetic mods installed the game crashed as soon as an
equipped custom item came into view, and so on every launch after it
was equipped.
Cosmetic presets (Engine/Resource/bundle_ref_table.*)
- The character bundle-reference table says which bundle holds each
preset, under the preset's full path and under its leaf name, one row
per hash. Mods number their presets alike in different folders
(cust_tops/1_ap, cust_bottoms/1_ap), so two mods can share a leaf
name. Adding the second preset threw "collides with an existing
lookup", the merge fell back to the highest-priority mod's copy of
the table, and every other cosmetic mod's presets were unregistered.
The game then built the skater with a preset it could not find and
called through a bad pointer.
- A preset whose leaf name is taken is now added under its full path
only; the leaf name stays with the preset that had it. Only a clash
of full paths still fails. MergedTable::shadowed lists each such
preset with the one that holds its name.
Merge log (Engine/Vfs/mod_merge_combine.cpp)
- Each mod with shadowed presets gets a line naming how many and an
example pair, so an item that shows the wrong preset can be traced to
the mods that clash.
Checked against 39 installed mods, 15 of them cosmetic: the table
combines 274 presets where it used to keep one mod's, all reachable by
full path and 267 by leaf name, with the game's own 6264 rows unchanged.
The seven left without a leaf row are a naming clash between the mods
themselves (two Carhartt packs, a Zero tee and deck); whether the game
finds those by full path is not known.
bundle_ref_table_tests takes an optional second mod and checks that two
mods' presets are all added, in either order, and that a shared leaf
name is reported with the preset that holds it.
Videos exported from the replay editor played two to three times too
fast unless the frame rate was capped, or the export was slow enough to
hold it down by itself (4K, WebM). The fault is in the game's own export
path.
Replay export (Extension/Rendering/replay_export.*)
- While exporting, the game steps the replay by one frame of video
(speed / fps) for every frame it renders. The engine's movie encoder
takes a frame only while a permit byte is set; a world update sets it
once, and a frame submitted without it is dropped. Rendering two or
three frames per update, most of a clip never reached the file, and an
MP4 is exactly as long as its frame count. One clip came out as 181
frames at 30 fps and 297 at 60, 6.0 s and 5.0 s.
- The encoder's frame submit is detoured and the permit set before the
original runs, for the replay editor's encoder only (matched by its
vtable). The frame is encoded on the render thread before the call
returns, so the renderer is held to the pace the encoder keeps and no
frame is lost. The replay still runs fast on screen during an export:
that is the encoder's speed, not the file's.
- The same clip afterwards, frame rate uncapped, MP4 1080p: 505 frames
at 30 fps and 1008 at 60, 16.8 s both, audio as long as the video.
WebM and 4K take the same path and were not run again.
- Addresses are in Engine/Game/Build/20260929/replay_export.h. The hook
starts with the others in runtime_init.cpp.
- Conflict in launcher_support.h: kept Linux guards, took upstream's
validated_remote_load_library(note) signature + comment.
- Upstream's path_utf8() helper is portable; merged cleanly into the
Linux closure. Build + 8/8 portable ctest green.
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.
A mod the merge could not use was left out silently. The game started
without it, three minutes into a loading screen, and only ReSkate.log said
which one.
Merging before launch
- PLAY runs the merge the game would have run behind its splash, on the
launcher's worker, with the step and progress on the STATUS tile
(Phase::merging). The launcher links dingosdk_mods for it, which costs
about 600 KB of exe.
- When every mod merges, the launch goes ahead and the game's own
load_catalog finds the patch already built, so the splash is shorter
rather than the work being done twice.
- When mods were left out, the launch stops at Phase::mods_broken and a
dialog names each one and its first problem, with three ways on: switch
them off and play (which writes mods.json, so the next launch does not
rediscover it the hard way), open the mod manager, or play anyway.
- Mods built for another game version do not block. They already have
their own notice and message box, and blocking on them would stop play
after every Steam update.
- The launcher resolves -dataPath out of the extra-arguments setting, so
it merges against the root the game will read.
One SDK identity for both binaries
- Two places recorded "which ReSkate did this" as the module the code was
running in, which is ReSkate.dll in the game and ReSkateLauncher.exe in
the launcher now that both merge. merge_fingerprint hashes it, so the
game matched no stamp the launcher wrote and merged everything a second
time; sdk_identity keys the exclusions file, so each would have called
the other's exclusions stale.
- Both now name ReSkate.dll beside the running executable. Skate.exe and
ReSkateLauncher.exe sit beside it and launch.cpp enforces that, so the
two agree, and unlike the data root it does not move under -dataPath.
The value is unchanged from what the runtime computed before, so nothing
re-merges on upgrade.
Naming the mod
- In game, a mod left out of the merge altogether said nothing on screen:
only a mod that merged partially did, which is the lesser failure. Both
are named now.
- The MOD MANAGER tile says which mod did not load, in the detail line's
colour rather than a pill, clipped so a long name cannot run under the
tile's artwork.
- MY MODS carries a line naming them above the list, and NOT LOADED and
OUTDATED are badges on the row instead of words at the front of its
summary.
MY MODS reaches Thunderstore parity
- Rows carry the package's icon, the same one the store shows, through a
shared mod_icon(); a mod that never came from Thunderstore keeps the
placeholder.
- An update is a button on the row, not a badge you had to open the mod to
act on, and every row has Uninstall. Rows grew to fit, and "Remove" is
"Uninstall" throughout.
- No pill for a mod that is up to date: the absence of an update button
says it, and eight green pills said nothing eight times.
Fixes
- The install panel was drawn on the Mods page's own draw list after its
children, and ImGui renders a child after its parent, so it was painted
behind the mod rows along with its Cancel button: installs ran with no
sign of it. It is its own window over the page now.
- pump_icons, which uploads decoded icons, only ran on the store page, so
MY MODS showed placeholders until a visit to GET MODS uploaded them for
it. The page does it for both lists.
- The merge dialog's closing advice overlapped its buttons.
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.
The game hands its auto push flag to the animation and nothing comes of it.
With it on, the trainer's push code now brings a rolling skater that is not
braking up to the auto push speed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The game's trick scripts launch the no comply and the boneless through its
jump-trajectory setter; the trainer multiplies the launch speed there, told
apart by the physics states around the call. Pushing is scripted too, so the
push tuning values were never read: the top pushing speed is now applied after
each physics step, and push strength is hidden.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The hippy jump's height is set by the game's trick scripts, not by the tuning asset (traced:
no tuning value is read for it). The trainer recognises one starting (riding -> off board,
rising) and scales the off-board upward velocity through the existing off-board velocity
path; a plain dismount is not rising and is left alone.
The jump read-out gains degrees of spin (and the peak rate) and of body tumble. The table of
tuning values the game reads now includes offsets seen in traced play sessions.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
JumpMaxHeight, JumpMinHeight and MaxSpinSpeed are not read by the game. Editing one now
scales the jump-height or body-spin graphs by the same ratio, so they work as plain
values; presets go through them. A preset reports the locked values it left alone.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Players changed values such as the Mode ollie heights and saw nothing happen: no game code
reads them. trainer_used.inc lists the asset offsets the code was found to read (the asset
pointer, the per-group accessors and the skater's mode pointer were followed); rows without
evidence are hidden by default and marked when shown. The Essentials list and the built-in
presets now only use values with evidence.
Switching one preset off keeps the values of the presets that are still on.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
All new code is in Extension/Trainer. The game thread owns the trainer's state; the menu
page only queues `trainer ...` console commands and reads two snapshots.
- physics_tuning_model keeps the field and curve names the game's EBX carries, so the
editable table is built at run time from the player's own data.
- physics_tuning exports game_tuning / read_live / write_live / refresh_skater, with the
same field whitelist and curve rules as the session sync.
- no_bail publishes the physics state the local skater's selector last chose (air time,
wipeouts).
- A guest under host tuning cannot edit; teleports follow the host's noclip permission.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.