From 77542e4db95ceb47f908a35b7dd5f92e69639900 Mon Sep 17 00:00:00 2001 From: AyeZeeBB Date: Wed, 21 Oct 2026 20:42:49 -0500 Subject: [PATCH] Throwdowns: a plugin refusing its card no longer blocks every later flag, and joins say what they did A plugin that calls flag.leave() on `open` (Tag and Infected outside a session) sent the game an exit from a setup that never opened. The game never took it, and every later flag's setup waited behind it. A registration left before its setup was asked for now just ends, from flag.leave() and from the waiting card's Leave alike (leave_unopened). The join card says which flag it read and how it was branded, or which buttons it has when it has no Enroll; its branding errors were swallowed. Joining another player's plugin flag says whether its plugin heard join, or why the player was taken out of its queue. A navigation that waits on the game's says so every 3 s, and a setup that does not open in 15 s says so. --- Extension/Throwdowns/native_flag_session.cpp | 28 ++++++++++++-- Extension/Throwdowns/throwdown_relay.cpp | 7 +++- .../UI/NativeMenu/throwdown_map_identity.inl | 38 +++++++++++++++++-- Extension/UI/NativeMenu/throwdown_menu.cpp | 8 ++++ 4 files changed, 73 insertions(+), 8 deletions(-) diff --git a/Extension/Throwdowns/native_flag_session.cpp b/Extension/Throwdowns/native_flag_session.cpp index 4cb5cfcd..f763726e 100644 --- a/Extension/Throwdowns/native_flag_session.cpp +++ b/Extension/Throwdowns/native_flag_session.cpp @@ -89,6 +89,16 @@ void to_cleanup(Placement& p, std::uint64_t now) { if (p.step != Step::cleanup) p.sent = 0; p.cancelling = true; p.step = Step::cleanup; p.until = now + 15000; } +// Left before its setup was asked for (a plugin refusing its card at `open`): nothing of the game's +// is open to back out of, so the registration just ends. An exit the game never takes stays +// pending, and every later flag's setup waits behind it (2026-10-21, hosting with a player). +bool leave_unopened(Placement& p) { + if (p.step != Step::open || p.created || p.setup_seen) return false; + p.owner_left = true; + retire(p); + p.status.clear(); + return true; +} void open_request(Placement& p, const Request& r, std::uint64_t now) { reset(p); p.step = Step::open; p.mode = r.mode; p.game = r.game; p.options = r.options; p.world = r.world; @@ -117,7 +127,17 @@ bool navigate(const menu_data::Context& c, std::string_view name, std::uint32_t const auto current = c.field(roots.front().model, 0xf3ba9cf8); const auto metadata = c.field(roots.front().model, 0xf437b255); const auto pending = read(c.address(current)); - if (pending && pending != consumed) return false; + if (pending && pending != consumed) { + // The game's own navigation is still on its way: tried again at the next tick. Said every + // few seconds, with what it is, for a setup that never opens. + static std::uint64_t next_note{}; + if (const auto now = GetTickCount64(); now >= next_note) { + next_note = now + 3000; + logging::log(logging::Level::info, logging::Channel::ui, "Native flag: navigation {} waits; the game's navigation 0x{:08x} is pending.", + name, pending); + } + return false; + } c.set(current, game::native_name_hash(name)); c.array(metadata, std::vector{-1}); logging::log(logging::Level::info, logging::Channel::ui, "Native flag: navigation {}.", name); @@ -210,7 +230,7 @@ bool leave() noexcept { if (p.step == Step::idle) return false; note(p, "leave", {std::to_string(p.game)}); p.pending.reset(); // Quit/Cancel must not reopen a queued replacement - to_cleanup(p, GetTickCount64()); + if (!leave_unopened(p)) to_cleanup(p, GetTickCount64()); } flush(); return true; @@ -381,6 +401,8 @@ void tick(const menu_data::Context& c) { return; } if ((p.step == Step::open || p.step == Step::setup) && !p.created && !p.setup_seen && now >= p.until) { + logging::log(logging::Level::warning, logging::Channel::ui, "Native flag: the setup for {} did not open in 15 s (step {}).", p.mode, + static_cast(p.step)); retire(p); p.status = "The setup did not open. Select the mode to try again."; return; // no native queue was created; do not close another menu } @@ -480,7 +502,7 @@ void leave() noexcept { using namespace multiplayer::native_flag; auto& p = placement(); std::lock_guard lock(p.mutex); p.pending.reset(); - if (p.step == Step::idle) return; + if (p.step == Step::idle || leave_unopened(p)) return; p.owner_left = true; to_cleanup(p, GetTickCount64()); } diff --git a/Extension/Throwdowns/throwdown_relay.cpp b/Extension/Throwdowns/throwdown_relay.cpp index 264734c1..3d79ab4a 100644 --- a/Extension/Throwdowns/throwdown_relay.cpp +++ b/Extension/Throwdowns/throwdown_relay.cpp @@ -543,8 +543,13 @@ void on_local(Relay &r, ThrowdownLocalAction &a, std::uint64_t now) { } if(joined) { close_hosted(r);leave_mirrors(r,a.mmid);flag.claimed=true; + note("you joined another player's {} (MMID {:#x}); its plugin hears join.",flag.offer.mode,a.mmid); + } + else { + note("you joined another player's {} (MMID {:#x}), but {}; you were taken out of its queue.",flag.offer.mode,a.mmid, + !mode?"no plugin here registers that mode":!flag.offer.open?"it is closed":"another throwdown of yours is still open"); + if(const auto local=local_native_player_id())queue_throwdown_remove(local); } - else if(const auto local=local_native_player_id())queue_throwdown_remove(local); return; } // Joining a queue removes the player from any other one and destroys a drop it hosts. diff --git a/Extension/UI/NativeMenu/throwdown_map_identity.inl b/Extension/UI/NativeMenu/throwdown_map_identity.inl index 5bd1869a..a6269a84 100644 --- a/Extension/UI/NativeMenu/throwdown_map_identity.inl +++ b/Extension/UI/NativeMenu/throwdown_map_identity.inl @@ -131,17 +131,37 @@ void restore_join_branding(const Context& c) { for(auto& [handle,card]:state().join_cards)restore_branding_presenters(c,card.presenters); state().join_cards.clear(); } +// What the join card of the flag the game last filled details for came to, said when it changes: +// the only record of a card that stays the donor's. +void join_card_report(std::string text) { + auto& s=state(); + if(text==s.join_report)return; + s.join_report=std::move(text); + if(!s.join_report.empty())logging::log(logging::Level::info,logging::Channel::ui,"Join card: {}",s.join_report); +} void custom_join_details(const Context& c) { auto& s=state();const auto mmid=s.join_mmid.load(std::memory_order_acquire); + if(!mmid) {join_card_report({});restore_join_branding(c);return;} const auto definition=flag_brand(mmid); - if(!definition || (!s.in_menu && !s.join_cards.empty())) { + if(!definition) { + const auto remote=custom_throwdown_flag_info(mmid); + join_card_report(std::format("MMID {:#x} {}; the card stays the game's.",mmid, + flag_plumbing::owns(mmid)?"is this game's own flag, whose mode has no definition": + remote?"is "+remote->key+"'s flag from another player, whose mode is not registered here":"is not a plugin mode's flag")); restore_join_branding(c);s.join_mmid.store(0);return; } + if(!s.in_menu && !s.join_cards.empty()) {restore_join_branding(c);s.join_mmid.store(0);return;} + std::vector seen; + bool branded{}; + try { for(const auto& root:c.roots({panel_schema.hash})) { const auto buttons=ui_models(c,c.field(root.model,0xbfc64535),button.hash); - const bool enroll=std::any_of(buttons.begin(),buttons.end(),[&](const auto b){ - const auto text=c.text(c.path(b,{0x58c9d354,text_field}));return text=="ID_ENROLL_LABEL" || text=="ENROLL"; - }); + bool enroll{}; + for(const auto b:buttons) { + const auto text=c.text(c.path(b,{0x58c9d354,text_field})); + if(text=="ID_ENROLL_LABEL" || text=="ENROLL")enroll=true; + else if(seen.size()<8)seen.push_back(text); + } if(!enroll)continue; auto& card=s.join_cards[root.model.handle]; if(std::any_of(card.presenters.begin(),card.presenters.end(),[&](const auto& p){return !branding_presenter_attached(c,p);})) @@ -170,7 +190,17 @@ void custom_join_details(const Context& c) { } mode_art(c,std::array{c.field(card.badge,0x6e469d2a)},definition->logo); c.set_if_changed(c.field(header,content),Widget{asset(c,"UI/Foundations/Components/Media/Icons/Texture_Widget"),{0,card.badge.handle}}); + branded=true; } + } catch(const std::exception& e) { + join_card_report(std::format("MMID {:#x} ({}): branding failed: {}",mmid,definition->name,e.what())); + throw; + } + if(branded) {join_card_report(std::format("MMID {:#x} shows {}.",mmid,definition->name));return;} + std::string buttons; + for(const auto& text:seen)buttons+=(buttons.empty()?"":", ")+(text.empty()?std::string("(no text)"):text); + join_card_report(std::format("MMID {:#x} ({}): no card with an Enroll button is up{}.",mmid,definition->name, + buttons.empty()?std::string{}:"; its buttons: "+buttons)); } void throwdown_map_identity(const Context& c) { auto& b=state().map_branding; diff --git a/Extension/UI/NativeMenu/throwdown_menu.cpp b/Extension/UI/NativeMenu/throwdown_menu.cpp index d0e5f46e..a67efa6d 100644 --- a/Extension/UI/NativeMenu/throwdown_menu.cpp +++ b/Extension/UI/NativeMenu/throwdown_menu.cpp @@ -202,6 +202,7 @@ struct State { MapBranding map_branding; std::atomic join_mmid{}; std::map join_cards; + std::string join_report; // join_card_report's last line MatchPanel match_panel; std::atomic match_panel_open{}; std::mutex panel_input_mutex; @@ -2232,6 +2233,13 @@ void observe_native_custom_join_details(std::uintptr_t vm) noexcept { try { const auto mmid=menu_data::native_join_mmid(vm,[](Address at,void* out,std::size_t count){return memory::peek_bytes(at,out,count);}); if(mmid)state().join_mmid.store(*mmid,std::memory_order_release); + // Said when it changes: a card whose MMID cannot be read is never branded. + static std::atomic last{~0ULL}; + const std::uint64_t value=mmid?*mmid:1ULL<<32; + if(last.exchange(value)!=value) { + if(mmid)logging::log(logging::Level::info,logging::Channel::ui,"Join card: the game filled the details of MMID {:#x}.",*mmid); + else logging::log(logging::Level::info,logging::Channel::ui,"Join card: the game filled a throwdown's details; its MMID could not be read."); + } } catch(...) {} } void observe_native_flag_interaction(std::uintptr_t vm) noexcept {