mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-17 16:17:06 +02:00
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.
513 lines
26 KiB
C++
513 lines
26 KiB
C++
#include "no_bail.h"
|
|
#include "Extension/BoardBreak/board_break.h"
|
|
#include "Engine/Core/Hooks/hooks.h"
|
|
#include "Engine/Core/Log/logging.h"
|
|
#include "Engine/Core/Platform/memory.h"
|
|
#include "Engine/Game/Build/supported_build.h"
|
|
#include "Engine/Game/Build/addresses.h"
|
|
#include "Engine/Game/Build/20260929/engine.h"
|
|
#include "Engine/Game/Build/20260929/no_bail.h"
|
|
#include <array>
|
|
#include <atomic>
|
|
#include <intrin.h>
|
|
|
|
namespace dingosdk {
|
|
namespace {
|
|
using namespace addr::no_bail;
|
|
// Native cause recorder: RCX=collector, EDX=reason, XMM2=magnitude.
|
|
using RecordCause = void (*)(std::uintptr_t, std::int32_t, float);
|
|
using ChooseState = std::uint32_t (*)(std::uintptr_t, std::uint32_t);
|
|
using SkeletonResponse = void (*)(std::uintptr_t, float, bool);
|
|
using PublishAnimation = void (*)(std::uintptr_t);
|
|
using ResetCauses = void (*)(std::uintptr_t);
|
|
constexpr std::uintptr_t highest = memory::highest_user_address;
|
|
// The cause most real slams record (Board Break's logs: cause 6 at forces of 9.6 to 10.7): a wipeout
|
|
// asked for goes in as one of those.
|
|
constexpr std::int32_t wanted_wipeout_reason = 6;
|
|
// The hooks re-resolve the local skater's ownership (some 35 fields) on every
|
|
// protected physics step: guarded same-process copies, not a system call each.
|
|
template<class T> bool read(std::uintptr_t address, T& value) noexcept { return memory::peek(address, value); }
|
|
struct LastError {
|
|
DWORD value = GetLastError();
|
|
~LastError() { SetLastError(value); }
|
|
};
|
|
std::uintptr_t pointer(std::uintptr_t object, std::uintptr_t offset = 0) noexcept {
|
|
std::uintptr_t value{};
|
|
if (object < 0x10000 || object > highest - offset || !read(object + offset, value) ||
|
|
value < 0x10000 || value > highest - 0x1000100) return 0;
|
|
return value;
|
|
}
|
|
struct Owner {
|
|
std::uintptr_t client{}, entity{}, player{}, handle{}, component{}, core{}, context{}, selector{}, causes{}, rig{};
|
|
bool operator==(const Owner&) const = default;
|
|
};
|
|
struct Lease {
|
|
Owner owner;
|
|
std::uint64_t manual_until{}, flight_until{};
|
|
bool active(std::uint64_t now) const noexcept {
|
|
return now < manual_until || now < flight_until;
|
|
}
|
|
};
|
|
struct BoardLock {
|
|
Owner owner;
|
|
std::uint64_t until{};
|
|
};
|
|
struct Protection {
|
|
std::uintptr_t base{};
|
|
bool mount_test{}; // the selector's mount request test is the known one
|
|
BoardLock board;
|
|
RecordCause cause_original{};
|
|
ChooseState choose_original{};
|
|
SkeletonResponse skeleton_original{};
|
|
PublishAnimation publish_original{};
|
|
ResetCauses reset_causes{};
|
|
std::atomic<bool> ready{};
|
|
SRWLOCK lock = SRWLOCK_INIT;
|
|
Lease lease;
|
|
std::atomic<NoBailStepObserver> step_observer{};
|
|
std::array<std::atomic<NoBailStepObserver>, no_bail_step_listener_slots> listeners{};
|
|
};
|
|
Protection& protection() { static auto* value = new Protection; return *value; }
|
|
struct StateWatch {
|
|
std::atomic<std::uintptr_t> selector{}, causes{}; // the watched skater's selector and cause collector
|
|
std::atomic<std::uint64_t> until{}, changes{}, wipeouts{};
|
|
std::atomic<std::uint32_t> state{}, previous{};
|
|
std::atomic<std::int64_t> since{}, previous_ticks{}; // performance-counter ticks
|
|
};
|
|
StateWatch& state_watch() { static auto* value = new StateWatch; return *value; }
|
|
// A wipeout asked for (no_bail_request_wipeout): the cause goes in at the watched selector's next pick.
|
|
struct WantedWipeout {
|
|
std::atomic<float> magnitude{};
|
|
std::atomic<std::uint64_t> until{};
|
|
};
|
|
WantedWipeout& wanted_wipeout() { static auto* value = new WantedWipeout; return *value; }
|
|
|
|
// Recheck live local ownership at use time. A retained physics address alone
|
|
// must never protect another skater after a respawn or level change.
|
|
bool resolve(std::uintptr_t client, std::uintptr_t entity, Owner& o) noexcept {
|
|
const auto base = protection().base;
|
|
unsigned state{}, manager_offset{};
|
|
if (pointer(client) != base + addr::engine::client_vtable || !read(client + 0xc4, state) ||
|
|
(state != 13 && state != 21) || pointer(entity) != base + addr::engine::skater_entity_vtable ||
|
|
!read(base + addr::engine::context_player_manager_offset, manager_offset) || manager_offset > 0x1000000) return false;
|
|
const auto context = pointer(client, 8);
|
|
const auto manager = pointer(context, manager_offset);
|
|
const auto begin = pointer(manager, 0x4c8), end = pointer(manager, 0x4d0);
|
|
if (!context || pointer(entity, 0x20) != context || pointer(manager) != base + addr::engine::local_player_manager_vtable ||
|
|
!begin || end != begin + 8) return false;
|
|
o.player = pointer(begin);
|
|
std::uint8_t local{}, remote{}, teleport{};
|
|
if (pointer(o.player) != base + addr::engine::local_player_vtable || pointer(o.player, 0x78) != context ||
|
|
!read(o.player + 0x45, local) || local != 1 || !read(o.player + 0x44, remote) || remote ||
|
|
pointer(o.player, 0xb8) != entity || pointer(entity, 0xf8) != o.player ||
|
|
!read(entity + 0x7e0, teleport) || teleport) return false;
|
|
o.handle = pointer(o.player, 0xb0);
|
|
const auto collection = pointer(entity, 0x70);
|
|
o.component = pointer(entity, 0x628);
|
|
o.core = pointer(o.component, 0x70);
|
|
o.context = pointer(o.core, 0x3c0);
|
|
o.selector = pointer(o.core, 0x440);
|
|
o.causes = pointer(o.core, 0x428);
|
|
o.rig = pointer(o.core, 0x438);
|
|
if (pointer(o.handle) != entity + 8 || pointer(collection) != entity ||
|
|
pointer(o.component) != base + addr::engine::skater_component_vtable || pointer(o.component, 0x18) != collection ||
|
|
pointer(o.core) != base + bail_core_vtable || !o.context || !o.selector ||
|
|
pointer(o.selector, 8) != o.context || pointer(o.causes, 0x20) != o.context || pointer(o.rig) != o.context || pointer(o.rig, 0x4630) != o.core)
|
|
return false;
|
|
o.client = client;
|
|
o.entity = entity;
|
|
return true;
|
|
}
|
|
bool protected_owner(std::uintptr_t object, std::uintptr_t Owner::* member, Owner* owner = nullptr) noexcept {
|
|
auto& p = protection();
|
|
if (!p.ready.load(std::memory_order_acquire)) return false;
|
|
// The lock only copies SDK data, and is never held across native code or
|
|
// memory reads. Hooks do not contend for the debug/camera action lock.
|
|
AcquireSRWLockShared(&p.lock);
|
|
const auto lease = p.lease;
|
|
ReleaseSRWLockShared(&p.lock);
|
|
if (!lease.active(GetTickCount64()) || object != lease.owner.*member) return false;
|
|
Owner current;
|
|
if (!resolve(lease.owner.client, lease.owner.entity, current) || current != lease.owner) return false;
|
|
if (owner) *owner = current;
|
|
return true;
|
|
}
|
|
// The published owner, its chain resolved again now; when `rig` is given, only if it is that owner's.
|
|
bool published_skater(std::uintptr_t rig, NoBailSkater& skater) noexcept {
|
|
auto& p = protection();
|
|
if (!p.ready.load(std::memory_order_acquire)) return false;
|
|
AcquireSRWLockShared(&p.lock);
|
|
const auto published = p.lease.owner;
|
|
ReleaseSRWLockShared(&p.lock);
|
|
Owner current;
|
|
if (!published.entity || (rig && rig != published.rig) || !resolve(published.client, published.entity, current) ||
|
|
current != published) return false;
|
|
skater = {p.base, current.entity, current.core, current.context, current.rig};
|
|
return true;
|
|
}
|
|
bool cancel_request(std::uintptr_t context, std::uintptr_t offset, LONG mask) noexcept {
|
|
const auto address = context + offset;
|
|
if (context < 0x10000 || context > highest - offset - sizeof(LONG) ||
|
|
(address & (alignof(LONG) - 1)) != 0) return false;
|
|
// Consume only the identified request bit. Preserve unrelated native flags
|
|
// even when another producer updates the same word.
|
|
__try {
|
|
_InterlockedAnd(reinterpret_cast<volatile LONG*>(address), ~mask);
|
|
return true;
|
|
} __except (EXCEPTION_EXECUTE_HANDLER) { return false; }
|
|
}
|
|
bool cancel_impact_request(std::uintptr_t context) noexcept {
|
|
return cancel_request(context, impact_request_offset, impact_request_mask);
|
|
}
|
|
bool cancel_wipeout_requests(std::uintptr_t context) noexcept {
|
|
return cancel_impact_request(context) &&
|
|
cancel_request(context, animation_request_offset, animation_request_mask);
|
|
}
|
|
bool reset_pending_causes(std::uintptr_t causes) noexcept {
|
|
const auto reset = protection().reset_causes;
|
|
if (!reset) return false;
|
|
__try {
|
|
reset(causes);
|
|
return true;
|
|
} __except (EXCEPTION_EXECUTE_HANDLER) { return false; }
|
|
}
|
|
bool filter_requests(std::uintptr_t object, std::uintptr_t Owner::* member) noexcept {
|
|
LastError error;
|
|
Owner owner;
|
|
// Native collision/landing checks also write the collector inline. Reset
|
|
// it at each consumer, after those writes, before either animation's bail
|
|
// or recover/runout query. Filtering record_cause alone misses this path.
|
|
return protected_owner(object, member, &owner) && cancel_wipeout_requests(owner.context) &&
|
|
reset_pending_causes(owner.causes);
|
|
}
|
|
bool suppress_cause(std::uintptr_t causes, std::int32_t reason, std::uintptr_t caller) noexcept {
|
|
LastError error;
|
|
Owner owner;
|
|
if (!protected_owner(causes, &Owner::causes, &owner)) return false;
|
|
for (const auto& impact : impact_bail_calls) {
|
|
if (caller == protection().base + impact.return_rva && reason == impact.reason)
|
|
return cancel_impact_request(owner.context);
|
|
}
|
|
return true;
|
|
}
|
|
void record_cause(std::uintptr_t causes, std::int32_t reason, float magnitude) {
|
|
const auto caller = reinterpret_cast<std::uintptr_t>(_ReturnAddress());
|
|
if (causes == state_watch().causes.load(std::memory_order_acquire)) board_break::note_cause(reason, magnitude);
|
|
const bool protect = suppress_cause(causes, reason, caller);
|
|
// Do not force the native recovery/stumble predicate (recovery_predicate). Its
|
|
// result is also exported to animation at +0x9e, even without a collision.
|
|
// Stop new causes before they reach either the wipeout or runout decision;
|
|
// the native per-step reset still owns clearing the collector's history.
|
|
if (!protect) protection().cause_original(causes, reason, magnitude);
|
|
}
|
|
// The local skater waiting on foot for its S.K.A.T.E. turn: its mount request goes.
|
|
void hold_off_board(std::uintptr_t selector, std::uint32_t current) noexcept {
|
|
auto& p = protection();
|
|
if (current != offboard_physics_state || !p.mount_test || !p.ready.load(std::memory_order_acquire)) return;
|
|
AcquireSRWLockShared(&p.lock);
|
|
const auto board = p.board;
|
|
ReleaseSRWLockShared(&p.lock);
|
|
if (GetTickCount64() >= board.until || selector != board.owner.selector) return;
|
|
Owner current_owner;
|
|
if (!resolve(board.owner.client, board.owner.entity, current_owner) || current_owner != board.owner) return;
|
|
(void)cancel_request(current_owner.context, animation_request_offset, mount_request_mask);
|
|
}
|
|
std::uint32_t choose_state(std::uintptr_t selector, std::uint32_t current) {
|
|
{
|
|
LastError error;
|
|
hold_off_board(selector, current);
|
|
}
|
|
// A wipeout asked for (no_bail_request_wipeout): its cause goes in as the game's contact tests
|
|
// record theirs, and the pick is Wipeout, as some of those tests return it directly.
|
|
bool force_wipeout{};
|
|
if (auto& wanted = wanted_wipeout(); wanted.until.load(std::memory_order_acquire) &&
|
|
selector == state_watch().selector.load(std::memory_order_acquire)) {
|
|
LastError error;
|
|
if (const auto until = wanted.until.exchange(0, std::memory_order_acq_rel); GetTickCount64() < until)
|
|
if (const auto causes = state_watch().causes.load(std::memory_order_acquire); causes && !protected_owner(selector, &Owner::selector)) {
|
|
record_cause(causes, wanted_wipeout_reason, wanted.magnitude.load(std::memory_order_relaxed));
|
|
force_wipeout = true;
|
|
}
|
|
}
|
|
// Clear shared requests before selection so native ground/air/walking
|
|
// transitions can still run. Some contact tests return Wipeout directly;
|
|
// retain the current state only for that result, never ordinary Offboard.
|
|
const bool filtered = filter_requests(selector, &Owner::selector);
|
|
auto next = protection().choose_original(selector, current);
|
|
LastError error;
|
|
if (force_wipeout && next != wipeout_physics_state) {
|
|
logging::log(logging::Level::debug, logging::Channel::skater, "No Bail: a wipeout was asked for (state {} would have been {}).", current, next);
|
|
next = wipeout_physics_state;
|
|
}
|
|
const auto chosen = filtered && next == wipeout_physics_state && protected_owner(selector, &Owner::selector) ? current : next;
|
|
auto& w = state_watch();
|
|
if (selector == w.selector.load(std::memory_order_acquire) && GetTickCount64() < w.until.load(std::memory_order_acquire)) {
|
|
if (const auto before = w.state.exchange(chosen, std::memory_order_acq_rel); before != chosen) {
|
|
LARGE_INTEGER now{};
|
|
QueryPerformanceCounter(&now);
|
|
w.previous.store(before, std::memory_order_relaxed);
|
|
w.previous_ticks.store(now.QuadPart - w.since.exchange(now.QuadPart, std::memory_order_relaxed), std::memory_order_relaxed);
|
|
w.changes.fetch_add(1, std::memory_order_relaxed);
|
|
if (chosen == wipeout_physics_state) {
|
|
// Before the count: the tick that sees the new wipeout finds its cause there.
|
|
board_break::note_wipeout(w.causes.load(std::memory_order_acquire));
|
|
w.wipeouts.fetch_add(1, std::memory_order_relaxed);
|
|
}
|
|
}
|
|
}
|
|
return chosen;
|
|
}
|
|
void skeleton_response(std::uintptr_t rig, float seconds, bool wipeout) {
|
|
// The state post-update can raise another request after the selector ran.
|
|
// Filter at this consumer, then let native constraints and recovery run.
|
|
if (filter_requests(rig, &Owner::rig)) wipeout = false;
|
|
const auto observer = protection().step_observer.load(std::memory_order_acquire);
|
|
std::array<NoBailStepObserver, no_bail_step_listener_slots> listeners{};
|
|
bool any = observer != nullptr;
|
|
for (std::size_t i = 0; i < listeners.size(); ++i) {
|
|
listeners[i] = protection().listeners[i].load(std::memory_order_acquire);
|
|
any = any || listeners[i];
|
|
}
|
|
if (any) {
|
|
LastError error;
|
|
NoBailSkater skater;
|
|
if (published_skater(rig, skater)) {
|
|
if (observer) observer(skater, seconds, wipeout);
|
|
for (const auto listener : listeners)
|
|
if (listener) listener(skater, seconds, wipeout);
|
|
}
|
|
}
|
|
protection().skeleton_original(rig, seconds, wipeout);
|
|
}
|
|
bool clear_contact_output(std::uintptr_t contacts) noexcept {
|
|
if (contacts < 0x10000 || contacts > highest - body_contact_output_offset) return false;
|
|
__try {
|
|
_InterlockedExchange8(reinterpret_cast<volatile char*>(contacts + body_contact_output_offset), 0);
|
|
return true;
|
|
} __except (EXCEPTION_EXECUTE_HANDLER) { return false; }
|
|
}
|
|
void filter_contact_animation(std::uintptr_t core) noexcept {
|
|
LastError error;
|
|
Owner owner;
|
|
std::uint32_t state{};
|
|
if (!protected_owner(core, &Owner::core, &owner) || !read(owner.context + 0x1414, state)) return;
|
|
// Ground/air and grind state families. Offboard, mounting, handplants and
|
|
// existing ragdolls retain their native contact reporting and recovery.
|
|
if (!((state >= 100 && state < 300) || (state >= 400 && state < 500))) return;
|
|
const auto contacts = pointer(pointer(core, 0x3b8), 0x30);
|
|
std::uint8_t contact{};
|
|
if (!contacts || !read(contacts + body_contact_output_offset, contact) || contact > 1) return;
|
|
// Animation gets this sensitive-body hit independently of the ordinary
|
|
// wipeout output and cause collector. Filter it after native publication,
|
|
// before the animation contact context copies it. Keep the general contact
|
|
// latch, collision timers, per-bone records, impulses and physics state.
|
|
if (clear_contact_output(contacts))
|
|
(void)cancel_request(owner.context, body_contact_context_offset, body_contact_context_mask);
|
|
}
|
|
void publish_animation(std::uintptr_t core) {
|
|
// Animation also reads requests without consulting the cause collector.
|
|
// Clear flags and pending causes before native publication, including
|
|
// its early recovery query and cause export. Leave the native query result
|
|
// unchanged: forcing it true used to put the skater into a stumbling state.
|
|
(void)filter_requests(core, &Owner::core);
|
|
protection().publish_original(core);
|
|
filter_contact_animation(core);
|
|
}
|
|
bool compatible(std::uintptr_t base) noexcept {
|
|
if (base < 0x10000 || base > highest - supported_build::game_image_size) return false;
|
|
IMAGE_DOS_HEADER dos{};
|
|
IMAGE_NT_HEADERS64 nt{};
|
|
if (!read(base, dos) || dos.e_magic != IMAGE_DOS_SIGNATURE || dos.e_lfanew <= 0 || dos.e_lfanew > 0x100000 ||
|
|
!read(base + dos.e_lfanew, nt) || nt.Signature != IMAGE_NT_SIGNATURE ||
|
|
nt.FileHeader.Machine != IMAGE_FILE_MACHINE_AMD64 || nt.OptionalHeader.SizeOfImage != supported_build::game_image_size)
|
|
return false;
|
|
std::array<unsigned char, reset_bail_causes_code.size()> reset_code{};
|
|
if (!read(base + reset_bail_causes_rva, reset_code) || reset_code != reset_bail_causes_code) return false;
|
|
for (const auto& contract : {record_bail_cause_contract, choose_physics_state_contract,
|
|
bail_animation_caller_contract, bail_state_caller_contract, bail_skeleton_contract, bail_publish_contract}) {
|
|
std::array<unsigned char, 32> actual{};
|
|
if (!read(base + contract.rva, actual) || actual != contract.bytes) return false;
|
|
}
|
|
for (const auto& contract : bail_consumer_contracts) {
|
|
std::array<unsigned char, 32> actual{};
|
|
if (!read(base + contract.rva, actual) || actual != contract.bytes) return false;
|
|
}
|
|
for (const auto& contract : body_contact_contracts) {
|
|
std::array<unsigned char, 32> actual{};
|
|
if (!read(base + contract.rva, actual) || actual != contract.bytes) return false;
|
|
}
|
|
for (const auto& impact : impact_bail_calls) {
|
|
std::array<unsigned char, 32> actual{};
|
|
if (!read(base + impact.caller.rva, actual) || actual != impact.caller.bytes) return false;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
bool start_no_bail(std::uintptr_t base) noexcept {
|
|
LastError error;
|
|
try {
|
|
auto& p = protection();
|
|
if (p.ready.load()) return p.base == base;
|
|
if (!compatible(base)) {
|
|
logging::write(logging::Level::warning, logging::Channel::skater,
|
|
"No Bail is unavailable: native bail contract did not match.");
|
|
return false;
|
|
}
|
|
p.base = base;
|
|
p.reset_causes = reinterpret_cast<ResetCauses>(base + reset_bail_causes_rva);
|
|
std::array<unsigned char, 32> mount_test{};
|
|
p.mount_test = read(base + mount_request_test_contract.rva, mount_test) &&
|
|
mount_test == mount_request_test_contract.bytes;
|
|
const std::array targets{
|
|
reinterpret_cast<void*>(base + record_bail_cause_contract.rva),
|
|
reinterpret_cast<void*>(base + choose_physics_state_contract.rva),
|
|
reinterpret_cast<void*>(base + bail_skeleton_contract.rva),
|
|
reinterpret_cast<void*>(base + bail_publish_contract.rva)};
|
|
const std::array replacements{
|
|
reinterpret_cast<void*>(&record_cause), reinterpret_cast<void*>(&choose_state),
|
|
reinterpret_cast<void*>(&skeleton_response), reinterpret_cast<void*>(&publish_animation)};
|
|
std::array<void*, 4> originals{};
|
|
auto status = HookOk;
|
|
std::size_t prepared{};
|
|
for (; prepared < targets.size(); ++prepared) {
|
|
status = hook_prepare(targets[prepared], replacements[prepared], &originals[prepared]);
|
|
if (status != HookOk) break;
|
|
if (!originals[prepared]) { ++prepared; status = HookUnsupportedFunction; break; }
|
|
}
|
|
if (status == HookOk) {
|
|
// Publish every relay before enabling any target.
|
|
p.cause_original = reinterpret_cast<RecordCause>(originals[0]);
|
|
p.choose_original = reinterpret_cast<ChooseState>(originals[1]);
|
|
p.skeleton_original = reinterpret_cast<SkeletonResponse>(originals[2]);
|
|
p.publish_original = reinterpret_cast<PublishAnimation>(originals[3]);
|
|
for (auto target : targets) {
|
|
status = hook_enable(target);
|
|
if (status != HookOk) break;
|
|
}
|
|
if (status == HookOk) {
|
|
p.ready.store(true, std::memory_order_release);
|
|
logging::write(logging::Level::info, logging::Channel::skater,
|
|
"No Bail ready: collision and landing causes filtered before physics/animation consume them; noclip is protected.");
|
|
return true;
|
|
}
|
|
}
|
|
// Published relays remain callable even if Detours reports an uncertain
|
|
// attach result. With ready=false any remaining hook simply forwards.
|
|
logging::log(logging::Level::warning, logging::Channel::skater,
|
|
"No Bail hook setup failed (status {}); protected noclip is unavailable.", static_cast<LONG>(status));
|
|
while (prepared) (void)hook_remove(targets[--prepared]);
|
|
} catch (...) {}
|
|
return false;
|
|
}
|
|
bool no_bail_available() noexcept { return protection().ready.load(std::memory_order_acquire); }
|
|
bool update_no_bail(std::uintptr_t client, std::uintptr_t entity, bool manual,
|
|
bool flying, std::uint64_t flight_expires) noexcept {
|
|
LastError error;
|
|
auto& p = protection();
|
|
Lease next;
|
|
const bool available = p.ready.load(std::memory_order_acquire) && resolve(client, entity, next.owner);
|
|
if (available) {
|
|
const auto now = GetTickCount64();
|
|
next.manual_until = manual ? now + 500 : 0;
|
|
next.flight_until = flying ? flight_expires : 0;
|
|
}
|
|
AcquireSRWLockExclusive(&p.lock);
|
|
p.lease = available ? next : Lease{};
|
|
ReleaseSRWLockExclusive(&p.lock);
|
|
return available;
|
|
}
|
|
bool set_teleport_on_board(std::uintptr_t component) noexcept {
|
|
__try {
|
|
*reinterpret_cast<volatile std::uint8_t*>(component + 0xc0) = 1;
|
|
return true;
|
|
} __except (EXCEPTION_EXECUTE_HANDLER) { return false; }
|
|
}
|
|
void update_board_lock(std::uintptr_t client, std::uintptr_t entity, bool locked) noexcept {
|
|
LastError error;
|
|
auto& p = protection();
|
|
if (!p.ready.load(std::memory_order_acquire)) return;
|
|
static bool rearm{}; // client tick only
|
|
if (locked) {
|
|
// The owner does not resolve while a teleport is under way: the last one stands
|
|
// until it expires.
|
|
BoardLock next;
|
|
if (resolve(client, entity, next.owner)) {
|
|
next.until = GetTickCount64() + 500;
|
|
AcquireSRWLockExclusive(&p.lock);
|
|
p.board = next;
|
|
ReleaseSRWLockExclusive(&p.lock);
|
|
}
|
|
rearm = true;
|
|
return;
|
|
}
|
|
AcquireSRWLockExclusive(&p.lock);
|
|
p.board = {};
|
|
ReleaseSRWLockExclusive(&p.lock);
|
|
// Released: teleports that keep the skater's own choice (the SDK's /tp) put it on the
|
|
// board again. Once the skater resolves, outside a teleport.
|
|
Owner owner;
|
|
if (rearm && resolve(client, entity, owner) && set_teleport_on_board(owner.component)) rearm = false;
|
|
}
|
|
void clear_no_bail() noexcept {
|
|
auto& p = protection();
|
|
AcquireSRWLockExclusive(&p.lock);
|
|
p.lease = {};
|
|
ReleaseSRWLockExclusive(&p.lock);
|
|
}
|
|
bool no_bail_skater(NoBailSkater& skater) noexcept { return published_skater(0, skater); }
|
|
void set_no_bail_step_observer(NoBailStepObserver observer) noexcept {
|
|
protection().step_observer.store(observer, std::memory_order_release);
|
|
}
|
|
void no_bail_request_wipeout(float magnitude) noexcept {
|
|
auto& wanted = wanted_wipeout();
|
|
wanted.magnitude.store(magnitude, std::memory_order_relaxed);
|
|
wanted.until.store(GetTickCount64() + 250, std::memory_order_release);
|
|
}
|
|
bool add_no_bail_step_listener(NoBailStepObserver listener) noexcept {
|
|
if (!listener) return false;
|
|
auto& slots = protection().listeners;
|
|
for (auto& slot : slots)
|
|
if (slot.load(std::memory_order_acquire) == listener) return true;
|
|
for (auto& slot : slots) {
|
|
NoBailStepObserver empty{};
|
|
if (slot.compare_exchange_strong(empty, listener, std::memory_order_acq_rel)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
void remove_no_bail_step_listener(NoBailStepObserver listener) noexcept {
|
|
if (!listener) return;
|
|
for (auto& slot : protection().listeners) {
|
|
auto expected = listener;
|
|
slot.compare_exchange_strong(expected, nullptr, std::memory_order_acq_rel);
|
|
}
|
|
}
|
|
void watch_physics_state(std::uintptr_t client, std::uintptr_t entity) noexcept {
|
|
auto& w = state_watch();
|
|
Owner owner;
|
|
if (!protection().ready.load(std::memory_order_acquire) || !resolve(client, entity, owner)) return;
|
|
if (w.selector.exchange(owner.selector, std::memory_order_acq_rel) != owner.selector)
|
|
w.state.store(0, std::memory_order_release);
|
|
w.causes.store(owner.causes, std::memory_order_release);
|
|
w.until.store(GetTickCount64() + 500, std::memory_order_release);
|
|
}
|
|
PhysicsStateWatch watched_physics_state() noexcept {
|
|
auto& w = state_watch();
|
|
PhysicsStateWatch result;
|
|
result.valid = protection().ready.load(std::memory_order_acquire) && w.selector.load(std::memory_order_acquire) &&
|
|
GetTickCount64() < w.until.load(std::memory_order_acquire);
|
|
result.state = w.state.load(std::memory_order_acquire);
|
|
result.previous = w.previous.load(std::memory_order_relaxed);
|
|
static const double frequency = [] { LARGE_INTEGER value{}; QueryPerformanceFrequency(&value); return static_cast<double>(value.QuadPart); }();
|
|
result.previous_seconds = static_cast<float>(static_cast<double>(w.previous_ticks.load(std::memory_order_relaxed)) / frequency);
|
|
result.changes = w.changes.load(std::memory_order_relaxed);
|
|
result.wipeouts = w.wipeouts.load(std::memory_order_relaxed);
|
|
return result;
|
|
}
|
|
void clear_no_bail_flight() noexcept {
|
|
auto& p = protection();
|
|
AcquireSRWLockExclusive(&p.lock);
|
|
p.lease.flight_until = 0;
|
|
ReleaseSRWLockExclusive(&p.lock);
|
|
}
|
|
}
|