Merge pull request #196 from FourthStudio/board-break/deck-packs

Board Break: one package for all decks, a button for each deck, baseplates on a broken board HOTFIX + TRUCK FIX
This commit is contained in:
zeex64 authored and GitHub committed 2026-10-11 18:21:55 -05:00
commit 794d757dc3
12 files changed
+300 -48

No files matched your search

+75 -15
View File
@@ -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<std::pair<std::string, fs::path>> decks_inside(const fs::path& mod) {
std::vector<std::pair<std::string, fs::path>> 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<std::pair<std::string, fs::path>> installed_decks() {
std::vector<std::pair<std::string, fs::path>> 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<BoardBreakApplied> 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 <pack's folder>#<deck>; 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<std::filesystem::path> board_break_folders() noexcept {
std::vector<fs::path> found;
try {
std::vector<std::pair<std::string, fs::path>> 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<std::string> installed_board_break_decks() noexcept {
std::vector<std::string> 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 <folder>#<deck>, 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<std::string> 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<std::ptrdiff_t>(++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
+21 -10
View File
@@ -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_<Name> (also behind a team name, as a mod manager installs it:
// Team-Board_Break_<Name>). 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_<Name> (also behind a team name, as a mod manager
// installs it: Team-Board_Break_<Name>) holds the deck <name>;
// - a deck pack, a mod of any name with a folder decks/<name>/ 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 <pack's folder>#<name>. 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_<Name> 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<std::string_view, 8> 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<std::string> installed_board_break_decks() noexcept;
std::vector<std::filesystem::path> board_break_folders() noexcept;
// What the mods in effect give the game: the launch's catalogue, or the one the last live apply
+2 -1
View File
@@ -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) {
@@ -133,6 +133,21 @@ void a_truck_goes_with_its_piece() {
const auto up = truck_bone(lifted, mount, mount_at, level, front_truck_place);
check(near(up.at + away, front_truck_place + Vec{0, 0.25f, 0}), "A piece that is lifted takes its truck up with it");
}
void a_baseplate_stays_on_the_deck_while_its_truck_steers() {
// A baseplate sits on the truck's effector, a child of the truck's bone. Whatever the truck's
// bone does, the truck's bone followed by the effector's is what it is at rest.
for (const auto& rest : {back_truck_rest, front_truck_rest}) {
const auto still = baseplate_bone({rest.facing, rest.at}, rest);
check(near(still.at, rest.effector) && near(std::abs(still.facing.w), 1.0f), "At rest the effector is where the rig has it");
const Vec bolt{0.03f, 0.08f, rest.at.z + 0.02f};
const auto on_deck = moved({rest.facing, rest.at}, moved({{}, rest.effector}, bolt));
for (const float lean : {-0.4f, 0.15f, 0.6f}) {
const Rigid steered{unit(rest.facing * about({0, 0, 1}, lean)), rest.at};
const auto effector = baseplate_bone(steered, rest);
check(near(moved(steered, moved(effector, bolt)), on_deck, 1e-4f), "Steered, the baseplate has not moved on the deck");
}
}
}
} // namespace
int main() {
@@ -146,6 +161,7 @@ int main() {
a_folded_board_meets_at_the_break();
a_hanging_end_turns_about_the_break();
a_truck_goes_with_its_piece();
a_baseplate_stays_on_the_deck_while_its_truck_steers();
std::cout << "board break tests passed\n";
return 0;
} catch (const std::exception& error) {
+24 -3
View File
@@ -45,6 +45,9 @@ constexpr float hand_reach = 1.3f, hand_upright = 0.8f;
// deck's mount, 3 the deck, 4 and 8 the trucks, and the spare bones that carry the four stretches
// (nose, front, back, tail; the tail's hangs on the front's).
constexpr int root_bone = 1, mount_bone = 2, deck_bone = 3, back_truck_bone = 4, front_truck_bone = 8;
// The trucks' effectors, each right after its truck's bone: a deck pack's truck model has the
// baseplates on them (board_break_model.h, baseplate_bone).
constexpr int back_plate_bone = 5, front_plate_bone = 9;
constexpr std::array<int, 4> carrier{12, 13, 15, 14};
constexpr std::uint32_t rig_bones = 16;
@@ -152,6 +155,21 @@ void carry_truck(std::uintptr_t buffer, const Rigid& motion, int index, Vec plac
(void)store_bone(bone_at(buffer, index), bone.facing, bone.at);
}
// Keeps the baseplates on the deck (baseplate_bone), from each truck's bone as the animation left
// it: a truck that a piece has carried off already, in a pose nobody has written since, is left.
void hold_baseplates(std::uintptr_t buffer) noexcept {
const auto hold = [&](int truck_index, int plate_index, const TruckRest& rest) {
PoseBone truck{};
if (!load_bone(bone_at(buffer, truck_index), truck)) return;
const Rigid now{facing_of(truck), place_of(truck)};
if (!(length(now.at - rest.at) < 0.05f)) return;
const auto bone = baseplate_bone(now, rest);
(void)store_bone(bone_at(buffer, plate_index), bone.facing, bone.at);
};
hold(back_truck_bone, back_plate_bone, back_truck_rest);
hold(front_truck_bone, front_plate_bone, front_truck_rest);
}
// ---- the piece that comes off --------------------------------------------------------------------
// The client tick starts a flight (a new id) when the board breaks; the render pose hook moves the
// piece through the world from there (a kick, gravity, the ground) and writes it relative to
@@ -276,14 +294,15 @@ std::uint32_t fly_piece(std::uintptr_t buffer, Rigid& piece) {
}
// ---- what the hook writes ------------------------------------------------------------------------
// The local board, broken: the real deck goes away, the trucks come back, each stretch goes to
// where its piece lies and one that belongs to no piece goes away too.
// The local board, broken: the real deck goes away, the trucks come back (their baseplates with
// them), each stretch goes to where its piece lies and one that belongs to no piece goes away too.
void draw_broken(std::uintptr_t base, std::uintptr_t holder, std::uint32_t kept, Hinge hinge, float angle) {
const auto buffer = board_pose(base, holder);
PoseBone root{}, mount{}, deck{};
if (!buffer || !load_bone(bone_at(buffer, root_bone), root) || !load_bone(bone_at(buffer, mount_bone), mount) ||
!load_bone(bone_at(buffer, deck_bone), deck))
return;
hold_baseplates(buffer);
// From where its animation put it this frame; a pose nobody has written since the last move stays as it is.
if (const Vec at = place_of(deck); length(at) < 100.0f) (void)store_bone(bone_at(buffer, deck_bone), facing_of(deck), at + away);
auto layout = lay_out(kept, hinge, angle, turn(facing_of(root), {0, 1, 0}).y, facing_of(mount));
@@ -298,10 +317,12 @@ void draw_broken(std::uintptr_t base, std::uintptr_t holder, std::uint32_t kept,
const auto bones = bones_of(layout.stretch);
for (std::size_t i = 0; i < bones.size(); ++i) (void)store_bone(bone_at(buffer, carrier[i]), bones[i].facing, bones[i].at);
}
// A whole board, any player's: its four stretches go far away from it.
// A whole board, any player's: its four stretches go far away from it, and its baseplates sit on
// the deck.
void park_pieces(std::uintptr_t base, std::uintptr_t holder) {
const auto buffer = board_pose(base, holder);
if (!buffer) return;
hold_baseplates(buffer);
const auto bones = bones_of({});
for (std::size_t i = 0; i < bones.size(); ++i) (void)store_bone(bone_at(buffer, carrier[i]), bones[i].facing, bones[i].at);
}
+17
View File
@@ -203,4 +203,21 @@ inline Rigid truck_bone(const Rigid& motion, Quat mount, Vec mount_at, Quat stee
const auto at = turn(conj(mount), turn(motion.facing, mount_at) + motion.at - mount_at);
return {unit(facing * steer), turn(facing, place) + at - away};
}
// The trucks' baseplates. The game's own truck model has them on the deck's bone, which goes away
// with the real deck when the board breaks, and they went with it. A deck pack's truck model has
// each baseplate on its truck's effector instead, a bone under the truck's that no model uses:
// wherever a piece carries the truck, its baseplate goes along. This is where that effector goes
// so that the baseplate sits on the deck however the truck steers: the truck's own turn and place
// taken back, its rest put in. `truck` is the truck's bone as the animation left it this frame.
struct TruckRest {
Quat facing; // the truck's bone on the deck's, at rest
Vec at;
Vec effector; // its effector on the truck's bone, at rest (not turned)
};
constexpr TruckRest back_truck_rest{{-0.3641458f, -0.3641458f, -0.6061335f, 0.6061335f}, {0.0005361f, -0.0183003f, -0.2344986f}, {0.0395021f, 0.0f, 0.0f}};
constexpr TruckRest front_truck_rest{{-0.6061674f, 0.6061674f, 0.3640894f, 0.3640894f}, {0.0005361f, -0.0182917f, 0.2346680f}, {0.0395677f, 0.0f, 0.0f}};
inline Rigid baseplate_bone(const Rigid& truck, const TruckRest& rest) noexcept {
const auto back = conj(truck.facing);
return {unit(back * rest.facing), turn(back, rest.at + turn(rest.facing, rest.effector) - truck.at)};
}
} // namespace dingosdk::board_break
+70 -5
View File
@@ -12,8 +12,66 @@
#include <filesystem>
#include <optional>
#include <stdexcept>
#include <string>
#include <vector>
namespace dingosdk::overlay {
namespace menu {
// Board Break's decks (Extension/BoardBreak/board_break.h). The game has one deck model and each
// deck is a mod's copy of it, so picking one applies the mods with that deck first and loads the
// level again. A button for each deck the feature was made with, greyed out while it is not
// installed, and for any other deck that is; the Mods folder is read again every 1.5 s while the
// card shows.
void board_break_deck_card(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
static std::vector<std::string> installed;
static std::string chosen;
static ULONGLONG next_scan{};
if (const auto now = GetTickCount64(); now >= next_scan) {
installed = mods::installed_board_break_decks();
chosen = mods::wanted_board_break_deck();
next_scan = now + 1500;
}
if (!model.board_break.available || installed.empty()) return;
const auto there = [&](const std::string& deck) { return std::find(installed.begin(), installed.end(), deck) != installed.end(); };
// The known decks in their own order, then the others that are installed: the numbers are the
// ones `boardbreak deck` prints.
std::vector<std::string> decks(mods::board_break_decks.begin(), mods::board_break_decks.end());
for (const auto& deck : installed)
if (std::find(decks.begin(), decks.end(), deck) == decks.end()) decks.push_back(deck);
const auto label = [&](const std::string& deck) {
auto name = deck;
bool start = true;
for (auto& c : name) {
if (c == '_') c = ' ';
else if (start && c >= 'a' && c <= 'z') c = static_cast<char>(c - 'a' + 'A');
start = c == ' ';
}
const auto place = std::find(decks.begin(), decks.end(), deck) - decks.begin() + 1;
return std::to_string(place) + " " + name;
};
const auto in_play = mods::board_break_in_play();
begin_card(menu, "board-break-decks", "BOARD BREAK DECK", in_play.empty() ? "none in the game" : label(in_play).c_str());
const bool busy = live_mods::busy();
const float gap = ImGui::GetStyle().ItemSpacing.x;
const float width = (ImGui::GetContentRegionAvail().x - gap * 3) / 4;
for (std::size_t i = 0; i < decks.size(); ++i) {
if (i % 4) ImGui::SameLine();
const auto& deck = decks[i];
ImGui::PushID(static_cast<int>(i));
ImGui::BeginDisabled(!there(deck) || busy || deck == in_play || !callbacks.queue_console_command);
if (ImGui::Button((label(deck) + (deck == in_play ? " *" : "")).c_str(), ImVec2(width, 0)))
send_console(menu, callbacks, "boardbreak deck " + deck);
ImGui::EndDisabled();
ImGui::PopID();
}
if (busy) note("Applying mods...");
else if (!in_play.empty() && chosen != in_play && there(chosen))
note((label(chosen) + " is chosen and shows once the level has loaded again.").c_str());
note("The deck every board in the game has, and the one that breaks. Picking another loads the level again; "
"decks that are not installed are greyed out.");
end_card();
}
}
namespace {
std::filesystem::path game_directory() {
std::array<wchar_t, 32768> executable{};
@@ -44,8 +102,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 +127,8 @@ void draw_installed_mods(SkateMenu& menu) {
// Mods still in effect whose folder has been deleted: applying unloads them.
std::vector<std::string> 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();
@@ -165,7 +227,7 @@ void draw_installed_mods(SkateMenu& menu) {
} else if (set_aside(entry)) {
status = "SET ASIDE";
hover = "Set aside while a Board Break deck is the one in the game: the game has one deck model, so one "
"mod's deck loads at a time. Pick another deck with Board Break deck, or disable Board Break's decks.";
"mod's deck loads at a time. Pick another deck under BOARD BREAK DECK, or disable Board Break's decks.";
} else if (entry.enabled && effect) {
status = "LOADED";
colour = skate_theme::good;
@@ -268,7 +330,7 @@ void draw_custom_scripts(SkateMenu& menu) {
menu::end_card();
}
}
void draw_modding_menu(SkateMenu& menu, int tab) {
void draw_modding_menu(SkateMenu& menu, int tab, const Model& model, const CallbacksV3& callbacks) {
// Loose files are switched in the launcher; custom scripts only need to know
// whether they are on.
if (!menu.loose_files_settings_loaded) {
@@ -277,6 +339,9 @@ void draw_modding_menu(SkateMenu& menu, int tab) {
catch (const std::exception&) {}
}
if (tab == 1) draw_custom_scripts(menu);
else draw_installed_mods(menu);
else {
menu::board_break_deck_card(menu, model, callbacks);
draw_installed_mods(menu);
}
}
}
+5 -2
View File
@@ -1,6 +1,9 @@
#pragma once
namespace dingosdk::overlay {
struct SkateMenu;
// The MODS page: tab 0 lists installed mods, tab 1 custom Lua scripts.
void draw_modding_menu(SkateMenu& menu, int tab);
struct Model;
struct CallbacksV3;
// The MODS page: tab 0 lists installed mods (with Board Break's deck above them, when one is
// installed), tab 1 custom Lua scripts.
void draw_modding_menu(SkateMenu& menu, int tab, const Model& model, const CallbacksV3& callbacks);
}
+1 -1
View File
@@ -435,7 +435,7 @@ void draw_skate_menu(SkateMenu& menu, const Model& model, const CallbacksV3& cal
menu.mods_tab = 0;
ImGui::PushID(menu.mods_tab);
ImGui::BeginChild("mods-tab", ImVec2(0, page_body_height(menu)));
draw_modding_menu(menu, menu.mods_tab);
draw_modding_menu(menu, menu.mods_tab, model, callbacks);
ImGui::EndChild();
ImGui::PopID();
break;
@@ -38,6 +38,9 @@ bool primary_button(SkateMenu& menu, const char* label, bool enabled = true);
float page_body_height(const SkateMenu& menu);
// Runs a console command and shows its reply as feedback.
void send_console(SkateMenu& menu, const CallbacksV3& callbacks, const std::string& command);
// Board Break's decks, a button for each (modding_menu.cpp): on the Mods page and under the
// feature's switch on the Skater page. Draws nothing while no deck is installed.
void board_break_deck_card(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks);
void debug_request(SkateMenu& menu, const CallbacksV3& callbacks, DebugRequest request);
// HOST shows the host form until a session exists, then that session (SESSION).
inline constexpr int multiplayer_session_tab = 1, multiplayer_voice_tab = 2, multiplayer_bans_tab = 3;
@@ -185,6 +185,7 @@ void movement_controls(SkateMenu& menu, const Model& model, const CallbacksV3& c
board_break, model.board_break.available && callbacks.queue_console_command))
send_console(menu, callbacks, board_break ? "boardbreak 1" : "boardbreak 0");
end_card();
board_break_deck_card(menu, model, callbacks);
begin_card(menu, "boosts", "BOOSTS", "Buttons are set in Settings > Controls");
ImGui::BeginDisabled(!callbacks.queue_debug);
+65 -11
View File
@@ -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
@@ -19,7 +19,9 @@ deck's place in the mod list in [mod_catalog.cpp](../Engine/Vfs/mod_catalog.cpp)
while it is ridden: the nose goes.
- `boardbreak 0|1`, `boardbreak deck [name|number|next]`, `boardbreak snap [nose|mid|tail]` to look
at a break (that one stays until `boardbreak mend` or a respawn). The switch and the deck are in
the menus beside Road Rash (Mod Options > Player, and the overlay's Skater page). The switch is
the menus beside Road Rash (Mod Options > Player, and the overlay's Skater page). The overlay has
a card BOARD BREAK DECK with a button for each deck, on the Mods page and under the switch: a
press asks for that deck, the mods are applied and the level loads again with it. The switch is
saved with the profile.
- A break plays back in the replay editor as it happened.
- Without a deck pack installed nothing breaks and nothing is changed.
@@ -66,6 +68,18 @@ deck's place in the mod list in [mod_catalog.cpp](../Engine/Vfs/mod_catalog.cpp)
`light`, `medium`, `heavy`, by the size of the break), played straight to the speakers.
- **The slam's force** comes through No Bail's existing hooks: the cause it records for the watched
skater, and the cause collector at the moment the selector chooses Wipeout. Nothing new is hooked.
- **The trucks' baseplates.** The game's truck model (one for every truck item,
`characters/skateboard/unlicensed/truck/generic/default/2022/truck_gen_default_mesh`) has its
hangers on the trucks' bones and everything bolted to the deck, the baseplates and their bolts,
on the deck's bone. With the deck's bone moved away to hide the real deck, a broken board had
hangers and wheels and no baseplates. The decks inside a deck pack bring a copy of the truck
model with those vertices on the trucks' effectors instead (`eff_truckBack`, `eff_truckFront`:
a bone under each truck's that no model uses), and the hook writes each effector from its
truck's bone as the animation left it, so that the truck's bone followed by the effector's is
what the two are at rest: on a whole board the baseplate sits on the deck however the truck
steers, and a truck that a piece carries off takes its baseplate along
(`baseplate_bone`, with a test). That is done on every board that passes the hook. With the
game's own truck model nothing is on the effectors and writing them shows nothing.
- **On foot the grip tape always holds.** A loose piece takes the ground from where the board
rolled or lies; a board in the skater's hand does not tell where the ground is, and a piece that
came off there hung in the air at hand height.
@@ -73,10 +87,28 @@ 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_<Name>`; 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_<Name>` holds the deck `<name>`;
- a deck pack, a mod folder of any name, holds a deck in each `decks/<name>/`, 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 `<folder>#<name>`: 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_<Name>` (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 truck model with the baseplates on
the effectors belongs in the decks inside the pack only: a ReSkate that does not place those
bones would draw the baseplates turning with the hangers, and such a version reads the pack's
own layout, never `decks/`.
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 +121,15 @@ 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 truck model for a deck inside a pack: the game's own, with every vertex that is on the bone
Deck moved to `eff_truckBack` (behind the middle of the board) or `eff_truckFront` (ahead of
it). Nothing else changes; Studio takes it back as it is.
- 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,9 +137,24 @@ 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.
On main at 8351654, with the pack as the only source of decks (its decks with the truck model
that has the baseplates on the effectors): a dozen breaks from real slams at the nose, in the
middle and at the tail, clean and held by the grip, with the baseplates on the pieces, and on the
whole board in hand and on the ground. The effectors are bones 5 and 9 of the pose, each at
0.0395 m along its truck's bone and not turned, as the rig has them. A press on Shaped in the
overlay's deck card: the mods applied in 3.8 s, the level loaded again in 15 s with that deck,
which then broke. In the game's Mod Options > Player page, on 2.0.6: one press on Board Break
deck is one deck further and one reload.
## Not tried yet
- Multiplayer: other players' boards are only handled by the code path (their stretches are moved
@@ -111,6 +162,9 @@ older build the feature was developed on, a middle break played back in the repl
- The online route: all of the above ran on the offline profile.
- The replay editor on this branch (the replay was checked on the older build the feature was
developed on).
- The rows in Mod Options > Player in the game itself (their view has a test; the overlay's switch
was used).
- The deck card with a deck that is not one of the eight, and with none installed (it draws
nothing then).
- A board that does not pass the render pose hook, with the truck model from a pack: its
baseplates would follow the hangers.
- A mod that brings a truck model of its own beside a deck pack: the pack's is the one that loads.
- A level change in the middle of a break.