#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 #include #include 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 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 ready{}; SRWLOCK lock = SRWLOCK_INIT; Lease lease; std::atomic step_observer{}; std::array, no_bail_step_listener_slots> listeners{}; }; Protection& protection() { static auto* value = new Protection; return *value; } struct StateWatch { std::atomic selector{}, causes{}; // the watched skater's selector and cause collector std::atomic until{}, changes{}, wipeouts{}; std::atomic state{}, previous{}; std::atomic 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 magnitude{}; std::atomic 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(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(_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 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(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 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 actual{}; if (!read(base + contract.rva, actual) || actual != contract.bytes) return false; } for (const auto& contract : bail_consumer_contracts) { std::array actual{}; if (!read(base + contract.rva, actual) || actual != contract.bytes) return false; } for (const auto& contract : body_contact_contracts) { std::array actual{}; if (!read(base + contract.rva, actual) || actual != contract.bytes) return false; } for (const auto& impact : impact_bail_calls) { std::array 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(base + reset_bail_causes_rva); std::array 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(base + record_bail_cause_contract.rva), reinterpret_cast(base + choose_physics_state_contract.rva), reinterpret_cast(base + bail_skeleton_contract.rva), reinterpret_cast(base + bail_publish_contract.rva)}; const std::array replacements{ reinterpret_cast(&record_cause), reinterpret_cast(&choose_state), reinterpret_cast(&skeleton_response), reinterpret_cast(&publish_animation)}; std::array 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(originals[0]); p.choose_original = reinterpret_cast(originals[1]); p.skeleton_original = reinterpret_cast(originals[2]); p.publish_original = reinterpret_cast(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(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(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(value.QuadPart); }(); result.previous_seconds = static_cast(static_cast(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); } }