mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-21 08:26:43 +02:00
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:
12 files changed
+300
-48
No files matched your search
+75
-15
@@ -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
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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.
|
||||
Reference in new issue
Block a user