Files
ReSkate/Extension/UI/NativeMenu/throwdown_match_panel.inl
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

1016 lines
62 KiB
C++

// Private Throwdown details inside the native activity layer, from the running match's kit Panel
// document. The native challenge navigation owns the camera, input context and Back transition.
struct MatchPanelView {
bool available{};
std::string plugin,mode,identity;
kit::Panel panel;
};
// A player's Steam profile picture, for the native web image in their panel row. Their public
// Steam profile names it (no key needed); skate. then downloads it from Steam's image servers,
// whose Let's Encrypt certificates ReSkate already trusts for news images. Looked up once per
// player in the background: empty until then, and for a lookup that failed.
struct SteamPictures { std::mutex mutex; std::map<std::uint64_t,std::string> urls; std::set<std::uint64_t> asked; };
SteamPictures& steam_pictures() { static auto* value=new SteamPictures; return *value; }
bool steam_image_url(std::string_view url) {
return url.starts_with("https://avatars.") && url.find(".steamstatic.com/")!=std::string_view::npos;
}
std::string steam_picture(std::uint64_t id) {
if(!individual_steam_id(id))return {};
auto& pictures=steam_pictures();
{
std::lock_guard lock(pictures.mutex);
if(const auto found=pictures.urls.find(id);found!=pictures.urls.end())return found->second;
if(!pictures.asked.insert(id).second)return {};
}
std::thread([id] {
std::string url;
try {
const auto profile=https::get_text(L"https://steamcommunity.com/profiles/"+std::to_wstring(id)+L"?xml=1",512*1024,10,L"ReSkate/1");
constexpr std::string_view open="<avatarFull><![CDATA[";
if(profile)if(const auto at=profile->find(open);at!=std::string::npos) {
const auto start=at+open.size(),end=profile->find("]]>",start);
if(end!=std::string::npos)url=profile->substr(start,end-start);
}
// Only Steam's own image servers.
if(!steam_image_url(url))url.clear();
} catch(...) {url.clear();}
logging::log(logging::Level::info,logging::Channel::ui,"Steam picture for {}: {}",id,url.empty()?std::string("none"):url);
auto& pictures=steam_pictures();std::lock_guard lock(pictures.mutex);pictures.urls[id]=url;
}).detach();
return {};
}
// The mode a plugin's documents are about, for its events: its waiting card's, its setup page's,
// or the native flag's while that flag is the plugin's.
std::string documents_mode(const kit::Documents& docs) {
if(docs.waiting && !docs.waiting->mode.empty())return full_mode(docs.plugin,docs.waiting->mode);
if(docs.setup && !docs.setup->mode.empty())return full_mode(docs.plugin,docs.setup->mode);
const auto flag=kit::flag::state();
if(!flag.mode.empty() && flag.mode.starts_with(docs.plugin+"/"))return flag.mode;
return {};
}
MatchPanelView match_panel_view() {
MatchPanelView result;
const auto docs=kit::active();
if(!docs || !docs->panel.present)return result;
result.available=true;result.plugin=docs->plugin;result.mode=documents_mode(*docs);
result.panel=docs->panel;
result.identity=docs->plugin+"|"+docs->panel.title;
return result;
}
// Borrow only the selected native activity Page. Never replace Core pause tabs.
struct MatchPanelLease {
Address manager{};
Handle generation_page{};
ActivityDetailsOwnership details;
ActivityDetailsClose closing;
Value selection;
Address page_blueprint{};
Value shortcut_stack,saved_shortcut,shortcut_content,highlight;
Value menu_stack,native_page,saved_native_page;
bool selection_requested{};
bool selection_consumed{};
bool selection_retirement_allowed{};
std::string back_navigation;
std::string wait_reason;
std::uint64_t wait_logged{};
};
MatchPanelLease& match_panel_lease(const Context& c) {
static MatchPanelLease lease;
if(lease.manager!=c.manager || (lease.generation_page && lease.generation_page!=state().match_panel.page.handle))
lease={c.manager};
return lease;
}
void match_panel_wait(const Context& c,std::uint64_t now,const char* reason) {
if(!state().match_panel.requested_until)return;
auto& lease=match_panel_lease(c);
if(lease.wait_reason==reason && now<lease.wait_logged+2000)return;
lease.wait_reason=reason;lease.wait_logged=now;
logging::log(logging::Level::warning,logging::Channel::ui,"Throwdowns: requested native details waiting for {}.",reason);
}
bool same_native_model_schema(Address authored,Address runtime,unsigned depth=0) {
if(authored==runtime)return true;
if(!authored || !runtime || depth>16 || kind(authored)!=kind(runtime) || size(authored)!=size(runtime))return false;
const auto a=read<Address>(authored),b=read<Address>(runtime);
if(read<std::uint32_t>(a)!=read<std::uint32_t>(b))return false;
if(kind(authored)==4)return same_native_model_schema(read<Address>(a+0x30),read<Address>(b+0x30),depth+1);
if(kind(authored)!=2)return true;
const auto count=read<std::uint16_t>(a+0x2a);
if(count>128 || count!=read<std::uint16_t>(b+0x2a))return false;
const auto af=read<Address>(a+0x60),bf=read<Address>(b+0x60);
if(count && (!af || !bf))return false;
for(unsigned i=0;i<count;++i) {
const auto x=af+i*24,y=bf+i*24;
if(read<std::uint32_t>(x)!=read<std::uint32_t>(y) || read<std::uint16_t>(x+8)!=read<std::uint16_t>(y+8) ||
!same_native_model_schema(read<Address>(x+16),read<Address>(y+16),depth+1))return false;
}
return true;
}
Value native_record_model(const Context& c,Address record,std::uint32_t expected_hash) {
require(record && read<Address>(record+8)==c.base+build::native_menu::asset_record_type,
"Native activity model record differs.");
const auto type=read<Address>(record+0x18);
require(type && kind(type)==2 && read<std::uint32_t>(read<Address>(type))==expected_hash,
"Native activity model TypeInfo differs.");
// Resolve in the same manager whose write lock and registry Context owns.
// The expression wrapper at 0x192ca80 chooses a manager through TLS; its
// handle can belong to another realm and cannot be read in this registry.
// Local-profile initialization validates this ABI before installing its
// record guard. The live entry now contains that detour, so comparing it
// with the original prologue rejects our own hook. Keep the guard in this
// call path and use its validated original trampoline.
require(model_record_load_original!=nullptr,"Native activity record resolver is not initialized.");
const auto handle=model_record_load_hook(c.manager,record,false);
if(!handle)return {};
const auto actual_type=c.type_of(handle);
if(!same_native_model_schema(type,actual_type)) {
logging::log(logging::Level::error,logging::Channel::ui,
"Native activity record {:#x} manager {:#x} returned handle {:#x}: expected TypeInfo {:#x} hash {:#x}, actual {:#x} hash {:#x}.",
record,c.manager,handle,type,expected_hash,actual_type,
actual_type?read<std::uint32_t>(read<Address>(actual_type)):0);
require(false,"Native activity model handle differs from its authored TypeInfo.");
}
// The native registry may canonicalize an authored dynamic TypeInfo.
// Accept only identical reflected schemas and retain the registry's type.
const Value result{handle,actual_type};c.address(result);return result;
}
Value activity_menu_selection(const Context& c) {
const auto model=native_record_model(c,asset(c,"Activities_Shared_ContentResource/Activities_SelectedMenu"),0xaf4c67d7);
return model.handle?c.field(model,0x88391637):Value{};
}
bool request_activity_details(const Context& c) {
const auto selected=activity_menu_selection(c);
if(!selected.handle)return false;
const auto current=read<std::int32_t>(c.address(selected));
// Rewards, celebrations and other native activity pages keep their owner.
if(current!=0 && current!=1)return false;
auto& lease=match_panel_lease(c);
if(current==1 && !lease.selection_requested)return false;
lease.selection=selected;lease.details.request();
if(!current) {
lease.selection_requested=true;
lease.selection_consumed=false;
lease.selection_retirement_allowed=false;
c.set(selected,std::int32_t{1}); // ActivityMenus.Activities_Details.
set_custom_activity_menu_request(c.address(selected));
logging::write(logging::Level::info,logging::Channel::ui,"Throwdowns: native activity menu selected Activities_Details.");
}
return true;
}
bool request_native_navigation(const Context& c,const char* action) {
const auto roots=c.roots({0xcc4776b5});
if(roots.size()!=1)return false;
const auto model=roots.front().model;
require(model.type==c.type({0xcc4776b5,static_cast<std::uint16_t>(size(model.type))}),
"Native menu navigation TypeInfo differs.");
const auto navigation=c.field(roots.front().model,0xf3ba9cf8);
if(read<std::uint32_t>(c.address(navigation)))return false;
c.array(c.field(roots.front().model,0xf437b255),std::vector<std::int32_t>{-1});
c.set(navigation,game::native_name_hash(action));
return true;
}
bool request_native_challenge_menu(const Context& c) {
// Official Start capture: Activities_OpenPause_Button (0x71760666)
// writes Activities_SelectedMenu=1, then native Flow_ActivityDetails
// owns the Page and camera. DelMar_UI_Navigation stays unchanged. Its
// stale Back=2 after placement previously blocked Start until Map opened.
return request_activity_details(c);
}
void retire_challenge_request(const Context& c) {
set_custom_activity_menu_request(0);
auto& lease=match_panel_lease(c);
// Expiry withdraws an unmounted request. Completed Back/restoration can
// also retire our consumed selection only if restoration began while the
// exact live selected Page/body was still ours. Restoring a retained old
// Page must never withdraw a replacement stock page's selection.
if(lease.selection_requested && (!lease.selection_consumed || lease.selection_retirement_allowed)) {
if(lease.selection.handle && c.type_of(lease.selection.handle)==lease.selection.type &&
read<std::int32_t>(c.address(lease.selection))==1)c.set(lease.selection,std::int32_t{0});
}
lease.selection_requested=false;
}
Value active_match_panel_layer_item(const Context& c,Value layer) {
const auto list=c.field(layer,items);
unsigned count{},stride{};c.array(list,32,count,stride);
if(stride!=stack_item.size)return {};
const auto selected=active_activity_stack_item(
read<int>(c.address(c.field(layer,0x49f87835))),count,
read<bool>(c.address(c.field(layer,0x369babfe))));
if(!selected)return {};
const auto item=c.element(list,*selected);
return read<bool>(c.address(c.field(item,0x369babfe)))?item:Value{};
}
bool match_panel_body_owned(const Context& c,Value native_page) {
const auto& p=state().match_panel;
if(!native_page.handle || c.type_of(native_page.handle)!=native_page.type || !p.outer_anchor.handle)return false;
const auto list=c.field(c.field(native_page,content),items);
unsigned count{},stride{};c.array(list,32,count,stride);
if(count!=1 || stride!=stack_item.size)return false;
const auto item=c.element(list,0);
const auto display=read<Widget>(c.address(c.field(item,content)));
return read<int>(c.address(c.field(item,0x1e95752c)))==hud_panel_id+2 &&
display.blueprint==asset(c,native_menu_detail::anchored_widget) && display.data.record==0 &&
display.data.handle==p.outer_anchor.handle;
}
bool match_panel_actions_owned(const Context& c,Value native_page) {
const auto& p=state().match_panel;
if(!p.quit.handle || c.type_of(p.quit.handle)!=p.quit.type)return false;
const auto actions=c.field(native_page,0xd2118ca8);
unsigned count{},stride{};c.array(actions,8,count,stride);
if(count!=1 || stride!=page_action_schema.size)return false;
const auto action=c.path(c.element(actions,0),{0xa704272a,0xc52416ef,0xa704272a,0xa2b71c93,0x9c76b86c});
const auto quit=c.path(p.quit,{0x0fb0d794,0xa704272a,0xc52416ef,0xa704272a,0xa2b71c93,0x9c76b86c});
const auto expected=read<Address>(c.address(quit));
return expected && read<Address>(c.address(action))==expected;
}
bool retains_current_match_panel_page(const Context& c) {
const auto& lease=match_panel_lease(c);const auto& p=state().match_panel;
if(!lease.details.requested || !lease.details.page || !lease.details.layer || !p.mounted_slot.handle ||
c.type_of(p.mounted_slot.handle)!=p.mounted_slot.type)return false;
const auto layer=lease.menu_stack;
if(!layer.handle || c.type_of(layer.handle)!=layer.type || layer.handle!=lease.details.layer)return false;
const auto item=active_match_panel_layer_item(c,layer);
if(!item.handle)return false;
const auto current=read<Widget>(c.address(c.field(item,content)));
return current.blueprint==lease.page_blueprint &&
lease.details.owns(current.data.handle,layer.handle,read<int>(c.address(c.field(item,0x1e95752c)))) &&
match_panel_body_owned(c,lease.native_page);
}
bool owns_selected_match_panel(const Context& c) {
const auto& lease=match_panel_lease(c);
return state().in_menu && lease.selection.handle && c.type_of(lease.selection.handle)==lease.selection.type &&
read<std::int32_t>(c.address(lease.selection))==1 && retains_current_match_panel_page(c);
}
bool request_menu_toggle(const Context& c) {
const auto& lease=match_panel_lease(c);
if(lease.details.requested)
return owns_selected_match_panel(c) && !lease.back_navigation.empty() &&
request_native_navigation(c,lease.back_navigation.c_str());
return false;
}
void detach_match_panel_prompt(const Context& c) {
state().match_panel.prompt_visible=false;
auto& lease=match_panel_lease(c);
if(!lease.shortcut_stack.handle || !lease.saved_shortcut.handle)return;
if(c.type_of(lease.shortcut_stack.handle)!=lease.shortcut_stack.type) {
lease.shortcut_stack={};return;
}
const auto list=c.field(lease.shortcut_stack,items);
unsigned count{},stride{};const auto old=c.array(list,32,count,stride);
require(stride==stack_item.size,"Native Throwdown shortcut stack differs during teardown.");
const auto type=c.type(stack_item);
const auto id_offset=c.member(type,0x1e95752c).offset;
const auto content_offset=c.member(type,content).offset;
std::vector<std::byte> retained;
unsigned removed{};
for(unsigned i=0;i<count;++i) {
int id{};Widget display{};
std::memcpy(&id,old.data()+i*stride+id_offset,sizeof(id));
std::memcpy(&display,old.data()+i*stride+content_offset,sizeof(display));
if(id==hud_panel_id && display.data.handle==lease.shortcut_content.handle) {++removed;continue;}
retained.insert(retained.end(),old.begin()+i*stride,old.begin()+(i+1)*stride);
}
if(removed) {
if(removed==count) {
// Installation only borrows an empty authored shortcut stack.
// Restore it once while its sole mounted item is still ours.
c.copy(lease.shortcut_stack,c.address(lease.saved_shortcut));
} else {
// Native UI has since mounted another shortcut. Remove only our
// item; leave that newer owner's active/focus/transition fields.
// Context::array retains the source snapshot in a transient typed
// owner before changing the destination's counted native array.
c.array(list,retained,static_cast<unsigned>(retained.size()/stride));
}
}
lease.shortcut_stack={};
}
void restore_match_panel_mount(const Context& c) {
auto& s=state();auto& p=s.match_panel;
auto& lease=match_panel_lease(c);
const bool owned_selection=owns_selected_match_panel(c);
// StackScreenController's destructor finds its item by the original Page
// DataContext/Blueprint. Keep that identity intact throughout native Back;
// restore only the body/actions we borrowed, even after the item is gone.
if(lease.saved_native_page.handle && lease.native_page.handle &&
c.type_of(lease.native_page.handle)==lease.native_page.type &&
c.type_of(lease.saved_native_page.handle)==lease.saved_native_page.type) {
if(match_panel_body_owned(c,lease.native_page)) {
c.copy(c.field(lease.native_page,content),c.address(c.field(lease.saved_native_page,content)));
lease.selection_retirement_allowed=owned_selection;
}
if(match_panel_actions_owned(c,lease.native_page))
c.copy(c.field(lease.native_page,0xd2118ca8),c.address(c.field(lease.saved_native_page,0xd2118ca8)));
}
lease.menu_stack={};lease.native_page={};lease.saved_native_page={};
p.mounted_slot={};s.match_panel_open.store(false,std::memory_order_release);
overlay::capture_challenge_menu_input(match_panel_view().available,false,s.panel_key_down);
if(s.in_menu || p.quitting || !match_panel_view().available)detach_match_panel_prompt(c);
lease.details.clear_mount();lease.back_navigation.clear();lease.page_blueprint=0;
}
void restore_match_panel(const Context& c) {
restore_match_panel_mount(c);
retire_challenge_request(c);
auto& lease=match_panel_lease(c);lease.details.retire();lease.selection={};
lease.closing.reset();
auto& p=state().match_panel;p.requested_until=0;p.request_after=0;
}
bool close_match_panel(const Context& c,std::uint64_t now) {
auto& lease=match_panel_lease(c);
lease.closing.begin(now);
if(!owns_selected_match_panel(c)) {
// Native Back has completed, or another owner replaced this page.
// Restore only cached copies still retaining our exact presenter.
restore_match_panel(c);
logging::write(logging::Level::info,logging::Channel::ui,
"Throwdowns: native details close completed; owned body/actions restored and input released.");
return true;
}
if(lease.closing.needs_back()) {
const bool queued=request_menu_toggle(c);
lease.closing.back_queued(queued);
if(queued)logging::write(logging::Level::info,logging::Channel::ui,
"Throwdowns: native details Back queued; retaining ownership until native teardown.");
}
// A stale busy navigation action must not strand a completed match. The
// captured native Back clears Activities_SelectedMenu to zero. Recover
// that selection only while this exact private page is still selected and
// we own the original 0->1 write; never touch a replacement stock page.
const bool own_selection=lease.selection_requested && lease.selection.handle &&
c.type_of(lease.selection.handle)==lease.selection.type &&
read<std::int32_t>(c.address(lease.selection))==1;
if(lease.closing.needs_selection_recovery(now,true,own_selection)) {
c.set(lease.selection,std::int32_t{0});
logging::write(logging::Level::warning,logging::Channel::ui,
"Throwdowns: native Back did not consume the owned details selection; withdrawing that exact selection.");
}
return false;
}
bool pending_match_panel_request(const Context&,std::uint64_t now) {
const auto& p=state().match_panel;
return p.requested_until && now<p.requested_until;
}
std::optional<float> native_activity_camera_fov(const Context& c) {
const auto manager=read<Address>(c.base+0x75cbc48);
if(!manager || read<Address>(manager)!=c.base+0x63ece68)return {};
const auto first=read<Address>(manager+8),last=read<Address>(manager+16);
if(last<first || last-first>32*8 || (last-first)%8)return {};
for(auto at=first;at<last;at+=8) {
const auto view=read<Address>(at);
if(!view || read<Address>(view)!=c.base+0x63ed0a0 || read<unsigned>(view+0x148) || read<unsigned>(view+0x14c))continue;
const auto camera=read<Address>(view+0x2b0);
if(!camera)return {};
const auto fov=read<float>(camera+0xac);
if(std::isfinite(fov) && fov>1.f && fov<180.f)return fov;
}
return {};
}
Value panel_stack_item(const Context& c,Value stack,Widget display,int id) {
const auto item=make(c,stack_item);
c.set(c.field(item,content),display);c.set(c.field(item,0x1e95752c),id);
c.copy(c.field(item,0xd78d240b),c.address(c.field(stack,0x54c9abee)));
c.set(c.field(item,0x369babfe),true);
for(const auto track:{0x8c0583eeU,0xc5c7cc11U}) {
c.set(c.path(item,{track,0x20247753}),1.f);
c.set(c.path(item,{track,0x2426103f}),1.f);
c.set(c.path(item,{track,0x1e3ce013}),true);
}
return item;
}
void panel_stack_content(const Context& c,Value stack,Widget display,int id) {
const auto item=panel_stack_item(c,stack,display,id);
std::vector<std::byte> bytes(stack_item.size);
require(memory::peek_bytes(c.address(item),bytes.data(),bytes.size()),"Native Throwdown Panel content unavailable.");
c.array(c.field(stack,items),bytes,1);
c.set(c.field(stack,0x369babfe),true);
c.set(c.field(stack,0xc912aa1d),false);
c.set(c.field(stack,0x2a98baa3),false);
c.set(c.field(stack,0x18f8355b),0);
c.set(c.field(stack,0x55511280),0);
}
// The prompts under the HUD while a match runs: the document's own (visual only; their inputs are
// sampled for panel_command) and the native "Throwdown Panel" prompt last.
bool mount_match_panel_prompt(const Context& c) {
auto& s=state();auto& p=s.match_panel;auto& lease=match_panel_lease(c);
if(s.in_menu) {detach_match_panel_prompt(c);return false;}
// This is a persistent visual prompt. A StackScreen with an empty focus
// target treats it as a dismissed shortcut and hides it after its intro.
// Present the native LabelButton directly; Start still belongs to the
// separately verified challenge flow, never this non-clickable prompt.
require(s.hud_root.handle && c.type_of(s.hud_root.handle)==s.hud_root.type,
"Native gameplay shortcut HUD is not mounted.");
if(lease.shortcut_stack.handle)detach_match_panel_prompt(c);
const bool fresh=!p.prompt_item.handle;
const auto button_widget=asset(c,"UI/Foundations/Components/Buttons/LabelButton/LabelButton_Widget");
const unsigned rows=p.extra_count+1;
constexpr float width=native_hud_shortcut_width;
if(p.prompt_rows!=rows || !hud_list_has_items(c,p.prompt_list,rows)) {
std::vector<Widget> controls;
for(unsigned i=0;i<p.extra_count;++i)controls.push_back({button_widget,{0,p.extra_prompts[i].handle}});
controls.push_back({button_widget,{0,p.prompt.handle}});
hud_list(c,p.prompt_list,controls,false,88.f,8.f,0.f,width);
const auto cells=c.field(p.prompt_list,0xf2c90867);
for(unsigned i=0;i<rows;++i){const auto handle=read<Handle>(c.address(c.element(cells,i)));const Value row{handle,c.type_of(handle)};
c.set_if_changed(c.field(row,0x1cff7243),std::array<float,2>{width,88.f});
for(const auto key:{0x99bbed5aU,0x49943bedU,0xfec5e0b1U})c.set_if_changed(c.path(row,{0xde7d30c7,key}),0.f);
}
p.prompt_rows=rows;
}
mount_hud(c,p.prompt_list,asset(c,"UI/Foundations/Components/Lists/LinearList/Widget/LinearList_Widget"),p.prompt_anchor,p.prompt_item,hud_panel_id,false);
c.set_if_changed(c.field(p.prompt_anchor,0x1cff7243),std::array<float,2>{width,96.f*float(rows)-8.f});
for(const auto axis:{0xde7d30c7U,0x185a5736U}) {
const float at=axis==0xde7d30c7?native_hud_screen_anchor(native_hud_shortcut_edge):native_hud_screen_anchor(.008f);
for(const auto field:{0x99bbed5aU,0x49943bedU})c.set_if_changed(c.path(p.prompt_anchor,{axis,field}),at);
for(const auto field:{0x40bfbff4U,0x1d233266U})c.set_if_changed(c.path(p.prompt_anchor,{axis,field}),0.f);
c.set_if_changed(c.path(p.prompt_anchor,{axis,0xfec5e0b1}),axis==0xde7d30c7?1.f:0.f);
c.set_if_changed(c.path(p.prompt_anchor,{axis,0xd01f4a21}),1.f);
}
p.prompt_visible=true;
if(fresh)logging::write(logging::Level::info,logging::Channel::ui,
"Throwdowns: native Activity pause control mounted on the visible custom HUD.");
return true;
}
Value match_panel_input_action(const Context& c,Value button_value) {
return c.path(button_value,{0x0fb0d794,0xa704272a,0xc52416ef,0xa704272a,0xa2b71c93});
}
void match_panel_input(const Context& c,Value button_value,Address input_asset,bool prompt) {
require(input_asset!=0,"Native Throwdown Panel input unavailable.");
const auto action=match_panel_input_action(c,button_value);
// The native glyph and shortcut resolve different input fields. Supplying
// both keeps the controller glyph and keyboard binding on the same action.
for(const auto key:{0x3d00de27U,0x0169c2dbU}) {
const auto input_field=c.field(action,key);
if(read<Address>(c.address(input_field))!=input_asset)c.set(input_field,input_asset);
}
const auto interactable=c.path(button_value,{0x0fb0d794,0xa704272a,0xc52416ef,0xa704272a});
const auto is_prompt=c.field(interactable,0x38f7355f);
if(read<bool>(c.address(is_prompt))!=prompt)c.set(is_prompt,prompt);
const auto clickable=c.field(interactable,0x4b3146e9);
if(read<bool>(c.address(clickable))==prompt)c.set(clickable,!prompt);
}
void match_panel_action(const Context& c,Value button_value,const std::string& caption,const MatchPanelView& view,bool quit) {
plain_label(c,c.field(button_value,0x58c9d354),caption);
const auto action=match_panel_input_action(c,button_value);
const auto delegate=c.field(action,0x9c76b86c);
const auto target=callback(c,quit?"panel-quit":"panel-open",view.identity);
// Keep the installed handler stable while held/pressed state is running.
// Calling callback still retains the descriptor for this UI generation.
if(read<Address>(c.address(delegate))!=target)c.set(delegate,target);
const auto navigation=c.field(action,0xb1e14cd6);
if(!c.text(navigation).empty())c.text(navigation,"");
}
std::string quit_caption(const MatchPanelView& view) { return view.panel.quit.empty()?std::string("QUIT"):view.panel.quit; }
void update_match_panel_quit(const Context& c,Value page_value,Value quit,const MatchPanelView& view) {
match_panel_action(c,quit,quit_caption(view),view,true);
const auto actions=c.field(page_value,0xd2118ca8);
unsigned count{},stride{};c.array(actions,8,count,stride);
require(count==1 && stride==page_action_schema.size,"Native Throwdown Panel Quit shortcut differs.");
const auto mounted=c.path(c.element(actions,0),{0xa704272a,0xc52416ef,0xa704272a,0xa2b71c93});
const auto source=match_panel_input_action(c,quit);
const auto page_action=c.element(actions,0);
const auto bar=c.field(page_action,0xb6e9bfa0); // Native generated ButtonBarLabelButton context handle.
if(size(bar.type)==sizeof(Handle)) {
const auto handle=read<Handle>(c.address(bar));
if(handle && game::native_data().models.value(c.manager,handle,0,1)) {
const Value hint{handle,c.type_of(handle)};
if(hint.type==c.type(button))plain_label(c,c.field(hint,0x58c9d354),quit_caption(view));
}
}
if(read<Address>(c.address(c.field(mounted,0x9c76b86c)))!=read<Address>(c.address(c.field(source,0x9c76b86c))))
c.copy(mounted,c.address(source));
}
void match_panel_notice_width(const Context& c,Value notice,float width) {
// LabelWidthConstraint is two optional floats, not a scalar. Match the
// reflected native schema so narrowing a cell cannot abort HUD building.
const auto constraint=c.field(notice,0xd369e2db);
c.set_if_changed(c.path(constraint,{0x3ccf8bcd,0x67800332}),false);
c.set_if_changed(c.path(constraint,{0x48269971,0x2358a817}),width);
c.set_if_changed(c.path(constraint,{0x48269971,0x67800332}),true);
}
void match_panel_table_style(const Context& c,Value notice) {
const auto slot=c.field(notice,0x8cc042ef);
const auto current=read<Ref>(c.address(slot));
if(!current.record)return; // Already a private style for this cell.
const auto source=reference(c,current);
require(source.type && size(source.type)==152,"Native notice table style differs.");
const auto style=make(c,{0x3870cb1b,152},source.type);c.copy(style,c.address(source));
// Custom frames already include their text inset. The inherited native
// Notice adds 24px on both sides, reducing a 100px position cell to a
// sliver and giving differently sized headings/rows different centers.
for(const auto padding:{0x8ca12548U,0xffaf82ffU,0x4a8068ceU})
for(const auto edge:{0xd7b53526U,0x29023866U,0xcb57674fU,0xf750f0a2U})
c.set(c.path(style,{padding,edge}),0);
c.set(c.field(style,3705201444U),false); // ShouldAnimate: a table has a single parent transition.
c.set(slot,Ref{0,style.handle});
}
void match_panel_list_size(const Context& c,Value list,unsigned index,float width,float height,float y=0.f) {
const auto handle=read<Handle>(c.address(c.element(c.field(list,0xf2c90867),index)));
const Value anchor{handle,c.type_of(handle)};
c.set_if_changed(c.field(anchor,0x1cff7243),std::array<float,2>{width,height});
c.set_if_changed(c.field(anchor,0x6efc1a61),false);
c.set_if_changed(c.field(anchor,0xbe5683d9),false);
for(const auto axis:{0xde7d30c7U,0x185a5736U}) {
c.set_if_changed(c.path(anchor,{axis,0xd01f4a21}),1.f);
for(const auto field:{0x40bfbff4U,0x1d233266U})
c.set_if_changed(c.path(anchor,{axis,field}),axis==0x185a5736U?y:0.f);
}
}
// The rules notice and the first `count` setting rows (up to three).
void match_panel_settings(const Context& c,unsigned count) {
auto& p=state().match_panel;
std::vector<Widget> settings{{asset(c,"UI/Foundations/Components/Notices/BasicNotice_Widget"),{0,p.notice.handle}}};
for(unsigned i=0;i<count && i<p.setting_rows.size();++i)
settings.push_back({asset(c,"UI/Foundations/Components/Settings/Widgets/SettingWrapper_Widget"),{0,p.setting_rows[i].handle}});
hud_list(c,p.settings,settings,false,144.f,16.f,0.f,1120.f);
const auto rules_anchor=read<Handle>(c.address(c.element(c.field(p.settings,0xf2c90867),0)));
c.set(c.field({rules_anchor,c.type_of(rules_anchor)},0x1cff7243),std::array<float,2>{1120.f,320.f});
p.setting_count=count;
}
// A setting's icon: players, timer, rounds (skate.'s own), or a texture path in white.
void match_panel_setting_icon(const Context& c,unsigned i,const std::string& icon) {
auto& p=state().match_panel;
const auto name=icon.empty()?std::string(i==0?"players":i==1?"timer":"rounds"):icon;
if(p.setting_icon_names[i]==name)return;
try {
const auto stock=name=="players"?"UI/Textures/Icons/img_Icon_PlayerGroup_64":name=="timer"?"UI/Textures/Icons/Generic/img_Icon_TimerOutline_64":
name=="rounds"?"UI/Textures/Icons/Generic/img_Icon_Rounds_128":nullptr;
if(stock) {
c.set(c.field(p.setting_icons[i],0x6e469d2a),asset(c,stock));
c.set(c.field(p.setting_icons[i],0xcd4a53c0),record_ref(c,"InteractivityColorStylesList/InteractiveColors.Default.Darkest"));
} else {
mode_art(c,std::array{c.field(p.setting_icons[i],0x6e469d2a)},name);
c.set_if_changed(c.field(p.setting_icons[i],0xcd4a53c0),record_ref(c,"InteractivityColorStylesList/InteractiveColors.Core.AllWhite"));
}
p.setting_icon_names[i]=name;
} catch(const std::exception& e) {
// Optional decoration must never prevent the native Quit page
// from opening if a map is still making an icon resident.
static std::set<std::string> reported;
if(reported.insert(name).second)logging::log(logging::Level::warning,logging::Channel::ui,"Throwdown setting icon deferred: {}",e.what());
}
}
void build_match_panel(const Context& c,const MatchPanelView& view) {
auto& p=state().match_panel;if(p.page.handle)return;
const auto checkpoint=state().owned.checkpoint();
try {
// Copy the complete mutable input graph once. Backend styles and immutable
// assets stay borrowed; no stock mode's model is changed by this page.
const auto page_value=record_model(c,"Throwdown_DetailsMenu_ContentResource/Throwdowns_Details_Page",page);
PrivateUiGraph graph{c};graph.walk(page_value,0);
p.panel=record_model(c,"Throwdown_DetailsMenu_ContentResource/Throwdowns_Details_StackPanel",panel_schema);
graph.walk(p.panel,0);
p.header=record_model(c,"Throwdown_DetailsMenu_ContentResource/Throwdown_Details_StackPanel_HeaderContent",header_schema);
graph.walk(p.header,0);
plain_label(c,c.field(p.header,0x44688629),"Throwdown");
plain_label(c,c.field(p.header,0x00f3b15e),view.panel.title);
label_styles(c,c.field(p.header,0x44688629),"TextStylesList/B52-SemiB_Bright","TextStylesList/B52-SemiB_Bright");
label_styles(c,c.field(p.header,0x00f3b15e),"TextStylesList/H72-XBold_White","TextStylesList/H72-XBold_White");
c.set(c.path(p.header,{0x00f3b15e,0xfc53d427}),false);
c.set(c.field(p.header,0x2840ecf6),Widget{});
p.badge=make(c,texture_schema);
c.set(c.field(p.badge,0xf524c836),192.f);c.set(c.field(p.badge,0x6fbd254e),192.f);
const auto header_label_widget=asset(c,"UI/Foundations/Components/Text/Label/Widget/Label_Widget");
const auto list_widget=asset(c,"UI/Foundations/Components/Lists/LinearList/Widget/LinearList_Widget");
for(const auto key:{0x44688629U,0x00f3b15eU}) {
const auto label=c.field(p.header,key);
c.set(c.field(label,0xc13ba637),false);c.set(c.field(label,0xfc53d427),false);
c.set(c.field(label,0x3d8639d0),0);
}
// StackPanel measures its header by the child widget's intrinsic height.
// A full-screen AnchoredContentPresenter reports a full-screen height,
// regardless of its inner DesiredSize. Mount the measured list directly.
hud_list(c,p.header_titles,{{header_label_widget,{0,c.field(p.header,0x44688629).handle}},
{header_label_widget,{0,c.field(p.header,0x00f3b15e).handle}}},false,88.f,8.f,0.f,760.f);
hud_list(c,p.header_rows,{{}, {asset(c,"UI/ReSkate/Throwdowns/LogoTexture_Widget"),{0,p.badge.handle}},
{list_widget,{0,p.header_titles.handle}}},true,192.f,32.f);
match_panel_list_size(c,p.header_rows,0,32.f,192.f);
match_panel_list_size(c,p.header_rows,1,192.f,192.f);
match_panel_list_size(c,p.header_rows,2,760.f,192.f);
c.set(c.field(p.panel,0xc0f1b449),Widget{list_widget,{0,p.header_rows.handle}});
p.notice=record_model(c,"TD_Page_ContentResources/TD_Notice_MenuDescriptionTitle",notice_schema);
plain_label(c,c.field(p.notice,0x94703efc),view.panel.description);
c.set(c.field(p.notice,0x13be0d51),std::array<float,2>{1120.f,320.f});
label_styles(c,c.field(p.notice,0x94703efc),"TextStylesList/B52-SemiB_Bright","TextStylesList/B52-SemiB_Bright");
c.set(c.path(p.notice,{0x94703efc,0xfc53d427}),true);
match_panel_notice_width(c,p.notice,1056.f);
c.set(c.field(p.panel,0x4fd7853d),Widget{});
for(unsigned i=0;i<p.setting_rows.size();++i) {
p.setting_rows[i]=make(c,setting_schema);
c.set(c.field(p.setting_rows[i],0xc9bf367d),record_ref(c,"SettingWrapperStylesList/Horizontal.WithDivider"));
c.set(c.field(p.setting_rows[i],0xf3d74cdd),0);
const auto caption=c.field(p.setting_rows[i],0x58c9d354);
c.copy(caption,c.address(state().source_label));
plain_label(c,caption,"");
label_styles(c,caption,"TextStylesList/B52-SemiB_Black","TextStylesList/B52-SemiB_Black");
p.setting_titles[i]=make(c,label_schema);
c.copy(p.setting_titles[i],c.address(state().source_label));
plain_label(c,p.setting_titles[i],"");
label_styles(c,p.setting_titles[i],"TextStylesList/B52-SemiB_Black","TextStylesList/B52-SemiB_Black");
c.set(c.field(p.setting_titles[i],0xc13ba637),false);
c.set(c.field(p.setting_titles[i],0xfc53d427),false);
c.set(c.field(p.setting_titles[i],0x3d8639d0),0);
p.setting_values[i]=make(c,label_schema);
c.copy(p.setting_values[i],c.address(state().source_label));
c.set(c.field(p.setting_values[i],0xc13ba637),false);
c.set(c.field(p.setting_values[i],0xfc53d427),false);
label_styles(c,p.setting_values[i],"TextStylesList/H64-XBold_Black","TextStylesList/H64-XBold_Black");
c.set(c.field(p.setting_values[i],0x3d8639d0),1);
p.setting_icons[i]=make(c,texture_schema);
c.set(c.field(p.setting_icons[i],0xf524c836),64.f);
c.set(c.field(p.setting_icons[i],0x6fbd254e),64.f);
for(const auto axis:{0xde7d30c7U,0x185a5736U}) {
const bool vertical=axis==0x185a5736U;
for(const auto edge:{0x99bbed5aU,0x49943bedU})
c.set(c.path(p.setting_icons[i],{axis,edge}),vertical?.5f:0.f);
c.set(c.path(p.setting_icons[i],{axis,0xfec5e0b1}),vertical?.5f:0.f);
c.set(c.path(p.setting_icons[i],{axis,0xd01f4a21}),1.f);
}
const auto label_widget=asset(c,"UI/Foundations/Components/Text/Label/Widget/Label_Widget");
hud_list(c,p.setting_lists[i],{{asset(c,"UI/Foundations/Components/Media/Icons/Texture_Widget"),{0,p.setting_icons[i].handle}},
{label_widget,{0,p.setting_titles[i].handle}},{label_widget,{0,p.setting_values[i].handle}}},true,112.f,24.f);
match_panel_list_size(c,p.setting_lists[i],0,80.f,112.f);
match_panel_list_size(c,p.setting_lists[i],1,512.f,112.f);
match_panel_list_size(c,p.setting_lists[i],2,320.f,112.f);
p.setting_anchors[i]=make(c,anchor_schema);
c.set(c.field(p.setting_anchors[i],widget),Widget{asset(c,"UI/Foundations/Components/Lists/LinearList/Widget/LinearList_Widget"),{0,p.setting_lists[i].handle}});
c.set(c.field(p.setting_anchors[i],0x1cff7243),std::array<float,2>{960.f,112.f});
for(const auto axis:{0xde7d30c7U,0x185a5736U}) {
const bool vertical=axis==0x185a5736U;
for(const auto edge:{0x99bbed5aU,0x49943bedU})c.set(c.path(p.setting_anchors[i],{axis,edge}),0.f);
c.set(c.path(p.setting_anchors[i],{axis,0xfec5e0b1}),0.f);
c.set(c.path(p.setting_anchors[i],{axis,0xd01f4a21}),1.f);
for(const auto edge:{0x40bfbff4U,0x1d233266U})c.set(c.path(p.setting_anchors[i],{axis,edge}),vertical?0.f:64.f);
}
c.set(c.field(p.setting_rows[i],content),Widget{asset(c,native_menu_detail::anchored_widget),{0,p.setting_anchors[i].handle}});
}
match_panel_settings(c,static_cast<unsigned>(p.setting_rows.size()));
panel_stack_content(c,c.field(p.panel,0x43967ece),
{asset(c,"UI/Foundations/Components/Lists/LinearList/Widget/LinearList_Widget"),{0,p.settings.handle}},hud_panel_id+1);
// This authored footer is an empty list; the stock challenge rule supplies
// its buttons later. Instantiate the separately exported native Quit button
// instead of looking for a button that is absent from the template graph.
// Resolve the exported footer list and anchor independently. The panel's
// inline footer graph contains ContentWithTitle and presenter wrappers;
// treating its first referenced model as an anchor asks for a field it
// does not have and aborts construction before a Page can be mounted.
const auto footer_list=record_model(c,"Throwdown_DetailsMenu_ContentResource/Throwdowns_Details_StackPanel_FooterContent",linear_list);
const auto footer_anchor=record_model(c,"Throwdown_DetailsMenu_ContentResource/Throwdowns_Details_StackPanel_Footer_AnchoredContentPresenter",anchor_schema);
graph.walk(footer_list,0);graph.walk(footer_anchor,0);
c.set(c.field(footer_anchor,widget),Widget{asset(c,"UI/Foundations/Components/Lists/LinearList/Widget/LinearList_Widget"),{0,footer_list.handle}});
c.set(c.field(p.panel,0xbfc64535),Widget{asset(c,native_menu_detail::anchored_widget),{0,footer_anchor.handle}});
p.quit=record_model(c,"Throwdown_DetailsMenu_ContentResource/Throwdowns_Details_Quit_Button",button);
graph.walk(p.quit,0);
clear_value(c,c.field(p.quit,0x67212c85));
// The donor Quit focus predicate queries its native challenge backend. This
// private page validates availability through our own match coordinator.
clear_value(c,c.path(p.quit,{0x0fb0d794,0xa704272a,0xc52416ef,0x66a997b9,0x66a997b9}));
c.set(c.field(p.quit,0x659db23e),false);
const auto quit_input=read<Address>(c.address(c.field(match_panel_input_action(c,p.quit),0x0169c2db)));
match_panel_input(c,p.quit,quit_input,false);
c.copy(c.field(p.quit,0x58c9d354),c.address(state().source_label));
label_styles(c,c.field(p.quit,0x58c9d354),"TextStylesList/B52-SemiB-Caps_Dark","TextStylesList/B52-SemiB_Black");
c.set(c.path(p.quit,{0x58c9d354,0x3d8639d0}),0);
match_panel_action(c,p.quit,quit_caption(view),view,true);
c.array(c.field(footer_list,0xf2c90867),std::vector<Handle>{});
c.array(c.field(footer_list,rows_field),std::vector<Ref>{{0,p.quit.handle}});
// A native PageAction keeps Quit available while a leaderboard row has
// focus, just as the original details page does for its shortcut controls.
const auto page_actions=c.field(page_value,0xd2118ca8);
const auto action_type=read<Address>(read<Address>(page_actions.type)+0x30);
const auto quit_action=make(c,page_action_schema,action_type);
c.copy(c.field(quit_action,0xa704272a),c.address(c.path(p.quit,{0x0fb0d794,0xa704272a})));
c.text(c.field(quit_action,0x6c0c58da),"Quit"); // PageAction.FilterKey is required by the button bar.
c.set(c.field(quit_action,0xb3da8457),true); // CanClick.
std::vector<std::byte> action_bytes(page_action_schema.size);
require(memory::peek_bytes(c.address(quit_action),action_bytes.data(),action_bytes.size()),
"Native Throwdown Panel Quit shortcut unavailable.");
c.array(page_actions,action_bytes,1);
p.board=record_model(c,"TD_Scoreboard_ContentResources/LEGACY_TD_Scoreboard");
graph.walk(p.board,0);
// LEGACY_TD_Scoreboard defaults to HUDLeaderboardListElement, whose input
// schema is different. It renders its sample stamps/999999 Pts when given
// a PlayerInlineScoreboardViewModel. Use that model's actual list bridge.
c.set(c.path(p.board,{0xfcdd0d32,0x55ca89f4}),
asset(c,"UI/Features/CommunityEvent/LEGACY_PlayerInlineScoreboardList_Widget"));
p.columns=make(c,linear_list);
p.outer_anchor=record_model(c,"Throwdown_DetailsMenu_ContentResource/Throwdowns_Details_AnchoredContentPresenter",anchor_schema);
c.set(c.field(p.outer_anchor,widget),Widget{asset(c,"UI/Foundations/Components/Lists/LinearList/Widget/LinearList_Widget"),{0,p.columns.handle}});
panel_stack_content(c,c.field(page_value,content),
{asset(c,native_menu_detail::anchored_widget),{0,p.outer_anchor.handle}},hud_panel_id+2);
const auto back_action=back(c,page_value);
clear_value(c,c.field(back_action,0x9c76b86c));
c.text(c.field(back_action,0xb1e14cd6),"ThrowdownDetailsBack");
const auto back_input=asset(c,"Configurations/Input/UI/UI_Back");
c.set(c.field(back_action,0x3d00de27),back_input);c.set(c.field(back_action,0x0169c2db),back_input);
p.page=page_value; // Publish the completed page only after every edge exists.
match_panel_lease(c).generation_page=p.page.handle;
logging::log(logging::Level::info,logging::Channel::ui,"{}: private native Throwdown details Page, Quit and scoreboard constructed.",view.panel.title);
} catch(...) {
const auto prompt=p.prompt,prompt_anchor=p.prompt_anchor,prompt_item=p.prompt_item;
const auto requested=p.requested_until;
const auto menu_was_open=p.menu_was_open;
p={};p.prompt=prompt;p.prompt_anchor=prompt_anchor;p.prompt_item=prompt_item;p.requested_until=requested;
p.menu_was_open=menu_was_open;
state().owned.rollback(checkpoint,[&](Value value){c.destroy(value);});
throw;
}
}
void refresh_match_panel(const Context& c,const MatchPanelView& view) {
auto& p=state().match_panel;auto& lease=match_panel_lease(c);const auto& panel=view.panel;
plain_label(c,c.field(p.header,0x00f3b15e),panel.title);
plain_label(c,c.field(p.notice,0x94703efc),panel.description);
mode_art(c,std::array{c.field(p.badge,0x6e469d2a)},panel.art);
const auto settings=static_cast<unsigned>(std::min<std::size_t>(panel.settings.size(),p.setting_rows.size()));
if(settings!=p.setting_count)match_panel_settings(c,settings);
for(unsigned i=0;i<settings;++i) {
plain_label(c,p.setting_titles[i],panel.settings[i].caption);
plain_label(c,p.setting_values[i],panel.settings[i].value);
match_panel_setting_icon(c,i,panel.settings[i].icon);
}
const auto action_page=match_panel_body_owned(c,lease.native_page)?lease.native_page:p.page;
update_match_panel_quit(c,action_page,p.quit,view);
// A row shows its value, and its outcome beside it.
const auto row_text=[](const kit::PanelPlayer& player) {
return player.outcome.empty()?player.value:player.value.empty()?player.outcome:player.value+" "+player.outcome;
};
std::array<float,3> widths{200.f,640.f,400.f};
for(const auto& row:panel.players) {
widths[1]=std::max(widths[1],float(native_hud_name_width(row.name)));
widths[2]=std::max(widths[2],1.5f*float(native_hud_value_width(row_text(row))));
}
const float width=widths[0]+widths[1]+widths[2]+16.f;
const bool resized=p.board_widths!=widths;
// Reuse the game's complete Throwdown scoreboard, profile rows, rank
// badges and local-player treatment. Its own template aligns the column
// headings and rows; separate notice boxes gave it a foreign appearance.
c.set_if_changed(c.field(p.board,0x5a7cec85),true);
c.set_if_changed(c.field(p.board,0xb7e92e95),true);
const auto headers=c.field(p.board,0x3d98d397);
for(unsigned col=0;col<3;++col) {
const auto label=c.element(headers,col);
plain_label(c,label,col==0?"POS":col==1?"NAME":panel.score_caption);
label_styles(c,label,"TextStylesList/H52-XBold_White","TextStylesList/H52-XBold_White");
}
std::vector<Ref> rows;std::vector<std::uint64_t> order;unsigned rank{};
for(const auto& player:panel.players) {
auto& row=p.players[player.id];++rank;
if(!row.row.handle) {
row.row=record_model(c,"TD_Scoreboard_ContentResources/TD_PlayerProfileInline");
PrivateUiGraph graph{c};graph.walk(row.row,0);
row.rank=record_model(c,"TD_Scoreboard_ContentResources/TD_DefaultRank");
row.outcome=record_model(c,"TD_Hud_ContentResources/TD_HUD_Header_Label");
auto display=read<Widget>(c.address(c.field(row.row,0xc76d8b73)));
display.data={0,row.rank.handle};c.set(c.field(row.row,0xc76d8b73),display);
const auto scores=c.field(row.row,0xa3d2bba9);
// The donor score list presents S.K.A.T.E. stamps. Supply a label
// template AND live handles, rather than inheriting those defaults.
c.set(c.field(scores,0x55ca89f4),asset(c,"UI/Foundations/Components/Text/Label/Widget/LabelListItem_Widget"));
c.array(c.field(scores,rows_field),std::vector<Ref>{});
c.array(c.field(scores,0xf2c90867),std::vector<Handle>{row.outcome.handle});
label_styles(c,row.outcome,"TextStylesList/B52-SemiB_Bright","TextStylesList/B52-SemiB_Bright");
c.set(c.field(row.outcome,0xfc53d427),false);
c.set(c.field(row.outcome,0xc13ba637),false);
}
c.set_if_changed(c.field(row.row,0x8810a01e),player.self);
const auto name=c.path(row.row,{0xaed127c3,profile_name_tag_field});
c.set_if_changed(c.field(name,0xc13ba637),false);
c.set_if_changed(c.field(name,0xfc53d427),false);
label_styles(c,name,"TextStylesList/H52-XBold_White","TextStylesList/H52-XBold_White");
plain_label(c,name,player.name);
// Use only this account's cached picture for this account's row.
// Optional portrait loading cannot prevent the native Quit panel.
if(!row.portrait_attempted)try {
const auto record=asset(c,("Throwdown_ModeSelect_ContentResources/SteamAvatar_"+std::to_string(player.id)).c_str());
const auto type=native_record_type(c,record);
require(read<std::uint32_t>(read<Address>(type))==category_schema.hash,"Steam picture record schema differs.");
const auto image=read<Address>(read<Address>(record+0x20)+c.member(type,0x189084da).offset);
require(image && (read<Address>(image+8)==c.base+build::native_menu::texture_asset_type ||
read<Address>(image+8)==c.base+build::native_menu::image_asset_type),"Steam picture is not resident yet.");
for(const auto texture:ui_models(c,c.field(row.row,0xaed127c3),2005356313U)) {
c.set_if_changed(c.field(texture,1888565495U),image);
c.text(c.field(texture,3630279999U),"");
row.portrait_attempted=true;
}
} catch(const std::exception&) { /* Other players keep their own native portrait. */ }
// The document's picture through the row's own native web image: the player's Steam
// picture ("steam"), or one on Steam's image servers.
if(!row.portrait_attempted && !player.portrait.empty()) {
const auto url=player.portrait=="steam"?steam_picture(player.id):steam_image_url(player.portrait)?player.portrait:std::string{};
if(!url.empty() && url!=row.picture_url) {
for(const auto texture:ui_models(c,c.field(row.row,0xaed127c3),2005356313U))c.text(c.field(texture,3630279999U),url);
row.picture_url=url;
}
}
plain_label(c,c.field(row.rank,0x2718c842),player.rank.empty()?std::to_string(rank):player.rank);
plain_label(c,row.outcome,row_text(player));
order.push_back(player.id);rows.push_back({0,row.row.handle});
}
const auto player_list=c.field(p.board,0xfcdd0d32);
if(order!=p.player_order) {
c.array(c.field(player_list,rows_field),std::vector<Ref>{});
std::vector<Handle> handles;for(const auto& row:rows)handles.push_back(row.handle);
c.array(c.field(player_list,0xf2c90867),handles);
}
p.player_order=std::move(order);p.board_widths=widths;
// Fixed, nonoverlapping columns replace the donor's fractional partition:
// its left side did not reserve the rules panel's measured width.
if(resized || !hud_list_has_items(c,p.columns,2))
hud_list(c,p.columns,{{asset(c,"UI/Foundations/Components/StackVariations/StackPanel/StackPanel_Widget"),{0,p.panel.handle}},
{asset(c,"UI/Features/CommunityEvent/LEGACY_Scoreboard_Widget"),{0,p.board.handle}}},true,1450.f,192.f);
match_panel_list_size(c,p.columns,0,1120.f,1450.f);
match_panel_list_size(c,p.columns,1,width,140.f+140.f*float(panel.players.size()),360.f);
c.set_if_changed(c.field(p.outer_anchor,0x1cff7243),std::array<float,2>{1312.f+width,1450.f});
for(const auto axis:{0xde7d30c7U,0x185a5736U}) {
const float at=axis==0xde7d30c7U?.04f:.12f;
for(const auto field:{0x99bbed5aU,0x49943bedU})c.set_if_changed(c.path(p.outer_anchor,{axis,field}),at);
c.set_if_changed(c.path(p.outer_anchor,{axis,0xfec5e0b1}),0.f);
c.set_if_changed(c.path(p.outer_anchor,{axis,0xd01f4a21}),1.f);
for(const auto field:{0x40bfbff4U,0x1d233266U})c.set_if_changed(c.path(p.outer_anchor,{axis,field}),0.f);
}
}
// Prepare the native Page's presentation as soon as the game publishes it.
// Camera/focus confirmation remains separate and cannot expose the stock body.
bool prepare_match_panel_body(const Context& c,std::uint64_t now) {
auto& s=state();auto& p=s.match_panel;auto& lease=match_panel_lease(c);
if(!pending_match_panel_request(c,now) || p.quitting || !p.page.handle || !lease.selection_requested)return false;
if(lease.native_page.handle)return match_panel_body_owned(c,lease.native_page);
if(!lease.selection.handle || c.type_of(lease.selection.handle)!=lease.selection.type ||
read<std::int32_t>(c.address(lease.selection))!=1)return false;
const auto layer=native_record_model(c,asset(c,"UI_Layer_Data/Layer_Menu"),0xd71ee0a1);
const auto selected=layer.handle?active_match_panel_layer_item(c,layer):Value{};
if(!selected.handle)return false;
const auto page_blueprint=asset(c,"UI/Foundations/Templates/Page/Page_Widget");
const auto slot=c.field(selected,content);const auto original=read<Widget>(c.address(slot));
if(original.blueprint!=page_blueprint || !original.data.handle || original.data.handle==p.page.handle)return false;
const Value native_page{original.data.handle,c.type_of(original.data.handle)};
if(native_page.type!=c.type(page))return false;
const auto native_back=c.text(c.field(back(c,native_page),0xb1e14cd6));
if(native_back!="ActivitiesDetailsBack" && native_back!="ThrowdownDetailsBack")return false;
const auto saved=make(c,page,native_page.type);c.copy(saved,c.address(native_page));
lease.menu_stack=layer;lease.native_page=native_page;lease.saved_native_page=saved;p.mounted_slot=slot;
const auto item_id=read<int>(c.address(c.field(selected,0x1e95752c)));
lease.details.mount(native_page.handle,layer.handle,item_id);lease.selection_consumed=true;
lease.page_blueprint=page_blueprint;lease.back_navigation=native_back;
// Preserve the exact outer Widget/DataContext the native destructor owns.
c.copy(c.field(native_page,content),c.address(c.field(p.page,content)));
c.copy(c.field(native_page,0xd2118ca8),c.address(c.field(p.page,0xd2118ca8)));
logging::log(logging::Level::info,logging::Channel::ui,
"Throwdowns: prepared native Details body before focus; Page {:#x}, layer {:#x}, item {}, body {:#x}.",
native_page.handle,layer.handle,item_id,p.outer_anchor.handle);
return true;
}
// The controller button (XInput bit) of a prompt's input, for the inputs a gameplay prompt can
// name without stealing the game's own: the D-pad and the stick clicks. 0 for any other.
std::uint16_t prompt_button(std::string_view name) noexcept {
return name=="UI_DpadUp"?0x0001:name=="UI_DpadDown"?0x0002:name=="UI_DpadLeft"?0x0004:name=="UI_DpadRight"?0x0008:
name=="UI_L3Click"?0x0040:name=="UI_R3Click"?0x0080:0;
}
void native_match_panel(const Context& c,std::uint64_t now) {
auto& s=state();auto& p=s.match_panel;const auto view=match_panel_view();
const auto identity=view.available?view.identity:std::string{};
if(match_panel_lease(c).closing.pending()) {
if(!close_match_panel(c,now))return;
}
if((!view.available || p.quitting || (p.match_key!=identity && !p.match_key.empty())) &&
owns_selected_match_panel(c)) {
close_match_panel(c,now);
return; // New matches wait for the previous native page to leave.
}
if(view.available && p.match_key!=identity) {
// Restore the native phone slot before retiring the old match's graph.
// Every match gets fresh press/hold state and callbacks, even when no
// unavailable frame occurred between the previous Quit and this match.
restore_match_panel(c);detach_match_panel_prompt(c);
p={};p.match_key=identity;p.menu_was_open=s.in_menu;
match_panel_lease(c)={c.manager};
}
p.menu_was_open=s.in_menu;
if(!view.available)p.quitting=false;
if(!view.available || p.quitting) {
restore_match_panel(c);p.requested_until=0;return;
}
const auto& current_lease=match_panel_lease(c);
if(current_lease.native_page.handle && (!current_lease.selection.handle ||
c.type_of(current_lease.selection.handle)!=current_lease.selection.type ||
read<std::int32_t>(c.address(current_lease.selection))!=1 || !retains_current_match_panel_page(c))) {
// Back can finish before camera confirmation. Retire that prepared
// body now, rather than keeping a stale four-second opening request.
restore_match_panel(c);return;
}
if(s.match_panel_open.load() && !owns_selected_match_panel(c)) {
// Native Back may already have removed our Page before this tick.
// Release input ownership without modifying the next native menu.
restore_match_panel(c);
return;
}
// A generic menu-open transition includes every D-pad phone shortcut.
// Borrow the phone only after an actual Start/Menu (or Escape) edge for
// this match, captured before the UI refresh throttle.
bool start_requested{};
{
std::lock_guard input_lock(s.panel_input_mutex);
start_requested=!s.panel_start_request.empty() && s.panel_start_request==identity;
if(start_requested)s.panel_start_request.clear();
}
if(start_requested) {
if(owns_selected_match_panel(c)) {
close_match_panel(c,now);
return;
}
if(pending_match_panel_request(c,now))return; // One press owns this transition; repeats cannot extend it.
p.transition={};p.camera_fov_before=native_activity_camera_fov(c).value_or(0.f);
sync_custom_activity_ui_presence(true);
p.requested_until=now+4000;
p.request_after=now+200; // Let the native activity observer initialize before selecting its page.
logging::write(logging::Level::info,logging::Channel::ui,
"Throwdowns: Start/Menu pressed for this custom match; requesting native ChallengeMenu.");
}
// Build a visual-only control, rather than cloning the native activity's
// focus/availability graph, which can hide it when that activity retires.
const auto visual_prompt=[&]() {
const auto donor=record_model(c,"Throwdown_DetailsMenu_ContentResource/ThrowdownDetails_HUDButton",button);
const auto candidate=make(c,button);
for(const auto key:{0x8cc042efU,0xf9515817U,0xf343d160U})
c.copy(c.field(candidate,key),c.address(c.field(donor,key)));
c.copy(c.path(candidate,{0x0fb0d794,0x8cc042ef}),c.address(c.path(donor,{0x0fb0d794,0x8cc042ef})));
PrivateUiGraph graph{c};graph.walk(candidate,0);
clear_value(c,c.field(candidate,0x67212c85));
const auto action=match_panel_input_action(c,candidate);
clear_value(c,c.field(action,0x9c76b86c));c.text(c.field(action,0xb1e14cd6),"");
label_styles(c,c.field(candidate,0x58c9d354),"TextStylesList/SP48-SemiB_White_DropShadow75","TextStylesList/SP48-SemiB_White_DropShadow75");
c.set(c.field(candidate,0x659db23e),false);
return candidate;
};
if(!p.prompt.handle)p.prompt=visual_prompt();
// The document's prompts, above the panel prompt. Their inputs are sampled (at the client
// tick rate) for panel_command; each prompt's glyph carries its command.
p.extra_count=0;s.panel_bindings.clear();s.panel_plugin=view.plugin;s.panel_mode=view.mode;
for(const auto& prompt:view.panel.prompts) {
if(p.extra_count>=p.extra_prompts.size() || prompt.inputs.empty())continue;
auto& extra=p.extra_prompts[p.extra_count++];
if(!extra.handle)extra=visual_prompt();
plain_label(c,c.field(extra,0x58c9d354),prompt.caption);
match_panel_input(c,extra,asset(c,("Configurations/Input/UI/"+prompt.inputs[0]).c_str()),true);
const auto action=match_panel_input_action(c,extra);clear_value(c,c.field(action,0x9c76b86c));c.text(c.field(action,0xb1e14cd6),"");
c.set_if_changed(c.path(extra,{0x0fb0d794,0xa704272a,0xc52416ef,0xa704272a,0x85a0b4e8}),!s.in_menu);
if(const auto mask=prompt_button(prompt.inputs[0]);mask && !prompt.glyph.empty())s.panel_bindings.push_back({mask,prompt.glyph});
}
plain_label(c,c.field(p.prompt,0x58c9d354),"Throwdown Panel");
match_panel_input(c,p.prompt,asset(c,"Configurations/Input/UI/UI_Activities_AccessPause"),true);
const auto prompt_action=match_panel_input_action(c,p.prompt);
clear_value(c,c.field(prompt_action,0x9c76b86c));
c.text(c.field(prompt_action,0xb1e14cd6),"");
c.set_if_changed(c.path(p.prompt,{0x0fb0d794,0xa704272a,0xc52416ef,0xa704272a,0x85a0b4e8}),!s.in_menu);
if(hud_shortcut_visible(view.available,s.in_menu,s.match_panel_open.load(),pending_match_panel_request(c,now),p.quitting))mount_match_panel_prompt(c);
else detach_match_panel_prompt(c);
// Gameplay Start is suppressed from the native poller. The separate
// challenge request owns its transition; ordinary pause stays untouched.
if(p.requested_until) {
if(now>=p.requested_until) {
logging::log(logging::Level::warning,logging::Channel::ui,"Throwdowns: native details request expired; native menu open={}, last wait={}.",s.in_menu,match_panel_lease(c).wait_reason);
restore_match_panel(c);
}
else if(p.request_after && now>=p.request_after) {
// Request the separate challenge flow once after the real edge.
const bool opening=request_native_challenge_menu(c);
if(opening)p.request_after=0;
}
}
if(now<p.retry_after) {restore_match_panel(c);return;}
build_match_panel(c,view);
refresh_match_panel(c,view);
auto& lease=match_panel_lease(c);
const auto layer=native_record_model(c,asset(c,"UI_Layer_Data/Layer_Menu"),0xd71ee0a1);
const auto selected=layer.handle?active_match_panel_layer_item(c,layer):Value{};
if(!selected.handle) {
// Do not try to mount a gameplay prompt during the pause transition:
// its parent HUD may already be hidden while InMenu is still cached.
match_panel_wait(c,now,"the native activity layer transition");
if(pending_match_panel_request(c,now))restore_match_panel_mount(c);
else restore_match_panel(c);
return;
}
if(s.in_menu || pending_match_panel_request(c,now) || s.match_panel_open.load())detach_match_panel_prompt(c);
if(s.match_panel_open.load() && owns_selected_match_panel(c))return;
if(!prepare_match_panel_body(c,now) && !match_panel_body_owned(c,lease.native_page))return;
const auto native_page=lease.native_page;
const bool selection_live=lease.selection.handle && c.type_of(lease.selection.handle)==lease.selection.type &&
read<std::int32_t>(c.address(lease.selection))==1;
const auto fov=native_activity_camera_fov(c);
const bool focused=fov && (*fov<=60.f || (p.camera_fov_before>0.f && *fov<p.camera_fov_before-3.f));
if(!p.transition.ready(native_page.handle,s.in_menu,selection_live,focused,now)) {
match_panel_wait(c,now,"confirmed native menu ownership and challenge camera focus");
return;
}
lease.wait_reason.clear();lease.wait_logged=0;
p.requested_until=0;p.request_after=0;
s.match_panel_open.store(true,std::memory_order_release);
overlay::capture_challenge_menu_input(true,true,s.panel_key_down);
kit::post_to(view.plugin,"panel_open",view.mode);
logging::log(logging::Level::info,logging::Channel::ui,
"{}: native activity details body mounted; native Page {:#x}, layer {:#x}, item {} identity and Back retained; private body {:#x}.",
view.panel.title,native_page.handle,layer.handle,lease.details.item,p.outer_anchor.handle);
}