The battleground queue slots answer "none" from the first frame, so a party login no longer breaks BattlefieldFrame_Update (#603)

The reference's three queue slots are a static array (`0xb6e9d0`): `GetBattlefieldStatus(1..3)`
always reads one (`0x4ab4df`-`0x4ab4f3`), and a zeroed slot answers "none". The VM held an empty
list until the app's first push, so an in-range index took the off-slot leg and answered a nil
status. A party login fires PARTY_LEADER_CHANGED before that push, `BattlefieldFrame_Update` read
the nil status as queued and concatenated the nil map name (`BattlefieldFrame.lua:242`). The VM
now holds exactly three slots, zeroed until pushed, and the push takes three.

Verified: the new app test fires PARTY_LEADER_CHANGED on the stock BattlefieldFrame before any
push and failed with the reported error before the change; live, Probefive relogged into a stored
party raised the same error on the old build and none on this one (slot 1 reads "none", "Eastern
Kingdoms"), and a solo login after leaving the party is clean. Built on macOS only; no platform
seam.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sam Sung
2026-09-29 15:53:20 +07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent ca7111ed1a
commit e4b06c5c2f
4 changed files with 46 additions and 17 deletions
+2 -3
View File
@@ -211,9 +211,8 @@ fn feed_battlefield(
let slots = queue
.slots()
.iter()
.map(|s| slot_view(s.as_ref(), catalog, now))
.collect();
.each_ref()
.map(|s| slot_view(s.as_ref(), catalog, now));
script.set_battlefield_queue(slots, queue.instance_expiration_ms(now));
if std::mem::take(&mut state.show) {
@@ -77,7 +77,7 @@ fn the_queue_icon_follows_the_slots_across_update_battlefield_status() {
let mut s = session();
assert!(!visible(&s, "MiniMapBattlefieldFrame"), "no queue at load");
s.set_battlefield_queue(vec![slot(529, "Arathi Basin", 1, 0), empty(), empty()], 0);
s.set_battlefield_queue([slot(529, "Arathi Basin", 1, 0), empty(), empty()], 0);
s.fire_event("UPDATE_BATTLEFIELD_STATUS", vec![]);
assert!(
visible(&s, "MiniMapBattlefieldFrame"),
@@ -103,7 +103,7 @@ fn the_queue_icon_follows_the_slots_across_update_battlefield_status() {
assert!(!visible(&s, "StaticPopup1"), "queued is not yet a question");
// Status 2, ready to enter: the confirm dialog and the port countdown.
s.set_battlefield_queue(vec![slot(529, "Arathi Basin", 2, 3), empty(), empty()], 0);
s.set_battlefield_queue([slot(529, "Arathi Basin", 2, 3), empty(), empty()], 0);
s.fire_event("UPDATE_BATTLEFIELD_STATUS", vec![]);
assert!(
visible(&s, "StaticPopup1"),
@@ -127,7 +127,7 @@ fn the_queue_icon_follows_the_slots_across_update_battlefield_status() {
s.run("StaticPopup1Button1:Click()").unwrap();
assert_eq!(s.take_battlefield_port_requests(), vec![(1, true)]);
s.set_battlefield_queue(vec![empty(), empty(), empty()], 0);
s.set_battlefield_queue([empty(), empty(), empty()], 0);
s.fire_event("UPDATE_BATTLEFIELD_STATUS", vec![]);
assert!(
!visible(&s, "MiniMapBattlefieldFrame"),
@@ -154,7 +154,7 @@ fn the_list_window_opens_on_battlefields_show_and_joins_the_selection() {
}),
group_queue: true,
});
s.set_battlefield_queue(vec![empty(), empty(), empty()], 0);
s.set_battlefield_queue([empty(), empty(), empty()], 0);
assert!(!visible(&s, "BattlefieldFrame"));
s.fire_event("BATTLEFIELDS_SHOW", vec![]);
assert!(visible(&s, "BattlefieldFrame"), "the event shows the panel");
@@ -228,7 +228,7 @@ fn the_list_rows_carry_the_queue_status() {
}),
..Default::default()
});
s.set_battlefield_queue(vec![slot(529, "Arathi Basin", 1, 4), empty(), empty()], 0);
s.set_battlefield_queue([slot(529, "Arathi Basin", 1, 4), empty(), empty()], 0);
s.fire_event("BATTLEFIELDS_SHOW", vec![]);
assert_eq!(
s.eval::<String>("return BattlefieldZone2Status:GetText()")
@@ -243,3 +243,14 @@ fn the_list_rows_carry_the_queue_status() {
(`FontString:GetText 0x79d690` substitutes nil)"
);
}
#[test]
fn a_party_leader_change_before_any_queue_push_updates_the_list_cleanly() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = session();
s.take_errors();
// A party login fires PARTY_LEADER_CHANGED, which runs `BattlefieldFrame_Update`; it reads
// `GetBattlefieldStatus(1..3)` and concatenates the name of any status but "none" (line 242).
s.fire_event("PARTY_LEADER_CHANGED", vec![]);
assert_eq!(s.take_errors(), Vec::<String>::new());
}
@@ -13,7 +13,8 @@ use super::Model;
pub struct BattlefieldQueueSlot {
/// `+0x00`, the Map.dbc row id; 0 is a cleared slot, its name looked up like any other.
pub map_id: u32,
/// The localized map name, or `None` when the id has no row (a real `nil`).
/// The localized map name, or `None` when the id has no row (a real `nil`). The VM holds no
/// Map.dbc, so a slot not yet pushed answers `nil` where the reference names row 0.
pub map_name: Option<String>,
/// `+0x04`: `0` none, `1` queued, `2` confirm, `3` active; anything else answers `"error"`.
pub status: u32,
@@ -102,7 +103,7 @@ impl super::UiScript {
/// clock getters move.
pub fn set_battlefield_queue(
&mut self,
slots: Vec<BattlefieldQueueSlot>,
slots: [BattlefieldQueueSlot; 3],
instance_expiration_ms: u32,
) {
let mut model = self.model_mut();
@@ -498,7 +499,7 @@ mod tests {
#[test]
fn get_battlefield_status_answers_five_values_on_every_leg() {
let mut s = UiScript::new().unwrap();
s.set_battlefield_queue(vec![slot(489, 1, 5), slot(0, 0, 0), slot(529, 9, 2)], 0);
s.set_battlefield_queue([slot(489, 1, 5), slot(0, 0, 0), slot(529, 9, 2)], 0);
let n = |s: &mut UiScript, i: &str| s.arity(&format!("GetBattlefieldStatus({i})")).unwrap();
assert_eq!(n(&mut s, "1"), 5);
assert_eq!(n(&mut s, "0"), 5);
@@ -532,7 +533,7 @@ mod tests {
assert!(err.contains("Usage: GetBattlefieldStatus(index)"), "{err}");
let mut empty = slot(0, 0, 0);
empty.map_name = None;
s.set_battlefield_queue(vec![empty], 0);
s.set_battlefield_queue([empty, Default::default(), Default::default()], 0);
assert!(
s.eval::<bool>("local _, name = GetBattlefieldStatus(1) return name == nil")
.unwrap(),
@@ -540,10 +541,26 @@ mod tests {
);
}
#[test]
fn every_slot_answers_none_before_the_first_push() {
let s = UiScript::new().unwrap();
for i in 1..=3 {
assert_eq!(
s.eval::<String>(&format!("return (GetBattlefieldStatus({i}))"))
.unwrap(),
"none",
"slot {i}"
);
}
}
#[test]
fn the_time_getters_read_the_pushed_milliseconds() {
let mut s = UiScript::new().unwrap();
s.set_battlefield_queue(vec![slot(489, 1, 5)], 120_000);
s.set_battlefield_queue(
[slot(489, 1, 5), Default::default(), Default::default()],
120_000,
);
assert_eq!(
s.eval::<i64>("return GetBattlefieldPortExpiration(1)")
.unwrap(),
@@ -594,7 +611,7 @@ mod tests {
#[test]
fn show_battlefield_list_gates_on_a_queued_slot() {
let mut s = UiScript::new().unwrap();
s.set_battlefield_queue(vec![slot(489, 1, 5), slot(529, 2, 1), slot(0, 1, 0)], 0);
s.set_battlefield_queue([slot(489, 1, 5), slot(529, 2, 1), slot(0, 1, 0)], 0);
s.run("ShowBattlefieldList(1) ShowBattlefieldList(2) ShowBattlefieldList(3) ShowBattlefieldList(4)")
.unwrap();
assert_eq!(s.take_battlefield_list_requests(), vec![489]);
+5 -3
View File
@@ -490,8 +490,10 @@ pub(crate) struct Model {
pub(crate) battlefield_list: super::battlefield_queue::BattlefieldListView,
/// The selected instance id (`[0xb6eba0]`), not an index; written by `SetSelectedBattlefield`.
pub(crate) battlefield_selected: u32,
/// The three queue slots, pushed each frame with their clocks reduced to values.
pub(crate) battlefield_slots: Vec<super::battlefield_queue::BattlefieldQueueSlot>,
/// The three queue slots, pushed each frame with their clocks reduced to values. Zeroed
/// until the first push, as the reference's static slots (`0xb6e9d0`) are, so each answers
/// "none" from the first frame.
pub(crate) battlefield_slots: [super::battlefield_queue::BattlefieldQueueSlot; 3],
/// `GetBattlefieldInstanceExpiration()`, pushed each frame.
pub(crate) battlefield_instance_expiration_ms: u32,
/// `JoinBattlefield` calls, `(instance id, as group)`.
@@ -1191,7 +1193,7 @@ impl Model {
battlefield_leave_requests: 0,
battlefield_list: Default::default(),
battlefield_selected: 0,
battlefield_slots: Vec::new(),
battlefield_slots: Default::default(),
battlefield_instance_expiration_ms: 0,
battlefield_join_requests: Vec::new(),
battlefield_list_requests: Vec::new(),