Files
Belcross 837bea4c40 Grind exit triggers: pick the grind exit side with LT / RT
LT / RT pressed during a grind pick the side the skater pops out to,
independently of the trick and the body's rotation (both come from the
left stick in skate.'s own controls). Only the sideways part of the
grind pop's launch is replaced, with the game's own sideways speed; the
trick, spin, pop height and along-rail speed are untouched.

- Input Mode: Toe / Heel (character relative, needs the stance) or
  Left / Right of the direction of travel.
- Grab guard: a trigger used to pick the side is hidden from the game
  until released, so it never starts a grab.
- Lean guard: while a claimed trigger is held in a grind, the left
  stick's left/right reaches the game at the pop, so the game's lean
  exit cannot drag the board into the rail.
- Off by default; switches in Mod Options > Player, the ReSkate skater
  page, the tools menu and the console (grindexit ...). Kept between
  sessions.
- Listener slots on existing hooks only: No Bail's physics step, the
  trainer's set_trajectory detour, and the overlay's XInput capture.
  No new detours. Build-locked to 20260929 through the trainer's byte
  check.

Measured on build 25414733: 48 vanilla grind pops, 144 trigger-set
pops. See Extension/Skater/GRIND_EXITS.md.
2026-10-12 19:33:50 +01:00

136 lines
7.2 KiB
C++

