Files
ReSkate/Extension/UI/NativeMenu/native_hud_values.h
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

151 lines
7.8 KiB
C++

#pragma once
#include <cstdint>
#include <algorithm>
#include <cmath>
#include <string_view>
#include <format>
#include <string>
#include <array>
#include <optional>
namespace dingosdk::multiplayer::menu_data {
// The supported desktop gameplay screen lays out inside a 90% safe frame.
// Our anchors belong to that frame, so a small positive anchor alone still
// leaves the outer 5% margin visible. Convert only our screen-edge anchors;
// stock screens, centered notices and the stable HUD presenters stay intact.
inline constexpr float native_hud_screen_anchor(float screen) noexcept {
return (screen-.05f)/.90f;
}
inline constexpr float native_hud_side_center=.60f;
// Rime's start/end offsets are opposing insets. Equal values resize a
// rectangle; opposite values translate it without changing its bounds.
inline constexpr std::array<float,2> native_hud_translation(float offset) noexcept {
return {offset,-offset};
}
inline constexpr float native_hud_shortcut_width=672.f;
inline constexpr float native_hud_shortcut_edge=.998f;
// Approximate brush-font advance, including padding; round to an authored
// native frame. UTF-8 continuation bytes do not inflate a gamertag's width.
inline unsigned native_hud_name_width(std::string_view name) {
unsigned advance=32;
for(const unsigned char ch:name) {
if((ch&0xc0)==0x80)continue;
advance+=(ch=='I' || ch=='i' || ch=='l' || ch==' ' || ch=='.')?12:
(ch=='W' || ch=='M' || ch=='w' || ch=='m' || ch>=0x80)?36:26;
}
return std::clamp((advance+31u)/32u*32u,224u,1280u);
}
struct HudHeaderGeometry {
unsigned frame_width,content_width,title_width,icon=64,content_height=80,padding=48;
float badge_y_shift{},title_y_shift{};
static constexpr unsigned height=112,text_height=80,gap=24;
};
inline std::string native_hud_title_text(std::string_view title) {
std::string result(title);for(auto& character:result)if(character>='a' && character<='z')character=char(character-'a'+'A');return result;
}
inline HudHeaderGeometry native_hud_header_geometry(std::string_view title) {
// Brush title advances are distinct from H52 player-name cells. Reserve a
// small font allowance, then fit the authored frame to this title's row.
// Short names must not inherit a scoreboard-wide minimum background.
unsigned advance=12;
for(const unsigned char ch:native_hud_title_text(title)) {
if((ch&0xc0)==0x80)continue;
if(ch==' ' || ch=='.')advance+=16;
else if(ch=='-' || ch=='t' || ch=='f' || ch=='r')advance+=24;
else if(ch=='I' || ch=='i' || ch=='l' || ch=='j')advance+=18;
else if(ch=='W' || ch=='M')advance+=58;
else if(ch=='w' || ch=='m' || ch>=0x80)advance+=48;
else if(ch>='a' && ch<='z')advance+=34;
else if(ch=='E' || ch=='F' || ch=='T' || ch=='P' || ch=='S')advance+=40;
else advance+=44;
}
const auto scaled=(advance*39u+31u)/32u; // Native D78 brush title, with a conservative width allowance.
const auto text=std::clamp((scaled+7u)/8u*8u,144u,1600u);
const unsigned icon=64u;
const auto content=text+icon+HudHeaderGeometry::gap;
const auto frame=(std::max(320u,content+96u)+31u)/32u*32u;
return {frame,content,text,icon,80u};
}
// A header with a document's icon size and frame width (0: the measured default). A larger icon's
// paint baseline sits higher than the 64 px one's; a fixed frame keeps 120 px either side of its
// icon and title, the brush title fitted inside it (a 704 px frame holds a 344 px title beside a
// 96 px icon, as a captured mode header did).
inline HudHeaderGeometry native_hud_header_geometry(std::string_view title,unsigned icon_size,unsigned frame_width) {
auto g=native_hud_header_geometry(title);
if(!icon_size && !frame_width)return g;
if(icon_size) {
g.icon=icon_size;g.content_height=std::max(80u,icon_size);
if(icon_size>64u) {g.badge_y_shift=-6.f;g.title_y_shift=-2.f;}
}
if(frame_width) {
const auto used=240u+g.icon+HudHeaderGeometry::gap;
if(frame_width>used)g.title_width=std::min(g.title_width,frame_width-used);
}
g.content_width=g.icon+HudHeaderGeometry::gap+g.title_width;
g.frame_width=frame_width?std::max(frame_width,g.content_width):(std::max(320u,g.content_width+96u)+31u)/32u*32u;
g.padding=(g.frame_width-g.content_width)/2u;
return g;
}
// A clock as the native HUD shows it: mm:ss.mmm.
inline std::string native_hud_clock_text(std::uint64_t milliseconds) {
return std::format("{:02}:{:02}.{:03}",milliseconds/60000,(milliseconds/1000)%60,milliseconds%1000);
}
// A published clock keeps advancing from the time it was sampled, so a plugin
// that publishes less often than a frame still shows a running clock. A clock
// counting up never passes its cap; one counting down stops at zero.
inline std::uint64_t native_hud_clock_elapsed(std::uint64_t observed,std::uint64_t sampled,
std::uint64_t now,bool running,std::uint64_t cap) {
const auto extra=running && sampled && now>sampled?now-sampled:0;
if(!cap)return observed+extra;
return observed>=cap?cap:observed+std::min(extra,cap-observed);
}
inline std::uint64_t native_hud_clock_remaining(std::uint64_t observed,std::uint64_t sampled,
std::uint64_t now,bool running) noexcept {
const auto extra=running && sampled && now>sampled?now-sampled:0;
return extra>=observed?0:observed-extra;
}
// The native compass strip: ahead is its centre, +/-90 degrees either end of
// its 1184 units; a point behind stays at the nearer end. `camera` is the world
// matrix (rows: right, up, back, position).
inline constexpr float native_hud_compass_icon=96.f;
inline constexpr float native_hud_compass_center_y=209.f;
inline std::optional<float> native_hud_compass_offset(const std::array<float,3>& target,const std::array<float,16>& camera) {
const float dx=target[0]-camera[12],dz=target[2]-camera[14];
const float right=dx*camera[0]+dz*camera[2],forward=-dx*camera[8]-dz*camera[10];
if(!std::isfinite(right) || !std::isfinite(forward) || std::hypot(camera[8],camera[10])<.001f)return {};
constexpr float reach=(1184.f-native_hud_compass_icon)*.5f;
return std::clamp(std::atan2(right,forward)/(3.14159265f*.5f),-1.f,1.f)*reach;
}
// A toast's opacity: in over 180 ms, held, out over 300 ms.
inline float native_hud_toast_alpha(std::uint64_t now,std::uint64_t shown,std::uint64_t duration) noexcept {
if(!shown || now<shown)return 0.f;
const auto age=now-shown,hold=std::max<std::uint64_t>(duration,480);
if(age<180)return static_cast<float>(age)/180.f;
if(age<hold-300)return 1.f;
return age<hold?static_cast<float>(hold-age)/300.f:0.f;
}
inline unsigned native_hud_value_width(std::string_view text) {
unsigned advance=40;
for(const unsigned char ch:text) {
if((ch&0xc0)==0x80)continue;
advance+=(ch==':' || ch=='.' || ch==' ')?12:(ch=='W' || ch=='M' || ch>=0x80)?36:26;
}
return std::clamp((advance+31u)/32u*32u,224u,640u);
}
// CountdownViewModel stores whole seconds as Uint64. The match clock stores
// milliseconds. Round up so 2.9 seconds displays 3, and GO occurs only at zero.
constexpr std::uint64_t native_hud_seconds(std::uint64_t milliseconds) {
return milliseconds / 1000 + (milliseconds % 1000 != 0);
}
inline unsigned native_slider_choice(float normalized,unsigned minimum,unsigned maximum,unsigned step) {
if(!std::isfinite(normalized))return minimum;
const auto steps=(maximum-minimum)/step;
const auto index=static_cast<unsigned>(std::round(std::clamp(normalized,0.f,1.f)*steps));
return minimum+std::min(index,steps)*step;
}
inline float native_slider_position(unsigned choice,unsigned minimum,unsigned maximum) {
if(maximum<=minimum)return 0.f;
return float(std::clamp(choice,minimum,maximum)-minimum)/float(maximum-minimum);
}
} // namespace dingosdk::multiplayer::menu_data