Files
ReSkate/Engine/Resource/mesh_set.cpp
T
Krischan-KlugandClaude Opus 5.5 c7d2ca3601 Add Hall of Meat: the bones a bail hurt and its Meat score
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>
2026-10-09 15:29:38 +02:00

146 lines
8.5 KiB
C++

#include "mesh_set.h"
#include <cmath>
#include <cstring>
#include <stdexcept>
#include <string>
namespace dingosdk::frostbite {
namespace {
// The resource: three layout words this reader knows, the skinned flag, the levels of detail
// (a table of pointers) and their count. A pointer counts from byte 16.
constexpr std::array<std::uint32_t, 4> known_header{208, 188, 384, 0};
constexpr std::size_t skinned_flag = 124, lod_count = 180, lod_table = 48, pointer_base = 16;
// A level of detail: its kind (1), its sections, the index format (33: 16-bit indices), the
// buffer sizes and the geometry chunk.
constexpr std::size_t lod_kind = 0, section_count = 8, section_table = 12, index_format = 84, index_bytes = 88,
vertex_bytes = 92, geometry_chunk = 116;
constexpr std::uint32_t known_lod_kind = 1, sixteen_bit_indices = 33;
// A section, 384 bytes: its bone palette, primitive type (3: a triangle list), triangles, first
// index, where its vertices start in the vertex buffer and how many, and its vertex declaration.
constexpr std::size_t section_size = 384, palette = 16, palette_count = 24, primitive = 31, triangle_count = 32,
first_index = 36, vertex_start = 40, vertex_count = 44, declaration = 112;
constexpr std::uint8_t triangle_list = 3;
// The declaration: 16 elements of {usage, format, offset, stream}, then {stride, classification}
// per stream, then the element and stream counts. Four bone influences a vertex, or eight with
// the second pair (the coarser levels of detail leave it out).
constexpr std::size_t stream_table = 64, element_total = 96, stream_total = 97;
enum Usage : std::uint8_t { position = 1, bones_low = 2, bones_high = 3, weights_low = 4, weights_high = 5 };
constexpr std::array<std::uint8_t, 6> usage_format{0, 3, 23, 23, 13, 13}; // float3, ushort4, ushort4, ubyte4n, ubyte4n
constexpr std::array<std::uint8_t, 6> usage_size{0, 12, 8, 8, 4, 4};
void require(bool valid, const char* message) {
if (!valid) throw std::runtime_error(std::string("MeshSet: ") + message);
}
struct Reader {
std::span<const std::byte> data;
template <class T> T at(std::size_t offset) const {
require(offset <= data.size() && sizeof(T) <= data.size() - offset, "it is truncated");
T value{};
std::memcpy(&value, data.data() + offset, sizeof(T));
return value;
}
std::size_t pointer(std::size_t offset) const {
const auto value = at<std::uint64_t>(offset);
require(value && value <= data.size() - pointer_base, "a pointer leads outside it");
return static_cast<std::size_t>(value) + pointer_base;
}
};
struct Attribute {
std::size_t base{}, stride{}, offset{};
bool present{};
};
}
MeshLod read_mesh_lod(std::span<const std::byte> resource, std::size_t lod) {
const Reader r{resource};
for (std::size_t i = 0; i < known_header.size(); ++i)
require(r.at<std::uint32_t>(i * 4) == known_header[i], "its layout is not the one this reader knows");
require(r.at<std::uint8_t>(skinned_flag) == 1, "it is not skinned");
require(lod < r.at<std::uint16_t>(lod_count), "it has no such level of detail");
const auto at = r.pointer(lod_table + lod * 8);
require(r.at<std::uint32_t>(at + lod_kind) == known_lod_kind, "the level of detail is of another kind");
require(r.at<std::uint32_t>(at + index_format) == sixteen_bit_indices, "its indices are not 16-bit");
MeshLod result;
result.chunk = r.at<Guid>(at + geometry_chunk);
result.vertex_bytes = r.at<std::uint32_t>(at + vertex_bytes);
result.index_bytes = r.at<std::uint32_t>(at + index_bytes);
require(result.chunk != Guid{} && result.vertex_bytes > 0 && result.index_bytes > 0, "the level of detail is empty");
return result;
}
SkinnedMesh read_skinned_mesh(std::span<const std::byte> resource, std::size_t lod, std::span<const std::byte> geometry) {
const auto layout = read_mesh_lod(resource, lod);
const Reader r{resource}, g{geometry};
require(geometry.size() >= std::size_t{layout.vertex_bytes} + layout.index_bytes, "the geometry chunk is too short");
const auto at = r.pointer(lod_table + lod * 8);
const auto sections = r.at<std::uint32_t>(at + section_count);
const auto table = r.pointer(at + section_table);
SkinnedMesh result;
for (std::uint32_t section = 0; section < sections; ++section) {
const auto s = table + section * section_size;
require(r.at<std::uint8_t>(s + primitive) == triangle_list, "a section is not a triangle list");
const auto count = r.at<std::uint32_t>(s + vertex_count), triangles = r.at<std::uint32_t>(s + triangle_count);
const auto bones = r.pointer(s + palette);
const auto bone_count = r.at<std::uint16_t>(s + palette_count);
const auto d = s + declaration;
const auto elements = r.at<std::uint8_t>(d + element_total), streams = r.at<std::uint8_t>(d + stream_total);
require(elements <= 16 && streams > 0 && streams <= 16, "a vertex declaration is malformed");
std::array<std::size_t, 16> bases{}, strides{};
std::size_t end = r.at<std::uint32_t>(s + vertex_start);
for (std::size_t stream = 0; stream < streams; ++stream) {
strides[stream] = r.at<std::uint8_t>(d + stream_table + stream * 2);
require(strides[stream] > 0 && r.at<std::uint8_t>(d + stream_table + stream * 2 + 1) == 0,
"a vertex stream is not per vertex");
bases[stream] = end;
end += strides[stream] * count;
}
require(end <= layout.vertex_bytes, "a section's vertices run past the vertex buffer");
std::array<Attribute, 6> attributes{};
for (std::size_t element = 0; element < elements; ++element) {
const auto usage = r.at<std::uint8_t>(d + element * 4);
if (usage < position || usage > weights_high) continue;
const auto stream = r.at<std::uint8_t>(d + element * 4 + 3);
const std::size_t offset = r.at<std::uint8_t>(d + element * 4 + 2);
require(r.at<std::uint8_t>(d + element * 4 + 1) == usage_format[usage] && stream < streams &&
offset + usage_size[usage] <= strides[stream],
"a skinning attribute has another format");
attributes[usage] = {bases[stream], strides[stream], offset, true};
}
for (const auto usage : {position, bones_low, weights_low}) require(attributes[usage].present, "a section is not skinned");
require(attributes[bones_high].present == attributes[weights_high].present, "a section's second influences are half there");
const std::size_t influences = attributes[bones_high].present ? 8 : 4;
const auto address = [&](std::uint8_t usage, std::size_t vertex) {
const auto& a = attributes[usage];
return a.base + vertex * a.stride + a.offset;
};
const auto first = static_cast<std::uint32_t>(result.vertices.size());
for (std::size_t vertex = 0; vertex < count; ++vertex) {
SkinnedVertex out;
out.position = g.at<std::array<float, 3>>(address(position, vertex));
for (const float coordinate : out.position) require(std::isfinite(coordinate), "a vertex is not finite");
unsigned total{};
for (std::size_t influence = 0; influence < influences; ++influence) {
const auto high = influence >= 4;
const auto weight = g.at<std::uint8_t>(address(high ? weights_high : weights_low, vertex) + influence % 4);
if (!weight) continue; // an unused slot's bone need not be set
const auto slot = g.at<std::uint16_t>(address(high ? bones_high : bones_low, vertex) + influence % 4 * 2);
require(slot < bone_count, "a vertex names a bone outside its palette");
out.bones[influence] = r.at<std::uint16_t>(bones + slot * 2u);
out.weights[influence] = weight;
total += weight;
}
require(total == 255, "a vertex's weights do not add up");
result.vertices.push_back(out);
}
const std::size_t start = std::size_t{r.at<std::uint32_t>(s + first_index)} * 2;
require(start + std::size_t{triangles} * 6 <= layout.index_bytes, "a section's indices run past the index buffer");
for (std::size_t i = 0; i < std::size_t{triangles} * 3; ++i) {
const auto index = g.at<std::uint16_t>(layout.vertex_bytes + start + i * 2);
require(index < count, "a triangle names a vertex outside its section");
result.triangles.push_back(first + index);
}
}
return result;
}
}