#include "trainer_jump.h"
#include "Engine/Core/Hooks/hooks.h"
#include "Engine/Core/Log/logging.h"
#include "Extension/Skater/no_bail.h"
#include <windows.h>
#include <array>
#include <atomic>
#include <cmath>
#include <cstring>
#include <intrin.h>
namespace dingosdk::trainer {
namespace {
// Game build 25414733. void set_trajectory(trajectory, parameters): the launch velocity is
// the vector at parameters + 0x80. Measured with a scripted controller:
// - a no comply calls it from 0x482f646 on entering the pop (state 200) after a crouch
// (state 103) of about 0.20 s; an ollie crouches for 0.08 s and launches elsewhere;
// - a boneless calls it from 0x47691f8 while the foot is planted (state 602).
constexpr std::uintptr_t set_trajectory = 0x4752aa0, no_comply_call = 0x482f646, boneless_call = 0x47691f8;
constexpr std::array<unsigned char, 32> set_trajectory_prefix{0x40, 0x53, 0x48, 0x83, 0xec, 0x20, 0x80, 0xb9, 0x88, 0xbb, 0x00,
0x00, 0x00, 0x48, 0x8b, 0xd9, 0x0f, 0x85, 0xf6, 0x00, 0x00, 0x00,
0xc5, 0xfc, 0x28, 0x05, 0x62, 0x7d, 0xe8, 0x01, 0xc5, 0xfc};
constexpr std::array<unsigned char, 5> no_comply_call_bytes{0xe8, 0x55, 0x34, 0xf2, 0xff}, boneless_call_bytes{0xe8, 0xa3, 0x98, 0xfe, 0xff};
constexpr std::uint32_t crouch_state = 103, pop_state = 200, plant_state = 602;
constexpr float no_comply_crouch = 0.12f; // seconds: between an ollie's crouch and a no comply's
constexpr std::size_t velocity_y = 0x84 / sizeof(float);
using SetTrajectory = void (*)(std::uintptr_t, float *);
struct Hooked {
std::atomic<SetTrajectory> original{};
std::uintptr_t base{};
std::atomic<float> no_comply{1}, boneless{1};
std::atomic<TrajectoryModifier> modifier{};
// The last few launches, for the trainer's log line: written by the physics thread, read
// by the client tick.
std::array<std::atomic<std::uint64_t>, 8> seen{};
std::atomic<std::uint32_t> written{}, read{};
bool attempted{}, ready{};
};
Hooked &hooked() {
static auto *value = new Hooked;
return *value;
}
void set_trajectory_hook(std::uintptr_t trajectory, float *parameters) {
auto &h = hooked();
const auto caller = reinterpret_cast<std::uintptr_t>(_ReturnAddress()) - h.base;
if (const auto modifier = h.modifier.load(std::memory_order_acquire); modifier && parameters) {
float velocity[3]{parameters[velocity_y - 1], parameters[velocity_y], parameters[velocity_y + 1]};
modifier(caller - 5, trajectory, velocity);
if (std::isfinite(velocity[0]) && std::isfinite(velocity[1]) && std::isfinite(velocity[2])) {
parameters[velocity_y - 1] = velocity[0];
parameters[velocity_y] = velocity[1];
parameters[velocity_y + 1] = velocity[2];
}
}
if (parameters && (caller == no_comply_call + 5 || caller == boneless_call + 5)) {
const auto watch = watched_physics_state(); // the local skater's
Trick trick{};
if (caller == boneless_call + 5 && watch.valid && watch.state == plant_state) trick = Trick::boneless;
else if (caller == no_comply_call + 5 && watch.valid && watch.state == pop_state && watch.previous == crouch_state &&
watch.previous_seconds >= no_comply_crouch)
trick = Trick::no_comply;
const float up = parameters[velocity_y];
if (trick != Trick::none && std::isfinite(up) && up > 0.5f) {
const float factor = (trick == Trick::boneless ? h.boneless : h.no_comply).load(std::memory_order_relaxed);
if (factor != 1 && up * factor < 200.0f) parameters[velocity_y] = up * factor;
std::uint32_t bits[2];
std::memcpy(&bits[0], &up, 4);
std::memcpy(&bits[1], &factor, 4);
// 31 bits of each float (both positive) and the trick in the top bit.
const auto packed = (std::uint64_t{bits[0]} << 32) | (std::uint64_t{bits[1]} << 1) | (trick == Trick::boneless ? 1u : 0u);
h.seen[h.written.fetch_add(1, std::memory_order_relaxed) % h.seen.size()].store(packed, std::memory_order_release);
}
}
if (const auto original = h.original.load(std::memory_order_acquire)) original(trajectory, parameters);
}
bool known_code(std::uintptr_t base) {
__try {
return std::memcmp(reinterpret_cast<const void *>(base + set_trajectory), set_trajectory_prefix.data(), set_trajectory_prefix.size()) == 0 &&
std::memcmp(reinterpret_cast<const void *>(base + no_comply_call), no_comply_call_bytes.data(), no_comply_call_bytes.size()) == 0 &&
std::memcmp(reinterpret_cast<const void *>(base + boneless_call), boneless_call_bytes.data(), boneless_call_bytes.size()) == 0;
} __except (EXCEPTION_EXECUTE_HANDLER) {
return false;
}
}
} // namespace
bool start_trick_heights(std::uintptr_t base) noexcept {
auto &h = hooked();
if (h.attempted || !base) return h.ready;
h.attempted = true;
try {
if (!known_code(base)) {
logging::write(logging::Level::warning, logging::Channel::skater,
"Trainer: this game build's trick code is not the known one; no comply and boneless heights are off.");
return false;
}
h.base = base;
auto *target = reinterpret_cast<void *>(base + set_trajectory);
void *original{};
if (hook_prepare(target, reinterpret_cast<void *>(&set_trajectory_hook), &original) != HookOk) return false;
if (!original) {
(void)hook_remove(target);
return false;
}
h.original.store(reinterpret_cast<SetTrajectory>(original), std::memory_order_release);
if (hook_enable(target) != HookOk) return false;
h.ready = true;
logging::write(logging::Level::info, logging::Channel::skater, "Trainer: no comply and boneless heights ready.");
} catch (...) {}
return h.ready;
}
void set_trick_heights(float no_comply, float boneless) noexcept {
const auto speed = [](float height) { return std::isfinite(height) && height > 0 ? std::sqrt(std::min(height, 1.0e6f)) : 1.0f; };
hooked().no_comply.store(speed(no_comply), std::memory_order_relaxed);
hooked().boneless.store(speed(boneless), std::memory_order_relaxed);
}
bool take_trick_launch(TrickLaunch &out) noexcept {
auto &h = hooked();
const auto written = h.written.load(std::memory_order_acquire);
auto read = h.read.load(std::memory_order_relaxed);
if (read == written) return false;
if (written - read > h.seen.size()) read = written - static_cast<std::uint32_t>(h.seen.size());
const auto packed = h.seen[read % h.seen.size()].load(std::memory_order_acquire);
h.read.store(read + 1, std::memory_order_relaxed);
const std::uint32_t bits[2]{static_cast<std::uint32_t>(packed >> 32), static_cast<std::uint32_t>(packed >> 1) & 0x7fffffffu};
out.trick = packed & 1 ? Trick::boneless : Trick::no_comply;
std::memcpy(&out.up_speed, &bits[0], 4);
std::memcpy(&out.factor, &bits[1], 4);
return true;
}
void set_trajectory_modifier(TrajectoryModifier modifier) noexcept {
hooked().modifier.store(modifier, std::memory_order_release);
}
} // namespace dingosdk::trainer