Throwdowns: the throwdown kit, so a mode is a plugin and the SDK knows no particular mode

Plugins register modes (card, replaces, order, flag, players) and publish documents through
reskate.throwdowns.ui: the setup page, the waiting card, HUD slots themed on the game's HUD_<theme>_*
widgets, the match panel, flags and match restrictions. SmokeyDawg58's native plumbing
(throwdown_menu.cpp and its .inl files) renders them and posts throwdown_ui events back. kit::flag
is the one native flag registration. General services added for it: reskate.files, reskate.ui.dialog,
reskate.skater push/hold/bone_cam, reskate.camera.pad_flight, reskate.physics competition,
reskate.session.world, reskate.tricks feed/gesture, and world entities.

His ten modes are now Lua in his plugin: the rules, referee and wire (td/rules, referee, wire), and
every page and HUD document (td/ui*.lua). Extension/Modes and reskate.modes are gone, along with
every race and 1-UP branch in the SDK. The plugin test drives each mode through kit events.

A native setup page is never written to while it closes into flag placement: its rows are not
republished, the stock page is put back once the exit is over, and the stock Players slider is left
alone while a plugin flag is out. This fixes the crashes on the Deathrace button and on 1-UP's
Confirm.
This commit is contained in:
AyeZeeBB committed 2026-10-20 01:04:54 -05:00
1 parent e14064250c
commit 8444553402
186 files changed
+28015 -2747

No files matched your search

