diff --git a/Engine/Vfs/mod_catalog.cpp b/Engine/Vfs/mod_catalog.cpp index a6b1500f..d0cd48db 100644 --- a/Engine/Vfs/mod_catalog.cpp +++ b/Engine/Vfs/mod_catalog.cpp @@ -120,6 +120,33 @@ bool deck_before(const std::string& a, const std::string& b) { }; return place(a) != place(b) ? place(a) < place(b) : a < b; } +// The decks a pack holds beside its own (mod_catalog.h), by name, in the menus' order. +std::vector> decks_inside(const fs::path& mod) { + std::vector> found; + std::error_code error; + for (fs::directory_iterator at(mod / L"decks", error), end; !error && at != end; at.increment(error)) { + if (!at->is_directory(error) || !fs::is_regular_file(at->path() / L"layout.toc", error)) continue; + auto name = lower(at->path().filename().string()); + if (deck_name(name)) found.emplace_back(std::move(name), at->path()); + } + std::sort(found.begin(), found.end(), [](const auto& a, const auto& b) { return deck_before(a.first, b.first); }); + return found; +} +// Every deck in the Mods folder with the folder of the mod that brings it, in the menus' order, +// whatever mods.json says of that mod: the decks inside a pack, or else the one a folder is named for. +std::vector> installed_decks() { + std::vector> decks; + std::error_code error; + for (fs::directory_iterator at(engine_data_root() / L"Mods", error), end; !error && at != end; at.increment(error)) { + if (!at->is_directory(error)) continue; + auto inside = decks_inside(at->path()); + for (auto& one : inside) decks.emplace_back(std::move(one.first), at->path()); + if (!inside.empty()) continue; + if (auto deck = board_break_deck_of(at->path().filename().string()); !deck.empty()) decks.emplace_back(std::move(deck), at->path()); + } + std::stable_sort(decks.begin(), decks.end(), [](const auto& a, const auto& b) { return deck_before(a.first, b.first); }); + return decks; +} // The choice made in this run, and what the last live apply gave the game (none since launch: the // launch's catalogue says). struct BoardBreakApplied { @@ -133,13 +160,22 @@ std::optional applied_board_break; } // namespace std::string board_break_deck_of(std::string_view mod) { - // Team-Board_Break_Wide and Board_Break_Wide are both the deck "wide". constexpr std::string_view prefix = "board_break_"; auto name = lower(mod); + // A pack's deck is listed as #; no folder has a # in its name. + if (const auto mark = name.rfind('#'); mark != std::string::npos) { + name.erase(0, mark + 1); + return deck_name(name) ? name : std::string{}; + } + // Team-Board_Break_Wide and Board_Break_Wide are both the deck "wide". if (const auto team = name.rfind('-'); team != std::string::npos) name.erase(0, team + 1); if (!name.starts_with(prefix) || !deck_name(std::string_view(name).substr(prefix.size()))) return {}; return name.substr(prefix.size()); } +std::string board_break_pack_of(std::string_view mod) { + const auto mark = mod.rfind('#'); + return mark == std::string_view::npos ? std::string{} : std::string(mod.substr(0, mark)); +} std::string wanted_board_break_deck() noexcept { try { { @@ -178,23 +214,16 @@ bool save_board_break_deck(std::string_view deck) noexcept { std::vector board_break_folders() noexcept { std::vector found; try { - std::vector> decks; - std::error_code error; - for (fs::directory_iterator at(engine_data_root() / L"Mods", error), end; !error && at != end; at.increment(error)) - if (at->is_directory(error)) - if (auto deck = board_break_deck_of(at->path().filename().string()); !deck.empty()) decks.emplace_back(std::move(deck), at->path()); - std::sort(decks.begin(), decks.end(), [](const auto& a, const auto& b) { return deck_before(a.first, b.first); }); - for (auto& deck : decks) found.push_back(std::move(deck.second)); + for (auto& deck : installed_decks()) + if (std::find(found.begin(), found.end(), deck.second) == found.end()) found.push_back(std::move(deck.second)); } catch (...) {} return found; } std::vector installed_board_break_decks() noexcept { std::vector found; try { - for (const auto& folder : board_break_folders()) { - auto deck = board_break_deck_of(folder.filename().string()); - if (found.empty() || found.back() != deck) found.push_back(std::move(deck)); - } + for (auto& deck : installed_decks()) + if (found.empty() || found.back() != deck.first) found.push_back(std::move(deck.first)); } catch (...) {} return found; } @@ -275,6 +304,37 @@ Catalog load_catalog(const std::filesystem::path& data_root, const MergeObserver try { for (const auto& name : list.missing) result.notes.push_back("mods.json names a folder that is not present: " + name); + // Board Break: a deck pack holds several decks in one mod folder (mod_catalog.h). Each deck + // inside becomes a mod of its own for the run, listed after the pack as #, so + // that a pack's decks are chosen, set aside and switched like decks in folders of their own. + // It carries the pack's build stamp; mods.json and the mod list only know the pack. The + // pack's own layout is no deck here, whatever its folder is named: it is the deck a ReSkate + // from before deck packs breaks, and is set aside for the decks inside. + std::vector packs; + for (std::size_t i = 0; i < list.entries.size(); ++i) { + auto inside = decks_inside(list.entries[i].mod.directory); + if (inside.empty()) continue; + const auto pack = list.entries[i].mod; + packs.push_back(pack.name); + const bool enabled = list.entries[i].enabled; + for (auto& [deck, directory] : inside) { + ModEntry part; + part.enabled = enabled; + part.mod.name = pack.name + "#" + deck; + part.mod.title = pack.title + " (" + deck + ")"; + part.mod.author = pack.author; + part.mod.version = pack.version; + part.mod.directory = std::move(directory); + part.mod.provides_layout = true; + part.mod.studio_marker = pack.studio_marker; + part.mod.skate_sha256 = pack.skate_sha256; + part.mod.outdated = pack.outdated; + list.entries.insert(list.entries.begin() + static_cast(++i), std::move(part)); + } + } + const auto deck_of = [&](const Mod& mod) { + return std::find(packs.begin(), packs.end(), mod.name) != packs.end() ? std::string{} : board_break_deck_of(mod.name); + }; // Board Break (Extension/BoardBreak/board_break.h). The deck that can break is a mod's own // copy of the game's one deck model, and any mod that reshapes the deck replaces that same // model: the one higher in the load order has it. So the deck the player chose goes first @@ -287,7 +347,7 @@ Catalog load_catalog(const std::filesystem::path& data_root, const MergeObserver { const auto enabled_deck = [&](const std::string& deck) { return std::find_if(list.entries.begin(), list.entries.end(), [&](const auto& entry) { - const auto found = board_break_deck_of(entry.mod.name); + const auto found = deck_of(entry.mod); return entry.enabled && !found.empty() && (deck.empty() || found == deck); }); }; @@ -298,7 +358,7 @@ Catalog load_catalog(const std::filesystem::path& data_root, const MergeObserver std::rotate(list.entries.begin(), first, first + 1); result.notes.push_back("Board Break: " + list.entries.front().mod.name + " loads first, so its deck is the one in the game"); for (auto entry = list.entries.begin() + 1; entry != list.entries.end(); ++entry) { - if (!entry->enabled || (board_break_deck_of(entry->mod.name).empty() && !changes_the_deck(entry->mod.directory))) continue; + if (!entry->enabled || (deck_of(entry->mod).empty() && !changes_the_deck(entry->mod.directory))) continue; entry->enabled = false; result.set_aside.push_back(entry->mod.name); result.notes.push_back("Board Break: " + entry->mod.name + " brings a deck model too and is set aside for this run"); @@ -392,7 +452,7 @@ Catalog load_catalog(const std::filesystem::path& data_root, const MergeObserver // Board Break's deck is the one in the game when its mod is among those that load and no mod // above it brings a deck model of its own. for (const auto& mod : result.mods) { - if (!(result.board_break_deck = board_break_deck_of(mod.name)).empty()) break; + if (!(result.board_break_deck = deck_of(mod)).empty()) break; if (changes_the_deck(mod.directory)) break; } // Any Mods folder gets a patch, even an empty one or one of disabled diff --git a/Engine/Vfs/mod_catalog.h b/Engine/Vfs/mod_catalog.h index add4faa9..66622347 100644 --- a/Engine/Vfs/mod_catalog.h +++ b/Engine/Vfs/mod_catalog.h @@ -49,22 +49,33 @@ struct Catalog { }; // Board Break's decks (Extension/BoardBreak/board_break.h). The game has one deck model for every -// board, so each deck that can break is a content mod of its own that replaces it: any mod whose -// folder is named Board_Break_ (also behind a team name, as a mod manager installs it: -// Team-Board_Break_). The name, in small letters, is what the deck goes by; the deck the -// player chose is the one that loads. These are the decks the feature was made with, in the order -// the menus list them, and the console also knows them by their place here (deck 1 is the first). -// Any other deck comes after them, by its name: a new shape needs no change to ReSkate. +// board, so each deck that can break is a copy of it in a content mod, and the deck the player +// chose is the one that loads. A deck comes in one of two ways: +// - a mod whose folder is named Board_Break_ (also behind a team name, as a mod manager +// installs it: Team-Board_Break_) holds the deck ; +// - a deck pack, a mod of any name with a folder decks// for each deck it brings, laid out +// like a mod folder (layout.toc, Win32/...). One package can bring every deck that way, where +// the launcher installs one mod folder per package. For the run each deck inside is a mod of +// its own, listed as #. The pack's own layout is not one of its decks, +// whatever the folder is named, and is set aside like any other deck model. It is what an +// older ReSkate loads from the pack: in a folder named Board_Break_ the deck 2.0.6 +// breaks, which knows a deck by its folder's name only; under another name the game's own. +// A deck goes by its name in small letters. These are the decks the feature was made with, in the +// order the menus list them, and the console also knows them by their place here (deck 1 is the +// first). Any other deck comes after them, by its name: a new shape needs no change to ReSkate. inline constexpr std::array board_break_decks{"standard", "wide", "concave", "powell", "slim", "egg", "square", "shaped"}; -// The name of the deck a mod folder holds, or nothing for any other mod. -std::string board_break_deck_of(std::string_view mod_folder); +// The name of the deck a mod holds going by the mod's name (a folder's, or a pack's deck's), or +// nothing; and the folder name of the pack that one of a pack's decks is listed under, or nothing. +std::string board_break_deck_of(std::string_view mod); +std::string board_break_pack_of(std::string_view mod); // The deck the player chose ("standard" when nothing was chosen): read whenever the mods are put // together, at launch and at every live apply. Saving a choice makes it this run's at once. std::string wanted_board_break_deck() noexcept; bool save_board_break_deck(std::string_view deck) noexcept; -// The decks whose mod's folder is in Mods/, whatever mods.json says of them, in the menus' order, -// and those folders in the same order. +// The decks that are in the Mods folder, whatever mods.json says of their mods, in the menus' +// order; and the folders of the mods that bring them (a pack's once), for what a pack carries +// beside its decks. std::vector installed_board_break_decks() noexcept; std::vector board_break_folders() noexcept; // What the mods in effect give the game: the launch's catalogue, or the one the last live apply diff --git a/Extension/Assets/live_mods.cpp b/Extension/Assets/live_mods.cpp index f33e82bc..59d9f5c3 100644 --- a/Extension/Assets/live_mods.cpp +++ b/Extension/Assets/live_mods.cpp @@ -505,7 +505,8 @@ ApplyEffect preview(const mods::ModList &list) { }; bool reload{}; for (const auto &name : before) { - if (named(now, name)) continue; + // One of a Board Break deck pack's decks stands for the pack (mod_catalog.h). + if (const auto pack = mods::board_break_pack_of(name); named(now, pack.empty() ? name : pack)) continue; const auto *entry = entry_of(name); // A folder deleted since: what it held is unknown, so assume the worst. if (!entry) { diff --git a/Extension/UI/Overlay/modding_menu.cpp b/Extension/UI/Overlay/modding_menu.cpp index 01349070..47128adf 100644 --- a/Extension/UI/Overlay/modding_menu.cpp +++ b/Extension/UI/Overlay/modding_menu.cpp @@ -44,8 +44,11 @@ void draw_installed_mods(SkateMenu& menu) { } const auto& launch = mods::catalog(); const auto applied = live_mods::applied_mods(); + // A Board Break deck pack is in effect when one of the decks inside it is (mod_catalog.h). const auto in_effect = [&](const std::string& name) { - return std::ranges::any_of(applied, [&](const std::string& other) { return _stricmp(other.c_str(), name.c_str()) == 0; }); + return std::ranges::any_of(applied, [&](const std::string& other) { + return _stricmp(other.c_str(), name.c_str()) == 0 || _stricmp(mods::board_break_pack_of(other).c_str(), name.c_str()) == 0; + }); }; const auto excluded = [&](const std::string& name) { return std::ranges::any_of(launch.excluded, [&](const mods::Mod& mod) { return _stricmp(mod.name.c_str(), name.c_str()) == 0; }); @@ -66,7 +69,8 @@ void draw_installed_mods(SkateMenu& menu) { // Mods still in effect whose folder has been deleted: applying unloads them. std::vector removed; for (const auto& name : applied) - if (std::ranges::none_of(list.entries, [&](const mods::ModEntry& entry) { return _stricmp(entry.mod.name.c_str(), name.c_str()) == 0; })) + if (mods::board_break_pack_of(name).empty() && + std::ranges::none_of(list.entries, [&](const mods::ModEntry& entry) { return _stricmp(entry.mod.name.c_str(), name.c_str()) == 0; })) removed.push_back(name); pending += removed.size(); loaded += removed.size(); diff --git a/contrib/board-break.md b/contrib/board-break.md index a4f23e1c..77ddce1b 100644 --- a/contrib/board-break.md +++ b/contrib/board-break.md @@ -4,7 +4,7 @@ A hard slam can break the local skater's board: at the nose, in the middle or at time the grip tape holds the pieces together and the board folds or an end hangs; otherwise a piece comes off, flies and lies where it lands, truck and wheels with it. The board is whole again once the skater has picked it up, rides again or respawns. Built into ReSkate; the broken pieces -themselves come from content mods, one for each deck shape. +themselves come from a content mod with the deck shapes in it. Code: [board_break.cpp](../Extension/BoardBreak/board_break.cpp) (client tick and the boards' render pose hook), [board_break_model.h](../Extension/BoardBreak/board_break_model.h) (the rules and @@ -73,10 +73,25 @@ deck's place in the mod list in [mod_catalog.cpp](../Engine/Vfs/mod_catalog.cpp) ## The deck packs -A content mod (no code) in a folder named `Board_Break_`; the name is what the deck goes by. -The eight the feature was made with are standard, wide, concave, powell, slim, egg, square and -shaped, and the console also knows those by number. Any other pack that follows the recipe works, -with no change to ReSkate: +A deck is a content mod's copy of the deck model (no code), and comes in one of two ways: + +- a mod folder named `Board_Break_` holds the deck ``; +- a deck pack, a mod folder of any name, holds a deck in each `decks//`, laid out like a mod + folder (`layout.toc`, `Win32/...`). The launcher installs one mod folder per package, so this is + how one package brings every deck. When the mods are put together each deck inside becomes a mod + of its own for the run, listed as `#`: it is chosen, set aside and switched like a + deck in a folder of its own (its archives are placed like a disabled mod's). mods.json and the + mod list only know the pack, which counts as loaded when one of its decks is. The launcher takes + a folder for a mod only when it has a `layout.toc` of its own. That layout is not one of the + pack's decks, whatever the folder is named, and is set aside like any other deck model; it is + what an older ReSkate loads from the pack. ReSkate 2.0.6 knows a deck by its folder's name only, + so a pack in a folder named `Board_Break_` (the package `Board_Break_Pack`, installed as + `Team-Board_Break_Pack`) with a breakable deck as its own layout breaks boards there with that + one deck, and brings all its decks from this version on. + +The name is what the deck goes by. The eight the feature was made with are standard, wide, +concave, powell, slim, egg, square and shaped, and the console also knows those by number. Any +other deck that follows the recipe works, with no change to ReSkate: - Start from the deck mesh as ReSkate Studio exports it (three parts on the bone Deck: the grip sheet, the wood, and a plain copy of the whole deck). Studio only takes back a mesh whose @@ -89,11 +104,12 @@ with no change to ReSkate: deck's own edge faces standing in the gap as the broken end. Push the row itself along the board by a jagged line, the same line for both sides of a break, so the pieces fit together. - Build the stretches at their own size, in the place they have in the whole deck. -- The standard deck's pack also carries `sounds/light|medium|heavy/*.wav`. +- The sounds are `sounds/light|medium|heavy/*.wav` in the folder of any mod that brings a deck. ## Seen working in game -Offline, on main at f4292d6, with eight deck packs installed: breaks from real slams at the nose, +Offline, on main at f4292d6, with eight decks installed, each in a folder of its own and all of +them in one deck pack: breaks from real slams at the nose, in the middle and at the tail, clean and held by the grip, the board whole again in hand and on respawn; breaks from the console on foot (folded, nose and tail hanging) that stay until mended. The grip side of a whole board, close up in the board menu, shows no cracks. A deck chosen while @@ -101,7 +117,13 @@ the game runs is the one in the board menu and in the world after the level has (about 20 s), and the one the next launch starts with. With a wide-deck mod switched on and first in the order, the breakable deck is still the one in the game and the mod is set aside; so is an older deck mod of Board Break's own. The mod list shows the decks that are set aside as such, with -nothing waiting to be applied. The switch on the overlay's Skater page turns it off and on. On the +nothing waiting to be applied. With a pack first in the order (on main at 1b15a04, 2.0.6), the +deck the game starts with and the deck chosen while it runs both come from inside the pack, the +deck list has the pack's eight decks and no more, and the pack's own layout is set aside: tried +with the game's own deck there, and with a breakable deck there in a folder named +`FourthStudio-Board_Break_Pack`. That last pack on 2.0.6 as released: its own layout is the deck +"pack", loads first and breaks at the nose, in the middle and at the tail. The switch on the +overlay's Skater page turns it off and on. On the older build the feature was developed on, a middle break played back in the replay editor. ## Not tried yet