Files
ReSkate/Extension/UI/NativeMenu/Test/native_tools_view_tests.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

193 lines
13 KiB
C++

#include "Extension/UI/NativeMenu/native_tools_view.h"
#include <cstdio>
#include <stdexcept>
using namespace dingosdk;
using namespace dingosdk::native_tools;
void check(bool condition, const char* message) { if (!condition) throw std::runtime_error(message); }
struct Capture {
unsigned calls{};
std::string command, root, destination, spawn;
overlay::DebugRequest debug;
};
int main() {
try {
{
// A BAM root with its seven time-of-day layers, as the level scan finds them.
WorldLayerCatalog catalog{{{"Levels/Game/BAM_LevelRoot/Root", WorldMap::bam, -1, true}}, {0}, {}};
for (const auto* time : time_of_day_keys) {
const auto slot = static_cast<unsigned>(catalog.nodes.size());
catalog.nodes.push_back({std::string("Levels/Game/BAM_LevelRoot/TOD_") + time, WorldMap::bam, 0, false});
catalog.layers.push_back({std::string("bam_tod_") + time, time, "", WorldMap::bam, slot, slot, "Lighting"});
}
install_world_layer_catalog(std::move(catalog));
}
State state;
overlay::Model model;
Capture capture;
overlay::CallbacksV3 cb;
cb.user = &capture;
cb.queue_load = [](void* user, const char* root, const char*, const char* destination, const char* spawn, char*, std::size_t) {
auto& c = *static_cast<Capture*>(user); ++c.calls; c.root = root; c.destination = destination; c.spawn = spawn; return true;
};
cb.queue_debug = [](void* user, const overlay::DebugRequest& request, char*, std::size_t) {
auto& c = *static_cast<Capture*>(user); ++c.calls; c.debug = request; return true;
};
cb.queue_console_command = [](void* user, const char* command, char*, std::size_t) {
auto& c = *static_cast<Capture*>(user); ++c.calls; c.command = command; return true;
};
model.levels = {{std::string(root_asset), "Root"}, {"Studio/Example", "Studio", {"EntryA"}, {}, false}};
model.can_queue_load = true;
activate(state, model, cb, "select-level", "Studio/Example");
check(capture.calls == 0 && state.destination == "Studio/Example", "Selecting a detached level must not load it");
activate(state, model, cb, "load-level", "");
check(capture.calls == 1 && capture.root == root_asset && capture.destination == "Studio/Example" && capture.spawn == "EntryA", "Detached level uses its sole manifest spawn automatically");
model.levels.back().native_registered = true;
activate(state, model, cb, "load-level", "");
check(capture.calls == 2 && capture.spawn.empty(), "Registered level uses the engine's default spawn without retaining a previous selection");
check(state.feedback.empty(), "Accepted changes do not display queue diagnostics");
model.multiplayer.active = true;
check(!can_load(state, model, cb), "Guest cannot load a level even with an otherwise ready loader");
const auto guest_page = render(state, model, cb, 0);
check(std::none_of(guest_page.main.begin(), guest_page.main.end(), [](const auto& row) {
return row.command == "select-level";
}), "Guest level choices are visibly locked");
check(std::none_of(guest_page.side.begin(), guest_page.side.end(), [](const auto& row) {
return row.command == "load-level";
}), "Guest load button has no activation delegate");
activate(state, model, cb, "load-level", "");
check(capture.calls == 2 && state.feedback == "Only the lobby host can change levels.",
"A stale level action cannot load after joining a host");
model.multiplayer.hosting = true;
check(can_load(state, model, cb), "Host retains level controls");
model.multiplayer = {};
check(can_load(state, model, cb), "Level controls return when the host ends the lobby");
model.multiplayer.lobby_joining = true;
check(!can_load(state, model, cb), "Joining cannot race a manual level change");
model.multiplayer = {};
model.can_queue_load = false;
activate(state, model, cb, "load-level", "");
check(capture.calls == 2, "Stale load button cannot queue during transition");
model.can_queue_load = true;
model.levels.pop_back();
activate(state, model, cb, "load-level", "");
check(capture.calls == 2 && state.destination.empty(), "Removed level selection must not load");
model.world.map = WorldMap::bam; model.parks.available = true;
activate(state, model, cb, "park-lot", "financial");
activate(state, model, cb, "park-family", "");
activate(state, model, cb, "load-park", "");
check(capture.command == "park financial flumppark_01", "Selected park targets its own lot");
const auto calls = capture.calls;
model.parks.controlled_by_host = true;
activate(state, model, cb, "load-park", "");
check(capture.calls == calls, "Host ownership is rechecked on activation");
activate(state, model, cb, "noclip", "");
check(capture.calls == calls, "Unavailable debug control cannot queue");
model.debug.noclip = true;
activate(state, model, cb, "noclip", "");
check(capture.debug.action == overlay::DebugAction::set_noclip && !capture.debug.enabled, "Active noclip can always be disabled");
model.graphics.available = true; model.graphics.ready[0] = true; model.graphics.enabled[0] = false;
activate(state, model, cb, "graphics", "film_grain");
check(capture.command == "graphics film_grain 1", "Toggle reads effective authored graphic state");
model.progression.challenges_enabled = true; model.progression.challenges_hidden = true;
activate(state, model, cb, "challenges", "");
check(capture.command == "challenges 1", "Hidden challenges use show command");
model.world.ready = true; model.world_controls.population_available = true;
model.world_controls.choices.traffic = 3;
activate(state, model, cb, "traffic", "");
check(capture.command == "environment traffic -1", "Population cycles back to authored default");
const auto parks = render(state, model, cb, parks_section);
const auto load = std::find_if(parks.side.begin(), parks.side.end(), [](const auto& row) { return row.id == "load-park"; });
check(load != parks.side.end() && load->command.empty(), "Host-owned park action is visibly unavailable");
cb.queue_debug = [](void*, const overlay::DebugRequest&, char* result, std::size_t size) {
std::snprintf(result, size, "Queued for the game update thread."); return true;
};
activate(state, model, cb, "noclip", "");
check(state.feedback.empty(), "Successful callback diagnostics stay out of the menu");
model.debug.status = "Native callback diagnostic";
const auto player_page = render(state, model, cb, player_section);
check(std::none_of(player_page.side.begin(), player_page.side.end(), [](const auto& row) {
return row.title.find("Native callback") != std::string::npos;
}), "Debug model status stays out of the player menu");
const auto find = [](const std::vector<Row>& rows, std::string_view id) {
return std::find_if(rows.begin(), rows.end(), [&](const auto& row) { return row.id == id; });
};
// Hall of Meat sits with the player's other switches.
const auto meat_row = [&] {
const auto page = render(state, model, cb, player_section);
return *find(page.main, "hall-of-meat");
};
check(meat_row().title == "Hall of Meat: Off" && meat_row().command.empty(), "Hall of Meat waits until it started");
model.hall_of_meat = {true, false};
activate(state, model, cb, "hall-of-meat", "");
check(meat_row().command == "hall-of-meat" && capture.command == "hallofmeat 1", "Hall of Meat turns on");
model.hall_of_meat = {true, true};
activate(state, model, cb, "hall-of-meat", "");
check(meat_row().title == "Hall of Meat: On" && capture.command == "hallofmeat 0", "and off again");
model.multiplayer.local_name = "steam_name";
model.player_card = {true, "Card Name", {}};
const auto card_page = render(state, model, cb, player_section);
const auto field = find(card_page.side, "card-name");
check(field != card_page.side.end() && field->input && field->title == "steam_name" && field->argument == "Card Name",
"Card name field shows the Steam name as its hint and the saved name as its text");
check(find(card_page.side, "card-name-reset")->command == "card-name-reset", "A custom card name can be reset");
model.player_card.custom_name.clear();
const auto steam_page = render(state, model, cb, player_section);
check(find(steam_page.side, "card-name-reset")->command.empty(), "Steam name reset is idle without a custom name");
{
// Time of day forces one of a map's seven time layers, others off.
overlay::Model world;
world.world.available = world.world.ready = true;
world.world.map = WorldMap::bam;
world.world.supported.assign(world_layers().size(), true);
State tod;
Capture log;
overlay::CallbacksV3 record;
record.user = &log;
record.queue_console_command = [](void* user, const char* command, char*, std::size_t) {
auto& c = *static_cast<Capture*>(user); ++c.calls; c.command = command; return true;
};
check(time_of_day_available(world, record) && time_of_day(world) == 0, "Time of day starts at the level's default");
activate(tod, world, record, "time-of-day", {});
check(log.calls == 7 && log.command == "world bam_tod_1_morning on", "Cycling selects Morning last, after the others are off");
for (unsigned slot = 0; slot < time_of_day_keys.size(); ++slot)
world.world.choices[time_of_day_layer(WorldMap::bam, slot)] = slot == 4 ? "on" : "off";
check(time_of_day(world) == 5, "A single forced layer reads back as that time");
const auto page = render(tod, world, record, 0);
check(std::any_of(page.side.begin(), page.side.end(), [](const auto& row) { return row.title == "Time of day: Night"; }),
"The World page shows the current time of day");
for (unsigned slot = 0; slot < 6; ++slot) activate(tod, world, record, "time-of-day", {});
world.world.choices[time_of_day_layer(WorldMap::bam, 6)] = "on";
check(time_of_day(world) == 0, "Two forced times read as no single choice");
for (auto& choice : world.world.choices) choice = "default";
world.world.choices[time_of_day_layer(WorldMap::bam, 6)] = "on";
for (unsigned slot = 0; slot < 6; ++slot) world.world.choices[time_of_day_layer(WorldMap::bam, slot)] = "off";
log.calls = 0;
activate(tod, world, record, "time-of-day", {});
check(log.calls == 7 && log.command.ends_with(" default"), "Cycling past the last time returns all seven to default");
world.world.controlled_by_host = true;
log.calls = 0;
activate(tod, world, record, "time-of-day", {});
check(log.calls == 0 && !time_of_day_available(world, record), "A host-forced lobby cannot change time of day");
}
{
overlay::Model maps;
maps.levels = {{std::string(root_asset), "Root"}, {"levels/Game/BAM_LevelRoot/BAM_LevelRoot"},
{"levels/Game/DingoLevel_Splash/DingoLevel_Splash"}, {"Levels/Custom/Bedroom", "Bedroom"}};
maps.levels.back().custom = true;
State split;
const auto shows = [](const Page& page, std::string_view asset) {
return std::any_of(page.main.begin(), page.main.end(), [&](const auto& row) { return row.argument == asset; });
};
const auto world_page = render(split, maps, cb, world_section), custom_page = render(split, maps, cb, custom_section);
check(shows(world_page, maps.levels[1].asset) && !shows(world_page, "Levels/Custom/Bedroom"), "World lists only official levels");
check(shows(custom_page, "Levels/Custom/Bedroom") && !shows(custom_page, maps.levels[1].asset), "Custom Maps lists only mod levels");
check(!shows(world_page, maps.levels[2].asset) && !shows(custom_page, maps.levels[2].asset), "Splash is never listed");
activate(split, maps, cb, "select-level", maps.levels[2].asset);
check(split.destination.empty(), "Splash cannot be selected");
}
std::puts("Native ReSkate control checks passed.");
return 0;
} catch (const std::exception& e) { std::fprintf(stderr, "%s\n", e.what()); return 1; }
}