mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-24 19:26:18 +02:00
Hall of Meat (contrib/plugins/hall-of-meat) and grind exit triggers (contrib/plugins/grind-exits) are Lua plugins: the bail model, its Meat card, each map's best, the trigger rules, settings, console commands and menu panels are theirs, with their own checks that dingosdk_plugin_tests runs. What has to run in the game stays in core as plugin services: - every physics step of the skater (skater_physics, the physics_step event, permission events.physics); - the skater's x-ray (xray, xray_render, xray_skeleton; reskate.skater.xray and the draw handler's d:skeleton); - skate.'s bottom left HUD corner (hud_corner; reskate.hud.corner); - the game's slow motion, never in a session (game_speed; reskate.time.slow and speed, permission game.speed); - the side of the next pop out of a grind and pad controls kept from the game (grind_exit; reskate.skater.grind_exit, the grind_exit event, reskate.input.hide); - skate.'s own pictures by name (game_images; d:game_image), d:gradient and a font for drawn text. Removed: Extension/HallOfMeat, grind_exit_control and its commands, the HALL OF MEAT and GRIND EXITS add-on pages, home tile, hub and native menu rows, their menu models, and the hallofmeat and grindexit built-in commands.
185 lines
9.1 KiB
C++
185 lines
9.1 KiB
C++
#include "xray_skeleton.h"
|
|
#include "Engine/Core/Log/logging.h"
|
|
#include "Engine/Game/Build/addresses.h"
|
|
#include "Engine/Game/Build/20260929/skater_skeleton.h"
|
|
#include "Engine/Resource/ebx_document.h"
|
|
#include "Engine/Vfs/game_bundles.h"
|
|
#include <Windows.h>
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <cmath>
|
|
#include <mutex>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <thread>
|
|
|
|
namespace dingosdk::xray {
|
|
namespace {
|
|
namespace build = addr::skater_skeleton;
|
|
|
|
void require(bool valid, const std::string& message) {
|
|
if (!valid) throw std::runtime_error(message);
|
|
}
|
|
Vec3 normalised(const Vec3& v) {
|
|
const float length = game::length(v);
|
|
return length > 1e-8f ? Vec3{v[0] / length, v[1] / length, v[2] / length} : Vec3{0, 1, 0};
|
|
}
|
|
constexpr int no_body = -1;
|
|
|
|
// Each bone's body: its own joint's, else its nearest ancestor's; none above the hips.
|
|
std::vector<int> bodies_of(const frostbite::Skeleton& skeleton) {
|
|
std::vector<int> body(skeleton.names.size(), no_body);
|
|
for (std::size_t b = 1; b < skater_body::count; ++b) {
|
|
const auto found = std::find(skeleton.names.begin(), skeleton.names.end(), skeleton_joints[b]);
|
|
require(found != skeleton.names.end(), "the skeleton has no " + std::string(skeleton_joints[b]));
|
|
body[static_cast<std::size_t>(found - skeleton.names.begin())] = static_cast<int>(b);
|
|
}
|
|
for (std::size_t bone = 0; bone < body.size(); ++bone) { // parents come first
|
|
const auto parent = skeleton.parents[bone];
|
|
if (body[bone] == no_body && parent >= 0) body[bone] = body[static_cast<std::size_t>(parent)];
|
|
}
|
|
return body;
|
|
}
|
|
|
|
vfs::GameBundle bundle(const vfs::GameData& data, const frostbite::TocDocument& toc, std::string_view name) {
|
|
auto found = data.read_bundle(toc, name);
|
|
require(found.has_value(), "the bundle " + std::string(name) + " is missing");
|
|
return std::move(*found);
|
|
}
|
|
std::vector<std::byte> asset(const vfs::GameData& data, const vfs::GameBundle& bundle, frostbite::AssetKind kind,
|
|
std::string_view name) {
|
|
std::size_t index{};
|
|
require(bundle.find(kind, name, &index) && bundle.payload(kind, index), "the asset " + std::string(name) + " is missing");
|
|
return data.read(*bundle.payload(kind, index));
|
|
}
|
|
// A geometry chunk: in the bundle when it has it, else in its superbundle's TOC.
|
|
std::vector<std::byte> chunk(const vfs::GameData& data, const vfs::GameBundle& bundle, const frostbite::TocDocument& toc,
|
|
const frostbite::Guid& id) {
|
|
std::size_t index{};
|
|
if (bundle.find_chunk(id, &index) && bundle.payload(frostbite::AssetKind::chunk, index))
|
|
return data.read(*bundle.payload(frostbite::AssetKind::chunk, index));
|
|
const auto found = std::find_if(toc.chunks.begin(), toc.chunks.end(),
|
|
[&](const frostbite::TocChunk& candidate) { return candidate.guid == id && !candidate.removed; });
|
|
require(found != toc.chunks.end(), "the mesh's geometry chunk is missing");
|
|
return data.read({found->location, found->offset, found->size});
|
|
}
|
|
|
|
struct Loader {
|
|
std::once_flag started;
|
|
std::atomic<std::shared_ptr<const SkeletonMesh>> mesh;
|
|
};
|
|
Loader& loader() { static auto* value = new Loader; return *value; }
|
|
}
|
|
|
|
SkeletonMesh bind_skeleton(const frostbite::Skeleton& skeleton, const frostbite::SkinnedMesh& mesh) {
|
|
const auto body_of = bodies_of(skeleton);
|
|
SkeletonMesh result;
|
|
result.bone_count = skeleton.names.size();
|
|
for (const auto& source : mesh.vertices) {
|
|
// The heaviest few bones, scaled back to 1.
|
|
std::array<std::size_t, 8> order{0, 1, 2, 3, 4, 5, 6, 7};
|
|
std::sort(order.begin(), order.end(), [&](auto a, auto b) { return source.weights[a] > source.weights[b]; });
|
|
SkeletonVertex vertex;
|
|
vertex.position = source.position;
|
|
float kept{};
|
|
for (std::size_t slot = 0; slot < max_influences && source.weights[order[slot]]; ++slot) {
|
|
const auto bone = source.bones[order[slot]];
|
|
require(bone < body_of.size(), "a vertex names a bone the skeleton lacks");
|
|
require(body_of[bone] != no_body, "a vertex rides on a bone no body moves");
|
|
vertex.bones[slot] = bone;
|
|
vertex.weights[slot] = source.weights[order[slot]] / 255.0f;
|
|
kept += vertex.weights[slot];
|
|
}
|
|
require(kept > 0, "a vertex has no weight");
|
|
for (auto& weight : vertex.weights) weight /= kept;
|
|
result.parts.push_back(static_cast<std::uint8_t>(body_of[vertex.bones[0]]));
|
|
result.vertices.push_back(vertex);
|
|
}
|
|
require(mesh.triangles.size() % 3 == 0, "a triangle is incomplete");
|
|
for (const auto index : mesh.triangles) require(index < result.vertices.size(), "a triangle names a missing vertex");
|
|
result.triangles = mesh.triangles;
|
|
// Area-weighted smooth normals: the costume's own are packed in a form not worth decoding
|
|
// for an untextured x-ray.
|
|
for (std::size_t i = 0; i < result.triangles.size(); i += 3) {
|
|
auto& a = result.vertices[result.triangles[i]];
|
|
auto& b = result.vertices[result.triangles[i + 1]];
|
|
auto& c = result.vertices[result.triangles[i + 2]];
|
|
const Vec3 u{b.position[0] - a.position[0], b.position[1] - a.position[1], b.position[2] - a.position[2]};
|
|
const Vec3 v{c.position[0] - a.position[0], c.position[1] - a.position[1], c.position[2] - a.position[2]};
|
|
const Vec3 n{u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0]};
|
|
for (auto* vertex : {&a, &b, &c})
|
|
for (std::size_t axis = 0; axis < 3; ++axis) vertex->normal[axis] += n[axis];
|
|
}
|
|
for (auto& vertex : result.vertices) vertex.normal = normalised(vertex.normal);
|
|
return result;
|
|
}
|
|
|
|
bool pose_skeleton(const SkeletonMesh& mesh, std::span<const LinearTransform> skin, PosedSkeleton& posed) {
|
|
if (skin.size() < mesh.bone_count) return false;
|
|
posed.positions.resize(mesh.vertices.size());
|
|
posed.normals.resize(mesh.vertices.size());
|
|
for (std::size_t i = 0; i < mesh.vertices.size(); ++i) {
|
|
const auto& vertex = mesh.vertices[i];
|
|
Vec3 position{}, normal{};
|
|
for (std::size_t slot = 0; slot < max_influences && vertex.weights[slot] > 0; ++slot) {
|
|
const auto& transform = skin[vertex.bones[slot]];
|
|
const auto p = game::place(vertex.position, transform), n = game::turn(vertex.normal, transform);
|
|
for (std::size_t axis = 0; axis < 3; ++axis) {
|
|
position[axis] += p[axis] * vertex.weights[slot];
|
|
normal[axis] += n[axis] * vertex.weights[slot];
|
|
}
|
|
}
|
|
posed.positions[i] = position;
|
|
posed.normals[i] = normalised(normal);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
std::filesystem::path game_folder() {
|
|
std::vector<wchar_t> path(32768);
|
|
const auto length = GetModuleFileNameW(nullptr, path.data(), static_cast<DWORD>(path.size()));
|
|
require(length && length < path.size(), "the game's folder is unknown");
|
|
return std::filesystem::path(std::wstring(path.data(), length)).parent_path();
|
|
}
|
|
|
|
SkeletonMesh read_skeleton_mesh(const std::filesystem::path& game_root) {
|
|
const vfs::GameData data(game_root);
|
|
const auto skeleton_toc = data.read_toc(build::skeleton_toc);
|
|
const auto skeleton_document =
|
|
frostbite::ebx::read_document(asset(data, bundle(data, skeleton_toc, build::skeleton_bundle), frostbite::AssetKind::ebx,
|
|
build::skeleton_asset));
|
|
const auto* root = skeleton_document.root();
|
|
require(root && root->object, "the render skeleton is empty");
|
|
const auto skeleton = frostbite::read_skeleton(*root->object);
|
|
const auto mesh_toc = data.read_toc(build::mesh_toc);
|
|
const auto mesh_bundle = bundle(data, mesh_toc, build::mesh_bundle);
|
|
const auto resource = asset(data, mesh_bundle, frostbite::AssetKind::resource, build::mesh_asset);
|
|
const auto lod = frostbite::read_mesh_lod(resource, build::mesh_lod);
|
|
const auto geometry = chunk(data, mesh_bundle, mesh_toc, lod.chunk);
|
|
return bind_skeleton(skeleton, frostbite::read_skinned_mesh(resource, build::mesh_lod, geometry));
|
|
}
|
|
|
|
void prepare_skeleton() noexcept {
|
|
try {
|
|
std::call_once(loader().started, [] {
|
|
std::thread([] {
|
|
const auto started = GetTickCount64();
|
|
try {
|
|
auto read = std::make_shared<const SkeletonMesh>(read_skeleton_mesh(game_folder()));
|
|
logging::log(logging::Level::info, logging::Channel::skater,
|
|
"X-ray: read the skater's skeleton ({} vertices, {} triangles) in {} ms.", read->vertices.size(),
|
|
read->triangles.size() / 3, GetTickCount64() - started);
|
|
loader().mesh.store(std::move(read));
|
|
} catch (const std::exception& failure) {
|
|
logging::log(logging::Level::warning, logging::Channel::skater, "The skater x-ray shows no skeleton: {}.", failure.what());
|
|
}
|
|
}).detach();
|
|
});
|
|
} catch (...) {
|
|
logging::write(logging::Level::warning, logging::Channel::skater, "The skater x-ray shows no skeleton: its reading could not start.");
|
|
}
|
|
}
|
|
|
|
std::shared_ptr<const SkeletonMesh> skeleton_mesh() noexcept { return loader().mesh.load(); }
|
|
}
|