Files
ReSkate/Extension/UI/Overlay/game_images.cpp
T
AyeZeeBB db52508bdb Plugins: Hall of Meat and grind exits are plugin mods now; their C++ is gone
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.
2026-10-19 16:51:28 -05:00

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; }
}