mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-23 23:26:06 +02:00
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.
170 lines
8.2 KiB
C++
170 lines
8.2 KiB
C++
#pragma once
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <optional>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <vector>
|
|
|
|
namespace dingosdk::multiplayer::menu_data {
|
|
// Native transitions also unload assets when no ReSkate load request exists.
|
|
// Suspend publication and queued actions before cleanup can dispatch callbacks.
|
|
// Only a new active world may resume them; shutdown remains permanent.
|
|
class MenuLifetime {
|
|
enum class Phase { loading, active, shutdown };
|
|
std::atomic<Phase> phase_{Phase::loading};
|
|
public:
|
|
bool blocked() const noexcept { return phase_.load() != Phase::active; }
|
|
template<class Cleanup> void before_transition(unsigned next, Cleanup&& cleanup) {
|
|
if (next == 24) {
|
|
if (phase_.exchange(Phase::shutdown) != Phase::shutdown) cleanup();
|
|
} else if (next == 14 || next == 22 || next == 3) {
|
|
auto expected = Phase::active;
|
|
if (phase_.compare_exchange_strong(expected, Phase::loading)) cleanup();
|
|
} else if (next == 13 || next == 21) {
|
|
auto expected = Phase::loading;
|
|
phase_.compare_exchange_strong(expected, Phase::active);
|
|
}
|
|
}
|
|
};
|
|
|
|
// The selected activity enum is shared by every native challenge. A custom
|
|
// Details request may close only the exact page it mounted, never a later
|
|
// stock page that happens to select the same enum or reuse an item id.
|
|
struct ActivityDetailsOwnership {
|
|
std::uint64_t page{}, layer{};
|
|
int item{};
|
|
bool requested{};
|
|
void request() noexcept { requested = true; }
|
|
void mount(std::uint64_t next_page, std::uint64_t next_layer, int next_item) noexcept {
|
|
page = next_page; layer = next_layer; item = next_item; requested = true;
|
|
}
|
|
bool owns(std::uint64_t live_page, std::uint64_t live_layer, int live_item) const noexcept {
|
|
return requested && page && layer && live_page == page && live_layer == layer && live_item == item;
|
|
}
|
|
// Back can deactivate the item before teardown runs. Cached presenters
|
|
// still retain this page and must be restored before a second entry.
|
|
bool retains(std::uint64_t live_page) const noexcept {
|
|
return requested && page && live_page == page;
|
|
}
|
|
void clear_mount() noexcept { page = layer = 0; item = 0; }
|
|
void retire() noexcept { clear_mount(); requested = false; }
|
|
};
|
|
|
|
// Native navigation is asynchronous and its action slot can be busy. Keep
|
|
// ownership until Back is consumed; never abandon it on a failed enqueue.
|
|
class ActivityDetailsClose {
|
|
std::uint64_t since_{};
|
|
bool pending_{},queued_{};
|
|
public:
|
|
void begin(std::uint64_t now) noexcept {
|
|
if(!pending_) {since_=now;pending_=true;queued_=false;}
|
|
}
|
|
bool pending() const noexcept { return pending_; }
|
|
bool needs_back() const noexcept { return pending_ && !queued_; }
|
|
void back_queued(bool accepted) noexcept { if(accepted)queued_=true; }
|
|
bool needs_selection_recovery(std::uint64_t now,bool exact_page,bool owned_selection) const noexcept {
|
|
return pending_ && exact_page && owned_selection && now>=since_ && now-since_>=2000;
|
|
}
|
|
void reset() noexcept { *this={}; }
|
|
};
|
|
|
|
// The native Layer_Menu capture has TargetIndex=-1 after its transition has
|
|
// finished. StackIndex identifies the current item; TargetIndex describes a
|
|
// transition request and must never select a page for publication or teardown.
|
|
inline std::optional<unsigned> active_activity_stack_item(int stack_index, unsigned count, bool active) noexcept {
|
|
if(!active || stack_index<0 || static_cast<unsigned>(stack_index)>=count)return std::nullopt;
|
|
return static_cast<unsigned>(stack_index);
|
|
}
|
|
|
|
// Capture the actual normalized Start/Menu button before UI publication is
|
|
// throttled. Opening another phone page is not evidence of a Start press.
|
|
// Never reopen on a held key, a closing press, or a new match entered held.
|
|
class MatchPanelStartInput {
|
|
bool down_{}, menu_open_{}, available_{}, initialized_{};
|
|
public:
|
|
bool sample(std::uint32_t buttons, bool escape, bool available, bool menu_open,bool owned_panel=false) noexcept {
|
|
const bool down=(buttons & 0x0010u)!=0 || escape;
|
|
const bool request=initialized_ && available_ && available && (!menu_open_ || owned_panel) && down && !down_;
|
|
down_=down;menu_open_=menu_open;available_=available;initialized_=true;
|
|
return request;
|
|
}
|
|
};
|
|
|
|
// A picker page can retire on the very Start press that opens the phone.
|
|
// Input belongs to the current match; only real world/manager teardown resets
|
|
// its latch and request. The request is still checked against the exact match.
|
|
inline void reset_match_panel_input(MatchPanelStartInput& latch,std::string& request,bool preserve_match_input) {
|
|
if(preserve_match_input)return;
|
|
latch={};request.clear();
|
|
}
|
|
|
|
// Model creation returns an independently owned root. Dropping the C++ handle
|
|
// does not destroy it. Track partial builds and old pause-menu generations too.
|
|
template<class Model> class OwnedMenuModels {
|
|
std::vector<Model> models_;
|
|
public:
|
|
bool empty() const { return models_.empty(); }
|
|
std::size_t checkpoint() const noexcept { return models_.size(); }
|
|
void track(Model value) {
|
|
if (std::none_of(models_.begin(), models_.end(), [&](const auto& existing) {
|
|
return existing.handle == value.handle && existing.type == value.type;
|
|
})) models_.push_back(value);
|
|
}
|
|
// For an unmounted build: release only the roots created after its start.
|
|
// The caller must detach any published references first. Pop only after a
|
|
// successful destroy so an interrupted rollback can safely be retried.
|
|
template<class Destroy> void rollback(std::size_t checkpoint, Destroy&& destroy) {
|
|
if(checkpoint>models_.size())throw std::out_of_range("Native menu model checkpoint is no longer valid.");
|
|
while(models_.size()>checkpoint) {
|
|
destroy(models_.back());
|
|
models_.pop_back();
|
|
}
|
|
}
|
|
template<class Detach, class Destroy> void release(Detach&& detach, Destroy&& destroy) {
|
|
if (empty()) return;
|
|
detach();
|
|
while (!models_.empty()) {
|
|
destroy(models_.back());
|
|
models_.pop_back(); // Keep unfinished cleanup available on failure.
|
|
}
|
|
}
|
|
// Use only after the native manager itself has been replaced.
|
|
void manager_replaced() { models_.clear(); }
|
|
};
|
|
|
|
// A command one of a page's native buttons runs: the pause-menu generation it was
|
|
// last handed out in, and the render pass that last asked for it.
|
|
struct MenuAction { std::uint64_t generation{}; std::string command, argument; std::uint64_t pass{}; };
|
|
inline bool waiting_card_action(std::string_view command) noexcept {
|
|
return command=="flag-wait-start" || command=="flag-wait-leave";
|
|
}
|
|
inline bool action_generation_current(const MenuAction& action,std::uint64_t owner) noexcept {
|
|
// Waiting actions must also pass the exact visible card/flag check when
|
|
// dispatched. Picker and match-specific actions retain their old guard.
|
|
return action.generation && (action.generation==owner || waiting_card_action(action.command));
|
|
}
|
|
|
|
// The slot a button gets for a command. A slot is a native callback whose
|
|
// descriptor the game borrows for the rest of the process, so a page has a
|
|
// fixed number of them and they are reused, never added to: a command keeps
|
|
// the slot it has, whichever generation of the menu gave it out (every map
|
|
// load starts a new one); a new command takes the next unused slot; and with
|
|
// none left, the slot asked for longest ago. Every button asks on every render
|
|
// pass, so a slot not asked for in this pass or the one before is on no button.
|
|
// Empty when every slot is on a button.
|
|
template<class At> std::optional<std::size_t> action_slot(std::size_t used, std::size_t capacity, At&& at,
|
|
std::string_view command, std::string_view argument, std::uint64_t pass) {
|
|
std::optional<std::size_t> idle;
|
|
for (std::size_t i = 0; i < used; ++i) {
|
|
const MenuAction& action = at(i);
|
|
if (action.command == command && action.argument == argument) return i;
|
|
if (action.pass + 1 < pass && (!idle || action.pass < at(*idle).pass)) idle = i;
|
|
}
|
|
return used < capacity ? used : idle;
|
|
}
|
|
} // namespace dingosdk::multiplayer::menu_data
|