Throwdowns: plugin modes on the native Throwdowns page, which is SmokeyDawg58's logo grid

reskate.throwdowns.mode{...} adds a mode to the game's own Throwdowns page: a card on
the picker, the real Spot Battle setup page relabelled with the mode's name, logo,
description, players, timer and rows (sliders, choices, buttons), and the native start
flag. The plugin hears throwdown_mode events (opened, setting, button, placed with the
flag's position and heading, start, leave, cancelled) and runs the mode itself;
release_flag takes the flag down. The flag's queue never starts a Spot Battle: its
waiting card's Start goes to the plugin, MaxPlayers is held at 10 and it is never
relayed as a drop. After SmokeyDawg58's one_up_menu and one_up_placement
(reskate-custom-mods), generalised over a mode registry.

The page is his 4-column logo grid whether or not a plugin has a mode. Its widgets and
grid records, and the game assets it uses that only some levels load, ship in
ReSkate.dll as the throwdown_menu built-in pack, taken from his asset mod's FBMOD
(dingosdk_ebx_pick trims the asset list in place). A mode's logo is a texture path in
its own mod, so no mod edits the shared list. The FPV drone is no longer a built-in pack.

Native menu data: text initialization, change-only writes, retained array publication,
owned-model rollback. Tests: registry and setup helpers, the plugin API, a test plugin
with two modes.
This commit is contained in:
AyeZeeBB committed 2026-10-19 18:38:10 -05:00
1 parent 89281f62fb
commit 0ff96018d0
33 files changed
+3013 -16

No files matched your search

+52 -4
View File
@@ -1,9 +1,11 @@
#include "native_menu_data.h"
#include "native_array_publication.h"
#include "Engine/Game/Build/addresses.h"
#include "Engine/Game/Build/20260929/engine.h"
#include "Engine/Game/Build/20260929/native_menu.h"
#include "Engine/Game/Build/20260929/native_party.h"
#include <algorithm>
#include <atomic>
#include <cstring>
#include <set>
@@ -140,7 +142,23 @@ Value Context::element(Value array, unsigned index) const {
Value Context::create(Schema schema, std::uint64_t id) const {
const auto t = type(schema);
const auto h = game::native_data().models.create(manager, t, id, game::native_name_hash("ReSkate.NativeMultiplayerMenu"), false, 2);
Value result{h, t}; address(result); return result;
Value result{h, t}; address(result); initialize_text(result); return result;
}
void Context::initialize_text(Value value) const {
if (kind(value.type) == 7) {
if (!read<Address>(address(value))) text(value, "");
return;
}
if (kind(value.type) != 2) return; // Asset/DataRef pointers and counted arrays are not inline structures.
const auto meta = read<Address>(value.type), fields = read<Address>(meta + 0x60);
const auto count = read<std::uint16_t>(meta + 0x2a);
require(count <= 128 && (!count || fields), "Native menu text field table differs.");
for (unsigned i = 0; i < count; ++i) {
const auto entry = fields + i * 24;
const auto child_type = read<Address>(entry + 16);
if (kind(child_type) != 2 && kind(child_type) != 7) continue;
initialize_text(field(value, read<std::uint32_t>(entry)));
}
}
void Context::destroy(Value value) const {
// The engine may already have removed a previous pause-menu generation.
@@ -162,6 +180,9 @@ void Context::copy(Value target, Address source) const {
std::vector<std::byte> bytes(size(target.type));
require(memory::read_bytes(source, bytes.data(), bytes.size()), "Native menu template is unavailable.");
publish(target, bytes.data());
// Some authored records carry null optional CString defaults. Give the
// copy engine-owned empty strings before it is attached to a widget.
initialize_text(target);
}
void Context::text(Value target, const std::string& text) const {
require(kind(target.type) == 7 && text.size() <= 2048, "Native menu text schema differs.");
@@ -188,14 +209,35 @@ void Context::array(Value target, std::span<const std::byte> bytes, unsigned cou
require(kind(target.type) == 4 && count <= 1024, "Native menu list is invalid.");
const auto stride = size(read<Address>(read<Address>(target.type) + 0x30));
require(bytes.size() == static_cast<std::size_t>(count) * stride, "Native menu list element size differs.");
// The engine deep-copies strings, delegates, DataRefs and nested arrays
// synchronously. No pointer into this temporary storage survives publish.
address(target);
std::vector<std::uint64_t> storage((bytes.size() + 15) / 8);
const std::array<std::uint32_t, 2> header{count, count};
std::memcpy(storage.data(), header.data(), 8);
if (!bytes.empty()) std::memcpy(storage.data() + 1, bytes.data(), bytes.size());
const auto pointer = storage.data() + 1;
set(target, pointer);
const auto& functions = game::native_data().models;
if (!functions.create || !functions.destroy) { set(target, pointer); return; }
// Native array publication (0x19289d0) resizes and destroys the
// destination rows (0x19158c0) before it copies the source rows. Raw row
// bytes alone cannot protect a later live row's text/asset arrays when it
// moves to an earlier slot. Stage them in an owned root of the array's own
// type while every source owner is live, then publish that retained copy.
static std::atomic<std::uint64_t> next_array{0x4e4d415252000000ULL};
Value retained;
publish_retained_array(
[&] {
retained = {functions.create(manager, target.type, next_array.fetch_add(1, std::memory_order_relaxed) + 1,
game::native_name_hash("ReSkate.NativeArrayPublication"), false, 2), target.type};
return retained;
},
[&](Value owned) { set(owned, pointer); },
[&](Value owned) { publish(target, reinterpret_cast<const void*>(address(owned))); },
[&](Value owned) noexcept {
if (!owned.handle || type_of(owned.handle) != owned.type) return;
std::array<Address, 2> iterator{};
functions.destroy(manager, iterator.data(), owned.handle, 0, false, false);
});
require(!retained.handle || type_of(retained.handle) != retained.type, "Native menu list source could not be released.");
}
Address named_asset(const Context& context, Address anchor, const char* name) {
const auto find = game::native_data().find_asset;
@@ -456,6 +498,12 @@ bool publish_tab_icons(const Context& context, Value tab, std::string_view idle,
context.set(context.field(tab, 0xbc92d1fa), assets[1]); // FocusIcon.
});
}
bool publish_texture(const Context& context, std::span<const Value> targets, std::string_view name) {
return with_texture_assets(context, std::array{name}, [&](const auto& assets) {
for (const auto target : targets)
if (read<Address>(context.address(target)) != assets[0]) context.set(target, assets[0]);
});
}
bool publish_texture(const Context& context, Value target, std::string_view name) {
return with_texture_assets(context, std::array{name}, [&](const auto& assets) { context.set(target, assets[0]); });
}