diff --git a/crates/benilla-app/src/lanes/ui_error_live_tests.rs b/crates/benilla-app/src/lanes/ui_error_live_tests.rs index dac6efcc6..e5e63c4b5 100644 --- a/crates/benilla-app/src/lanes/ui_error_live_tests.rs +++ b/crates/benilla-app/src/lanes/ui_error_live_tests.rs @@ -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), [ diff --git a/crates/benilla-app/src/ui_party/feed.rs b/crates/benilla-app/src/ui_party/feed.rs index 572e7cbad..8cd5fdc74 100644 --- a/crates/benilla-app/src/ui_party/feed.rs +++ b/crates/benilla-app/src/ui_party/feed.rs @@ -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::() - .apply_ready_check_request(false); + .apply_ready_check_request(false, &|_| None); frame(&mut app); assert_eq!( app.world_mut() diff --git a/crates/benilla-app/src/ui_party/mod.rs b/crates/benilla-app/src/ui_party/mod.rs index 7059715c0..219bc5be7 100644 --- a/crates/benilla-app/src/ui_party/mod.rs +++ b/crates/benilla-app/src/ui_party/mod.rs @@ -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 { + /// 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, + ) -> Vec { 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(); diff --git a/crates/benilla-app/src/ui_party/net.rs b/crates/benilla-app/src/ui_party/net.rs index 46846eb06..49b78077c 100644 --- a/crates/benilla-app/src/ui_party/net.rs +++ b/crates/benilla-app/src/ui_party/net.rs @@ -250,11 +250,15 @@ fn on_ready_check_request( In(ev): In, mut group: ResMut, self_guid: Res, + names: Res, ) { 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::().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::() + .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