Resolve private effects only from the level-wide Game compartment and validate
their resident declarations with the same parser used by the mod merge.
Release surviving native services during world changes, check decal realm
ownership before cleanup, defer ordinary off-thread releases, and prune expired
VM handles so they return their quota. Size the temporary decal allowance from
the live count while preserving external settings changes.
Gate contact geometry behind events.physics.geometry, document the manifest
migration, and test Blood impacts through the plugin host using its own manifest.
Add regression coverage for residency, lifetime, quota and draw-budget handling.
Validation: Release builds of the runtime and plugin/effect test targets succeed.
The plugins, effect_geometry and effect_lifetime tests pass. Existing MSBuild
asset-directory dependency warnings remain; native visuals need an in-game check.
grind_exit gave way to shape_launch upstream, so Enums.GrindSide becomes
Enums.Launch, the words shape_launch checks; Delete joins Enums.Key.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
skater.pose's places are worked out with the pose's scale in them (keep_children, or a joint placed
relative to another joint or the world under a scaled one), but the others were sent only rotation
and position, so such a joint landed up to a thousand times its distance away on their screens. A
place may also be far off in the parent's frame, which drew a skater's arms and head across the map
for everyone. The sent pose now carries each joint's scale (0.001 to 10), and a joint's offset from
its parent is cut to 4 m in what is sent. What the player sees locally is unchanged.
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).
Since the declarations were read from the applied catalogue, a file that could not be read or
parsed threw out of the whole refresh, so no mod's declared inputs applied (from launch, or after a
live apply). Each file is read on its own again: a broken one is logged with its mod and skipped,
and only a failed read of item ownership keeps the previous set.
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.
Add permission-gated effect handles, native surface attachments, and world lifetime cleanup. Extend physics_step with optional contact geometry and body poses. Document the generic API and validate it with self-contained plugin and geometry fixtures.
So far a plugin could only add to what the game's animation does with a joint. Now it can give the
joint its own transform, which the animation's is replaced by: its own animations, joints reused for
body parts the skater doesn't have, joints moved by a simulation in Luau, joints pinned to a point.
A joint's entry takes place = {position, rotation | angles, relative_to} and keep_children = true
(its children stay where they were); a pose takes up to 128 joints. Each frame the place is turned
into the joint's value under its real parent, the joints posed in an order that puts each after its
posed parents and the joint it is placed relative to; a pose without such an order is refused with
the reason.
Extension/Skater/pose_place.h/.cpp hold the maths, with pose_place_tests; skater_animation applies it
after a joint's offsets; plugin_api_world reads it and sorts the joints.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
2.1.2 left such an actor alive and forgotten. When the level went, the game released its parent
through the same pointer, took one off the byte below the parent's vtable pointer and called
through it (Skate.exe+0x2b8092f and +0x2b7fcfb, every report after the "link ... is damaged"
warning with the same bus). A link exactly one less than the parent the bus also holds at +0x48 is
now repaired before the destroy; other damage is still left alive. What shortens the link is not
known yet.
The skater sound state sent the packed material words of the surfaces under a skater as they were.
A Studio map's surfaces are appended to the root grid from slot 116 on, numbered by each install's
own set of map mods, so the same word named another surface on the other side, or a slot past the
end of its grid, and the game's material lookups (0x7111c0, 0x45e120) read past MaterialIndexMap
(Skate.exe+0x41da7b and +0x6fc93b, 240 installs in a day on 55a7342, on Tokyo 2026, Community
Center, Olay Way and other maps with their own surfaces). Those words now cross as the grid's
default, when sent and when received, so older clients sending their own numbers are covered too.
reskate.outfit.worn() is meant to list full-body costumes: it has a "Costume" slot for them, between
the head and the feet. But no costume ever reached it. With a costume on, the list came back without
it, so a plugin that goes with a costume mod could not tell whether its costume was worn. Now it is
listed like any other item: {slot = "Costume", item = "Own_Hobkin", title = "Hobkin"}.
Why: the list leaves out the body's own parts (head shape, nose, skin...) by their catalog categories,
which start with "cust" (custhead, custnoses, custbodies...) or name the body. A full-body costume's
category is "cust_fullBodyCostume", so it matched both and was dropped before its slot was looked up.
A category that names a costume is now kept; everything else is decided as before.
The rule (category to slot) moves out of worn_items.cpp into outfit_kind.h as it was, so it can be
tested without the game: outfit_kind_tests (with the plugin tests) checks clothes by their slots, the
costume, the body's own parts, and an unknown category.
Checked in the game: with a costume mod's costume on, outfit.worn() returns
Costume | Own_Hobkin | Hobkin, where it returned nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
skater.pose changes the skater relative to the game's animation. Some of those changes were applied
twice every third frame, so a posed skater shook in a three-frame rhythm, and their board with them.
The more a plugin changed, the worse it got: an avatar with the hips scaled and offsets on most of
the body needed workarounds in its plugin to hold still. Now every pose is applied exactly once, and
a plugin can give skater.pose the values it means.
Why: ReSkate draws a pose by writing it into the game's bone buffer just before the render, and the
game evaluates the next frame from that same buffer without overwriting all of it. Two of three
frames it resets the buffer itself; in the third the pose drawn was still there and the next pose
went on top of it. Now the game's own values go back as soon as the renderer has its copy, so the
game, the body physics and the next animation update only ever see the game's pose.
This replaces own_scale, the earlier fix for the same cause (the Filmer's forearm at 0.001 shrinking
to nothing). It covered only scale, and put it back after the game had already run on the posed
values, so rotations and offsets still doubled. The new way covers every part of every joint, for
pose, carry and reach, with nothing to track per joint and nothing to clean up after a pose ends.
Checked in the game: the avatar with the hips at 0.667 and offsets on 48 joints holds still with
only skater.pose (shoulder distance 0.117 m every frame, where it was 0.117 0.117 0.067 ...). The
Filmer 1.0.0's camcorder preview looks and measures as on main, and the skater is back to normal
after it.
Details: the renderer copies the bones in submit_render_pose (0x4c7e7a0), which the render pose
caller (0x2c20d99) calls right after render_pose with the same render data; found with a guard page
on the bone buffer that logged every access with its call stack. native_skater hooks it and, for the
render data the pose override just wrote, calls the new set_pose_override_drawn_listener.
skater_animation keeps what on_render wrote over and puts it back in on_drawn.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Installs, updates and uninstalls are jobs run one after another in the order asked for: asking for
one while another runs queues it instead of being refused. A job waits while the mods are applied or
a session's map downloads, and works out what its mod needs when it starts, from the Mods folder as
it is then. Buttons say ADD TO QUEUE while a job runs and QUEUED once in line; the foot of the page
says how many wait, and CANCEL ALL empties the queue.
The carousel's middle card: the mod's picture as its backdrop, darkened, and big as a tipped sticker
beside the text; a FEATURED and INSTALLED / UPDATE AVAILABLE tag, the facts as chips, as much of the
description as fits above the buttons, and the buttons under the text with the pad's prompts beside
them.
The front page stopped after four category rows. Now every category has one, the biggest first,
after Most popular, Recently updated and New; mod packs, libraries, tools and AI generated stay off
the front page (still found by search and the CATEGORIES panel).
The map grids (Mod Options' MAPS, the ReSkate menu's world page) found a custom map's icon among the
mods merged at launch, so a map installed or switched on since had none. The live apply now keeps
which mod each level comes from (live_mods::level_mod_directory), and map_icons asks it, falling
back to the launch's list before any live apply.
What the live apply already does without one, how the engine loads and holds bundles (the resource
manager's load operations, the level's compartments), why cosmetics need the reload (the root
level's cas_main_sharedbundle), and a plan for new items: a child BundleRefTable, a delta bundle
and the cosmetics manager. With experiments to run in game and the open questions. On hold for now.
The reload after a live apply was a console "load", which in a session either ended it (a guest's
map going away is "your local map changed"), was refused (a guest without map rights), or moved
everyone (a server admin's load changes the server's map; a lobby host's reload sent every guest
through it too).
Now client_tick tells the session first (multiplayer::begin_local_reload) and loads the same map
locally with no multiplayer rule (queue_local_load). The session keeps the reload as travelling in
the same world: a guest keeps telling the host or server it is loading (world_ready, so it does not
time out) and that it is back; a host says it is not in the world for that long, so its guests wait
instead of moving, and nothing is a host map change. A reload refused by the loader is taken back
(cancel_local_reload); one that never starts ends after 20 s; in no state to reload (changing maps,
joining) the mods show at the next level load. The dedicated server already handles a player
reloading on their own.
A third ReSkate tab in the game's pause menu (native_menu.cpp slot 2, the Store's tab art), drawn by
the overlay as the other two are (mod_manager_page.inl, included into hub_page.cpp):
- BROWSE: search, categories in a panel of their own (shown only, hidden or either, kept in the
profile), sort, a featured carousel and rows (most popular, recently updated, new, the biggest
categories), a grid for results, and a mod's own page with its README, what it brings with it,
and INSTALL, switch on or off and uninstall. Works from a controller: the carousel's card is
one stop that left and right turn, and B goes back from a mod's page (the page's Back is held
meanwhile).
- MY MODS: the Mods folder with the states the ReSkate menu shows; UPDATES: Update all.
- The foot of the page shows the install's progress and the apply's, and Apply (View on a pad).
Leaving the menu with changes not applied asks first (a two-answer card in message_card.cpp,
answered by the runtime as the message is); LATER closes the menu.
Behind it, Extension/Assets/mod_store: Thunderstore's listing (cached on disk), the installed mods,
and a queue that downloads and installs packages with their dependencies, updates and uninstalls
them. An update or uninstall of a mod the game holds open waits in Mods/.reskate-pending and the
launcher makes it before the next start (launcher_mods::apply_pending). Package icons load into
their own SRV slots (store_icons.cpp), sharing the launcher's icon cache.
Tests: mod_store_view (search, categories, the front page's rows, install plans), the pending
updates and uninstalls in mod_manager_tests, and the third tab in native_menu_view_tests.
- Forgetting uninstalled packs' records cannot close the launcher: a
brought-in.json that cannot be rewritten (a locked or read-only Mods
folder) is logged as a warning; the mods are gone already.
- A dependency the player had before is matched ignoring case, as Windows
matches folders: one installed under another spelling counts as installed
(it is not downloaded again or renamed) and is never recorded as brought in.
- UNINSTALL waits while an install runs, so the two never rewrite
brought-in.json at once.
- A mod pack installs from GET MODS: a package with nothing to load of its
own is taken when its manifest.json lists dependencies, as the install
options already said; it was refused, so no pack ever got a record.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With "Keep my other mods on when loading a profile", in_use compared every enabled mod in order, so
a player's own mod placed above the profile's mods made the profile read NO PROFILE IN USE although
all its mods were on, and loading it again moved that mod back down. Only the profile's own mods
count now: which are on, and their order among themselves.