Files
AyeZeeBB 76330e40bb Skater slot, menu and throwdown fixes; MY MODS rework; lobby parties
Skater slot, menu and throwdown fixes; MY MODS rework; lobby parties

Saved skaters (Extension/Customization, Extension/Skater)
- The selected slot, and slots before it, came up as the standard
  skater after a start or map change. The game asks for a slot's saved
  outfit once per level, and that happened before the cosmetics catalog
  could be read. The first selection now waits for the catalog (60 s at
  most), and later map changes check against the catalog already read.
- The retry from abdda77 is removed: the record it wrote to is a
  temporary on the game's stack, so it could never work.

Pause menu (Extension/UI/NativeMenu)
- ReSkate's entries stopped working after enough map changes: each
  level change took new slots in a fixed table of 1024 for the same
  buttons. A button now keeps its slot, and a full table hands out the
  slot that has gone longest unused.

Throwdowns (Extension/Throwdowns, Runtime/runtime_init.cpp)
- Multiplayer crashed when someone placed a throwdown after a map
  change. The event types are freed with the level but were cached for
  the process. They are now cleared when a level is left, checked
  before use, and looked for again in each level.

MY MODS (Launcher/gui_mods.cpp, gui.cpp)
- Search, a filter and an order; a switch per mod; a menu per row.
- Multi-select with tick boxes, Shift-click and Ctrl+A, and a bar to
  enable, disable, move, update or uninstall everything ticked.

Lobby parties (Extension/Multiplayer/Session)
- A lobby is no longer one party. Its host keeps parties the way a
  dedicated server does: invite, accept, join, leave, kick, promote,
  open and close, with "/p" party chat. The "Lobby party" switch is
  gone. Protocol version unchanged (38).

Mod merge (Engine/Vfs), follow-ups to e715b9b
- Files rewritten into the merged patch are tracked by position, so
  another mod's added assets no longer shift the lookup.
- Two mods adding different assets under one name is now noted, with
  whose copy the maps get.
- mod_merge_unshift_tests compiles with the launcher tests on.
2026-10-04 13:27:21 -05:00

79 lines
3.5 KiB
C++

#pragma once
#include <algorithm>
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <optional>
#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);
}
}
};
// 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(); }
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);
}
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{}; };
// 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