Files
ReSkate/Extension/Multiplayer/Remote/native_vfx.cpp
T
Fourth Studio b999f37e7b Board Break: wear and tear, rail snaps, break and body sounds, settings
Boards wear: hard landings, hits onto rails and ledges, grinding and
slams wear the board of the deck in the game, kept per deck. From a fifth
worn it steers lazier and its trucks go soft (the game's own physics
tuning, outside multiplayer sessions only); a worn board breaks from less
and, worn out, can give way under the rider. A board that broke is a new
one once it is whole again, and the game's own board wear is cleared with
it.

A boardslide, lipslide or darkslide that comes down too hard onto a rail
can snap the board, and the rider goes down with it: No Bail can now be
asked for a wipeout, which goes in as a cause before the selector picks.

Sounds play through XAudio2: a crack by the size of the break, metal when
the board broke on metal (the material comes from the game's contact
effects; which are metal is marked in play or listed by a pack), and the
body's hits in a bail by how hard they land.

The settings are a card on the overlay's Skater page, two rows in Mod
Options and console verbs, kept in BoardBreak.settings. A deck may name
itself and set its wheels wider apart in a deck.ini; Old School and Pig
join the decks the feature knows.
2026-10-15 19:22:21 +02:00

454 lines
20 KiB
C++

