mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-26 11:36:43 +01:00
As in skate. 3: when the local skater bails, the bones it hurt show over the world through the body, yellow where a hit bruised one and red where one broke it, with the bail's Meat card in the bottom left corner in the place of skate.'s own HUD there: its time, hits (head hits double, vehicles half again), broken bones, road rash, airtime, fall, top speed and rotations, each scoring, and the map's best. A bone breaking pulses the screen's edges red and, in single player, slows the game down a moment. Off until switched on, as in skate.: in skate.'s menu (Custom Stuff > Player) or with `hallofmeat 0|1`, saved with the profile. Everything it draws is skate.'s own, read from the installed game: its skeleton mesh (the Dem Bones costume) posed with the renderer's own skinning matrices, its scoring icons, the THRASHER wordmark and its UI brush strokes. Nothing is shipped. How it works: No Bail already hooks the skeleton response, which runs once a physics step right after the body contacts; it now hands that step on, with the local skater it follows, but only while Hall of Meat is on. Each step reads what every ragdoll body hit and how hard, and whether the skater is a ragdoll; the bail runs on the game's own clock so slow motion measures what the body did. The feature's own hooks, on the renderer's skinned draw packets and main view and on the UI model's write, are prepared at startup and switched on only with the feature, and off again once the HUD corner and the game's speed are given back: switched off, the game never calls them and the physics step pays nothing. The skeleton mesh is read the first time it is on; only the images are read at startup, since the overlay builds its atlas once. The renderer's picture of the skater is read only while a bail shows. The menu's switch reads the overlay model like the others. Every address is in the build table with a fingerprint checked before use. Extension/HallOfMeat/README.md describes the parts; its tests build with DINGOSDK_BUILD_HALL_OF_MEAT_TESTS. The slow motion changes SimulationTime's rate, frame cap and time scale as a session override and steps the local skater along, since skate. sets a skater's step only when it builds its core. It runs the same at every break, but never faster than the game ran: with the trainer's game speed below 1 it only takes the part that is slower still. Afterwards it writes back what it found, each setting only while it still holds what the slow motion wrote, so the trainer's speed comes back as it was; a setting changed from elsewhere meanwhile is left as it is and ends that slow motion. named_setting_value reads a setting's value for it. Tuned in play: a bruise from 7 m/s and a break from 12 m/s along the contact normal (raised twice: a hard fall on flat ground broke bones), a break worth ten of the lightest hits (a heavy bail's hits and breaks outweighed the rest of the card) and a fall half of skate. 3's rate. Fix: the overlay now draws in the back buffer's pixels, not the window's, so it lines up on displays Windows scales (1920x1080 borderless on 4K). The skater's render hooks, its skeleton and its body maps were first found by gBGYo's ReSkate fork (github.com/gBGYo/ReSkate). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
178 lines
8.3 KiB
C++
178 lines
8.3 KiB
C++
#include "hall_of_meat_skater.h"
|
|
#include "Engine/Core/Log/logging.h"
|
|
#include "Engine/Core/Platform/memory.h"
|
|
#include "Engine/Game/Build/addresses.h"
|
|
#include "Engine/Game/Build/20260929/no_bail.h"
|
|
#include "Engine/Game/Build/20260929/skater_body.h"
|
|
#include "Engine/Game/Build/20260929/skater_state.h"
|
|
#include <Windows.h>
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <cmath>
|
|
#include <cstring>
|
|
|
|
namespace dingosdk::hall_of_meat {
|
|
namespace {
|
|
namespace body = addr::skater_body;
|
|
namespace state = addr::skater_state;
|
|
using game::Vec3;
|
|
static_assert(body::body_bone_count == skater_body::count);
|
|
|
|
std::atomic<bool>& ready() {
|
|
static std::atomic<bool> value{};
|
|
return value;
|
|
}
|
|
|
|
// What each body touched in the step: the contact struct the rig's contact processing fills.
|
|
using Records = std::array<unsigned char, body::body_bone_count * body::bone_record_size>;
|
|
template <class T> T field(const Records& records, std::size_t index, std::uintptr_t offset) noexcept {
|
|
T value{};
|
|
std::memcpy(&value, records.data() + index * body::bone_record_size + offset, sizeof(T));
|
|
return value;
|
|
}
|
|
Vec3 vec3(const std::array<float, 4>& value) noexcept {
|
|
for (std::size_t i = 0; i < 3; ++i)
|
|
if (!std::isfinite(value[i])) return {};
|
|
return {value[0], value[1], value[2]};
|
|
}
|
|
float speed(float value) noexcept { return std::isfinite(value) && value > 0 && value < 1000 ? value : 0; }
|
|
bool read_contacts(const NoBailSkater& skater, skater_body::Contacts& result) noexcept {
|
|
std::uintptr_t holder{}, contacts{};
|
|
Records records{};
|
|
std::array<std::uint8_t, skater_body::count> touching{};
|
|
if (!memory::peek(skater.rig + body::contact_holder_offset, holder) || !memory::peek(holder + body::contact_struct_offset, contacts) ||
|
|
!memory::peek(contacts + body::bone_records_offset, records) || !memory::peek(contacts + body::bone_touching_offset, touching))
|
|
return false;
|
|
for (std::size_t index = 0; index < skater_body::count; ++index) {
|
|
auto& b = result.bodies[index];
|
|
b.touching = touching[index] != 0;
|
|
const float ordinary = speed(field<float>(records, index, body::bone_peak_offset));
|
|
const float tracked = speed(field<float>(records, index, body::bone_tracked_peak_offset));
|
|
b.impact = std::max(ordinary, tracked);
|
|
// The slide of the contact that set the peak: one near a tracked point keeps its own.
|
|
const std::uintptr_t side = tracked > ordinary ? body::bone_tracked_offset : 0;
|
|
b.slide = vec3(field<std::array<float, 4>>(records, index, body::bone_slide_offset + side));
|
|
const auto hit = [&](std::uintptr_t offset) { return field<std::uint8_t>(records, index, offset) != 0; };
|
|
b.hit = {hit(body::bone_hit_board_offset), hit(body::bone_hit_vehicle_offset), hit(body::bone_hit_world_offset),
|
|
hit(body::bone_hit_kind_5_offset), hit(body::bone_hit_kind_11_offset)};
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// The offboard state's flags: the body in a ragdoll, and in the air.
|
|
struct Offboard {
|
|
bool ragdoll{}, in_the_air{};
|
|
};
|
|
bool read_offboard(const NoBailSkater& skater, Offboard& flags) noexcept {
|
|
std::uintptr_t trick_state{}, offboard{};
|
|
std::uint8_t ragdoll{}, in_the_air{};
|
|
if (!memory::peek(skater.core + state::trick_state_offset, trick_state) ||
|
|
!memory::peek(trick_state + state::offboard_state_offset, offboard) ||
|
|
!memory::peek(offboard + state::ragdoll_offset, ragdoll) || !memory::peek(offboard + state::in_the_air_offset, in_the_air))
|
|
return false;
|
|
flags = {ragdoll != 0, in_the_air != 0};
|
|
return true;
|
|
}
|
|
|
|
bool finite(const Vec3& v) noexcept {
|
|
return std::all_of(v.begin(), v.end(), [](float value) { return std::isfinite(value) && std::abs(value) < 1000; });
|
|
}
|
|
// Body `index` of the board's or the skeleton's physics, when the layout holds: its velocity, and
|
|
// its spin when asked for.
|
|
bool body_motion(std::uintptr_t physics, std::uintptr_t vtable, std::uint32_t count, std::size_t index, Vec3& velocity,
|
|
Vec3* spin = nullptr) noexcept {
|
|
std::uintptr_t type{}, bodies{}, owner{};
|
|
std::uint32_t found{};
|
|
const auto at = [&] { return bodies + index * state::physics_body_size; };
|
|
if (!memory::peek(physics, type) || type != vtable || !memory::peek(physics + state::physics_bodies_offset, bodies) ||
|
|
!memory::peek(bodies, found) || found != count || index >= count ||
|
|
!memory::peek(at() + state::body_owner_offset, owner) || owner != physics ||
|
|
!memory::peek(at() + state::body_velocity_offset, velocity) || (spin && !memory::peek(at() + state::body_spin_offset, *spin)))
|
|
return false;
|
|
return finite(velocity) && (!spin || finite(*spin));
|
|
}
|
|
// The board's root, and the pelvis with its spin.
|
|
bool read_motion(const NoBailSkater& skater, Vec3& board, Vec3& pelvis, Vec3& spin) noexcept {
|
|
std::uintptr_t holder{}, board_physics{}, rig_physics{};
|
|
return memory::peek(skater.core + state::board_holder_offset, holder) &&
|
|
memory::peek(holder + state::board_physics_offset, board_physics) &&
|
|
memory::peek(skater.rig + state::rig_physics_offset, rig_physics) &&
|
|
body_motion(board_physics, skater.base + state::board_physics_vtable, state::board_body_count, state::board_root_body, board) &&
|
|
body_motion(rig_physics, skater.base + state::rig_physics_vtable, state::rig_body_count,
|
|
skater_body::index(skater_body::Bone::hips), pelvis, &spin);
|
|
}
|
|
|
|
// Where the local skater's physics step length is kept now (a float, seconds of game time), 0 while
|
|
// there is no local skater.
|
|
std::uintptr_t step_length_address() noexcept {
|
|
NoBailSkater skater;
|
|
std::uintptr_t input{};
|
|
if (!no_bail_skater(skater) || !memory::peek(skater.core + body::core_step_input_offset, input) || !input) return 0;
|
|
return input + body::step_input_length_offset;
|
|
}
|
|
}
|
|
|
|
bool start_skater(std::uintptr_t base) noexcept {
|
|
if (ready().load(std::memory_order_acquire)) return true;
|
|
for (const auto& contract : body::contracts) {
|
|
std::array<unsigned char, 32> actual{};
|
|
if (!memory::peek(base + contract.rva, actual) || actual != contract.bytes) {
|
|
logging::log(logging::Level::warning, logging::Channel::skater,
|
|
"Hall of Meat cannot read the skater's body: the native contract at 0x{:x} did not match.", contract.rva);
|
|
return false;
|
|
}
|
|
}
|
|
if (!no_bail_available()) {
|
|
logging::write(logging::Level::warning, logging::Channel::skater,
|
|
"Hall of Meat cannot follow the skater: its physics steps come through No Bail's hooks, which did not start.");
|
|
return false;
|
|
}
|
|
ready().store(true, std::memory_order_release);
|
|
return true;
|
|
}
|
|
|
|
bool skater_available() noexcept { return ready().load(std::memory_order_acquire); }
|
|
|
|
Step read_step(const NoBailSkater& skater, float seconds, bool wipeout) noexcept {
|
|
Step step;
|
|
step.seconds = seconds;
|
|
step.wipeout = wipeout;
|
|
std::uint32_t physics_state{};
|
|
if (memory::peek(skater.context + state::physics_state_offset, physics_state)) {
|
|
// On the board by the physics state; off it (on foot, and through a whole bail) by the offboard state.
|
|
const bool offboard = physics_state == addr::no_bail::offboard_physics_state;
|
|
Offboard flags;
|
|
const bool known = offboard && read_offboard(skater, flags);
|
|
step.airborne = std::find(state::board_air_states.begin(), state::board_air_states.end(), physics_state) !=
|
|
state::board_air_states.end() || (known && flags.in_the_air);
|
|
if (!offboard || known) step.ragdoll = known && flags.ragdoll;
|
|
Vec3 board{}, pelvis{}, spin{};
|
|
if (read_motion(skater, board, pelvis, spin)) {
|
|
step.velocity = offboard ? pelvis : board;
|
|
step.spin = game::length(spin);
|
|
}
|
|
}
|
|
(void)read_contacts(skater, step.body);
|
|
return step;
|
|
}
|
|
|
|
float step_length() noexcept {
|
|
const auto address = step_length_address();
|
|
float seconds{};
|
|
return address && memory::peek(address, seconds) ? seconds : 0.0f;
|
|
}
|
|
|
|
bool set_step_length(float seconds) noexcept {
|
|
const auto address = step_length_address();
|
|
float current{};
|
|
if (!address || !memory::peek(address, current)) return false;
|
|
if (current == seconds) return true;
|
|
__try {
|
|
*reinterpret_cast<volatile float*>(address) = seconds; // read by the physics thread each step
|
|
return true;
|
|
} __except (EXCEPTION_EXECUTE_HANDLER) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|