Files
ReSkate/Extension/UI/hud_corner.cpp
T
AyeZeeBB 8444553402 Throwdowns: the throwdown kit, so a mode is a plugin and the SDK knows no particular mode
Plugins register modes (card, replaces, order, flag, players) and publish documents through
reskate.throwdowns.ui: the setup page, the waiting card, HUD slots themed on the game's HUD_<theme>_*
widgets, the match panel, flags and match restrictions. SmokeyDawg58's native plumbing
(throwdown_menu.cpp and its .inl files) renders them and posts throwdown_ui events back. kit::flag
is the one native flag registration. General services added for it: reskate.files, reskate.ui.dialog,
reskate.skater push/hold/bone_cam, reskate.camera.pad_flight, reskate.physics competition,
reskate.session.world, reskate.tricks feed/gesture, and world entities.

His ten modes are now Lua in his plugin: the rules, referee and wire (td/rules, referee, wire), and
every page and HUD document (td/ui*.lua). Extension/Modes and reskate.modes are gone, along with
every race and 1-UP branch in the SDK. The plugin test drives each mode through kit events.

A native setup page is never written to while it closes into flag placement: its rows are not
republished, the stock page is put back once the exit is over, and the stock Players slider is left
alone while a plugin flag is out. This fixes the crashes on the Deathrace button and on 1-UP's
Confirm.
2026-10-20 01:04:54 -05:00

350 lines
15 KiB
C++

