The ready check's start line names a leader the name cache holds (#2814)

A ready check's start line took the leader's name from the party roster, so it printed even when the client did not know the name, with an empty name if the roster lacked the leader.

**Reference:** the handler asks the name cache with no callback (`0x4ba50f` → `0x55f080`). A miss returns at once without a query (`0x55f120`-`0x55f125`, `0x55f221`), and the line is skipped (`0x4ba514`-`0x4ba516`). The deadline and READY_CHECK follow either way (`0x4ba535`, `0x4ba53a`).

**Changed:** the line reads the leader through the name cache, as the leave line does (#2000), and a miss prints nothing and sends nothing.

**Tests:** a cached leader prints the line before the popup, and an uncached one prints none and sends no query, while READY_CHECK still fires.

Fixes #2792.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Sam Sung
2026-10-29 20:39:42 +07:00
committed by GitHub
parent e8e9969f91
commit cee8a999fc
4 changed files with 125 additions and 20 deletions
@@ -288,12 +288,30 @@ fn a_guild_event_shows_its_line_ahead_of_its_roster_update() {
);
}
/// A ready check another leads shows the leader's line (`0x4ba51e`), then `READY_CHECK`
/// (`0x4ba53a`).
/// A ready check another leads shows the leader's line (`0x4ba51e`) when the name cache holds the
/// leader (`0x4ba50f`, no callback), then `READY_CHECK` (`0x4ba53a`); with the name still to
/// come the line is skipped and `READY_CHECK` fires all the same (`0x4ba514`-`0x4ba53a`).
#[test]
fn a_ready_check_shows_its_line_ahead_of_its_popup() {
let mut c = client();
grouped(&mut c, MATE);
assert_eq!(
between(&mut c, SessionEvent::ReadyCheckRequest),
[
"CHAT_MSG_MONSTER_SAY:a",
"READY_CHECK",
"CHAT_MSG_MONSTER_SAY:b",
],
"the leader's name is not cached"
);
c.frame(vec![vec![SessionEvent::PlayerName {
guid: MATE,
name: "Dorn".into(),
race: 1,
gender: 0,
class: 1,
}]]);
c.seen();
assert_eq!(
between(&mut c, SessionEvent::ReadyCheckRequest),
[
+2 -2
View File
@@ -994,7 +994,7 @@ fn apply_test_intent(group: &mut GroupState, req: &PartyRequest, self_guid: Opti
PartyRequest::ReadyCheckStart => {
// The server's echo to the whole raid, us included. Only the leader has the button,
// and the leader's echo takes the collection arm: no popup for the presser.
group.apply_ready_check_request(true);
group.apply_ready_check_request(true, &|_| None);
true
}
PartyRequest::RequestRaidInfo => {
@@ -2875,7 +2875,7 @@ mod tests {
let _ = app
.world_mut()
.resource_mut::<GroupState>()
.apply_ready_check_request(false);
.apply_ready_check_request(false, &|_| None);
frame(&mut app);
assert_eq!(
app.world_mut()
+61 -15
View File
@@ -1048,22 +1048,23 @@ impl GroupState {
/// `MSG_RAID_READY_CHECK`, open form, echoed to the whole group: `0x4ba360` splits on the
/// leader guid, and only a non-leader prints the leader's line (`0x4ba51e`), then gets the
/// popup (`0x4ba53a`). Both arms arm the engine's side first.
pub fn apply_ready_check_request(&mut self, we_lead: bool) -> Vec<UiError> {
/// popup (`0x4ba53a`). Both arms arm the engine's side first. The line's name is the name
/// cache's record for the leader, read with no callback (`cached_name`, `0x4ba50f` →
/// `0x55f080`): a miss prints nothing and queries nothing (`0x4ba514`-`0x4ba516`, `0x55f120`),
/// and the deadline and `READY_CHECK` follow either way (`0x4ba535`, `0x4ba53a`).
pub fn apply_ready_check_request(
&mut self,
we_lead: bool,
cached_name: &dyn Fn(u64) -> Option<String>,
) -> Vec<UiError> {
self.signal(GroupSignal::ReadyCheckRequest { we_lead });
if we_lead {
return Vec::new();
}
// The reference prints its name cache's entry for the leader; the roster holds the same.
let leader = self
.members
.iter()
.find(|m| m.guid == self.leader)
.map(|m| m.name.clone())
.unwrap_or_default();
let line = self.show(UiError::s("ERR_RAID_LEADER_READY_CHECK_START_S", leader));
let line = cached_name(self.leader)
.map(|name| self.show(UiError::s("ERR_RAID_LEADER_READY_CHECK_START_S", name)));
self.signal(GroupSignal::ReadyCheck);
vec![line]
line.into_iter().collect()
}
/// `MSG_RAID_READY_CHECK`, answer form: one member's answer, forwarded to the leader alone.
@@ -1596,7 +1597,7 @@ mod tests {
..Default::default()
};
g.raid_targets[0] = 0xA11CE;
g.apply_ready_check_request(false);
g.apply_ready_check_request(false, &|_| None);
g.apply_raid_instance_info(Vec::new());
g.apply_raid_instance_info(vec![lockout]);
assert_eq!(
@@ -1630,6 +1631,50 @@ mod tests {
assert!(g.saved_instances.is_empty());
}
/// The leader's line is the name cache's record for the leader, read with no callback
/// (`0x4ba50f` → `0x55f080`), not the roster's row: a cached leader prints it ahead of
/// `READY_CHECK`, spelt as the cache has it.
#[test]
fn a_cached_leader_prints_the_ready_check_line_before_the_popup() {
let mut g = GroupState {
leader: 0xA11CE,
members: vec![member("Roster", 0xA11CE)],
..Default::default()
};
let line = UiError::s("ERR_RAID_LEADER_READY_CHECK_START_S", "Cached");
assert_eq!(
g.apply_ready_check_request(false, &|guid| (guid == 0xA11CE).then(|| "Cached".into())),
vec![line.clone()]
);
assert_eq!(
g.signals.take(),
[
GroupSignal::ReadyCheckRequest { we_lead: false },
GroupSignal::Line(line),
GroupSignal::ReadyCheck
]
);
}
/// A leader the cache lacks prints nothing, even with the roster holding the name, and
/// the deadline and `READY_CHECK` follow all the same (`0x4ba514`-`0x4ba53a`).
#[test]
fn an_uncached_leader_prints_no_ready_check_line_but_the_popup_fires() {
let mut g = GroupState {
leader: 0xA11CE,
members: vec![member("Alice", 0xA11CE)],
..Default::default()
};
assert!(g.apply_ready_check_request(false, &|_| None).is_empty());
assert_eq!(
g.signals.take_events(),
[
GroupSignal::ReadyCheckRequest { we_lead: false },
GroupSignal::ReadyCheck
]
);
}
#[test]
fn the_open_form_pops_for_a_member_and_stays_quiet_for_the_leader() {
let mut g = GroupState {
@@ -1640,7 +1685,7 @@ mod tests {
g.apply_ready_check_answer(0xB0B, true);
assert_eq!(
g.apply_ready_check_request(false),
g.apply_ready_check_request(false, &|_| Some("Alice".into())),
vec![UiError::s("ERR_RAID_LEADER_READY_CHECK_START_S", "Alice")]
);
assert_eq!(
@@ -1657,7 +1702,8 @@ mod tests {
);
assert!(
g.apply_ready_check_request(true).is_empty(),
g.apply_ready_check_request(true, &|_| Some("Alice".into()))
.is_empty(),
"the leader's echo"
);
assert_eq!(
@@ -2463,7 +2509,7 @@ mod tests {
lines.extend(g.apply_destroyed());
lines.extend(g.apply_uninvited());
g.leader = 1;
lines.extend(g.apply_ready_check_request(false));
lines.extend(g.apply_ready_check_request(false, &|_| Some("Alice".into())));
lines.extend(g.apply_list(0, 0, Vec::new(), 0, None, None).lines);
let mut keys: Vec<&str> = lines.iter().map(|m| m.key).collect();
+42 -1
View File
@@ -250,11 +250,15 @@ fn on_ready_check_request(
In(ev): In<SessionEvent>,
mut group: ResMut<GroupState>,
self_guid: Res<SelfGuid>,
names: Res<NameCache>,
) {
if let SessionEvent::ReadyCheckRequest = ev {
// We lead when our guid is the leader's, the reference's test at `0x4ba3a0`.
let we_lead = self_guid.0 == Some(group.leader);
group.apply_ready_check_request(we_lead);
// The leader's line reads the cache with no callback (`0x4ba50f`).
group.apply_ready_check_request(we_lead, &|guid| {
names.resolve_player(guid, Ask::Read).map(str::to_string)
});
}
}
@@ -1304,6 +1308,43 @@ mod tests {
assert_eq!(shown(&mut app), [], "and its answer shows nothing");
}
/// The open ready check reads its leader's name from the cache with no callback (`0x4ba50f`):
/// a roster that holds the name is not the cache, so the line waits on the cache, queries
/// nothing, and `READY_CHECK` fires either way.
#[test]
fn a_ready_check_line_follows_the_name_cache_not_the_roster() {
use bevy::ecs::system::RunSystemOnce;
const ME: u64 = 0x10;
let (mut app, rx) = handler_app(ME);
let a = 0x21;
list_event(&mut app, 0, vec![member(a, "Aldwyn")], a);
rx.try_iter().for_each(drop);
shown(&mut app);
let check = |app: &mut App| {
app.world_mut()
.run_system_once_with(on_ready_check_request, SessionEvent::ReadyCheckRequest)
.expect("the ready-check handler");
app.world_mut().resource_mut::<GroupState>().signals.take()
};
assert!(
!check(&mut app)
.iter()
.any(|s| matches!(s, GroupSignal::Line(_))),
"the leader is on the roster and not in the cache"
);
assert_eq!(rx.try_iter().count(), 0, "and the miss asks nothing");
app.world_mut()
.resource_mut::<NameCache>()
.insert_player(a, "Aldwyn".into(), None);
let signals = check(&mut app);
let at = |f: &dyn Fn(&GroupSignal) -> bool| signals.iter().position(f);
let line = at(
&|s| matches!(s, GroupSignal::Line(l) if l.key == "ERR_RAID_LEADER_READY_CHECK_START_S"),
);
let popup = at(&|s| matches!(s, GroupSignal::ReadyCheck));
assert!(line.is_some() && line < popup, "the line, then READY_CHECK");
}
/// The raid roster writer ends with `MSG_RAID_TARGET_UPDATE` 0xff, the request for every
/// icon, when the roster held no rows on entry (`0x4ba61c` keeps that, `0x4babf4` tests it,
/// `0x4bac01`-`0x4bac33` send), after the rebuild's own sends, whatever the new list holds; a