Files
ReSkate/Extension/BoardBreak/board_break.cpp
T
Fourth Studio bd628abc76 Board Break: a hard slam can snap the board
A slam can break the local skater's board at the nose, in the middle or at
the tail. Half the 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. The board is whole again in hand, on riding and on a respawn.

The pieces are four stretches of the deck in a content mod's copy of the
game's one deck model, each on a spare bone of the board's rig. The boards'
render pose hook only moves bones, because the replay records where a bone is
and not its size: a broken board's deck goes away and its stretches go to
where the pieces lie, and a whole board's stretches go away.

The deck the player chose loads first and other mods that bring a deck model
are set aside for the run, without touching mods.json. Another deck is chosen
while the game runs, through a live apply of the mods.

contrib/board-break.md says how it works and how a deck pack is made.
2026-10-11 20:07:00 +02:00

671 lines
37 KiB
C++

#include "board_break.h"
#include "board_break_model.h"
#include "Engine/Core/Log/logging.h"
#include "Engine/Core/Platform/memory.h"
#include "Engine/Game/Build/20260929/no_bail.h"
#include "Engine/Vfs/mod_catalog.h"
#include "Extension/Assets/live_mods.h"
#include "Extension/Multiplayer/Remote/native_pose_layout.h"
#include "Extension/Multiplayer/Remote/native_skater.h"
#include "Extension/Profile/local_profile_runtime.h"
#include "Extension/Skater/no_bail.h"
#include <Windows.h>
#include <mmsystem.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <cctype>
#include <cmath>
#include <cstring>
#include <cwctype>
#include <filesystem>
#include <format>
#include <limits>
#include <mutex>
#include <optional>
#include <random>
#include <string>
#include <vector>
#pragma comment(lib, "winmm.lib")
namespace dingosdk::board_break {
namespace {
constexpr const char* enabled_preference = "BoardBreak";
// A slam of `heavy` force or more always breaks the board; a lighter one with `light`, rising to
// certain between 60% of `heavy` and `heavy` (break_chance). Half of all breaks stay joined by the
// grip tape. Now and then a board gives way under the rider: per second of riding, the nose goes.
constexpr float heavy = 10.0f, light = 0.25f, joined_share = 0.5f;
constexpr double gives_way = 0.0001;
constexpr std::size_t cause_count = 35; // the collector's cause bytes (+0x28) and forces (+0x4c)
constexpr std::uint64_t recent_ms = 1500; // a cause belongs to a wipeout this soon before it
// The board is back in the skater's hand: close to the skater and not lying flat.
constexpr float hand_reach = 1.3f, hand_upright = 0.8f;
// The board rig's pose, in the order the game evaluates it: 1 the rig's place in the world, 2 the
// 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;
constexpr std::array<int, 4> carrier{12, 13, 15, 14};
constexpr std::uint32_t rig_bones = 16;
bool readable(std::uintptr_t p, void* out, std::size_t size) { return size ? memory::peek_bytes(p, out, size) : p >= 0x10000; }
template <class... Args> void say(logging::Level level, std::format_string<Args...> text, Args&&... args) {
logging::log(level, logging::Channel::skater, "Board Break: {}", std::format(text, std::forward<Args>(args)...));
}
// Written by the physics step, read by the client tick.
struct Hit {
std::atomic<std::uint64_t> at{};
std::atomic<std::int32_t> reason{-1};
std::atomic<float> force{};
};
Hit& last_cause() { static Hit value; return value; }
Hit& last_wipeout() { static Hit value; return value; }
struct Broken {
std::uintptr_t board{};
std::uint64_t since{};
bool gave_way{}; // it went under the rider: it stays broken while it is ridden
bool asked{}; // it was broken from the console, to be looked at: it stays until it is mended there
};
struct State {
// Any thread.
std::atomic<bool> ready{}, on{true}, wanted_mend{};
std::atomic<std::uintptr_t> base{};
std::atomic<bool> has_deck{}; // one of Board Break's decks is the one in the game
std::atomic<int> wanted_snap{-1}; // asked for from another thread, done at the next tick
std::mutex names;
std::string deck, wanted_deck; // under `names`: the deck in the game, and one asked for (done at the next tick)
// Client thread from here on.
std::optional<Broken> broken;
Break parts; // what the broken board keeps, what flew off, how they hang together
float angle{}; // how far a hanging end droops (radians), or how deep a fold sags (0 to 1)
std::uint32_t flights{}; // counts the breaks, so each starts a flight of its own
bool counted{}, hooked{};
std::uint64_t wipeouts{}, back_since{}, last_error{}, rode_at{}, board_seen{};
// The ground under the board: its own height while it rolls on it, and what of that was fresh
// when the board broke (else not a number).
float ride_height{};
std::uint64_t ride_when{};
float break_ground{std::numeric_limits<float>::quiet_NaN()};
std::mt19937 random{std::random_device{}()};
};
State& state() {
static auto* value = new State;
return *value;
}
// What the render pose hook does to the boards, published by the client tick.
struct RenderEdit {
std::atomic<std::uintptr_t> holder{}; // the local board's animation holder
std::atomic<std::uint32_t> kept{}; // the piece it shows (Piece bits), 0 while it is whole
std::atomic<int> hinge{};
std::atomic<float> angle{};
std::atomic<bool> park{}; // a breakable deck is in the game: whole boards hide their stretches
};
RenderEdit& render_edit() { static RenderEdit value; return value; }
// ---- the board's pose --------------------------------------------------------------------------
// One bone: scale, rotation and position, four floats each.
struct PoseBone {
float scale[4], rotation[4], position[4];
};
bool load_bone(std::uintptr_t address, PoseBone& out) noexcept {
__try {
std::memcpy(&out, reinterpret_cast<const void*>(address), sizeof(out));
return true;
} __except (EXCEPTION_EXECUTE_HANDLER) { return false; }
}
// Rotation and position of one bone, at its own size; the fourth lane of each is left as it is.
bool store_bone(std::uintptr_t address, Quat rotation, Vec position) noexcept {
__try {
auto* lane = reinterpret_cast<float*>(address);
lane[0] = lane[1] = lane[2] = 1.0f;
lane[4] = rotation.x; lane[5] = rotation.y; lane[6] = rotation.z; lane[7] = rotation.w;
lane[8] = position.x; lane[9] = position.y; lane[10] = position.z;
return true;
} __except (EXCEPTION_EXECUTE_HANDLER) { return false; }
}
Quat facing_of(const PoseBone& bone) noexcept { return unit({bone.rotation[0], bone.rotation[1], bone.rotation[2], bone.rotation[3]}); }
Vec place_of(const PoseBone& bone) noexcept { return {bone.position[0], bone.position[1], bone.position[2]}; }
// The pose buffer of a board, or 0 when it is not the rig this knows.
std::uintptr_t board_pose(std::uintptr_t base, std::uintptr_t holder) {
const auto pose = multiplayer::read_native_pose_layout(readable, base, holder, 64);
return pose.buffer && pose.count == rig_bones ? pose.buffer : 0;
}
std::uintptr_t bone_at(std::uintptr_t buffer, int index) noexcept { return buffer + static_cast<std::uint32_t>(index) * 0x30ULL; }
// The board's place in the world as its pose has it.
bool board_place(std::uintptr_t base, std::uintptr_t holder, Vec& at, Quat& facing) {
const auto buffer = board_pose(base, holder);
PoseBone root{};
if (!buffer || !load_bone(bone_at(buffer, root_bone), root)) return false;
at = place_of(root);
facing = facing_of(root);
return std::isfinite(at.x) && std::isfinite(at.y) && std::isfinite(at.z);
}
// Moves a truck with a piece (truck_bone).
void carry_truck(std::uintptr_t buffer, const Rigid& motion, int index, Vec place) noexcept {
PoseBone mount{}, truck{};
if (!load_bone(bone_at(buffer, mount_bone), mount) || !load_bone(bone_at(buffer, index), truck)) return;
const auto bone = truck_bone(motion, facing_of(mount), place_of(mount), facing_of(truck), place);
(void)store_bone(bone_at(buffer, index), bone.facing, bone.at);
}
// ---- 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
// wherever the board itself is in that frame, so the two part company although both are the one board.
struct Flight {
std::mutex mutex;
// From the tick.
std::uint32_t id{}; // changes with every break; 0 = no piece in flight
std::uint32_t mask{}; // the piece's stretches of deck and its trucks
float ground{}; // world height of the ground under the skater
float rest{}; // how far above the ground the piece's origin lies once it rests
Vec kick{}, spin{}; // start speed (m/s) and turning (axis x rad/s), in the deck's own axes
float ridden{std::numeric_limits<float>::quiet_NaN()}; // the ground the board rolled on as it broke
// The hook's.
std::uint32_t running{};
std::int64_t last{};
Vec at{}, speed{}, turning{};
Quat facing{};
bool landed{}; // lying on what is taken for the ground right now
bool settled{}; // has landed once: flat from then on, whichever side came up
bool upside_down{};
Vec from{}; // where the board was when it broke
float limit{}; // the tick's first guess of the ground: never taken higher than this
float tipped{}; // 0..1: how far a half on one truck has tipped onto its broken end
};
Flight& flight() { static Flight value; return value; }
// The stretches of deck that are in flight (0 for none) and, in `piece`, where they are from the
// board's own place; the piece's trucks are moved here.
std::uint32_t fly_piece(std::uintptr_t buffer, Rigid& piece) {
auto& f = flight();
std::unique_lock lock(f.mutex, std::try_to_lock);
if (!lock.owns_lock() || !f.id) return 0;
PoseBone root{}, mount{};
if (!load_bone(bone_at(buffer, root_bone), root) || !load_bone(bone_at(buffer, mount_bone), mount)) return 0;
const Vec board_at = place_of(root);
const Quat board_facing = facing_of(root), deck_turn = facing_of(mount);
LARGE_INTEGER now{};
QueryPerformanceCounter(&now);
static const double frequency = [] { LARGE_INTEGER value{}; QueryPerformanceFrequency(&value); return static_cast<double>(value.QuadPart); }();
if (f.running != f.id) {
f.running = f.id;
f.last = now.QuadPart;
f.at = f.from = board_at;
f.facing = board_facing;
f.speed = turn(board_facing, turn(deck_turn, f.kick));
f.turning = turn(board_facing, turn(deck_turn, f.spin));
f.landed = f.settled = f.upside_down = false;
f.limit = f.ground;
f.tipped = 0;
}
const auto dt = static_cast<float>(std::clamp((now.QuadPart - f.last) / frequency, 0.0, 0.05));
f.last = now.QuadPart;
// The ground is where the board itself lies: its lowest point right now, which physics brings
// to rest on the ground. The skater's height at the slam (the hips, and the skater may be in the
// air) is only an upper limit. It is the board's height NOW, not the lowest ever seen: one odd
// pose (a teleport, a frame before the board is placed) must not sink the piece for good, and a
// pose that is nowhere near the break is passed over altogether.
const bool plausible = std::isfinite(board_at.x) && std::isfinite(board_at.y) && std::isfinite(board_at.z) && length(board_at - f.from) < 30.0f;
float lowest = board_at.y + 1.0f;
if (plausible) {
for (const Vec corner : {Vec{0, 0, 0}, Vec{0, 0.093f, 0}, Vec{0, 0.141f, 0.41f}, Vec{0, 0.136f, -0.40f}, Vec{0.105f, 0.10f, 0},
Vec{-0.105f, 0.10f, 0}, Vec{0.0876f, 0, front_axle}, Vec{-0.0876f, 0, front_axle}, Vec{0.0876f, 0, back_axle},
Vec{-0.0876f, 0, back_axle}})
lowest = (std::min)(lowest, (board_at + turn(board_facing, turn(deck_turn, corner))).y);
// Only a board that is still near the piece says anything about the ground under the piece.
if (std::hypot(board_at.x - f.at.x, board_at.z - f.at.z) < 4.0f) {
// The ground the board rolled on as it broke is exact, where its lowest point depends on
// how it has come to lie and on what: that height stays, unless the board has plainly
// dropped below it since (off a ledge, down a stair).
if (std::isfinite(f.ridden)) f.ground = lowest < f.ridden - 0.15f ? lowest : f.ridden;
else f.ground = (std::min)(f.limit, lowest);
}
}
// Where the piece's origin is once it lies on the ground: on its grip when it is upside down,
// else on its underside or its wheels. A little above it: real ground is rough, and a deck is a
// centimetre thick. A gap under a piece goes unseen, a piece cut off by the ground does not.
const float lift = f.mask & (front_truck | back_truck) ? 0.004f : 0.012f;
const auto lying = [&] {
const bool flipped = f.settled ? f.upside_down : turn(f.facing, {0, 1, 0}).y < 0;
return (flipped ? f.ground + 0.141f : f.ground + f.rest) + lift; // upside down it rests on the raised end of its grip side
};
if (f.landed && f.at.y > lying() + 0.01f) {
f.landed = false; // the ground turned out to be lower: it drops the rest of the way
f.speed = {};
f.turning = {};
}
if (!f.landed) {
f.speed.y -= 9.81f * dt;
f.at = f.at + f.speed * dt;
if (const auto rate = length(f.turning); rate > 1e-4f) f.facing = unit(about(f.turning * (1.0f / rate), rate * dt) * f.facing);
if (f.speed.y <= 0 && f.at.y <= lying()) { // on its way down: it starts on the ground it will land on
if (!f.settled) {
// Down: lying flat, pointing the way it was, the same side up.
const auto ahead = turn(f.facing, {0, 0, 1});
f.upside_down = turn(f.facing, {0, 1, 0}).y < 0;
f.facing = about({0, 1, 0}, std::atan2(ahead.x, ahead.z));
if (f.upside_down) f.facing = unit(f.facing * about(turn(deck_turn, {0, 0, 1}), 3.14159265f)); // rolled over, about its length
f.speed = {f.speed.x * 0.3f, 0, f.speed.z * 0.3f};
f.settled = true;
} else f.speed = {};
f.at.y = lying();
f.landed = true;
}
} else {
f.at = f.at + f.speed * dt; // a short slide
f.speed = f.speed * (std::max)(0.0f, 1.0f - 7.0f * dt);
f.at.y = lying(); // and it stays on the ground if that comes up again
}
// A half board that came down on its wheels tips onto its broken end.
const bool on_one_truck = (f.mask & (back_truck | front_truck)) && f.settled && !f.upside_down;
f.tipped = on_one_truck && f.landed ? (std::min)(1.0f, f.tipped + dt / 0.18f) : on_one_truck ? f.tipped : 0.0f;
const auto tip = on_root(deck_turn, f.mask & back_truck ? tipped_on_axle(back_axle, 1.0f, back_tip * f.tipped)
: tipped_on_axle(front_axle, -1.0f, front_tip * f.tipped));
// Where that is from the board's own place.
const auto back_to_board = conj(board_facing);
const auto lies_facing = unit(back_to_board * f.facing);
const auto lies_at = turn(back_to_board, f.at - board_at);
piece = {unit(lies_facing * tip.facing), lies_at + turn(lies_facing, tip.at)};
if (f.mask & back_truck) carry_truck(buffer, piece, back_truck_bone, back_truck_place);
if (f.mask & front_truck) carry_truck(buffer, piece, front_truck_bone, front_truck_place);
return f.mask & stretches;
}
// ---- 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.
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;
// 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));
if (layout.back_truck) carry_truck(buffer, layout.back_motion, back_truck_bone, back_truck_place);
if (layout.front_truck) carry_truck(buffer, layout.front_motion, front_truck_bone, front_truck_place);
if (hinge == Hinge::none) {
Rigid piece{};
const auto flying = fly_piece(buffer, piece);
for (std::size_t i = 0; i < layout.stretch.size(); ++i)
if (flying & (1u << i)) layout.stretch[i] = {true, piece};
}
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.
void park_pieces(std::uintptr_t base, std::uintptr_t holder) {
const auto buffer = board_pose(base, holder);
if (!buffer) return;
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);
}
// ---- breaking and mending --------------------------------------------------------------------------
// The crack of the deck, straight to the speakers (not through the game's mixer): a wave file out
// of the `sounds` folder of one of Board Break's deck mods. Its subfolders light, medium and heavy
// hold the sounds for a break of that size, as many as one likes under any names; a size with none
// of its own takes the nearest size that has some, and wave files in the folder itself serve when
// no subfolder does.
void crack_sound(State& s, Size size) {
static const std::array<std::vector<std::filesystem::path>, 4> sounds = [] {
std::array<std::vector<std::filesystem::path>, 4> found;
constexpr std::array<const wchar_t*, 4> names{L"light", L"medium", L"heavy", L""};
for (const auto& mod : mods::board_break_folders()) {
std::error_code failed;
if (!std::filesystem::is_directory(mod / L"sounds", failed)) continue;
for (std::size_t i = 0; i < names.size(); ++i) {
for (std::filesystem::directory_iterator at(mod / L"sounds" / names[i], failed), end; !failed && at != end; at.increment(failed)) {
auto extension = at->path().extension().wstring();
for (auto& c : extension) c = static_cast<wchar_t>(towlower(c));
if (at->is_regular_file(failed) && extension == L".wav") found[i].push_back(at->path());
}
std::sort(found[i].begin(), found[i].end());
}
break;
}
return found;
}();
// Nearest first: a medium break without sounds of its own takes a heavy one before a light one.
static constexpr std::array<std::array<std::size_t, 4>, 3> order{{{0, 1, 2, 3}, {1, 2, 0, 3}, {2, 1, 0, 3}}};
for (const auto from : order[static_cast<std::size_t>(size)]) {
const auto& files = sounds[from];
if (files.empty()) continue;
const auto& file = files[std::uniform_int_distribution<std::size_t>(0, files.size() - 1)(s.random)];
(void)PlaySoundW(file.c_str(), nullptr, SND_FILENAME | SND_ASYNC | SND_NODEFAULT);
return;
}
}
// `carried`: the skater is on foot with the board. The grip tape always holds then: a piece that
// came off would have to fall to the ground under the skater's feet, and where that is the board
// in a hand does not tell (a loose piece takes the ground from where the board rolled or lies).
bool snap(State& s, std::uintptr_t board, std::optional<Place> at, bool gave_way, bool carried, std::string_view why, bool asked = false) {
if (!s.has_deck.load(std::memory_order_acquire) || s.broken || !board) return false;
const auto place = at ? *at : static_cast<Place>(std::uniform_int_distribution<int>(0, 2)(s.random));
const bool joined = carried || std::uniform_real_distribution<float>(0.0f, 1.0f)(s.random) < joined_share;
const auto now = GetTickCount64();
s.parts = broken_at(place, joined);
s.angle = s.parts.hinge == Hinge::fold ? std::uniform_real_distribution<float>(0.35f, 1.0f)(s.random)
: std::uniform_real_distribution<float>(0.30f, 0.65f)(s.random);
s.break_ground = s.ride_when && now - s.ride_when < 400 ? s.ride_height : std::numeric_limits<float>::quiet_NaN();
++s.flights;
s.broken = Broken{board, now, gave_way, asked};
s.back_since = 0;
crack_sound(s, break_size(place, joined, s.angle));
say(logging::Level::debug, "the board broke at the {}{} ({}).", place_names[static_cast<std::size_t>(place)],
joined ? ", held together by the grip tape" : "", why);
return true;
}
void mend(State& s, std::string_view why) {
if (!s.broken) return;
say(logging::Level::debug, "the board is whole again after {:.1f} s ({}).", (GetTickCount64() - s.broken->since) / 1000.0, why);
s.broken.reset();
s.parts = {};
}
// Where the board is relative to the skater: how far, and how flat it lies (1 = flat on the ground
// either way up, 0 = on its edge, as when it is carried).
struct Hold {
bool known{};
float distance{}, upright{};
bool in_hand() const noexcept { return known && distance < hand_reach && upright < hand_upright; }
};
Hold board_hold(std::uintptr_t base, std::uintptr_t client) {
Hold out;
const auto frame = multiplayer::capture_local(base, client, true);
if (!frame.ready || frame.pose.board.empty()) return out;
const auto& skater = frame.pose.root.position;
const auto& board = frame.pose.board.front();
out.distance = std::hypot(board.position[0] - skater[0], board.position[1] - skater[1], board.position[2] - skater[2]);
const auto& q = board.rotation; // x, y, z, w: the board's up axis, turned into the world
out.upright = std::abs(1.0f - 2.0f * (q[0] * q[0] + q[2] * q[2]));
out.known = std::isfinite(out.distance) && std::isfinite(out.upright);
return out;
}
bool riding(std::uint32_t physics) noexcept {
// Ground/air and grind families; 300 is the wipeout, 5xx is on foot and getting on.
return (physics >= 100 && physics < 300) || (physics >= 400 && physics < 500);
}
// The deck `wanted` means, of those installed: a name, a number in the list (from 1), or "next"
// for the one after the deck in the game. Empty when there is no such deck.
std::string deck_meant(std::string wanted, const std::string& in_game, const std::vector<std::string>& installed) {
for (auto& c : wanted) c = c == ' ' ? '_' : static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (installed.empty()) return {};
if (wanted.empty() || wanted == "next") {
const auto now = std::find(installed.begin(), installed.end(), in_game);
return now == installed.end() || now + 1 == installed.end() ? installed.front() : *(now + 1);
}
if (std::all_of(wanted.begin(), wanted.end(), [](char c) { return c >= '0' && c <= '9'; }) && wanted.size() <= 3) {
// The decks the feature was made with keep their numbers whether they are installed or not;
// the others follow, in the order they are listed.
const auto number = static_cast<std::size_t>(std::stoi(wanted));
const auto& known = mods::board_break_decks;
if (number >= 1 && number <= known.size()) return std::string(known[number - 1]);
std::size_t next = known.size();
for (const auto& deck : installed)
if (std::find(known.begin(), known.end(), deck) == known.end() && ++next == number) return deck;
return {};
}
return wanted;
}
// What the hook is told this tick; `holder` 0 leaves the local board to be treated as any other.
void publish(State& s, std::uintptr_t base, std::uintptr_t client, std::uintptr_t holder) {
auto& edit = render_edit();
const bool broken = s.broken && holder;
edit.park.store(s.has_deck.load(std::memory_order_acquire), std::memory_order_release);
edit.angle.store(s.angle, std::memory_order_release);
edit.hinge.store(static_cast<int>(broken ? s.parts.hinge : Hinge::none), std::memory_order_release);
edit.kept.store(broken ? s.parts.kept : 0, std::memory_order_release);
edit.holder.store(holder, std::memory_order_release);
// The broken-off piece: one flight per break, gone when the board is whole again.
auto& f = flight();
std::lock_guard lock(f.mutex);
if (!broken || !s.parts.loose) f.id = 0;
else if (f.id != s.flights) {
const auto frame = multiplayer::capture_local(base, client, true);
const auto roll = [&](float low, float high) { return std::uniform_real_distribution<float>(low, high)(s.random); };
const bool loose_end = !(s.parts.loose & (front_truck | back_truck)); // a nose or tail, no truck on it
const float along = s.parts.loose & nose ? 1.0f : -1.0f; // the way it leaves, along the board
const float push = loose_end ? 1.0f : 0.55f;
f.mask = s.parts.loose;
f.ground = frame.ready ? frame.pose.root.position[1] : 0.0f;
f.rest = loose_end ? -0.083f : 0.0f; // a bare piece lies on its underside, one with a truck stands on its wheels
f.ridden = s.break_ground;
f.kick = {roll(-0.9f, 0.9f) * push, (1.4f + roll(0.0f, 1.2f)) * push, along * (1.2f + roll(0.0f, 1.6f)) * push};
f.spin = {along * (5.0f + roll(0.0f, 6.0f)) * push, roll(-3.0f, 3.0f) * push, roll(-2.0f, 2.0f) * push};
f.id = s.flights;
}
}
} // namespace
bool start(std::uintptr_t base) noexcept {
auto& s = state();
if (s.ready.load(std::memory_order_acquire)) return true;
if (!no_bail_available()) {
logging::write(logging::Level::warning, logging::Channel::skater,
"Board Break cannot follow the skater: its slams come through No Bail's hooks, which did not start.");
return false;
}
s.base.store(base, std::memory_order_release);
s.on.store(profile_runtime::local_preference(enabled_preference).value_or(true), std::memory_order_release);
const auto in_game = mods::board_break_in_play();
{
std::lock_guard lock(s.names);
s.deck = in_game;
}
s.has_deck.store(!in_game.empty(), std::memory_order_release);
s.ready.store(true, std::memory_order_release);
logging::log(logging::Level::info, logging::Channel::skater, "Board Break ready ({}, deck: {}).", s.on.load() ? "on" : "off",
deck_label(in_game));
return true;
}
void note_cause(std::int32_t reason, float magnitude) noexcept {
auto& hit = last_cause();
const auto now = GetTickCount64();
// Keep the strongest of a burst: one slam records several causes.
if (now - hit.at.load(std::memory_order_relaxed) < 250 && magnitude <= hit.force.load(std::memory_order_relaxed)) return;
hit.force.store(magnitude, std::memory_order_relaxed);
hit.reason.store(reason, std::memory_order_relaxed);
hit.at.store(now, std::memory_order_release);
}
void note_wipeout(std::uintptr_t causes) noexcept {
std::array<std::uint8_t, cause_count> set{};
std::array<float, cause_count> force{};
auto& hit = last_wipeout();
std::int32_t reason{-1};
float strongest{};
if (causes && memory::peek_bytes(causes + 0x28, set.data(), set.size()) && memory::peek_bytes(causes + 0x4c, force.data(), sizeof(force)))
for (std::size_t i = 0; i < cause_count; ++i)
if (set[i] && (reason < 0 || force[i] > strongest)) {
reason = static_cast<std::int32_t>(i);
strongest = force[i];
}
hit.force.store(strongest, std::memory_order_relaxed);
hit.reason.store(reason, std::memory_order_relaxed);
hit.at.store(GetTickCount64(), std::memory_order_release);
}
void on_render_pose(std::uintptr_t animation_interface) noexcept {
auto& edit = render_edit();
// Without a breakable deck in the game the boards have no stretches: nothing to do.
if (!edit.park.load(std::memory_order_acquire) || animation_interface <= 0xc0) return;
const auto base = state().base.load(std::memory_order_acquire);
const auto holder = edit.holder.load(std::memory_order_acquire);
const auto kept = edit.kept.load(std::memory_order_acquire);
const auto error = GetLastError();
try {
if (kept && holder && animation_interface == holder + 0xc0)
draw_broken(base, holder, kept, static_cast<Hinge>(edit.hinge.load(std::memory_order_acquire)), edit.angle.load(std::memory_order_acquire));
else park_pieces(base, animation_interface - 0xc0);
} catch (...) { /* A board that is being built or torn down: the next frame. */ }
SetLastError(error);
}
void on_client_tick(std::uintptr_t client) noexcept {
auto& s = state();
if (!s.ready.load(std::memory_order_acquire)) return;
const auto base = s.base.load(std::memory_order_acquire);
try {
const auto now = GetTickCount64();
// The deck in the game: the launch's, or the one the last live apply of the mods put there.
const auto deck_in_game = mods::board_break_in_play();
const bool in_game = !deck_in_game.empty();
std::string wanted;
bool changed{};
{
std::lock_guard lock(s.names);
changed = s.deck != deck_in_game;
s.deck = deck_in_game;
wanted.swap(s.wanted_deck);
}
s.has_deck.store(in_game, std::memory_order_release);
if (changed) say(logging::Level::info, "the deck in the game is now: {}{}.", deck_label(deck_in_game), in_game ? "" : " (boards cannot break)");
if (!wanted.empty() && wanted != deck_in_game && mods::save_board_break_deck(wanted)) {
mend(s, "another deck");
say(logging::Level::info, "deck {} chosen. {}", deck_label(wanted), live_mods::apply(true));
}
if (in_game && !s.hooked) {
// The boards' render pose hook is shared with multiplayer and first person, and only
// installed when one of them asks.
std::string detail;
s.hooked = multiplayer::install_entity_hooks(base, detail);
if (!s.hooked && now - s.last_error > 10000) {
s.last_error = now;
say(logging::Level::warning, "the boards' render pose hook is unavailable ({}).", detail);
}
}
NoBailSkater skater;
std::uintptr_t board{}, holder{};
if (in_game && no_bail_skater(skater)) {
const auto found = multiplayer::read_native_board(readable, base, skater.entity);
board = found.entity;
holder = found.holder;
}
if (!board) {
// No local skater or no board (a respawn, a level change): the next board is whole. A
// broken one that is only out of sight for a moment stays as it is.
if (s.broken && in_game && now - s.board_seen < 500) return;
mend(s, "the board is gone");
s.counted = false;
s.wanted_snap.store(-1, std::memory_order_release);
s.wanted_mend.store(false, std::memory_order_release);
publish(s, base, client, 0);
return;
}
s.board_seen = now;
if (s.broken && s.broken->board != board) mend(s, "the skater has another board");
watch_physics_state(client, skater.entity);
const auto watch = watched_physics_state();
if (s.wanted_mend.exchange(false, std::memory_order_acq_rel)) mend(s, "asked for");
const bool carried = watch.valid && watch.state >= 500 && watch.state < 600; // on foot and getting on
if (const int place = s.wanted_snap.exchange(-1, std::memory_order_acq_rel); place >= 0)
(void)snap(s, board, place < 3 ? std::optional(static_cast<Place>(place)) : std::nullopt, false, carried, "asked for", true);
if (watch.valid) {
if (watch.state == 100) {
// Rolling on the ground, the right way up: the board's own height is the ground's.
Vec at{};
Quat facing{};
if (board_place(base, holder, at, facing) && turn(facing, {0, 1, 0}).y > 0.9f) {
s.ride_height = at.y;
s.ride_when = now;
}
}
const bool on = s.on.load(std::memory_order_acquire);
if (!s.counted) {
s.wipeouts = watch.wipeouts;
s.counted = true;
}
if (watch.wipeouts != s.wipeouts) {
s.wipeouts = watch.wipeouts;
// The cause that made the selector choose Wipeout, or else the last one recorded.
auto* hit = &last_wipeout();
if (hit->reason.load() < 0 || now - hit->at.load() > recent_ms) hit = &last_cause();
const bool known = now - hit->at.load() <= recent_ms && hit->reason.load() >= 0;
const auto force = known ? hit->force.load() : 0.0f;
if (on && !s.broken && std::uniform_real_distribution<float>(0.0f, 1.0f)(s.random) < break_chance(force, heavy, light))
(void)snap(s, board, std::nullopt, false, watch.previous >= 500 && watch.previous < 600, std::format("a slam of force {:.1f}", force));
}
// Now and then a board simply gives way under the rider: the nose goes.
if (on && !s.broken && s.rode_at && riding(watch.state)) {
const auto seconds = (std::min<std::uint64_t>)(now - s.rode_at, 250) / 1000.0;
if (std::uniform_real_distribution<double>(0.0, 1.0)(s.random) < gives_way * seconds)
(void)snap(s, board, Place::nose, true, false, "it gave way while riding");
}
s.rode_at = now;
if (s.broken) {
// Whole again once the skater rides it, or has picked it up after the fall. A board that
// gave way under the rider stays as it is while it is ridden; after a fall it is a
// broken board like any other.
const auto hold = board_hold(base, client);
const bool on_it = riding(watch.state);
if (s.broken->gave_way && watch.state == addr::no_bail::wipeout_physics_state) s.broken->gave_way = false;
const bool back = s.broken->gave_way ? !on_it && hold.in_hand()
: on_it || (watch.state != addr::no_bail::wipeout_physics_state && hold.in_hand());
// A respawn gives the skater a new physics selector, which the watch reports as state 0
// until it has chosen once: the board is whole from that moment.
if (watch.state == 0 && now - s.broken->since > 300) mend(s, "a respawn");
else if (s.broken->asked) s.back_since = 0;
else if (!back) s.back_since = 0;
else if (!s.back_since) s.back_since = now;
else if (now - s.back_since > 300 && now - s.broken->since > 1200) mend(s, on_it ? "riding again" : "back in hand");
}
}
publish(s, base, client, holder);
} catch (const std::exception& error) {
// The skater or its board is being rebuilt: tried again at a later tick.
const auto now = GetTickCount64();
if (now - s.last_error > 10000 && logging::enabled(logging::Level::debug)) {
s.last_error = now;
say(logging::Level::debug, "waiting ({}).", error.what());
}
} catch (...) { /* A broken board is never worth the client tick. */ }
}
bool available() noexcept { return state().ready.load(std::memory_order_acquire); }
bool enabled() noexcept { return available() && state().on.load(std::memory_order_acquire); }
void set_enabled(bool enabled) noexcept {
state().on.store(enabled, std::memory_order_release);
profile_runtime::set_local_preference(enabled_preference, enabled);
}
std::string deck() {
auto& s = state();
std::lock_guard lock(s.names);
return s.deck;
}
std::string deck_label(std::string_view deck) {
if (deck.empty()) return "None";
std::string name(deck);
bool start = true;
for (auto& c : name) {
if (c == '_') c = ' ';
else if (start) c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
start = c == ' ';
}
return name;
}
std::string choose_deck(std::string wanted) {
auto& s = state();
if (!available()) return "Board Break is unavailable for this game build.";
const auto installed = mods::installed_board_break_decks();
if (installed.empty()) return "None of Board Break's decks is installed.";
const auto in_game = deck();
const auto chosen = deck_meant(wanted, in_game, installed);
if (chosen.empty()) return "There is no deck " + wanted + ".";
if (std::find(installed.begin(), installed.end(), chosen) == installed.end())
return "The " + deck_label(chosen) + " deck is not installed: its mod (Board_Break_" + chosen + ") is not in the Mods folder.";
if (chosen == in_game) return "The " + deck_label(chosen) + " deck is the one in the game.";
{
std::lock_guard lock(s.names);
s.wanted_deck = chosen;
}
return "The " + deck_label(chosen) + " deck: the mods are applied and the level loads again with it.";
}
void snap(int place) noexcept { state().wanted_snap.store(place >= 0 && place < 3 ? place : 3, std::memory_order_release); }
void mend() noexcept { state().wanted_mend.store(true, std::memory_order_release); }
} // namespace dingosdk::board_break