mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-18 01:37:05 +02:00
Players reported the game hanging for about a second while skating. A profile showed the memory line in the log walking every heap (HeapSummary) on the client thread, 0.5 to 2 s every two minutes. An audit of everything that runs on the game's update and render threads found more of the same, and Sentry's top crashes had causes of ours. Freezes: - Memory log line: no heap walk or thread snapshot; the GPU part and the write happen on a thread of their own. - Profile database: synchronous=NORMAL in WAL mode. A save is still kept when the game crashes; every play event no longer waits for the disk inside the game's hooks. - ReSkate's own preferences and values (menu clicks, session sliders, Discord) are saved by a background writer in order; reads see a value until it is saved. - Input: the foreground window and the keyboard are sampled on a thread of their own (GetAncestor and GetAsyncKeyState stalled the client tick 40-50 ms when another program held Windows' lock); flight input uses the sample; ClipCursor at most twice a second while a menu has the pointer. - Hooks installed while the game runs go in one transaction per place (HookBatch): throwdowns, remote players, the party, Hall of Meat. - Off the update thread: the host's map package lookup (one table read in the background, not the Mods folder per offer), the trainer's map file and settings save, Board Break's settings save, mod music playlists and their covers, the Spotify station covers. - Off the render thread: the Modding page's and the deck card's Mods folder reads. The overlay's three startup waits share five seconds. - Less work per frame: Steam names reused while one a frame is asked again, 2 friends a tick, steam_api64.dll checked once per process, cosmetic names read in place rather than a system call per character, an outfit load no longer copies the inventory per slot, the throwdown type scan runs at background priority over resident memory, the Board Break deck label is kept a minute. Crashes: - Render-resource pool: the metadata fields are written to the state owner only. The other renderer is 0x2daa00 bytes, and +0x11aae28 is far past its end (store_field crash; heap corruption in every release). - Other players' skateboard effects: a stale skateboard component is no longer followed, and the joint lookup is walked with a bound first (an endless loop in reused memory ended in GameStall). - Launcher: Oodle decodes into the buffer it asks for with corrupt data possible, and its assertions no longer stop the process (heap corruption in the store-copy scan; a breakpoint in oo2core). Timings (Send performance timings): - A frame of half a second or more in a level is sent as game.freeze: where the time went (ReSkate's tick, the game's frame, or outside it), whether the renderer kept presenting, and ReSkate's busiest zones. - Closing the game sends the level session it was in. Music after a connection drop: - Spotify device: an account Spotify could not be reached with is kept and tried again (3 s doubling to a minute); a session that drops is taken up again. Only an account Spotify refuses is asked for anew. - YouTube playlist pictures that could not be fetched are tried again, from 30 s to 10 minutes apart.
477 lines
22 KiB
C++
477 lines
22 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;
|
|
}
|
|
// Whether the game's lookup of a joint in this rig would come to an end. It walks the
|
|
// skeleton's map with no bound, and a rig whose memory has been taken for something else can
|
|
// hold a loop: the game's frame then never ends and its watchdog closes it ("GameStall", seen
|
|
// with this frame's thread in call_joint, 2026-10-15). The same walk is made here first, with a
|
|
// bound no real skeleton comes near.
|
|
bool joint_lookup_ends(Address rig, std::uint32_t joint) {
|
|
auto node = ptr(ptr(rig, vfx::rig_skeleton), vfx::skeleton_joints_root);
|
|
for (int steps = 0; steps < 64; ++steps) {
|
|
if (!node) return true;
|
|
std::uint32_t id{};
|
|
if (!readable(node + vfx::joint_node_id, &id, sizeof(id))) return false;
|
|
Address next{};
|
|
if (!readable(node + (id < joint ? vfx::joint_node_right : vfx::joint_node_left), &next, sizeof(next))) return false;
|
|
node = next;
|
|
}
|
|
return false;
|
|
}
|
|
int call_joint(State &s, Address rig, std::uint32_t joint) noexcept {
|
|
if (!joint_lookup_ends(rig, joint)) return -1;
|
|
__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)) {
|
|
// (The skateboard's component as it was when the pair was made: one the game has
|
|
// since destroyed is no longer an effects component of that skateboard, and its
|
|
// rig is not looked at. No set then fits.)
|
|
const bool live = ptr(pair->board_component) == s.base + vfx::component_vtable &&
|
|
owns(pair->board_component, pair->board);
|
|
const auto rig = live ? ptr(ptr(pair->board_component, vfx::component_rig_owner), vfx::rig_owner_rig) : 0;
|
|
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, ®ister_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
|