Files
ReSkate/Extension/Throwdowns/Test/custom_throwdown_tests.cpp
T
AyeZeeBB 37516e24fc Throwdowns: a plugin mode's flag is offered to the session, joined with the game's own Join
When a plugin mode's flag goes down in a session, the relay offers it to the other players
as that mode's: a new mode message (kind 17) names it, its flag and its player count, and
its offer's series is one older versions do not show. A player whose plugin has the same
mode gets a copy with the mode's own flag mesh and joins it natively; the copies are held
at ten players so none fills and starts a Spot Battle, and the mode's player count decides
who may still join. throwdown_mode events now carry host, leader, player and players: the
host hears joined and left, a player in another's flag hears joined, left, closed and
start, and start names who plays, the leader first. The plugins run the match from there.

A mode can take a stock throwdown's place on the picker (replaces = "skate", "spot_battle"
or "jam"), so his R.E.S.K.A.T.E. sits where S.K.A.T.E. does.
2026-10-19 19:04:11 -05:00

109 lines
5.7 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");
bad = one_up();
bad.replaces = "bowling";
check(!custom_throwdowns::set_mode(bad).empty(), "replaces names a stock throwdown");
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;
}
}