Files
benilla-octo/crates/benilla-app/src/capture/probe_bg_queue.rs
T
Sam SungandClaude Opus 5.5 c6767d6a79 SendChatMessage resolves its language name against Languages.dbc, raising "Unknown language" on a miss, and the chat send carries that id, so a racial language picked in the chat menu is spoken (#164) (#498)
`SendChatMessage`'s language argument is spoken, as in 1.12.1: a racial language picked from the chat menu (`ChatFrame.lua:2404`, passed on by `ChatEdit_SendText` at `:1950-1954`) or named by an addon goes out in `CMSG_MESSAGECHAT`, where before every line went in the faction tongue.

- `0x49f1e0` starts from the speaker's default language (`0x5ec890`); when argument 3 passes `lua_isstring` (a string or a number) it is matched by name against every `Languages.dbc` row, case-insensitively (`0x49f8a0`, `SStrCmpI`), and a miss raises "Unknown language" (`0x49f2e1`). nil, an absent argument, a table or a boolean keep the default. A language the character has not learned is sent and left to the server, which drops it (vmangos `ChatHandler.cpp:175`).
- The packet builder writes the type, the language and a target for a whisper or a channel only, then the text (`0x49f6c3`-`0x49f72a`); fourteen per-type writers become one, with byte-exact goldens.
- Each new UI state is seeded with the language table.

Checked live against the local vmangos: a say, a yell, a whisper to self and a GM command round-trip through the new builder; a channel line reaches the server's channel lookup; an unknown language raises in Lua.

Fixes #164

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 13:09:44 +07:00

178 lines
7.1 KiB
Rust

//! The battleground-queue live probe (`WOW_PROBE_BGQUEUE=1`): the fixture for a login while
//! queued. It levels the body past the bracket floor, hops to the Stormwind Warsong Gulch
//! battlemaster, greets him (`CMSG_BATTLEMASTER_HELLO`, `SMSG_BATTLEFIELD_LIST`), joins through
//! the listed guid (`CMSG_BATTLEMASTER_JOIN`) and logs `PROBE bgqueue:` with the slot it got.
//!
//! The body stays queued: vmangos only marks a logged-out entry offline
//! (`BattleGroundMgr.cpp:1768`) and sends its status again at login
//! (`BattleGroundMgr.cpp:1728`), so the next plain login is the measurement. Dequeue it with
//! `WOW_PROBE_LUA='AcceptBattlefieldPort(1,0)'` on a plain login.
//!
//! The bodyless `CMSG_BATTLEFIELD_JOIN` cannot queue: vmangos passes an empty battlemaster guid
//! (`BattleGroundHandler.cpp:78`), which the NPC interaction check never resolves.
//!
//! Non-combat; the switches are `docs/CONTRIBUTING.md`, "Running it unattended".
use bevy::prelude::*;
use super::probes::ProbeClock;
use crate::net::{ClientCommand, Guid, NetEntity, ObjectStore, SelfPlayer};
use crate::player::Player;
use crate::target::cursor_mode::npc_flags;
use crate::ui_battlefield::Battlefield;
use crate::ui_dialog_verbs::BattlefieldQueue;
/// Elfarran's spawn (vmangos `creature` guid 54614, entry 14981, map 0), Stormwind's Warsong
/// Gulch battlemaster; he is then found in the streamed world by his npc flag.
const BATTLEMASTER_AT: [f32; 3] = [-8454.62, 318.85, 120.97];
/// Warsong Gulch's Map.dbc row, which `GetBattleGroundTypeIdByMapId` maps to template 2.
const WSG_MAP: u32 = 489;
/// The level the body is raised to: `battleground_template` opens WSG at 10, 20 or 21 by content
/// patch, and 25 clears all three.
const QUEUE_LEVEL: u32 = 25;
/// The hop's search radius, not a gate: the server applies its own interaction range.
const NEAR_YD: f32 = 15.0;
pub(crate) struct ProbeBgQueuePlugin;
impl Plugin for ProbeBgQueuePlugin {
fn build(&self, app: &mut App) {
app.init_resource::<BgQueueProbe>()
.add_systems(Update, bg_queue_probe);
}
}
#[derive(Resource, Default)]
struct BgQueueProbe {
phase: Phase,
}
/// `Wait`, `Hopped` (levelled, hop sent), `Greeted` (hello sent), `Joined` (join sent), `Done`.
#[derive(Default, PartialEq)]
enum Phase {
#[default]
Wait,
Hopped {
sent_at: f64,
},
Greeted {
master: u64,
sent_at: f64,
},
Joined {
sent_at: f64,
},
Done,
}
fn bg_queue_probe(
time: ProbeClock,
mut probe: ResMut<BgQueueProbe>,
me: Query<&ObjectStore, With<SelfPlayer>>,
player: Res<Player>,
battlefield: Res<Battlefield>,
queue: Res<BattlefieldQueue>,
units: Query<(&Guid, &NetEntity, &ObjectStore, &Transform), Without<SelfPlayer>>,
net: Res<crate::net::NetCommands>,
) {
let Ok(store) = me.single() else {
return;
};
let now = time.elapsed_secs_f64();
match probe.phase {
Phase::Wait => {
// Revive first: `CanInteractWithNPC` refuses a dead player (vmangos `Player.cpp:2529`)
// with a log line that reads like a wrong guid; `.revive` on the living is a no-op.
let _ = net.0.send(ClientCommand::Chat {
kind: crate::net::ChatKind::Say,
target: None,
text: ".revive".to_string(),
language: None,
});
// A body under the bracket floor is refused at the hello, with no list to join from.
let level = store.0.unit_level().unwrap_or(0);
if level < QUEUE_LEVEL {
info!("PROBE bgqueue: level {level} — raising to {QUEUE_LEVEL} for the bracket");
let _ = net.0.send(ClientCommand::Chat {
kind: crate::net::ChatKind::Say,
target: None,
text: format!(".levelup {}", QUEUE_LEVEL - level),
language: None,
});
}
let [x, y, z] = BATTLEMASTER_AT;
info!("PROBE bgqueue: hopping to the Warsong Gulch battlemaster at ({x}, {y}, {z})");
let _ = net.0.send(ClientCommand::Chat {
kind: crate::net::ChatKind::Say,
target: None,
text: format!(".go xyz {x} {y} {z} 0"),
language: None,
});
probe.phase = Phase::Hopped { sent_at: now };
}
Phase::Hopped { sent_at } => {
if now - sent_at < 3.0 {
return; // let the battlemaster stream in
}
let here = player.pos;
let master = units.iter().find(|(_, net_e, store, tf)| {
net_e.kind == benilla_protocol::EntityKind::Unit
&& store.0.unit_npc_flags() & npc_flags::BATTLEMASTER != 0
&& tf.translation.distance(here) < NEAR_YD
});
if let Some((guid, ..)) = master {
info!(
"PROBE bgqueue: battlemaster {:#x} in range — greeting",
guid.0
);
let _ = net.0.send(ClientCommand::BattlemasterHello { npc: guid.0 });
probe.phase = Phase::Greeted {
master: guid.0,
sent_at: now,
};
} else if now - sent_at > 15.0 {
error!("PROBE bgqueue: FAILURE — no battlemaster streamed in within 15 s");
probe.phase = Phase::Done;
}
}
Phase::Greeted { master, sent_at } => {
// Wait for the list: the join quotes its guid, and it proves the level gate cleared.
if battlefield.battlemaster() == Some(master) {
info!("PROBE bgqueue: list landed — queueing for map {WSG_MAP}");
let _ = net.0.send(ClientCommand::BattlemasterJoin {
battlemaster: master,
map_id: WSG_MAP,
instance_id: 0,
as_group: false,
});
probe.phase = Phase::Joined { sent_at: now };
} else if now - sent_at > 8.0 {
error!(
"PROBE bgqueue: FAILURE — no SMSG_BATTLEFIELD_LIST 8 s after the hello \
(a level under the bracket floor is refused here, silently)"
);
probe.phase = Phase::Done;
}
}
Phase::Joined { sent_at } => {
let queued = queue
.slots()
.iter()
.enumerate()
.find_map(|(i, s)| s.as_ref().map(|(status, _)| (i, status)));
if let Some((slot, status)) = queued {
info!(
"PROBE bgqueue: QUEUED — slot {slot}, map {}, status {}, instance {} \
(the body stays queued; the next plain login is the measurement)",
status.map_id, status.status, status.instance_id,
);
probe.phase = Phase::Done;
} else if now - sent_at > 15.0 {
error!("PROBE bgqueue: FAILURE — no queue slot 15 s after CMSG_BATTLEMASTER_JOIN");
probe.phase = Phase::Done;
}
}
Phase::Done => {}
}
}