+52 -12
View File
@@ -1,8 +1,9 @@
#include "native_menu.h"
#include "native_menu_internal.h"
#include "native_menu_dump.h"
#include "ui_sound.h"
#include "throwdown_menu.h"
#include "Extension/UI/own_ui_writes.h"
#include "ui_sound.h"
#include "Extension/Multiplayer/Session/session.h"
#include "Extension/Multiplayer/Hud/game_ui_state.h"
#include "Engine/Core/Log/logging.h"
@@ -26,7 +27,7 @@ constexpr std::string_view store_tab_label = "ID_MENU_STORE", social_tab_label =
constexpr unsigned social_tab_index = 4; // Before the Store is removed.
constexpr std::array<unsigned, page_count> page_slots{0, 1};
// The Store item taken out of the pause menu, its index there, and that menu.
struct RemovedStore { std::vector<std::byte> item; unsigned index{}; Address core{}; };
struct RemovedStore { Value item; unsigned index{}; Handle core{}; Address manager{};std::uint32_t key{}; };
RemovedStore removed_store;
// When insert() last left a copy of that Store item last in the menu (0: not there); trim_store drops it.
std::uint64_t store_last_since{};
@@ -140,6 +141,15 @@ void ensure_body(const Context& context) {
context.set(context.path(s.page, {content, 0x18f8355b}), static_cast<int>(s.section));
context.set(context.path(s.page, {content, 0x55511280}), static_cast<int>(s.section));
}
std::vector<std::byte> removed_store_bytes(const Context& context) {
require(removed_store.manager==context.manager && removed_store.item.handle &&
context.type_of(removed_store.item.handle)==removed_store.item.type,
"Retained native Store item has expired.");
std::vector<std::byte> result(stack_item.size);
require(memory::peek_bytes(context.address(removed_store.item),result.data(),result.size()),
"Retained native Store item is unavailable.");
return result;
}
void tab_label(const Context& context, Value item, const std::string& name, std::uint32_t key) {
const auto tab = context.field(item, tab_data);
context.text(context.path(tab, {label, text_field}), name);
@@ -278,7 +288,24 @@ void insert(const Context& context) {
if (const auto store = find_tab(context, list, count, store_tab_label);
store && !missing.empty() && count - 1 + missing.size() >= 5) {
const auto at = values.begin() + static_cast<std::ptrdiff_t>(*store) * stride;
removed_store = {std::vector<std::byte>(at, at + stride), *store, s.core.handle};
// The live Store row is about to disappear. Its TabData includes
// owned strings/delegates/texture arrays, so retain one typed row for
// this menu instead of keeping borrowed bytes until level teardown.
const auto checkpoint=s.owned_models.checkpoint();
auto retained=removed_store.item;
if(removed_store.manager!=context.manager || removed_store.core!=s.core.handle ||
!retained.handle || context.type_of(retained.handle)!=retained.type)retained={};
try {
if(!retained.handle)retained=make(context,stack_item);
context.copy(retained,context.address(context.element(list,*store)));
} catch(...) {
s.owned_models.rollback(checkpoint,[&](Value value){context.destroy(value);});
throw;
}
const auto key_offset=context.member(context.type(stack_item),key_field).offset;
std::uint32_t store_key{};
std::memcpy(&store_key,values.data()+std::size_t{*store}*stride+key_offset,sizeof(store_key));
removed_store = {retained, *store, s.core.handle, context.manager, store_key};
values.erase(at, at + stride);
--count;
// The game has already opened the page at its selected index, which it keeps
@@ -300,7 +327,8 @@ void insert(const Context& context) {
++count;
}
if (store_last) {
values.insert(values.end(), removed_store.item.begin(), removed_store.item.end());
const auto retained=removed_store_bytes(context);
values.insert(values.end(), retained.begin(), retained.end());
++count;
store_last_since = GetTickCount64();
}
@@ -321,8 +349,9 @@ void insert(const Context& context) {
}
// The copy is the item last in the stack with the removed Store's key.
bool is_store_copy(std::span<const std::byte> item) {
return removed_store.item.size() == item.size() &&
std::equal(item.begin() + 0x568, item.begin() + 0x56c, removed_store.item.begin() + 0x568);
if(!removed_store.item.handle || item.size()!=stack_item.size)return false;
std::uint32_t key{};std::memcpy(&key,item.data()+0x568,sizeof(key));
return key==removed_store.key;
}
// Drops the Store copy insert() left last, a few frames after the stack saw it there.
void trim_store(const Context& context, Value menu) {
@@ -332,7 +361,8 @@ void trim_store(const Context& context, Value menu) {
auto values = context.array(list, 16, count, stride);
store_last_since = 0;
// Not there: the game rebuilt its tabs, the Store back in place, as the menu reopened.
if (stride != stack_item.size || count < 6 || !is_store_copy(std::span(values).last(stride))) return;
if (stride != stack_item.size || count < 6 || removed_store.manager!=context.manager ||
removed_store.core!=menu.handle || !is_store_copy(std::span(values).last(stride))) return;
const auto target = context.path(menu, {content, 0x18f8355b});
const auto selected = read<int>(context.address(target));
if (selected == static_cast<int>(count - 1)) {
@@ -657,7 +687,7 @@ bool prepare_native_menu_level_load(std::uintptr_t base) noexcept {
for (unsigned i = 0; i < count; ++i) {
const auto first = values.data() + i * stride;
// A Store copy insert() left last: the Store goes back to its own index below.
if (removed_store.core == s.core.handle && i + 1 == count &&
if (removed_store.manager==manager && removed_store.core == s.core.handle && i + 1 == count &&
is_store_copy(std::span(first, stride))) continue;
std::uint32_t key{}; std::memcpy(&key, first + key_offset, sizeof(key));
if (key >= our_key && key < our_key + page_count * 0x100 && (key - our_key) % 0x100 == 0) {
@@ -667,11 +697,13 @@ bool prepare_native_menu_level_load(std::uintptr_t base) noexcept {
retained.insert(retained.end(), first, first + stride);
}
// Put the native Store item back at its original index: it is out while ours are in.
if (ours && removed_store.core == s.core.handle && removed_store.item.size() == stride &&
bool restored_store{};
if (ours && removed_store.manager==manager && removed_store.core == s.core.handle && removed_store.item.handle &&
removed_store.index * stride <= retained.size()) {
const auto store=removed_store_bytes(context);
retained.insert(retained.begin() + static_cast<std::ptrdiff_t>(removed_store.index) * stride,
removed_store.item.begin(), removed_store.item.end());
removed_store = {};
store.begin(), store.end());
restored_store=true;
}
if (retained.size() == values.size() && std::equal(retained.begin(), retained.end(), values.begin())) return;
// Return focus to the first native tab before removing our
@@ -679,6 +711,9 @@ bool prepare_native_menu_level_load(std::uintptr_t base) noexcept {
context.set(context.field(stack, 0x18f8355b), 0);
context.set(context.field(stack, 0x55511280), 0);
context.array(list, retained, static_cast<unsigned>(retained.size() / stride));
// Keep the retained source and restoration identity if
// publication failed, so a cleanup retry can still restore it.
if(restored_store) {removed_store={};store_last_since=0;}
}, [&](Value value) { context.destroy(value); });
s.core = {}; s.page = {}; s.menu_item = {};
s.rows.clear(); s.assets.clear();
@@ -716,13 +751,18 @@ overlay::HubPage native_menu_page() noexcept {
return {true, page_shown.load(std::memory_order_relaxed)};
}
void tick_native_menu(std::uintptr_t base, bool loading) noexcept {
const ui::OwnUiWrites own_writes; // Custom labels are not the native trick list.
run_pending_ui_dump(base);
// The menu sounds the overlay's pages asked for (ui_sound.h).
tick_ui_sounds(base, loading);
// (Read here: it takes the UI model's lock, which a page's tick then holds.)
game_in_menu.store(!loading && sample_game_ui_state(base).in_menu, std::memory_order_relaxed);
if (lifetime.blocked()) return;
tick_native_throwdown_menu(base, loading);
tick_native_throwdown_menu(base, loading, game_in_menu.load(std::memory_order_relaxed));
if (native_throwdown_match_active()) {
page_seen.store(0,std::memory_order_relaxed);
return;
}
for (unsigned slot = 0; slot < page_count; ++slot) {
current_state = &page_state(slot);
current_state->slot = slot;