mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-24 14:46:19 +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.
237 lines
11 KiB
C++
237 lines
11 KiB
C++
#include "game_images.h"
|
|
#include "Extension/Skater/xray_skeleton.h"
|
|
#include "Engine/Core/Log/logging.h"
|
|
#include "Engine/Game/Build/addresses.h"
|
|
#include "Engine/Game/Build/20260929/ui_textures.h"
|
|
#include "Engine/Vfs/game_textures.h"
|
|
#include "Extension/UI/Overlay/overlay_internal.h"
|
|
#include "Extension/UI/skate_theme.h"
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstring>
|
|
#include <future>
|
|
#include <optional>
|
|
|
|
namespace dingosdk::overlay {
|
|
namespace {
|
|
namespace ui = addr::ui_textures;
|
|
namespace theme = dingosdk::skate_theme;
|
|
|
|
// The pictures, by the names plugins give them.
|
|
enum Picture : std::size_t { picture_count = 13 };
|
|
constexpr std::array<std::string_view, picture_count> names{
|
|
"stopwatch", "wipeout", "wipeout_broken", "spread_eagle", "airtime", "gap_height", "flaming_wheel", "roll",
|
|
"thrasher", "brush_bar", "rough_tile", "scratches", "streak"};
|
|
struct Source {
|
|
const ui::Texture* texture;
|
|
std::uint32_t side; // fitted into a side x side square, its aspect kept
|
|
bool brightness{}; // white with the texture's brightness as alpha (light marks on black), else its own alpha
|
|
};
|
|
constexpr std::array<Source, picture_count> sources{{
|
|
{&ui::stopwatch, 64}, {&ui::wipeout, 64}, {&ui::wipeout_broken, 64}, {&ui::spread_eagle, 64},
|
|
{&ui::airtime, 64}, {&ui::gap_height, 64}, {&ui::flaming_wheel, 64}, {&ui::roll, 64},
|
|
{&ui::thrasher_wordmark, 512}, {&ui::brush_bar, 512}, {&ui::rough_tile, 256}, {&ui::scratches, 512, true},
|
|
{&ui::streak, 512},
|
|
}};
|
|
using Images = std::array<std::optional<frostbite::Image>, picture_count>;
|
|
|
|
struct Reads {
|
|
std::mutex mutex;
|
|
bool started{};
|
|
std::future<Images> pending;
|
|
Images decoded; // kept: every atlas the overlay builds (one per swapchain) needs them
|
|
};
|
|
Reads& reads() {
|
|
static auto* state = new Reads;
|
|
return *state;
|
|
}
|
|
|
|
// The picture's part of the texture, white: with its own alpha, or its brightness as alpha.
|
|
frostbite::Image cut(const frostbite::Image& image, const ui::Region& region, bool brightness) {
|
|
const auto at = [](float fraction, std::uint32_t size) { return static_cast<std::uint32_t>(fraction * static_cast<float>(size) + 0.5f); };
|
|
if (!image.width || !image.height || image.rgba.size() != std::size_t{image.width} * image.height * 4)
|
|
throw std::runtime_error("the texture is empty");
|
|
const auto x0 = std::min(at(region.left, image.width), image.width - 1), y0 = std::min(at(region.top, image.height), image.height - 1);
|
|
const auto x1 = std::clamp(at(region.right, image.width), x0 + 1, image.width);
|
|
const auto y1 = std::clamp(at(region.bottom, image.height), y0 + 1, image.height);
|
|
frostbite::Image result{x1 - x0, y1 - y0, std::vector<std::uint8_t>(std::size_t{x1 - x0} * (y1 - y0) * 4)};
|
|
for (std::uint32_t y = y0; y < y1; ++y)
|
|
std::memcpy(result.rgba.data() + std::size_t{y - y0} * result.width * 4,
|
|
image.rgba.data() + (std::size_t{y} * image.width + x0) * 4, std::size_t{result.width} * 4);
|
|
for (std::size_t i = 0; i < result.rgba.size(); i += 4) {
|
|
auto* p = result.rgba.data() + i;
|
|
if (brightness) p[3] = std::max({p[0], p[1], p[2]});
|
|
p[0] = p[1] = p[2] = 255;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
Images decode() {
|
|
Images result;
|
|
try {
|
|
vfs::GameTextures textures(xray::game_folder());
|
|
for (std::size_t index = 0; index < picture_count; ++index) {
|
|
const auto& source = sources[index];
|
|
try {
|
|
result[index] = cut(textures.read(source.texture->toc, source.texture->bundle, source.texture->name, source.side),
|
|
source.texture->region, source.brightness);
|
|
} catch (const std::exception& error) {
|
|
logging::log(logging::Level::warning, logging::Channel::graphics, "The game's picture {} is unavailable: {}.",
|
|
source.texture->name, error.what());
|
|
}
|
|
}
|
|
} catch (const std::exception& error) {
|
|
logging::log(logging::Level::warning, logging::Channel::graphics, "The game's pictures are unavailable: {}.", error.what());
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// The images in the atlas being built or drawn with: render thread only.
|
|
struct Loaded {
|
|
ImFontAtlas* atlas{};
|
|
bool filled{};
|
|
std::array<int, picture_count> rects{}; // each one's custom rect in the atlas; -1 none
|
|
};
|
|
Loaded& loaded() {
|
|
static Loaded state;
|
|
return state;
|
|
}
|
|
// A picture in the atlas the overlay draws with: where, and its texture coordinates.
|
|
struct Found {
|
|
const ImFontAtlasCustomRect* rect{};
|
|
ImVec2 uv0, uv1;
|
|
};
|
|
bool find(Picture picture, Found& found) {
|
|
const auto& l = loaded();
|
|
if (!l.filled || !l.atlas || ImGui::GetIO().Fonts != l.atlas || !l.atlas->TexID || l.rects[picture] < 0) return false;
|
|
found.rect = l.atlas->GetCustomRectByIndex(l.rects[picture]);
|
|
if (!found.rect || !found.rect->IsPacked() || !found.rect->Width || !found.rect->Height) return false;
|
|
l.atlas->CalcCustomRectUV(found.rect, &found.uv0, &found.uv1);
|
|
return true;
|
|
}
|
|
|
|
// Fitted into the box with its aspect kept, centred: an icon, a logo.
|
|
bool draw_image(ImDrawList* draw, Picture picture, ImVec2 min, ImVec2 max, ImU32 tint) {
|
|
Found found;
|
|
if (!find(picture, found)) return false;
|
|
const auto* rect = found.rect;
|
|
const float scale = std::min((max.x - min.x) / rect->Width, (max.y - min.y) / rect->Height);
|
|
const ImVec2 size(rect->Width * scale, rect->Height * scale);
|
|
const ImVec2 at(min.x + (max.x - min.x - size.x) * 0.5f, min.y + (max.y - min.y - size.y) * 0.5f);
|
|
draw->AddImage(loaded().atlas->TexID, at, ImVec2(at.x + size.x, at.y + size.y), found.uv0, found.uv1, tint);
|
|
return true;
|
|
}
|
|
// Its body (ui_textures.h Texture::body) stretched onto the box, the rest of it around: a brush
|
|
// stroke's bar on the box, its splatter beyond.
|
|
bool draw_shape(ImDrawList* draw, Picture picture, ImVec2 min, ImVec2 max, ImU32 tint) {
|
|
Found found;
|
|
if (!find(picture, found)) return false;
|
|
const auto& body = sources[picture].texture->body;
|
|
const float width = (max.x - min.x) / (body.right - body.left), height = (max.y - min.y) / (body.bottom - body.top);
|
|
const ImVec2 at(min.x - body.left * width, min.y - body.top * height);
|
|
draw->AddImage(loaded().atlas->TexID, at, ImVec2(at.x + width, at.y + height), found.uv0, found.uv1, tint);
|
|
return true;
|
|
}
|
|
// Nine-sliced onto the box: its corners (Texture::slice of the picture) kept `edge` pixels square,
|
|
// its edges stretched along them and its middle across the rest. A box smaller than two edges
|
|
// shrinks them to half of it.
|
|
bool draw_panel(ImDrawList* draw, Picture picture, ImVec2 min, ImVec2 max, float edge, ImU32 tint) {
|
|
Found found;
|
|
if (!find(picture, found)) return false;
|
|
const float slice = sources[picture].texture->slice;
|
|
const float edge_x = std::clamp(edge, 0.0f, (max.x - min.x) * 0.5f), edge_y = std::clamp(edge, 0.0f, (max.y - min.y) * 0.5f);
|
|
const float uv_x = slice * (found.uv1.x - found.uv0.x), uv_y = slice * (found.uv1.y - found.uv0.y);
|
|
const std::array<float, 4> xs{min.x, min.x + edge_x, max.x - edge_x, max.x}, ys{min.y, min.y + edge_y, max.y - edge_y, max.y};
|
|
const std::array<float, 4> us{found.uv0.x, found.uv0.x + uv_x, found.uv1.x - uv_x, found.uv1.x};
|
|
const std::array<float, 4> vs{found.uv0.y, found.uv0.y + uv_y, found.uv1.y - uv_y, found.uv1.y};
|
|
for (std::size_t row = 0; row < 3; ++row)
|
|
for (std::size_t column = 0; column < 3; ++column)
|
|
draw->AddImage(loaded().atlas->TexID, ImVec2(xs[column], ys[row]), ImVec2(xs[column + 1], ys[row + 1]),
|
|
ImVec2(us[column], vs[row]), ImVec2(us[column + 1], vs[row + 1]), tint);
|
|
return true;
|
|
}
|
|
} // namespace
|
|
|
|
void prepare_game_images() noexcept {
|
|
try {
|
|
auto& state = reads();
|
|
std::lock_guard lock(state.mutex);
|
|
if (std::exchange(state.started, true)) return;
|
|
state.pending = std::async(std::launch::async, &decode);
|
|
} catch (...) {
|
|
logging::write(logging::Level::warning, logging::Channel::graphics, "The game's pictures are unavailable: their reading could not start.");
|
|
}
|
|
}
|
|
|
|
void reserve_game_images(ImFontAtlas& atlas, std::chrono::milliseconds wait) noexcept {
|
|
auto& l = loaded();
|
|
l = {};
|
|
l.rects.fill(-1);
|
|
try {
|
|
auto& state = reads();
|
|
std::lock_guard lock(state.mutex);
|
|
// A read still running stays pending for the next atlas.
|
|
if (state.pending.valid() && state.pending.wait_for(wait) == std::future_status::ready) state.decoded = state.pending.get();
|
|
for (std::size_t index = 0; index < picture_count; ++index) {
|
|
const auto& image = state.decoded[index];
|
|
if (!image) continue;
|
|
if (!l.atlas) {
|
|
atlas.TexDesiredWidth = std::max(atlas.TexDesiredWidth, 2048);
|
|
atlas.Flags |= ImFontAtlasFlags_NoPowerOfTwoHeight;
|
|
l.atlas = &atlas;
|
|
}
|
|
l.rects[index] = atlas.AddCustomRectRegular(static_cast<int>(image->width), static_cast<int>(image->height));
|
|
}
|
|
} catch (...) {
|
|
l = {};
|
|
}
|
|
}
|
|
|
|
void fill_game_images(ImFontAtlas& atlas) noexcept {
|
|
auto& l = loaded();
|
|
if (l.atlas != &atlas) return;
|
|
try {
|
|
unsigned char* pixels{};
|
|
int width{}, height{};
|
|
atlas.GetTexDataAsRGBA32(&pixels, &width, &height);
|
|
if (!pixels || width <= 0 || height <= 0) {
|
|
l = {};
|
|
return;
|
|
}
|
|
auto& state = reads();
|
|
std::lock_guard lock(state.mutex);
|
|
for (std::size_t index = 0; index < picture_count; ++index) {
|
|
if (l.rects[index] < 0) continue;
|
|
const auto& image = *state.decoded[index];
|
|
const auto* rect = atlas.GetCustomRectByIndex(l.rects[index]);
|
|
if (!rect || !rect->IsPacked() || rect->X + image.width > static_cast<unsigned>(width) ||
|
|
rect->Y + image.height > static_cast<unsigned>(height)) {
|
|
l = {};
|
|
return;
|
|
}
|
|
for (std::uint32_t y = 0; y < image.height; ++y)
|
|
std::memcpy(pixels + ((static_cast<std::size_t>(rect->Y) + y) * width + rect->X) * 4,
|
|
image.rgba.data() + static_cast<std::size_t>(y) * image.width * 4, static_cast<std::size_t>(image.width) * 4);
|
|
}
|
|
atlas.TexPixelsUseColors = true;
|
|
l.filled = true;
|
|
} catch (...) {
|
|
l = {};
|
|
}
|
|
}
|
|
|
|
bool game_image_known(std::string_view name) noexcept { return std::find(names.begin(), names.end(), name) != names.end(); }
|
|
bool draw_game_image(ImDrawList* draw, std::string_view name, ImVec2 min, ImVec2 max, ImU32 tint, GameImageFit fit, float edge) {
|
|
const auto at = std::find(names.begin(), names.end(), name);
|
|
if (at == names.end()) return false;
|
|
const auto picture = static_cast<Picture>(at - names.begin());
|
|
switch (fit) {
|
|
case GameImageFit::contain: return draw_image(draw, picture, min, max, tint);
|
|
case GameImageFit::shape: return draw_shape(draw, picture, min, max, tint);
|
|
case GameImageFit::panel: return draw_panel(draw, picture, min, max, edge, tint);
|
|
}
|
|
return false;
|
|
}
|
|
float thrasher_arch() noexcept { return ui::thrasher_wordmark_arch; }
|
|
}
|