mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-24 09:26:13 +02:00
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.
106 lines
5.5 KiB
C++
106 lines
5.5 KiB
C++
// Plugin throwdown modes without the game: the registry's checks and news, and the
|
|
// pure helpers the native setup page uses.
|
|
#include "Extension/Throwdowns/custom_modes.h"
|
|
#include "Extension/UI/NativeMenu/native_menu_presenters.h"
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <iostream>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
|
|
namespace {
|
|
using namespace dingosdk::multiplayer;
|
|
void check(bool value, const std::string& message) { if (!value) throw std::runtime_error(message); }
|
|
|
|
custom_throwdowns::Mode one_up() {
|
|
custom_throwdowns::Mode mode;
|
|
mode.plugin = "smokey";
|
|
mode.id = "oneup";
|
|
mode.name = "1-UP";
|
|
mode.description = "Beat the last total.";
|
|
mode.players = 9; // more than max: clamped
|
|
mode.timer = custom_throwdowns::Row{custom_throwdowns::Row::Kind::slider, "timer", "Turn", 10, 120, 10, 20};
|
|
mode.rows.push_back({custom_throwdowns::Row::Kind::choice, "tricks", "Tricks", 0, 0, 1, 5, {"All", "Flips"}});
|
|
mode.rows.push_back({custom_throwdowns::Row::Kind::button, "area", "Area", 0, 0, 1, 0, {}, "Set area"});
|
|
return mode;
|
|
}
|
|
|
|
void modes_are_checked_and_listed() {
|
|
check(custom_throwdowns::set_mode(one_up()).empty(), "1-UP registers");
|
|
auto bad = one_up();
|
|
bad.id = "a/b";
|
|
check(!custom_throwdowns::set_mode(bad).empty(), "an id with / is refused");
|
|
bad = one_up();
|
|
bad.rows.push_back({custom_throwdowns::Row::Kind::slider, "players", "Players", 1, 4});
|
|
check(!custom_throwdowns::set_mode(bad).empty(), "a row may not be called players");
|
|
bad = one_up();
|
|
bad.rows.push_back({custom_throwdowns::Row::Kind::slider, "huge", "Huge", 0, 100000, 1});
|
|
check(!custom_throwdowns::set_mode(bad).empty(), "a slider has at most 600 steps");
|
|
const auto mode = custom_throwdowns::find("smokey/oneup");
|
|
check(mode && mode->players == 6 && mode->rows[0].value == 1, "players and a choice are clamped");
|
|
const auto settings = custom_throwdowns::settings("smokey/oneup");
|
|
check(settings.at("players") == 6 && settings.at("timer") == 20 && settings.at("tricks") == 1 && !settings.contains("area"),
|
|
"settings start at the defaults, without buttons");
|
|
}
|
|
|
|
void settings_and_news_reach_the_plugin() {
|
|
const auto before = custom_throwdowns::revision();
|
|
custom_throwdowns::set_setting("smokey/oneup", "players", 3);
|
|
custom_throwdowns::set_setting("smokey/oneup", "players", 3); // unchanged: no news
|
|
custom_throwdowns::News placed;
|
|
placed.action = "placed";
|
|
placed.at = std::array<float, 3>{1, 2, 3};
|
|
placed.heading = 90;
|
|
custom_throwdowns::post("smokey/oneup", placed);
|
|
const auto news = custom_throwdowns::take("smokey");
|
|
check(news.size() == 2, "one setting and the flag");
|
|
check(news[0].mode == "oneup" && news[0].action == "setting" && news[0].row == "players" && news[0].value == 3, "the setting");
|
|
check(news[1].action == "placed" && news[1].at && (*news[1].at)[2] == 3 && news[1].settings.at("players") == 3,
|
|
"the flag, with the settings chosen");
|
|
check(custom_throwdowns::take("smokey").empty() && custom_throwdowns::take("other").empty(), "taken once, by its own plugin");
|
|
// An update keeps what was chosen; a reset goes back to the defaults.
|
|
auto again = one_up();
|
|
again.description = "New words.";
|
|
check(custom_throwdowns::set_mode(again).empty() && custom_throwdowns::revision() != before, "an update");
|
|
check(custom_throwdowns::settings("smokey/oneup").at("players") == 3, "keeps the players chosen");
|
|
custom_throwdowns::reset_settings("smokey/oneup");
|
|
check(custom_throwdowns::settings("smokey/oneup").at("players") == 6, "a reset starts over");
|
|
custom_throwdowns::remove_plugin("smokey");
|
|
check(custom_throwdowns::modes().empty() && !custom_throwdowns::find("smokey/oneup"), "a plugin's modes go with it");
|
|
}
|
|
|
|
struct Presenter { std::uintptr_t blueprint{}; struct { std::uintptr_t record{}; std::uint64_t handle{}; } data; };
|
|
void setup_page_helpers() {
|
|
using menu_data::native_slider_choice, menu_data::native_slider_position;
|
|
check(native_slider_choice(0.f, 10, 120, 10) == 10 && native_slider_choice(1.f, 10, 120, 10) == 120, "slider ends");
|
|
check(native_slider_choice(native_slider_position(70, 10, 120), 10, 120, 10) == 70, "a choice survives the round trip");
|
|
check(native_slider_choice(.52f, 1, 7, 1) == 4, "rounds to the nearest step");
|
|
check(native_slider_choice(std::nanf(""), 1, 7, 1) == 1, "a broken value is the minimum");
|
|
// Two 32-byte rows with a presenter at 8: the same presenters need no publish; a new one keeps the rest.
|
|
std::vector<std::byte> live(64), saved(64);
|
|
const Presenter a{1, {0, 10}}, b{2, {0, 20}};
|
|
std::memcpy(live.data() + 8, &a, sizeof a);
|
|
std::memcpy(saved.data() + 8, &a, sizeof a);
|
|
live[0] = std::byte{7}; // native focus state
|
|
check(!menu_data::remount_presenters<Presenter>(live, saved, 32, 8), "same presenters: nothing to publish");
|
|
std::memcpy(saved.data() + 40, &b, sizeof b);
|
|
const auto next = menu_data::remount_presenters<Presenter>(live, saved, 32, 8);
|
|
check(next && (*next)[0] == std::byte{7}, "a new presenter keeps the live focus bytes");
|
|
check(menu_data::remount_presenters<Presenter>(live, std::span(saved).first(32), 32, 8)->size() == 32, "another count takes the saved rows");
|
|
}
|
|
} // namespace
|
|
|
|
int main() {
|
|
try {
|
|
modes_are_checked_and_listed();
|
|
settings_and_news_reach_the_plugin();
|
|
setup_page_helpers();
|
|
std::cout << "custom throwdown checks passed\n";
|
|
return 0;
|
|
} catch (const std::exception& e) {
|
|
std::cerr << "FAILED: " << e.what() << "\n";
|
|
return 1;
|
|
}
|
|
}
|