mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-18 04:55:16 +02:00
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 fromabdda77is 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 toe715b9b- 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.
193 lines
10 KiB
C++
193 lines
10 KiB
C++
#include "Extension/UI/NativeMenu/native_menu_lifetime.h"
|
|
#include <cstdio>
|
|
#include <array>
|
|
#include <map>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
using dingosdk::multiplayer::menu_data::OwnedMenuModels;
|
|
using dingosdk::multiplayer::menu_data::MenuLifetime;
|
|
using dingosdk::multiplayer::menu_data::MenuAction;
|
|
using dingosdk::multiplayer::menu_data::action_slot;
|
|
struct Model { unsigned handle, type; };
|
|
void check(bool value, const char* message) { if (!value) throw std::runtime_error(message); }
|
|
|
|
void map_transition() {
|
|
OwnedMenuModels<Model> owned;
|
|
std::map<unsigned, unsigned> live{{1, 100}, {2, 200}, {3, 300}};
|
|
owned.track({2, 200}); // First pause-menu instance.
|
|
owned.track({3, 300}); // Partially built replacement instance.
|
|
owned.track({3, 300}); // Engine create can return an existing owned root.
|
|
bool attached = true, assets_loaded = true;
|
|
std::vector<unsigned> destroyed;
|
|
const auto destroy = [&](Model value) {
|
|
check(!attached, "Detach tabs before destroying their data");
|
|
check(assets_loaded, "Release list templates while their assets are still loaded");
|
|
check(live.at(value.handle) == value.type, "Destroy only matching owned roots");
|
|
live.erase(value.handle);
|
|
destroyed.push_back(value.handle);
|
|
};
|
|
owned.release([&] { attached = false; }, destroy);
|
|
assets_loaded = false; // The common loader may now submit either menu's request.
|
|
check(owned.empty() && live.size() == 1 && live.contains(1), "Native base-game roots remain owned by the game");
|
|
check(destroyed == std::vector<unsigned>{3, 2}, "Partial builds and older menu models are released once");
|
|
owned.release([&] { throw std::runtime_error("Repeated teardown must be inert"); }, destroy);
|
|
assets_loaded = true;
|
|
live.emplace(4, 400);
|
|
owned.track({4, 400}); // Reopening after loading creates a fresh menu.
|
|
owned.release([] {}, destroy);
|
|
check(destroyed.back() == 4 && owned.empty(), "The next world gets an independent menu lifetime");
|
|
}
|
|
void interrupted_cleanup() {
|
|
OwnedMenuModels<Model> owned;
|
|
owned.track({10, 100}); owned.track({20, 200});
|
|
unsigned calls{};
|
|
bool failed{};
|
|
try {
|
|
owned.release([] { throw std::runtime_error("Detach failed"); }, [&](Model) { ++calls; });
|
|
} catch (const std::runtime_error&) { failed = true; }
|
|
check(failed && calls == 0 && !owned.empty(), "Failed detach must preserve models and prevent unload");
|
|
std::vector<unsigned> destroyed;
|
|
try {
|
|
owned.release([] {}, [&](Model value) {
|
|
if (value.handle == 10) throw std::runtime_error("Destroy failed");
|
|
destroyed.push_back(value.handle);
|
|
});
|
|
} catch (const std::runtime_error&) { }
|
|
owned.release([] {}, [&](Model value) { destroyed.push_back(value.handle); });
|
|
check(destroyed == std::vector<unsigned>{20, 10}, "Retry must finish cleanup without double destruction");
|
|
owned.track({30, 300});
|
|
owned.manager_replaced();
|
|
owned.release([] {}, [&](Model) { throw std::runtime_error("Old manager handle reused"); });
|
|
check(owned.empty(), "Replacing the native manager must discard its expired handles");
|
|
}
|
|
void window_close() {
|
|
MenuLifetime lifetime;
|
|
lifetime.before_transition(13, [] {});
|
|
std::array<OwnedMenuModels<Model>, 2> pages;
|
|
pages[0].track({10, 100}); pages[1].track({20, 200});
|
|
std::array<bool, 2> attached{true, true};
|
|
bool assets_loaded = true;
|
|
unsigned destroyed{}, cleanups{};
|
|
const auto cleanup = [&] {
|
|
++cleanups;
|
|
check(lifetime.blocked(), "Stop updates before cleanup can invoke UI callbacks");
|
|
for (unsigned slot = 0; slot < pages.size(); ++slot) {
|
|
pages[slot].release([&] { attached[slot] = false; }, [&](Model) {
|
|
check(!attached[slot] && assets_loaded,
|
|
"Close-window cleanup must release widget references before asset unload");
|
|
++destroyed;
|
|
});
|
|
}
|
|
};
|
|
for (unsigned state : {13U, 13U, 21U}) lifetime.before_transition(state, cleanup);
|
|
check(cleanups == 0 && !lifetime.blocked(), "Active-state notifications must not destroy menu models");
|
|
lifetime.before_transition(24, cleanup); // Before native state-24 handler.
|
|
assets_loaded = false; // The native handler unloads the widget asset arena.
|
|
for (unsigned state : {24U, 25U, 25U, 13U, 21U, 14U}) {
|
|
lifetime.before_transition(state, cleanup);
|
|
if (!lifetime.blocked()) pages[0].track({30, 300}); // A remaining client tick.
|
|
}
|
|
check(cleanups == 1 && destroyed == 2 && pages[0].empty() && pages[1].empty(),
|
|
"Both menu tabs must stay released through all remaining shutdown ticks");
|
|
for (auto& page : pages)
|
|
page.release([] {}, [&](Model) { throw std::runtime_error("Late widget release reproduces the shutdown crash"); });
|
|
|
|
MenuLifetime failed;
|
|
failed.before_transition(24, [] {}); // Cleanup can report failure without clearing the latch.
|
|
check(failed.blocked(), "Even an incomplete cleanup must prevent new models during shutdown");
|
|
}
|
|
void native_transition_without_load_request() {
|
|
// The multiplayer crash used the game's transition path. No ReSkate load
|
|
// was queued, so the scheduler's explicit cleanup never ran. Reopening the
|
|
// pause menu and later ticks must not retain/recreate the old asset refs.
|
|
for (unsigned leaving : {14U, 22U, 3U}) {
|
|
MenuLifetime lifetime;
|
|
std::array<OwnedMenuModels<Model>, 2> pages;
|
|
check(lifetime.blocked(), "Do not build menus before the first active world");
|
|
lifetime.before_transition(13, [] { throw std::runtime_error("Unexpected initial cleanup"); });
|
|
pages[0].track({10, 100}); pages[1].track({20, 200});
|
|
bool assets_loaded = true;
|
|
unsigned cleanups{}, destroyed{}, published{}, actions{};
|
|
const auto cleanup = [&] {
|
|
++cleanups;
|
|
check(lifetime.blocked(), "Block reentrant callbacks before native transition cleanup");
|
|
for (auto& page : pages) page.release([] {}, [&](Model) {
|
|
check(assets_loaded, "Destroy menu asset references before the engine frees assets");
|
|
++destroyed;
|
|
if (!lifetime.blocked()) ++actions;
|
|
});
|
|
};
|
|
lifetime.before_transition(leaving, cleanup);
|
|
assets_loaded = false;
|
|
for (unsigned state : {leaving, 3U, 4U, 8U, 9U, 12U}) {
|
|
lifetime.before_transition(state, cleanup);
|
|
if (!lifetime.blocked()) { ++published; ++actions; }
|
|
}
|
|
check(cleanups == 1 && destroyed == 2 && published == 0 && actions == 0,
|
|
"Native unload must clean both pages once and suppress updates/actions throughout loading");
|
|
for (auto& page : pages) page.release([] {}, [&](Model) {
|
|
throw std::runtime_error("Late model destruction reproduces the multiplayer crash");
|
|
});
|
|
assets_loaded = true;
|
|
lifetime.before_transition(leaving == 22 ? 21 : 13, cleanup);
|
|
check(!lifetime.blocked(), "The next active level/sublevel must allow reopening the menu");
|
|
pages[0].track({30, 300}); pages[1].track({40, 400});
|
|
lifetime.before_transition(14, cleanup);
|
|
check(cleanups == 2 && destroyed == 4, "Clean subsequent worlds, not just the first transition");
|
|
lifetime.before_transition(24, cleanup);
|
|
check(lifetime.blocked() && destroyed == 4, "Shutdown during loading must not release roots twice");
|
|
}
|
|
}
|
|
void action_slots() {
|
|
// A page's buttons, rendered the way the menu does it: every button asks for
|
|
// its command's slot on every pass, and a map load starts a new generation.
|
|
constexpr std::size_t capacity = 8;
|
|
std::array<MenuAction, capacity> actions;
|
|
std::size_t used{};
|
|
std::uint64_t pass{}, generation{};
|
|
const auto ask = [&](std::string command, std::string argument = {}) {
|
|
const auto slot = action_slot(used, capacity, [&](std::size_t i) -> const MenuAction& { return actions[i]; },
|
|
command, argument, pass);
|
|
if (!slot) return capacity;
|
|
actions[*slot] = {generation, std::move(command), std::move(argument), pass};
|
|
if (*slot == used) ++used;
|
|
return *slot;
|
|
};
|
|
const auto render = [&](std::initializer_list<const char*> buttons) {
|
|
++pass;
|
|
std::vector<std::size_t> slots;
|
|
for (const auto* button : buttons) slots.push_back(ask("load", button));
|
|
return slots;
|
|
};
|
|
const auto first = render({"a", "b", "c", "d", "e", "f"});
|
|
check(used == 6 && first == std::vector<std::size_t>{0, 1, 2, 3, 4, 5}, "Each command takes the next unused slot");
|
|
// Fifteen map loads filled the table when every generation took new slots.
|
|
for (unsigned load = 0; load < 100; ++load) {
|
|
++generation;
|
|
check(render({"a", "b", "c", "d", "e", "f"}) == first && used == 6,
|
|
"The same buttons after a map load keep their slots");
|
|
check(actions[0].generation == generation, "A kept slot belongs to the menu that is up now");
|
|
}
|
|
// A list whose rows change (lobbies, players): the slots of rows that went away are reused.
|
|
check(render({"a", "b", "c", "d", "e", "f", "g", "h"}).back() == 7 && used == capacity, "The table can fill");
|
|
check(render({"a", "b", "c", "d", "e", "f", "g", "h"}).back() == 7, "A full table still serves the commands it holds");
|
|
check(ask("load", "i") == capacity, "A slot a button asked for in this pass or the last is not given away");
|
|
render({"a", "b", "c", "d", "e", "f"});
|
|
check(ask("load", "i") == capacity, "One pass without its button is not enough: the pass may not be over");
|
|
const auto later = render({"a", "b", "c", "d", "e", "f", "i"});
|
|
check(later.back() == 6 && actions[6].argument == "i", "A slot no button has asked for is handed to a new command");
|
|
check(render({"a", "b", "c", "d", "e", "f", "i", "g"}).back() == 7 && actions[7].argument == "g",
|
|
"A command that lost its slot gets another when its button returns");
|
|
}
|
|
int main() {
|
|
try {
|
|
map_transition(); interrupted_cleanup(); window_close(); native_transition_without_load_request(); action_slots();
|
|
std::puts("Native menu lifetime: scheduled/native transitions, blocked callbacks, reload, shutdown, partial builds, retries and action slots passed.");
|
|
return 0;
|
|
} catch (const std::exception& e) {
|
|
std::fprintf(stderr, "%s\n", e.what()); return 1;
|
|
}
|
|
}
|