mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-18 02:47:01 +02: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>
254 lines
11 KiB
C++
254 lines
11 KiB
C++
#include "hall_of_meat_model.h"
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
namespace dingosdk::hall_of_meat {
|
|
namespace {
|
|
// The physics thread steps and the render thread draws, each reading the real clock itself:
|
|
// a moment ago may be a moment ahead.
|
|
std::uint64_t elapsed(std::uint64_t now, std::uint64_t since) noexcept { return now > since ? now - since : 0; }
|
|
// A step's length on the game's clock; nothing for one the game did not report sensibly.
|
|
double length_ms(float seconds) noexcept { return std::isfinite(seconds) && seconds > 0 ? seconds * 1000.0 : 0.0; }
|
|
float speed_of(float value) noexcept { return std::isfinite(value) && value > 0 ? value : 0.0f; }
|
|
bool is_foot(std::size_t bone) noexcept { return skater_body::foot(static_cast<Bone>(bone)); }
|
|
// Sliding along anything but the board scrapes: the feet ride on it all the time.
|
|
bool scrapes(const skater_body::HitKinds& hit) noexcept {
|
|
return hit.world || hit.vehicle || hit.kind_5 || hit.kind_11;
|
|
}
|
|
int points(float amount, float per_unit) noexcept { return static_cast<int>(amount * per_unit + 0.5f); }
|
|
}
|
|
|
|
bool Tracker::step(std::uint64_t now, const Step& step, Summary* ended) noexcept {
|
|
const double step_ms = length_ms(step.seconds);
|
|
played_ += step_ms;
|
|
const double flown = step.airborne ? step_ms : 0.0;
|
|
if (step.airborne) landed_.reset();
|
|
else if (!landed_) landed_ = played_;
|
|
const bool ragdoll = step.ragdoll.value_or(false);
|
|
// How far the skater went down in the step.
|
|
const float descent = step.velocity ? std::max(0.0f, -(*step.velocity)[1]) * static_cast<float>(step_ms / 1000.0) : 0.0f;
|
|
bool began{};
|
|
if (phase_ == Phase::riding) {
|
|
if (step.airborne) {
|
|
flight_ms_ += flown;
|
|
flight_fallen_ += descent;
|
|
} else if (played_ - *landed_ > wipeout_after_impact_ms) { // landed, and stayed up
|
|
flight_ms_ = 0;
|
|
flight_fallen_ = 0;
|
|
}
|
|
// Each body's hardest recent hit, for a bail that starts a few steps after it.
|
|
for (std::size_t index = 1; index < skater_body::count; ++index) { // 0 is the board
|
|
const auto& contact = step.body.bodies[index];
|
|
const float speed = speed_of(contact.impact);
|
|
auto& lead = bones_[index].lead;
|
|
if (speed <= 0 || (lead.at && played_ - *lead.at <= wipeout_after_impact_ms && speed <= lead.speed)) continue;
|
|
lead = {speed, contact.hit.vehicle, played_};
|
|
}
|
|
if (!step.wipeout) return false;
|
|
begin(now); // the flight it came from holds this step's airtime and fall
|
|
began = true;
|
|
}
|
|
ragdolled_ = ragdolled_ || ragdoll;
|
|
const auto speed = step.velocity ? std::optional<float>(game::length(*step.velocity)) : std::nullopt;
|
|
rest(speed);
|
|
if (phase_ == Phase::bailing) {
|
|
if (!began) {
|
|
airtime_ms_ += flown;
|
|
fallen_ += descent;
|
|
}
|
|
if (speed) top_speed_ = std::max(top_speed_, *speed);
|
|
if (step.spin) turned_ += speed_of(*step.spin) * static_cast<float>(step_ms / 1000.0);
|
|
count(now, step, step_ms);
|
|
}
|
|
// The skater stood up, or never went down.
|
|
const bool stood_up = ragdolled_ ? step.ragdoll.has_value() && !*step.ragdoll : played_ - started_ >= ragdoll_wait_ms;
|
|
if (breaks_ != seen_breaks_) { // a break this step: its effect runs from now
|
|
seen_breaks_ = breaks_;
|
|
if (breaks_seen_ < breaks_seen_at_.size()) breaks_seen_at_[breaks_seen_++] = now;
|
|
}
|
|
return stood_up && end(now, ended);
|
|
}
|
|
|
|
// Whether the body has come to rest, by how fast it moves: then the bail's time and points are final.
|
|
void Tracker::rest(std::optional<float> speed) noexcept {
|
|
if (phase_ != Phase::bailing || !speed) return;
|
|
if (*speed >= still_speed) {
|
|
still_since_.reset();
|
|
} else if (!still_since_) {
|
|
still_since_ = played_;
|
|
} else if (played_ - *still_since_ >= rest_ms) {
|
|
phase_ = Phase::down;
|
|
stopped_at_ = still_since_; // it has rested since it went still
|
|
}
|
|
}
|
|
|
|
// What each body hit and how far it slid in one step of the bail.
|
|
void Tracker::count(std::uint64_t now, const Step& step, double step_ms) noexcept {
|
|
const float step_seconds = static_cast<float>(step_ms / 1000.0);
|
|
float sliding{}; // the scraping bodies' slides, summed
|
|
int scraping{};
|
|
for (std::size_t index = 1; index < skater_body::count; ++index) {
|
|
const auto& contact = step.body.bodies[index];
|
|
const float speed = speed_of(contact.impact);
|
|
if (speed > 0) hit(index, speed, contact.hit.vehicle, played_, now);
|
|
const float slide = game::length(contact.slide);
|
|
if (!contact.touching || is_foot(index) || !scrapes(contact.hit) || !std::isfinite(slide) || slide < scrape_speed)
|
|
continue;
|
|
auto& bone = bones_[index];
|
|
bone.scraped += slide * step_seconds;
|
|
if (bone.scraped >= bruising_scrape) hurt_ = true;
|
|
sliding += slide;
|
|
++scraping;
|
|
}
|
|
if (scraping) scraped_ += sliding / static_cast<float>(scraping) * step_seconds;
|
|
}
|
|
|
|
bool Tracker::lose(std::uint64_t now, Summary* ended) noexcept {
|
|
// Nothing from before the skater went carries over to the one that comes back.
|
|
flight_ms_ = 0;
|
|
landed_.reset();
|
|
breaks_ = seen_breaks_ = 0;
|
|
breaks_seen_ = 0;
|
|
flight_fallen_ = 0;
|
|
for (auto& bone : bones_) bone.lead = {};
|
|
return phase_ != Phase::riding && end(now, ended);
|
|
}
|
|
|
|
// A bail from this step (`now` in real time), with the flight it came from and the hits just before it.
|
|
void Tracker::begin(std::uint64_t now) noexcept {
|
|
const auto played = played_;
|
|
const auto flight = flight_ms_;
|
|
const auto flight_fallen = flight_fallen_;
|
|
const auto before = bones_;
|
|
*this = {};
|
|
played_ = played;
|
|
phase_ = Phase::bailing;
|
|
started_ = played_;
|
|
airtime_ms_ = flight; // this step's share is in it
|
|
fallen_ = flight_fallen;
|
|
for (std::size_t index = 1; index < skater_body::count; ++index) {
|
|
const auto& lead = before[index].lead;
|
|
if (lead.at && played_ - *lead.at <= wipeout_after_impact_ms) hit(index, lead.speed, lead.vehicle, *lead.at, now);
|
|
}
|
|
}
|
|
|
|
// A hit of one bone at `at` on the game's clock, shown from `now`: a new impact, or the harder step
|
|
// of the contact it belongs to.
|
|
void Tracker::hit(std::size_t index, float speed, bool vehicle, double at, std::uint64_t now) noexcept {
|
|
auto& bone = bones_[index];
|
|
if (bone.peak < broken_speed && speed >= broken_speed) ++breaks_;
|
|
bone.peak = std::max(bone.peak, speed);
|
|
if (speed < hit_speed) return;
|
|
hurt_ = true;
|
|
if (bone.hit_at && at - *bone.hit_at <= impact_gap_ms) {
|
|
auto& impact = impacts_[bone.impact];
|
|
impact.speed = std::max(impact.speed, speed);
|
|
impact.vehicle = impact.vehicle || vehicle;
|
|
} else if (impact_count_ < max_impacts) {
|
|
bone.impact = impact_count_;
|
|
impacts_[impact_count_++] = {index, speed, vehicle};
|
|
}
|
|
bone.hit_at = at;
|
|
bone.flashed_at = now;
|
|
}
|
|
|
|
bool Tracker::end(std::uint64_t now, Summary* ended) noexcept {
|
|
phase_ = Phase::riding;
|
|
ended_ = now;
|
|
if (!stopped_at_) stopped_at_ = played_;
|
|
if (ended) {
|
|
ended->shown = hurt_;
|
|
for (std::size_t index = 0; index < skater_body::count; ++index) {
|
|
ended->peaks[index] = bones_[index].peak;
|
|
ended->scraped[index] = bones_[index].scraped;
|
|
}
|
|
ended->tally = tally();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// The bail's time, on the game's clock: until the body came to rest, or the bail ended, or the latest step.
|
|
double Tracker::bail_ms() const noexcept {
|
|
return std::max(0.0, (stopped_at_ ? *stopped_at_ : played_) - started_);
|
|
}
|
|
|
|
Phase Tracker::phase(std::uint64_t now) const noexcept {
|
|
if (phase_ != Phase::riding) return phase_;
|
|
return ended_ && elapsed(now, ended_) < linger_ms + fade_ms ? Phase::getting_up : Phase::riding;
|
|
}
|
|
|
|
Injury Tracker::injury_of(const BoneState& bone) const noexcept {
|
|
const auto by_hits = injury(bone.peak);
|
|
return by_hits == Injury::none && bone.scraped >= bruising_scrape ? Injury::hit : by_hits;
|
|
}
|
|
|
|
Tally Tracker::tally() const noexcept {
|
|
Tally result;
|
|
for (std::size_t index = 0; index < impact_count_; ++index) {
|
|
const auto& impact = impacts_[index];
|
|
result.hit_points += hit_points(impact.speed);
|
|
result.head_bonus += head_bonus(impact);
|
|
result.vehicle_bonus += vehicle_bonus(impact);
|
|
}
|
|
result.impacts = static_cast<int>(impact_count_);
|
|
for (std::size_t index = 1; index < skater_body::count; ++index) // 0 is the board
|
|
if (injury(bones_[index].peak) == Injury::broken) ++result.broken;
|
|
result.scraped = scraped_;
|
|
result.scrape_points = points(result.scraped, points_per_scraped_metre);
|
|
result.damage = result.hit_points + result.head_bonus + result.vehicle_bonus + result.scrape_points;
|
|
result.seconds = static_cast<float>(bail_ms() / 1000.0);
|
|
result.time_points = points(result.seconds, points_per_second);
|
|
result.airtime = static_cast<float>(airtime_ms_ / 1000.0);
|
|
result.airtime_points = points(result.airtime, points_per_air_second);
|
|
result.fallen = fallen_;
|
|
result.fall_points = points(result.fallen, points_per_metre_fallen);
|
|
result.top_speed = top_speed_;
|
|
result.speed_points = points(result.top_speed, points_per_speed);
|
|
result.rotations = turned_ / radians_per_rotation;
|
|
result.rotation_points = points(result.rotations, points_per_rotation);
|
|
result.score = result.damage + result.broken * points_per_break + result.time_points + result.airtime_points +
|
|
result.fall_points + result.speed_points + result.rotation_points;
|
|
return result;
|
|
}
|
|
|
|
View Tracker::view(std::uint64_t now) const noexcept {
|
|
View view;
|
|
const auto phase = this->phase(now);
|
|
if (phase == Phase::riding || !hurt_) return view;
|
|
view.phase = phase;
|
|
const auto since_end = phase == Phase::getting_up ? elapsed(now, ended_) : 0;
|
|
view.alpha = since_end > linger_ms ? 1.0f - static_cast<float>(since_end - linger_ms) / static_cast<float>(fade_ms) : 1.0f;
|
|
view.tally = tally();
|
|
for (std::size_t index = 0; index < skater_body::count; ++index) {
|
|
const auto& bone = bones_[index];
|
|
view.injuries[index] = injury_of(bone);
|
|
const auto since_hit = elapsed(now, bone.flashed_at);
|
|
if (bone.flashed_at && since_hit < flash_ms)
|
|
view.flashes[index] = 1.0f - static_cast<float>(since_hit) / static_cast<float>(flash_ms);
|
|
}
|
|
view.break_pulse = break_effect(now);
|
|
return view;
|
|
}
|
|
|
|
float Tracker::break_effect(std::uint64_t now) const noexcept {
|
|
const auto smooth = [](std::uint64_t part, std::uint64_t whole) {
|
|
const float t = static_cast<float>(part) / static_cast<float>(whole);
|
|
return t * t * (3.0f - 2.0f * t);
|
|
};
|
|
float strongest = 0.0f;
|
|
for (std::size_t index = 0; index < breaks_seen_; ++index) {
|
|
const auto since = elapsed(now, breaks_seen_at_[index]);
|
|
if (since >= break_effect_ms) continue;
|
|
const float effect = since < break_rise_ms ? smooth(since, break_rise_ms)
|
|
: since < break_rise_ms + break_hold_ms ? 1.0f
|
|
: 1.0f - smooth(since - break_rise_ms - break_hold_ms, break_fall_ms);
|
|
strongest = std::max(strongest, effect);
|
|
}
|
|
return strongest;
|
|
}
|
|
|
|
float Tracker::game_speed(std::uint64_t now) const noexcept {
|
|
return 1.0f - (1.0f - break_game_speed) * break_effect(now);
|
|
}
|
|
}
|