probe bg: a leave leg, so WOW_PROBE_BG can measure the worldport out
The probe's last act was a GM teleport, which never exercises the worldport a leave causes — so it could say nothing about the scoreboard or the movement latches after one. `WOW_PROBE_BG_LEAVE=1` swaps the flag and graveyard legs for the real thing: wait inside for the match to end, leave with the stock `LeaveBattlefield()`, and census for 90 s across the worldport. A solo match never reaches `min_players_per_team`, so vmangos ends it with `WINNER_NONE` about 300 s after the doors, and a non-nil winner is what the modal needs to show itself (`BattleGround.cpp:318` is the AV exception). A census gained `SCOREFRAME IsShown=` with the three latches that can freeze a body — `settling`, `world_stale`, `control_lost` — beside the worldport ack, and a `moved=` column on the position so "stuck" is one number rather than an eyeball. `PLAYER_ENTERING_WORLD` joins the tap, to see whether the world-entry event fires at all. The tap's window is drained once per frame before the phase match, for the three in-battleground phases, so the one-shot consume stays in `bg_probe` where `game_plugins.rs` argues for it.
This commit is contained in:
@@ -11,10 +11,17 @@
|
||||
//! off once inside: while testing is on, the premature-finish countdown never decrements
|
||||
//! (`BattleGround.cpp:337`).
|
||||
//!
|
||||
//! A census reports the map, area and position, terrain streaming, entity counts by kind, the
|
||||
//! gameobject entries, the raw world-state table, `GetNumWorldStateUI()`,
|
||||
//! `GetBattlefieldStatus(1..3)`, the scoreboard, the nearest spirit guide, the dropped opcodes and
|
||||
//! the Lua event tap's window.
|
||||
//! A census reports the map, area and position (and how far the body moved since the last one),
|
||||
//! terrain streaming, entity counts by kind, the gameobject entries, the raw world-state table,
|
||||
//! `GetNumWorldStateUI()`, `GetBattlefieldStatus(1..3)`, the scoreboard, the end-of-battleground
|
||||
//! modal's visibility, the three player latches that can freeze a body, the nearest spirit guide,
|
||||
//! the dropped opcodes and the Lua event tap's window.
|
||||
//!
|
||||
//! `WOW_PROBE_BG_LEAVE=1` runs one leg instead of the flag and graveyard: it waits inside for the
|
||||
//! match to end, leaves with the stock `LeaveBattlefield()`, and censuses for [`LEAVE_WATCH_SECS`]
|
||||
//! across the worldport. That is the only shape that reaches the leave's worldport, which a GM
|
||||
//! teleport does not. Hold forward across it with `WOW_PROBE_KEY="W@<s>:<hold>"`, and read the
|
||||
//! stuck symptom off the `moved=` column.
|
||||
//!
|
||||
//! The probe stamps input every frame, so it never exercises the idle handler.
|
||||
//!
|
||||
@@ -146,6 +153,24 @@ const FLAG_SAMPLES: u32 = 5;
|
||||
/// `0x4924c0` keys on.
|
||||
const NPC_FLAG_SPIRITGUIDE: u32 = 1 << 6;
|
||||
|
||||
/// [`WOW_PROBE_BG_LEAVE`] swaps the flag and graveyard legs for the leave leg: wait for the match to
|
||||
/// end, leave it with `LeaveBattlefield()`, and keep censusing across the worldport. Without it the
|
||||
/// run's last act is a GM teleport, which never exercises the worldport the leave causes.
|
||||
fn leave_leg() -> bool {
|
||||
std::env::var("WOW_PROBE_BG_LEAVE")
|
||||
.ok()
|
||||
.is_some_and(|v| !v.is_empty() && v != "0")
|
||||
}
|
||||
|
||||
/// How long the leave leg watches: the worldport, the modal's return and the settle all land inside
|
||||
/// it, and the worldport ack is paid on the settle release.
|
||||
const LEAVE_WATCH_SECS: f64 = 90.0;
|
||||
/// The leave leg's sample period, short enough to place a modal's return between two samples.
|
||||
const LEAVE_WATCH_EVERY: f64 = 4.0;
|
||||
/// How long the leave leg waits for a match to end before calling it a failure: the doors plus
|
||||
/// vmangos's 300 s, with room for the queue.
|
||||
const LEAVE_END_TIMEOUT: f64 = 600.0;
|
||||
|
||||
/// A Lua frame that records every battleground event the stock interface listens for, with its
|
||||
/// `arg1`: a chat kind that reaches the VM logs nothing, so only a tap shows the countdown's
|
||||
/// `CHAT_MSG_BG_SYSTEM_NEUTRAL` lines (`BattleGround.cpp:383-418`). Written in 1.12 dialect
|
||||
@@ -169,6 +194,7 @@ BenillaBgTap:RegisterEvent("AREA_SPIRIT_HEALER_OUT_OF_RANGE");
|
||||
BenillaBgTap:RegisterEvent("PLAYER_DEAD");
|
||||
BenillaBgTap:RegisterEvent("PLAYER_UNGHOST");
|
||||
BenillaBgTap:RegisterEvent("PLAYER_ALIVE");
|
||||
BenillaBgTap:RegisterEvent("PLAYER_ENTERING_WORLD");
|
||||
BenillaBgTap:RegisterEvent("ZONE_CHANGED_NEW_AREA");
|
||||
"#;
|
||||
|
||||
@@ -221,6 +247,9 @@ struct BgProbe {
|
||||
waiting_since: Option<f64>,
|
||||
/// Events drained before the first census, reported with it rather than dropped.
|
||||
pending_events: String,
|
||||
/// The last position a census printed, so each line can say how far the body has moved since.
|
||||
/// A frozen body under a held forward key is the stuck symptom, as one number.
|
||||
last_pos: Option<Vec3>,
|
||||
}
|
||||
|
||||
/// `Wait` → `Hopped` → `Toggled` → `Scanning` → `Greeted` → `Joined` (↔ `Rejoining`) → `Ported` →
|
||||
@@ -258,6 +287,21 @@ enum Phase {
|
||||
next_census: f64,
|
||||
samples: u32,
|
||||
},
|
||||
/// [`WOW_PROBE_BG_LEAVE`]: inside, waiting for the match to end on its own. A solo match never
|
||||
/// reaches `min_players_per_team`, so vmangos ends it with `WINNER_NONE` about 300 s after the
|
||||
/// doors (`BattleGround.cpp:318` is the AV exception), and a non-nil winner is what the
|
||||
/// end-of-battleground modal needs to show itself.
|
||||
WaitingEnd {
|
||||
entered_at: f64,
|
||||
next_census: f64,
|
||||
},
|
||||
/// The leave leg proper: `LeaveBattlefield()` has gone out, and the body holds forward for the
|
||||
/// census window so a frozen body shows as a zero `moved` against a live one.
|
||||
AfterLeave {
|
||||
at: f64,
|
||||
next_census: f64,
|
||||
samples: u32,
|
||||
},
|
||||
FlagHop {
|
||||
at: f64,
|
||||
},
|
||||
@@ -347,7 +391,45 @@ fn ws(states: &WorldStates, key: u32) -> String {
|
||||
.map_or_else(|| "absent".to_string(), |(_, v)| v.to_string())
|
||||
}
|
||||
|
||||
/// The in-battleground census preamble every arm shares. The tap's window is drained by
|
||||
/// `bg_probe` before the phase match and handed in here, so the self-heal can still reach `probe`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn census_inside(
|
||||
probe: &mut BgProbe,
|
||||
t: f64,
|
||||
arena: &Arena,
|
||||
player: &Player,
|
||||
units: &Query<(&Guid, &NetEntity, &ObjectStore, &Transform), Without<SelfPlayer>>,
|
||||
map: Option<&CurrentMap>,
|
||||
area: Option<&CurrentArea>,
|
||||
load: Option<&WorldLoadProgress>,
|
||||
states: &WorldStates,
|
||||
dropped: &DroppedOpcodes,
|
||||
go_templates: &crate::go_templates::GameObjectTemplates,
|
||||
queue: &BattlefieldQueue,
|
||||
script: &mut UiScript,
|
||||
events: &str,
|
||||
tap_broken: bool,
|
||||
) {
|
||||
census(
|
||||
t,
|
||||
arena,
|
||||
player,
|
||||
units,
|
||||
map,
|
||||
area,
|
||||
load,
|
||||
states,
|
||||
dropped,
|
||||
go_templates,
|
||||
queue,
|
||||
&mut probe.last_pos,
|
||||
script,
|
||||
events,
|
||||
tap_broken,
|
||||
);
|
||||
}
|
||||
|
||||
fn bg_probe(
|
||||
time: ProbeClock,
|
||||
mut probe: ResMut<BgProbe>,
|
||||
@@ -394,6 +476,22 @@ fn bg_probe(
|
||||
}
|
||||
}
|
||||
|
||||
// Drain the tap's window once a frame, for the phases that are inside the battleground: its
|
||||
// self-heal needs the VM, and the census prints what came in. The three arms share one drain so
|
||||
// the consume stays here, in the system that argues for it in `game_plugins.rs`.
|
||||
let inside_bg = matches!(
|
||||
probe.phase,
|
||||
Phase::Inside { .. } | Phase::WaitingEnd { .. } | Phase::AfterLeave { .. }
|
||||
);
|
||||
let mut events = String::new();
|
||||
let tap_broken = probe.tap_broken;
|
||||
if inside_bg {
|
||||
events.push_str(&std::mem::take(&mut probe.pending_events));
|
||||
if let Some(script) = script.as_deref_mut() {
|
||||
events.push_str(&drain_events(&mut probe, script));
|
||||
}
|
||||
}
|
||||
|
||||
match probe.phase {
|
||||
Phase::Wait => {
|
||||
// No start in GM mode: `CanUseBattleGroundObject` refuses a GM every objective with no
|
||||
@@ -622,17 +720,30 @@ fn bg_probe(
|
||||
Phase::Ported { sent_at } => {
|
||||
let here = map.as_ref().map_or(0, |m| m.0);
|
||||
if here == arena.map {
|
||||
info!(
|
||||
"PROBE bg: ENTERED map {} after {:.1}s — census every {CENSUS_EVERY:.0}s × {}",
|
||||
arena.map,
|
||||
now - sent_at,
|
||||
census_samples(),
|
||||
);
|
||||
probe.phase = Phase::Inside {
|
||||
entered_at: now,
|
||||
next_census: now + CENSUS_EVERY,
|
||||
samples: 0,
|
||||
};
|
||||
if leave_leg() {
|
||||
info!(
|
||||
"PROBE bg: ENTERED map {} after {:.1}s — WOW_PROBE_BG_LEAVE: waiting for the \
|
||||
match to end on its own, then leaving it and watching for {LEAVE_WATCH_SECS:.0}s",
|
||||
arena.map,
|
||||
now - sent_at,
|
||||
);
|
||||
probe.phase = Phase::WaitingEnd {
|
||||
entered_at: now,
|
||||
next_census: now + CENSUS_EVERY,
|
||||
};
|
||||
} else {
|
||||
info!(
|
||||
"PROBE bg: ENTERED map {} after {:.1}s — census every {CENSUS_EVERY:.0}s × {}",
|
||||
arena.map,
|
||||
now - sent_at,
|
||||
census_samples(),
|
||||
);
|
||||
probe.phase = Phase::Inside {
|
||||
entered_at: now,
|
||||
next_census: now + CENSUS_EVERY,
|
||||
samples: 0,
|
||||
};
|
||||
}
|
||||
} else if now - sent_at > 30.0 {
|
||||
error!(
|
||||
"PROBE bg: FAILURE — 30 s after the port the map is still {here}, not {}",
|
||||
@@ -654,11 +765,8 @@ fn bg_probe(
|
||||
finish(&mut probe, &net);
|
||||
return;
|
||||
};
|
||||
// Drained here, not in `census`, so the tap's self-heal can reach `probe`.
|
||||
let mut events = std::mem::take(&mut probe.pending_events);
|
||||
events.push_str(&drain_events(&mut probe, &mut script));
|
||||
let tap_broken = probe.tap_broken;
|
||||
census(
|
||||
census_inside(
|
||||
&mut probe,
|
||||
now - entered_at,
|
||||
arena,
|
||||
&player,
|
||||
@@ -709,6 +817,115 @@ fn bg_probe(
|
||||
};
|
||||
}
|
||||
}
|
||||
Phase::WaitingEnd {
|
||||
entered_at,
|
||||
next_census,
|
||||
} => {
|
||||
if now < next_census {
|
||||
return;
|
||||
}
|
||||
let Some(mut script) = script else {
|
||||
error!("PROBE bg: FAILURE — the VM went away waiting for the match to end");
|
||||
finish(&mut probe, &net);
|
||||
return;
|
||||
};
|
||||
census_inside(
|
||||
&mut probe,
|
||||
now - entered_at,
|
||||
arena,
|
||||
&player,
|
||||
&units,
|
||||
map.as_deref(),
|
||||
area.as_deref(),
|
||||
load.as_deref(),
|
||||
&states,
|
||||
&dropped,
|
||||
&go_templates,
|
||||
&queue,
|
||||
&mut script,
|
||||
&events,
|
||||
tap_broken,
|
||||
);
|
||||
// The end-of-battleground modal shows itself only while `GetBattlefieldWinner()` is
|
||||
// non-nil (`WorldStateFrame.lua:351-355`), so a winner is the precondition for the
|
||||
// leave this leg is here to measure. `LeaveBattlefield` sends nothing without the
|
||||
// scoreboard's "ended" byte (`0x4abe66`), which is the same arrival.
|
||||
let winner = script
|
||||
.eval::<Option<i64>>("return GetBattlefieldWinner()")
|
||||
.unwrap_or(None);
|
||||
match winner {
|
||||
Some(w) => {
|
||||
info!(
|
||||
"PROBE bg: ENDED — winner={w} at t={:.0}s; leaving with LeaveBattlefield(), \
|
||||
watching for {LEAVE_WATCH_SECS:.0}s (hold forward with WOW_PROBE_KEY=\"W@…:…\")",
|
||||
now - entered_at
|
||||
);
|
||||
let _ = script.eval::<()>("LeaveBattlefield()");
|
||||
probe.phase = Phase::AfterLeave {
|
||||
at: now,
|
||||
next_census: now + LEAVE_WATCH_EVERY,
|
||||
samples: 0,
|
||||
};
|
||||
}
|
||||
None => {
|
||||
if now - entered_at > LEAVE_END_TIMEOUT {
|
||||
error!(
|
||||
"PROBE bg: FAILURE — no winner {LEAVE_END_TIMEOUT:.0}s after entering \
|
||||
map {}; vmangos ends a solo match about 300 s after the doors, so the \
|
||||
match never formed (check the `.debug bg` lines above)",
|
||||
arena.map
|
||||
);
|
||||
finish(&mut probe, &net);
|
||||
return;
|
||||
}
|
||||
probe.phase = Phase::WaitingEnd {
|
||||
entered_at,
|
||||
next_census: now + CENSUS_EVERY,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Phase::AfterLeave {
|
||||
at,
|
||||
next_census,
|
||||
samples,
|
||||
} => {
|
||||
if now - at >= LEAVE_WATCH_SECS {
|
||||
info!("PROBE bg: WATCH done — {LEAVE_WATCH_SECS:.0}s after the leave");
|
||||
finish(&mut probe, &net);
|
||||
return;
|
||||
}
|
||||
if now < next_census {
|
||||
return;
|
||||
}
|
||||
let Some(mut script) = script else {
|
||||
error!("PROBE bg: FAILURE — the VM went away after the leave");
|
||||
finish(&mut probe, &net);
|
||||
return;
|
||||
};
|
||||
census_inside(
|
||||
&mut probe,
|
||||
now - at,
|
||||
arena,
|
||||
&player,
|
||||
&units,
|
||||
map.as_deref(),
|
||||
area.as_deref(),
|
||||
load.as_deref(),
|
||||
&states,
|
||||
&dropped,
|
||||
&go_templates,
|
||||
&queue,
|
||||
&mut script,
|
||||
&events,
|
||||
tap_broken,
|
||||
);
|
||||
probe.phase = Phase::AfterLeave {
|
||||
at,
|
||||
next_census: now + LEAVE_WATCH_EVERY,
|
||||
samples: samples + 1,
|
||||
};
|
||||
}
|
||||
Phase::Rejoining { at } => {
|
||||
if now - at < 2.0 {
|
||||
return; // the decline lands and the slot clears
|
||||
@@ -942,6 +1159,7 @@ fn census(
|
||||
dropped: &DroppedOpcodes,
|
||||
go_templates: &crate::go_templates::GameObjectTemplates,
|
||||
queue: &BattlefieldQueue,
|
||||
last_pos: &mut Option<Vec3>,
|
||||
script: &mut UiScript,
|
||||
events: &str,
|
||||
tap_broken: bool,
|
||||
@@ -949,6 +1167,11 @@ fn census(
|
||||
use benilla_protocol::EntityKind as K;
|
||||
|
||||
let p = player.pos;
|
||||
let moved = last_pos.map_or_else(
|
||||
|| "first".to_string(),
|
||||
|last| format!("{:.1} yd", (p - last).length()),
|
||||
);
|
||||
*last_pos = Some(p);
|
||||
let terrain = load.map_or_else(
|
||||
|| "no-progress".to_string(),
|
||||
|l| {
|
||||
@@ -960,7 +1183,8 @@ fn census(
|
||||
);
|
||||
info!(
|
||||
// `map=none`, not `map=0`: 0 is Eastern Kingdoms.
|
||||
"PROBE bg: CENSUS t={t:.0}s map={} area={:?} pos=({:.0},{:.0},{:.0}) terrain={terrain}",
|
||||
"PROBE bg: CENSUS t={t:.0}s map={} area={:?} pos=({:.0},{:.0},{:.0}) moved={moved} \
|
||||
terrain={terrain}",
|
||||
map.map_or_else(|| "none".to_string(), |m| m.0.to_string()),
|
||||
area.and_then(|a| a.0),
|
||||
p.x,
|
||||
@@ -1086,6 +1310,28 @@ fn census(
|
||||
.unwrap_or_else(|e| format!("<raised: {e}>"));
|
||||
info!("PROBE bg: SCORE n={score}");
|
||||
|
||||
// The end-of-battleground modal, and the gates that can freeze the body. `WorldStateScoreFrame`
|
||||
// shows itself on any score or world-state event while `GetBattlefieldWinner()` is non-nil
|
||||
// (`WorldStateFrame.lua:221-224`, `:351-355`), so its visibility across a leave is the whole
|
||||
// question; `settling`/`world_stale`/`control_lost` are the three latches that can keep the
|
||||
// body off the wire after the worldport.
|
||||
let shown = script
|
||||
.eval::<String>(
|
||||
"local f = getglobal(\"WorldStateScoreFrame\") \
|
||||
if not f then return \"not-loaded\" end \
|
||||
return tostring(f:IsShown())",
|
||||
)
|
||||
.unwrap_or_else(|e| format!("<raised: {e}>"));
|
||||
info!(
|
||||
"PROBE bg: SCOREFRAME IsShown={shown} gates settling={} world_stale={} control_lost={} \
|
||||
owes_ack={} map_id={:?}",
|
||||
player.settling,
|
||||
player.world_stale,
|
||||
player.control_lost,
|
||||
player.owes_worldport_ack,
|
||||
map.map(|m| m.0),
|
||||
);
|
||||
|
||||
// Every opcode the codec dropped, by name; `DroppedOpcodes` is never cleared, so the counts
|
||||
// are cumulative over the session.
|
||||
let mut drops: Vec<(u16, u64, u64)> = dropped
|
||||
|
||||
@@ -187,6 +187,11 @@ pub(crate) const PROBE_VARS: &[ProbeVar] = &[
|
||||
purpose: "wsg|ab|av — queue for that battleground, take the port through the stock verb and census the instance from inside",
|
||||
wall_clock: true,
|
||||
},
|
||||
ProbeVar {
|
||||
name: "WOW_PROBE_BG_LEAVE",
|
||||
purpose: "1 — with WOW_PROBE_BG, wait inside for the match to end, leave it through the stock LeaveBattlefield() and census for 90 s across the worldport",
|
||||
wall_clock: true,
|
||||
},
|
||||
ProbeVar {
|
||||
name: "WOW_PROBE_BGQUEUE",
|
||||
purpose: "1 — level past the bracket floor, greet Stormwind's Warsong Gulch battlemaster and queue through his list",
|
||||
|
||||
Reference in New Issue
Block a user