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.
This commit is contained in:
AyeZeeBB committed 2026-10-21 20:42:49 -05:00
1 parent a598ce602b
commit 77542e4db9
4 files changed
+73 -8

No files matched your search

+25 -3
View File
@@ -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<std::uint32_t>(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<std::int32_t>{-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<unsigned>(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());
}
+6 -1
View File
@@ -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.
@@ -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<std::string> 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;
@@ -202,6 +202,7 @@ struct State {
MapBranding map_branding;
std::atomic<std::uint32_t> join_mmid{};
std::map<Handle,JoinCard> join_cards;
std::string join_report; // join_card_report's last line
MatchPanel match_panel;
std::atomic<bool> 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<std::uint64_t> 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 {