#include "native_vfx.h"
#include "native_skater.h"
#include "Engine/Core/Hooks/hooks.h"
#include "Engine/Core/Log/logging.h"
#include "Engine/Core/Platform/memory.h"
#include "Engine/Game/Build/addresses.h"
#include "Engine/Game/Build/20260929/engine.h"
#include "Engine/Game/Build/20260929/native_vfx.h"
#include "Extension/Multiplayer/Session/peer_slots.h"
#include <Windows.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <bit>
#include <cstring>
#include <format>
#include <deque>
#include <mutex>
// Other players' skaters and skateboards are the game's own entities, and the game gives each
// the effects its outfit comes with (a costume's trail, a deck's fire) as it does this player's.
// Three things are missing for them, and are done here:
// - The game builds their effects before their outfit is on. They are built again after it.
// - An effect a skater's item puts on the skateboard's wheels follows the skater's own copy of
// those joints, which only this player's skater keeps up to date. It is built on the
// skateboard, which has the same joints, instead.
// - Sparks, dust and puffs come from a skater touching the world, which only happens in that
// player's game. Each game sends its contacts (effects.h) and the others play them.
namespace dingosdk::multiplayer {
namespace {
namespace vfx = addr::native_vfx;
using Address = std::uintptr_t;
bool readable(Address p, void *out, std::size_t size) {
if (p < 0x10000 || p > memory::highest_user_address - size)
return false;
__try {
std::memcpy(out, reinterpret_cast<const void *>(p), size);
return true;
} __except (EXCEPTION_EXECUTE_HANDLER) {
return false;
}
}
template <class T> T get(Address p, std::size_t off = 0) {
T value{};
readable(p + off, &value, sizeof(value));
return value;
}
Address ptr(Address p, std::size_t off = 0) { return get<Address>(p, off); }
constexpr Address set_of(Address element) noexcept { return element & ~Address{4}; }
using Register = Address (*)(Address manager, Address key, Address component);
using ImpactCall = Address (*)(Address manager, Address key, const void *impact);
using Refresh = Address (*)(Address manager, Address key);
using ThreadWorld = Address *(*)(Address *out);
using BuildEffects = Address (*)(Address manager, std::uint64_t *entity, int id, Address *settings, char flag, char queued);
using JointIndex = int (*)(Address rig, std::uint32_t joint, int);
// A material no surface names: the game then uses the level's default one.
constexpr std::uint16_t default_material = max_impact_material;
struct Registered {
Address key{}, component{};
};
// Another player's skater and skateboard, and the effect sets of the skater's that are built on
// the skateboard.
struct Pair {
Address skater{}, board{}, board_component{};
std::vector<Address> moved;
bool changed{}; // `moved` is not what the skateboard was last built with
};
struct State {
Address base{};
DWORD client_thread{};
bool attempted{}, installed{};
std::string issue;
Register register_skater{};
ImpactCall impact{};
BuildEffects build_effects{};
std::mutex mutex;
std::vector<Impact> captured; // the local game's, since the last drain
std::deque<LocalImpact> recent; // the local game's, the last few seconds' (recent_local_impacts)
std::vector<Registered> registered; // every effects component seen registering, newest last
std::vector<Pair> pairs;
std::array<bool, max_impact_material + 1> seen{}; // materials this game's own contacts named
std::atomic<std::uint64_t> captured_count{}, played{}, refused{}, refreshed{}, moved{}, changes{};
std::atomic<unsigned> materials{}; // the level's material index table, as last read
std::string refusal; // why the last one was refused (client thread; status reads under mutex)
};
State &state() {
static auto *s = new State;
return *s;
}
// One other player: their skater and skateboard entities, and each one's effects component and
// its key with the manager.
struct Remote {
Address entity{}, key{}, component{}, board{}, board_key{}, board_component{};
std::uint64_t generation{}, refresh_at{}, next_lookup{}, changes{};
};
Remote &remote() {
static auto *r = new PeerStorage<Remote>;
return r->current();
}
Address register_hook(Address manager, Address key, Address component) {
auto &s = state();
{
std::lock_guard lock(s.mutex);
std::erase_if(s.registered, [&](const Registered &r) { return r.component == component; });
if (s.registered.size() >= 512) s.registered.erase(s.registered.begin(), s.registered.begin() + 256);
s.registered.push_back({key, component});
}
return s.register_skater(manager, key, component);
}
// Only the game calls through here: this game's own skater touching the world. What is played
// for other players goes straight to the original.
Address impact_hook(Address manager, Address key, const void *native) {
auto &s = state();
const auto result = s.impact(manager, key, native);
const auto error = GetLastError();
std::array<float, 12> vectors{};
std::uint32_t material{};
if (readable(reinterpret_cast<Address>(native), vectors.data(), sizeof(vectors)) &&
readable(reinterpret_cast<Address>(native) + vfx::impact_material, &material, sizeof(material))) {
Impact impact;
for (unsigned i = 0; i < 3; ++i) {
impact.position[i] = vectors[i];
impact.velocity[i] = vectors[4 + i];
impact.normal[i] = vectors[8 + i];
}
impact.material = material & vfx::material_valid
? static_cast<std::uint16_t>((material >> vfx::material_shift) & max_impact_material)
: default_material;
impact = wire_impact(impact);
if (valid_impact(impact)) {
std::lock_guard lock(s.mutex);
s.seen[impact.material] = true;
if (s.captured.size() >= 16) s.captured.erase(s.captured.begin());
s.captured.push_back(impact);
if (s.recent.size() >= 96) s.recent.pop_front();
s.recent.push_back({impact, GetTickCount64()});
++s.captured_count;
}
}
SetLastError(error);
return result;
}
// The effects component registered with the manager under this id.
void walk(Address node, int id, Address &component, unsigned depth = 0) {
if (!node || depth > 24 || component) return;
if (get<int>(node, vfx::skater_node_id) == id) {
component = ptr(node, vfx::skater_node_component);
return;
}
walk(ptr(node, 8), id, component, depth + 1);
walk(ptr(node), id, component, depth + 1);
}
// Whether an effects component belongs to this skater or skateboard entity.
bool owns(Address component, Address entity) {
if (!component || !entity) return false;
if (ptr(ptr(component, vfx::component_owner)) == entity) return true;
for (std::size_t off = 8; off < vfx::component_size; off += 8)
if (const auto q = ptr(component, off); q && (q == entity || ptr(q) == entity)) return true;
return false;
}
int call_joint(State &s, Address rig, std::uint32_t joint) noexcept {
__try {
return reinterpret_cast<JointIndex>(s.base + vfx::joint_index.rva)(rig, joint, 1);
} __except (EXCEPTION_EXECUTE_HANDLER) {
return -1;
}
}
// Whether every effect of a set sits on a joint this rig has (a skateboard's wheels).
bool set_fits(State &s, Address set, Address rig) {
const auto items = ptr(set_of(set), vfx::set_items);
const auto count = items ? get<std::uint32_t>(items - 4) & 0x7fffffffU : 0U;
if (!rig || !ptr(rig, vfx::rig_skeleton) || !count || count > 64) return false;
for (std::uint32_t i = 0; i < count; ++i) {
const auto joint = get<std::uint32_t>(set_of(ptr(items, i * 8ULL)), vfx::item_joint);
if (joint == 0xffffffffU || call_joint(s, rig, joint) < 0) return false;
}
return true;
}
// A skater's or skateboard's effects are built from its sets. For another player's, the
// skater's wheel effects are taken out of the skater's and added to the skateboard's.
Address build_hook(Address manager, std::uint64_t *entity, int id, Address *settings, char flag, char queued) {
auto &s = state();
thread_local std::vector<Address> block;
Address data{}, replaced{};
if (settings && readable(reinterpret_cast<Address>(settings), &data, sizeof(data)) && data) {
Address component{};
walk(ptr(manager, vfx::manager_skaters_root), id, component);
const auto count = get<std::uint32_t>(data - 4) & 0x7fffffffU;
std::lock_guard lock(s.mutex);
const auto pair = component && count <= 256
? std::find_if(s.pairs.begin(), s.pairs.end(),
[&](const Pair &p) { return owns(component, p.skater) || owns(component, p.board); })
: s.pairs.end();
if (pair != s.pairs.end()) {
block.assign(1, 0);
if (owns(component, pair->skater)) {
const auto rig = ptr(ptr(pair->board_component, vfx::component_rig_owner), vfx::rig_owner_rig);
std::vector<Address> moved;
for (std::uint32_t i = 0; i < count; ++i) {
const auto element = ptr(data, i * 8ULL);
// Always on only: the others wait on the skater's own state.
if (get<std::int32_t>(set_of(element), vfx::set_trigger) == vfx::trigger_always && set_fits(s, element, rig))
moved.push_back(element);
else block.push_back(element);
}
if (moved != pair->moved) {
pair->moved = std::move(moved);
pair->changed = true;
++s.changes;
}
} else {
for (std::uint32_t i = 0; i < count; ++i) block.push_back(ptr(data, i * 8ULL));
for (const auto element : pair->moved)
if (std::none_of(block.begin() + 1, block.end(), [&](Address own) { return set_of(own) == set_of(element); }))
block.push_back(element);
pair->changed = false;
}
const auto total = static_cast<std::uint32_t>(block.size() - 1);
if (total != count) {
block[0] = (Address{total} << 32) | total; // capacity, then the count
replaced = reinterpret_cast<Address>(block.data() + 1);
++s.moved;
}
}
}
return s.build_effects(manager, entity, id, replaced ? &replaced : settings, flag, queued);
}
template <class Original> bool attach(State &s, const vfx::Function &function, void *replacement, Original &original) {
void *forward{};
auto *target = reinterpret_cast<void *>(s.base + function.rva);
if (hook_prepare(target, replacement, &forward) != HookOk || !forward) {
s.issue = std::format("cannot prepare the hook at {:#x}", function.rva);
return false;
}
original = reinterpret_cast<Original>(forward);
if (hook_enable(target) != HookOk) {
s.issue = std::format("cannot enable the hook at {:#x}", function.rva);
return false;
}
return true;
}
bool matches(State &s, const vfx::Function &function) {
std::array<unsigned char, 16> actual{};
if (readable(s.base + function.rva, actual.data(), actual.size()) && actual == function.prefix) return true;
s.issue = std::format("the game's function at {:#x} differs", function.rva);
return false;
}
Address thread_world(State &s) noexcept {
Address world{};
__try {
reinterpret_cast<ThreadWorld>(s.base + vfx::thread_world.rva)(&world);
} __except (EXCEPTION_EXECUTE_HANDLER) {
world = 0;
}
return world;
}
Address manager(State &s, Address world) {
if (!world) return 0;
const auto m = world + get<std::uint32_t>(s.base + vfx::manager_offset);
return get<std::uint8_t>(m, vfx::manager_ready) ? m : 0;
}
// Whether the game can be handed this material: its index table is read without a bound. The
// table's length sits before it, as with the game's other arrays; a material this game's own
// contacts have named is known good whatever that says.
bool material_known(State &s, Address world, std::uint16_t material) {
const auto object = world + get<std::uint32_t>(s.base + vfx::materials_offset);
if (!get<std::uint8_t>(object, vfx::materials_ready)) return false; // the game would crash on it
const auto grid = ptr(object, vfx::materials_grid);
if (!grid) return false;
if (material == default_material) return true;
const auto table = ptr(ptr(grid, vfx::grid_data) & ~Address{4}, vfx::grid_index_table);
const auto count = table ? get<std::uint32_t>(table - 4) & 0x7fffffffU : 0U;
s.materials.store(count);
if (count <= max_impact_material && material < count) return true;
std::lock_guard lock(s.mutex);
return s.seen[material];
}
// The current slot's skater's and skateboard's keys with the manager, from the components
// that registered for them.
Remote &resolve(State &s) {
auto &r = remote();
const auto entity = remote_skater_entity(), board = remote_board_entity();
const auto generation = remote_skater_generation();
if (!entity) {
r.key = r.board_key = r.component = r.board_component = 0;
return r;
}
const auto now = GetTickCount64();
if (r.entity == entity && r.board == board && r.generation == generation &&
((r.key && (r.board_key || !board)) || now < r.next_lookup))
return r;
r.entity = entity;
r.board = board;
r.generation = generation;
r.key = r.board_key = r.component = r.board_component = 0;
r.next_lookup = now + 500;
std::lock_guard lock(s.mutex);
for (auto found = s.registered.rbegin(); found != s.registered.rend(); ++found) {
if (!found->key || ptr(found->component) != s.base + vfx::component_vtable) continue;
if (!r.key && owns(found->component, entity)) {
r.key = found->key;
r.component = found->component;
} else if (!r.board_key && board && owns(found->component, board)) {
r.board_key = found->key;
r.board_component = found->component;
}
}
if (r.key && r.board_key) {
std::erase_if(s.pairs, [&](const Pair &p) { return p.skater == entity || p.board == board; });
if (s.pairs.size() >= 256) s.pairs.erase(s.pairs.begin());
s.pairs.push_back({entity, board, r.board_component, {}, false});
}
return r;
}
bool call_impact(State &s, Address manager, Address key, const void *native) noexcept {
__try {
s.impact(manager, key, native);
return true;
} __except (EXCEPTION_EXECUTE_HANDLER) {
return false;
}
}
bool call_refresh(State &s, Address manager, Address key) noexcept {
__try {
reinterpret_cast<Refresh>(s.base + vfx::refresh.rva)(manager, key);
return true;
} __except (EXCEPTION_EXECUTE_HANDLER) {
return false;
}
}
void refuse(State &s, const char *why) {
++s.refused;
std::lock_guard lock(s.mutex);
s.refusal = why;
}
} // namespace
void prepare_effects(std::uintptr_t base) noexcept {
auto &s = state();
if (s.attempted) return;
s.attempted = true;
s.base = base;
s.client_thread = GetCurrentThreadId();
try {
s.installed = matches(s, vfx::register_skater) && matches(s, vfx::impact) && matches(s, vfx::refresh) &&
matches(s, vfx::thread_world) && matches(s, vfx::build_effects) && matches(s, vfx::joint_index) &&
attach(s, vfx::register_skater, &register_hook, s.register_skater) &&
attach(s, vfx::build_effects, &build_hook, s.build_effects) &&
attach(s, vfx::impact, &impact_hook, s.impact);
} catch (...) {
s.issue = "the hooks could not be installed";
}
if (!s.installed)
logging::log(logging::Level::warning, logging::Channel::runtime,
"Multiplayer: skater effects are not shared: {}.", s.issue);
}
std::vector<Impact> drain_impacts() {
auto &s = state();
std::lock_guard lock(s.mutex);
auto out = std::exchange(s.captured, {});
if (out.size() > max_impacts) out.erase(out.begin(), out.end() - max_impacts);
return out;
}
std::vector<LocalImpact> recent_local_impacts(std::uint64_t within_ms) {
auto &s = state();
const auto now = GetTickCount64();
std::lock_guard lock(s.mutex);
std::vector<LocalImpact> out;
for (const auto &one : s.recent)
if (now - one.at <= within_ms) out.push_back(one);
return out;
}
void play_impact(const Impact &impact) noexcept {
auto &s = state();
if (!s.installed || GetCurrentThreadId() != s.client_thread || !valid_impact(impact)) return;
try {
const auto world = thread_world(s);
const auto m = manager(s, world);
if (!m) return refuse(s, "the game's effects are not running on this thread");
if (!material_known(s, world, impact.material)) return refuse(s, "a surface this level does not have");
const auto key = resolve(s).key;
if (!key) return refuse(s, "the player's skater has no effects of its own yet");
alignas(16) std::array<std::uint8_t, vfx::impact_size> native{};
const auto vector = [&](std::size_t at, const std::array<float, 3> &v) {
std::memcpy(native.data() + at, v.data(), sizeof(v));
};
vector(0, impact.position);
vector(vfx::impact_velocity, impact.velocity);
vector(vfx::impact_normal, impact.normal);
const std::uint32_t material =
impact.material == default_material
? 0U
: (std::uint32_t{impact.material} << vfx::material_shift) | vfx::material_valid;
const std::array<std::uint32_t, 4> tail{material, material, 0U, std::bit_cast<std::uint32_t>(10.f)};
std::memcpy(native.data() + vfx::impact_material, tail.data(), sizeof(tail));
if (call_impact(s, m, key, native.data())) ++s.played;
else refuse(s, "the game faulted playing one");
} catch (...) {
}
}
void note_remote_outfit(std::uint64_t now) noexcept { remote().refresh_at = now + 500000; }
void tick_remote_effects(std::uint64_t now) noexcept {
auto &s = state();
auto &r = remote();
if (!s.installed || GetCurrentThreadId() != s.client_thread) return;
const auto changes = s.changes.load();
const bool due = r.refresh_at && now >= r.refresh_at;
if (!due && changes == r.changes) return;
try {
const auto m = manager(s, thread_world(s));
resolve(s);
if (!m || !r.key || (r.board && !r.board_key)) {
if (due) r.refresh_at = now + 500000; // not registered yet
return;
}
r.changes = changes;
// The skater first: what it hands over is there when the skateboard is built.
bool board = due;
if (due) {
r.refresh_at = 0;
if (call_refresh(s, m, r.key)) ++s.refreshed;
} else {
// The game built their skater again by itself and its wheel effects changed.
std::lock_guard lock(s.mutex);
board = std::any_of(s.pairs.begin(), s.pairs.end(), [&](const Pair &p) { return p.skater == r.entity && p.changed; });
}
if (board && r.board_key && call_refresh(s, m, r.board_key)) ++s.refreshed;
} catch (...) {
}
}
void reset_effects() noexcept {
auto &s = state();
each_peer([] { remote() = {}; });
std::lock_guard lock(s.mutex);
s.captured.clear();
s.pairs.clear();
s.refusal.clear();
}
std::string native_effects_status() {
auto &s = state();
if (!s.attempted) return "Effects: not started (join or host a session, or run `mp echo`).";
if (!s.installed) return "Effects: not shared: " + s.issue + ".";
std::lock_guard lock(s.mutex);
return std::format("Effects: {} of your contacts captured; {} played on other players, {} refused{}{}; "
"{} skaters and skateboards built again after their outfit, {} with wheel effects moved to the "
"skateboard; {} players' skaters known.",
s.captured_count.load(), s.played.load(), s.refused.load(),
s.refusal.empty() ? "" : " (last: ", s.refusal.empty() ? std::string{} : s.refusal + ")",
s.refreshed.load(), s.moved.load(), s.pairs.size());
}
} // namespace dingosdk::multiplayer