#include "Extension/Skater/trick_feed.h"
#include "hud_corner.h"
#include "Engine/Core/Hooks/hooks.h"
#include "Engine/Core/Log/logging.h"
#include "Engine/Core/Platform/memory.h"
#include "Engine/Game/Abi/native_data.h"
#include "Engine/Game/Build/addresses.h"
#include "Engine/Game/Build/20260929/engine.h"
#include "Engine/Game/Build/20260929/hud_corner.h"
#include "Engine/Game/Build/20260929/ui_model.h"
#include "Extension/UI/NativeMenu/native_menu_data.h"
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <cstring>
#include <map>
#include <string>
#include <vector>
namespace dingosdk::hud_corner {
namespace {
namespace corner = addr::hud_corner;
namespace menu_data = multiplayer::menu_data;
using Address = std::uintptr_t;
using Clock = std::chrono::steady_clock;
using Value = std::uint64_t; // a field's value bytes, as the model's write takes them
using ModelWrite = std::uint64_t (*)(Address manager, std::uint64_t handle, Address type, const void* value, std::uint8_t flag);
// Our stack item: counted beside the d-pad (above its priority, within what the stack counts).
constexpr std::uint32_t cover_key = 0x52534843; // "RSHC": apart from the game's own keys (0x8000....)
constexpr std::uint64_t cover_id = 0x5253484300000000ULL;
constexpr std::int32_t cover_priority = corner::dpad_priority + 1;
static_assert(cover_priority < corner::score_priority);
// The score HUD's fields that hold it down, their sizes and the values that do.
struct Switch {
std::uint32_t field;
std::size_t size;
Value down;
};
constexpr std::array<Switch, 2> score_switches{{
{corner::hud_widget_active, 1, 0},
{corner::extra_info_style, 4, static_cast<std::uint32_t>(corner::extra_info_none)},
}};
// A score HUD field as the model write hook sees it, on whichever thread the game writes from.
struct Taken {
std::atomic<std::uint64_t> handle{}; // the field's in the current model; 0 none
std::atomic<bool> taken{};
std::atomic<Value> meant{}; // what the game last wrote, to give back
};
struct Hook {
std::atomic<ModelWrite> original{};
void* target{};
bool ready{}; // prepared
std::atomic<bool> hooked{}; // and enabled (hook_hud)
std::array<Taken, score_switches.size()> fields;
};
Hook& hook() {
static auto* value = new Hook;
return *value;
}
// The game's write of a taken field writes ours instead and keeps the game's.
std::uint64_t model_write(Address manager, std::uint64_t handle, Address type, const void* value, std::uint8_t flag) {
auto& h = hook();
// SmokeyDawg58's trick feed reads the score HUD's trick list off the same writes.
if (handle && value && tricks::feed::wanted()) tricks::feed::tap(handle, type, value);
if (handle && value) {
for (std::size_t index = 0; index < score_switches.size(); ++index) {
auto& field = h.fields[index];
if (!field.taken.load(std::memory_order_acquire) || handle != field.handle.load(std::memory_order_acquire)) continue;
Value meant{};
std::memcpy(&meant, value, score_switches[index].size);
field.meant.store(meant, std::memory_order_release);
value = &score_switches[index].down;
break;
}
}
return h.original.load(std::memory_order_acquire)(manager, handle, type, value, flag);
}
bool start_model_write_hook(Address base) noexcept {
auto& h = hook();
const auto& contract = addr::ui_model::model_write;
std::array<unsigned char, 32> code{};
auto status = HookNotFound;
if (memory::peek(base + contract.rva, code) && code == contract.bytes) {
auto* target = reinterpret_cast<void*>(base + contract.rva);
void* relay{};
status = hook_prepare(target, reinterpret_cast<void*>(&model_write), &relay);
if (status == HookOk && !relay) status = HookUnsupportedFunction;
if (status == HookOk) {
h.original.store(reinterpret_cast<ModelWrite>(relay), std::memory_order_release); // before the hook is live
h.target = target;
} else (void)hook_remove(target);
}
if (status != HookOk) {
logging::log(logging::Level::warning, logging::Channel::ui,
"Plugins leave skate.'s score HUD as it is: hooking the UI model's write failed ({}).", hook_status_string(status));
return false;
}
h.ready = true;
return true;
}
// Client thread only.
struct Corner {
Address base{};
Address manager{};
menu_data::Value stack; // the d-pad's stack, once found
menu_data::Value cover; // our stack item, while the corner is hidden
menu_data::Value score_model; // ScoringHUDViewModel, once found
std::array<std::uint16_t, score_switches.size()> score_offsets{};
Clock::time_point next_stack_scan, next_score_scan;
bool warned{}, slip_logged{};
};
Corner& corner_state() {
static auto* value = new Corner;
return *value;
}
// The field's handle in the current model (0: none); a new one gives up what was taken, as a new
// model holds the game's own state.
void bind(std::size_t index, std::uint64_t handle) noexcept {
auto& field = hook().fields[index];
if (field.handle.load(std::memory_order_acquire) == handle) return;
field.taken.store(false, std::memory_order_release);
field.handle.store(handle, std::memory_order_release);
}
bool score_held() noexcept {
const auto& fields = hook().fields;
return std::any_of(fields.begin(), fields.end(), [](const Taken& field) { return field.taken.load(std::memory_order_acquire); });
}
// Runs `use(context)` on the game's UI model registry under its model lock, which the game's
// writers take too. False while there is no registry.
template <class Use> bool with_ui_models(Address base, Use&& use) {
if (!base) return false;
const auto ui = menu_data::read<Address>(base + addr::engine::ui_manager);
const auto manager = ui ? menu_data::read<Address>(ui + 0x140) : 0;
if (!manager) return false;
const menu_data::Context context(base, manager);
game::ModelWriteLock lock(manager);
use(context);
return true;
}
std::string asset_name(Address asset) { return asset ? menu_data::string(menu_data::read<Address>(asset + 0x18), 256) : std::string{}; }
std::string widget_of(const menu_data::Context& context, const menu_data::Value& item) {
return asset_name(menu_data::read<Address>(context.address(context.path(item, {corner::item_content, corner::content_blueprint}))));
}
menu_data::Value items_of(const menu_data::Context& context, const menu_data::Value& stack) {
return context.field(stack, corner::stack_items);
}
// Whether `next` says it is time to look again (at most once a second while something is not found).
bool time_to_look(Clock::time_point& next) {
const auto now = Clock::now();
if (now < next) return false;
next = now + std::chrono::seconds(1);
return true;
}
// A new registry holds the game's own state: our item and what was taken went with the old one.
void follow_registry(Corner& c, const menu_data::Context& context) {
if (c.manager == context.manager) return;
c.manager = context.manager;
c.stack = {};
c.cover = {};
c.score_model = {};
for (std::size_t index = 0; index < score_switches.size(); ++index) bind(index, 0);
}
// The stack the d-pad is in.
bool find_stack(Corner& c, const menu_data::Context& context) {
if (c.stack.handle && context.type_of(c.stack.handle) == c.stack.type) return true;
c.stack = {};
if (!time_to_look(c.next_stack_scan)) return false;
for (const auto& root : context.roots({corner::hud_view_schema})) {
const auto stack = context.field(root.model, corner::dpad_scoring_stack);
const auto items = items_of(context, stack);
unsigned count{}, stride{};
(void)context.array(items, 16, count, stride);
for (unsigned index = 0; index < count; ++index)
if (widget_of(context, context.element(items, index)) == corner::dpad_widget) {
c.stack = stack;
return true;
}
}
return false;
}
// Puts our item into the d-pad's stack, or takes it out; the game's items stay as they are. Our
// item: empty content, our key, the stack's transition as a push gives it.
void hide_dpad(Corner& c, const menu_data::Context& context, bool hidden) {
const auto field = items_of(context, c.stack);
unsigned count{}, stride{};
auto items = context.array(field, 16, count, stride);
menu_data::require(!count || stride == menu_data::stack_item.size, "the stack's items differ");
const auto key_at = context.member(context.type(menu_data::stack_item), corner::item_key).offset;
unsigned ours = count;
for (unsigned index = 0; index < count; ++index) {
std::uint32_t key{};
std::memcpy(&key, items.data() + std::size_t{index} * stride + key_at, sizeof key);
if (key == cover_key) ours = index;
}
items.resize(std::size_t{count} * menu_data::stack_item.size);
if (hidden && ours == count) {
if (!c.cover.handle) {
c.cover = context.create(menu_data::stack_item, cover_id);
context.set(context.field(c.cover, corner::item_key), cover_key);
context.set(context.field(c.cover, corner::item_priority), cover_priority);
context.copy(context.field(c.cover, corner::item_transition),
context.address(context.field(c.stack, corner::default_transition)));
}
std::vector<std::byte> bytes(menu_data::stack_item.size);
menu_data::require(memory::read_bytes(context.address(c.cover), bytes.data(), bytes.size()), "our stack item is unavailable");
items.insert(items.end(), bytes.begin(), bytes.end());
context.array(field, items, count + 1);
} else if (!hidden && ours < count) {
const auto begin = items.begin() + static_cast<std::ptrdiff_t>(std::size_t{ours} * stride);
items.erase(begin, begin + stride);
context.array(field, items, count - 1);
}
if (!hidden && c.cover.handle) {
context.destroy(c.cover);
c.cover = {};
}
}
// ScoringHUDViewModel, its switches bound to the model's fields.
bool find_score(Corner& c, const menu_data::Context& context) {
if (c.score_model.handle && context.type_of(c.score_model.handle) == c.score_model.type) return true;
c.score_model = {};
for (std::size_t index = 0; index < score_switches.size(); ++index) bind(index, 0);
if (!time_to_look(c.next_score_scan)) return false;
const auto roots = context.roots({corner::score_view_schema});
if (roots.empty()) return false;
const auto model = roots.front().model;
for (std::size_t index = 0; index < score_switches.size(); ++index) {
const auto member = context.member(model.type, score_switches[index].field);
menu_data::require(menu_data::size(member.type) == score_switches[index].size, "a score HUD field's size differs");
c.score_offsets[index] = member.offset;
bind(index, context.field(model, score_switches[index].field).handle);
}
c.score_model = model;
return true;
}
// Holds the score HUD down (each switch taken at its value that does), or gives it back.
void hold_score(Corner& c, const menu_data::Context& context, bool held) {
for (std::size_t index = 0; index < score_switches.size(); ++index) {
const auto& sw = score_switches[index];
auto& field = hook().fields[index];
Value now{};
menu_data::require(memory::peek_bytes(context.address(c.score_model) + c.score_offsets[index], &now, sw.size),
"a score HUD field is unavailable");
if (held) {
const bool taken = field.taken.load(std::memory_order_acquire);
if (taken && now != sw.down && !std::exchange(c.slip_logged, true))
logging::write(logging::Level::warning, logging::Channel::ui,
"HUD corner: the game wrote the score HUD past the model write hook.");
if (!taken) field.meant.store(now, std::memory_order_release); // what the game means until it writes otherwise
field.taken.store(true, std::memory_order_release);
if (now != sw.down) context.publish(context.field(c.score_model, sw.field), &sw.down);
} else if (field.taken.exchange(false, std::memory_order_acq_rel)) {
const auto meant = field.meant.load(std::memory_order_acquire); // what the game wrote while it was held
if (now != meant) context.publish(context.field(c.score_model, sw.field), &meant);
}
}
}
} // namespace
void start_hud(std::uintptr_t base) noexcept {
corner_state().base = base;
(void)start_model_write_hook(base); // without it the d-pad still hides
}
bool hook_hud(bool on) noexcept {
auto& h = hook();
if (!h.ready) return !on; // nothing to hook
if (h.hooked.load(std::memory_order_acquire) == on) return true;
const auto status = on ? hook_enable(h.target) : hook_disable(h.target);
if (status != HookOk) {
logging::log(logging::Level::warning, logging::Channel::ui, "The HUD corner could not {} its UI model write hook ({}).",
on ? "enable" : "disable", hook_status_string(status));
return false;
}
h.hooked.store(on, std::memory_order_release);
return true;
}
bool hide_hud(bool hidden) noexcept {
auto& c = corner_state();
// Without the hook the score HUD is never taken, but what was taken is always given back.
const bool hold = hidden && hook().hooked.load(std::memory_order_acquire);
try {
if (!hidden && !c.cover.handle && !score_held()) return true; // the game's own corner, untouched
with_ui_models(c.base, [&](const menu_data::Context& context) {
follow_registry(c, context);
if (find_stack(c, context)) hide_dpad(c, context, hidden);
if ((hold || score_held()) && find_score(c, context)) hold_score(c, context, hold);
c.warned = false;
});
} catch (const std::exception& error) {
c.stack = {};
c.score_model = {};
if (!std::exchange(c.warned, true))
logging::log(logging::Level::warning, logging::Channel::ui, "Plugins cannot hide skate.'s HUD corner: {}.", error.what());
} catch (...) {
c.stack = {};
c.score_model = {};
}
return !c.cover.handle && !score_held();
}
}
namespace dingosdk::hud_corner {
namespace {
struct Asks {
std::map<std::string, bool, std::less<>> owners; // hidden, by plugin
bool hooked{};
};
Asks& asks() {
static auto* value = new Asks;
return *value;
}
}
void ask(std::string_view owner, bool hidden) { asks().owners.insert_or_assign(std::string(owner), hidden); }
void release(std::string_view owner) {
auto& a = asks();
if (const auto found = a.owners.find(owner); found != a.owners.end()) a.owners.erase(found);
}
void tick() noexcept {
auto& a = asks();
try {
bool hidden{};
for (const auto& [owner, on] : a.owners) hidden = hidden || on;
const bool feeding = tricks::feed::wanted();
if ((!a.owners.empty() || feeding) && !a.hooked) a.hooked = hook_hud(true);
if (!a.hooked) return;
const bool back = hide_hud(hidden);
// No plugin left: the hook goes once the corner is the game's again.
if (a.owners.empty() && !feeding && back && hook_hud(false)) a.hooked = false;
} catch (...) {
}
}
}