- the minimap cluster and the battleground list are the reference's own `Minimap.xml` and `BattlefieldFrame.xml`, ours deleted, and the sixteen engine names the static probe found unbound are built, not stubbed: the battleground list and queue family (the as-group join refused client-side with no packet when the map's `MaxPlayers` is below the group), the bodyless `CMSG_BATTLEFIELD_STATUS` on world enter — benilla never sent it, so a queue joined before a reconnect was invisible — the 5.5556 yd `BATTLEFIELDS_CLOSED` leash, and the meeting-stone pair over the `0x295` five-way table with both asymmetries; the ping's lifetime is the stock Lua's now (held 5 s, faded 0.5 s), `GetPingPosition` answers two numbers always — our nil was a Lua error on every frame of the stock `Minimap_OnUpdate` — and the map-change drop the reference never had is gone (1974)
- the world map is the reference's own `WorldMapFrame.xml`, our transcription deleted; built for it: the arrow-frame family (an anonymous `Model` child at its 33.6 px footprint, drawn app-side as the minimap's own arrow sprite through one shared constant), the battleground position family over `MSG_BATTLEGROUND_PLAYER_POSITIONS` both ways (the reference's 5000 ms request throttle), and `GetPlayerMapPosition` resolving `raid1..raid40` where it knew only the party (B320); `SetupFullscreenScale` — the engine binding `0x48c270` the map's and the cinematic frame's OnShow call, which our map had stood in for with Lua — is bound to the carved law `min(0.75 · aspect, 1)` off the configured aspect, written through the frame's own `SetScale` (1.0 at 4:3 and wider, 0.9375 at 5:4); a hovered POI writes the map's own area label, not the tooltip; named gap: `WorldMapPing` draws nothing yet (1980, 1981)
- the tutorial frame is the reference's own `TutorialFrame.xml`, over a tutorial system benilla never had: two bit banks filled from `SMSG_TUTORIAL_FLAGS` — the fire-once bank `TriggerTutorial` reads and sets (never a send) and the acknowledged bank `FlagTutorial` writes and sends; there is no tutorial CVar in the binary (210 `CVar::Register` sites censused), so "disable tutorials" is every bit acknowledged; the sites this app can produce are wired (the world-enter Welcome cascade, the level-up arm, item pushes, the loot sparkle, the meeting-stone and battleground arms), the rest named; the loader learned `<TitleRegion>` — the stock frame's drag handle, a region that emits no quad; and the `TabardModel` widget kind arrives ahead of its window, five ints bounded by the `.rdata` table `{170, 17, 6, 17, 51}` and the constant 100 000 tabard cost (1976, 1977)
- the chat window drew as an opaque white plate: the chat-cache loader, gated on `InWorld` alone, restored into the boot VM 130 ms before the deferred entry load filled it, and its per-VM memo then blocked the restore that would have reached the real frames — the plate the stock Lua tints black stayed the XML's white; between the entry edge and the load there is now no VM in the world (the edge parks the boot VM, the load takes it back), so no in-world feed pushes into a VM with no interface; the perf pill reads `59 fps 7.0 ms 17.3 cpu` — the MAIN thread's milliseconds in full text and the process-wide sum dim, because the sum of every thread's CPU time had been read as a frame time (a raid read 17 on it at a solid 60 with the main thread at 7); and `WOW_PROBE_UI_ONE_TEX=1` is a pricing lever, never a look, for the draw-count ceiling a UI atlas would reach before one is built (1978, 1954)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
698 lines
28 KiB
Rust
698 lines
28 KiB
Rust
//! The battleground **list and queue** family (decision 1974; wow-re
|
||
//! `system/ui/scratch/battlefield-verb-family.md` §2.1, §2.2, §3.1–3.6, §5.2): the fourteen
|
||
//! verbs the stock `BattlefieldFrame.lua` and `Minimap.xml` call, over the instance list and the
|
||
//! three queue slots the app pushes. The scoreboard half of the same TU is
|
||
//! [`super::battlefield_score`]; `AcceptBattlefieldPort` is [`super::dialog_verbs`]'s.
|
||
//!
|
||
//! ## What is the app's and what is the VM's
|
||
//!
|
||
//! The reference keeps three caches in `BattlefieldInfo.cpp`: the instance-id vector the
|
||
//! `SMSG_BATTLEFIELD_LIST` handler fills (§2.1), the fixed three-slot queue array
|
||
//! `SMSG_BATTLEFIELD_STATUS` writes (§2.2), and four scalars beside them — the battlemaster
|
||
//! guid, the listed map, the bracket-adjusted level pair, and the **selected instance id**. Every
|
||
//! verb below reads those and nothing else. The app owns the wire, the clock and the Map.dbc
|
||
//! resolves, so it pushes the list ([`BattlefieldListView`]) when a list lands and the slots
|
||
//! ([`BattlefieldQueueSlot`], with every clock-shaped value already in milliseconds) every frame;
|
||
//! the VM owns the selection (`[0xb6eba0]`), the index arithmetic and the getters' shapes.
|
||
//!
|
||
//! ## The selection is a value, not an index (§3.5)
|
||
//!
|
||
//! `SetSelectedBattlefield(n)` stores the **instance id** at list position `n`, and
|
||
//! `GetSelectedBattlefield()` scans the current list for that id — so a fresh list between the
|
||
//! two calls can move or drop the selection, which the stock window's `zoneIndex − 1 ==
|
||
//! GetSelectedBattlefield()` highlight then follows. A client that remembered an index would
|
||
//! diverge the moment a list reorders.
|
||
//!
|
||
//! ## Shapes (§3, §3.1)
|
||
//!
|
||
//! Every required index is shape A (`lua_isnumber`, so a numeric string passes; truncated toward
|
||
//! zero; a non-number raises the binding's own `Usage:`), 1-based with the reference's unsigned
|
||
//! `dec; cmp; jae` gate that rejects 0 and negatives in one branch. `GetBattlefieldStatus`
|
||
//! answers **five values on every leg** — `(nil, nil, 0, 0, 0)` off the three slots.
|
||
//! `GetBattlefieldInfo` answers nine or none. `GetBattlefieldInstanceInfo` raises with
|
||
//! `GetBattlefieldInfo`'s usage string — the shipped client's own mislabel, reproduced.
|
||
//! `CloseBattlefield` does nothing (`xor eax,eax; ret`).
|
||
|
||
use mlua::{Lua, MultiValue, Value};
|
||
|
||
use super::binding_abi::{bool_or_default, number_arg, predicate};
|
||
use super::Model;
|
||
|
||
/// One queue slot as the app pushes it — the reference's `0x20`-byte slot (§2.2) plus the map
|
||
/// name its `GetBattlefieldStatus` resolves off Map.dbc, and the three clock-shaped fields
|
||
/// already reduced against the app's clock.
|
||
#[derive(Clone, Debug, PartialEq, Eq, Default)]
|
||
pub struct BattlefieldQueueSlot {
|
||
/// `+0x00` — the Map.dbc row id; `0` for a cleared slot. The name is resolved for that id too
|
||
/// (the reference looks map 0 up like any other row).
|
||
pub map_id: u32,
|
||
/// The localized map name, or `None` when the id has no row (a real `nil`).
|
||
pub map_name: Option<String>,
|
||
/// `+0x04` — `0` none, `1` queued, `2` confirm, `3` active; anything else answers `"error"`.
|
||
pub status: u32,
|
||
/// `+0x10` — the instance id.
|
||
pub instance_id: u32,
|
||
/// `+0x08`/`+0x0c` — the bracket-adjusted level pair.
|
||
pub min_level: u32,
|
||
pub max_level: u32,
|
||
/// `GetBattlefieldPortExpiration`: `deadline − now` in ms, 0 when unset or past.
|
||
pub port_expiration_ms: u32,
|
||
/// `GetBattlefieldEstimatedWaitTime`: the stored value, raw (no clock).
|
||
pub estimated_wait_ms: u32,
|
||
/// `GetBattlefieldTimeWaited`: `now − stamp` in ms, 0 when unset.
|
||
pub time_waited_ms: u32,
|
||
}
|
||
|
||
/// The Map.dbc half of `GetBattlefieldInfo` (§3.4), resolved by the app for the listed map.
|
||
#[derive(Clone, Debug, PartialEq, Default)]
|
||
pub struct BattlefieldMapInfo {
|
||
/// The localized map name (value 1).
|
||
pub name: String,
|
||
/// The faction-side description (value 2) — `None` on the reference's `-1` faction leg, where
|
||
/// it pushes nothing at all and Lua reads a stack slot below the tuple; here that leg answers
|
||
/// `nil`, the one shape this binding does not reproduce (the leg is data-unreachable: every
|
||
/// playable race carries a faction bit).
|
||
pub description: Option<String>,
|
||
/// The row's raw `MinLevel`/`MaxLevel` columns (values 3 and 4 — the stock Lua names them
|
||
/// `minLevel, maxLevel`; the bracket-adjusted pair is values 8 and 9).
|
||
pub min_level: u32,
|
||
pub max_level: u32,
|
||
/// Values 5–7: `[row+0x40]` signed, `[row+0x44]` and `[row+0x48]` f32.
|
||
pub field_16: i32,
|
||
pub field_17: f32,
|
||
pub field_18: f32,
|
||
}
|
||
|
||
/// The instance list and its scalars as the app pushes them ([`super::UiScript::set_battlefield_list`]).
|
||
#[derive(Clone, Debug, PartialEq, Default)]
|
||
pub struct BattlefieldListView {
|
||
/// The instance ids in wire order (`[0xb6e868]`).
|
||
pub instances: Vec<u32>,
|
||
/// The bracket-adjusted level pair the list handler derived (`[0xb6eba8]`/`[0xb6ebac]`).
|
||
pub bracket_min: u32,
|
||
pub bracket_max: u32,
|
||
/// The listed map's row, `None` when the id resolves to no row (the second zero-values gate).
|
||
pub info: Option<BattlefieldMapInfo>,
|
||
/// The listed map row's group-queue flag (`[row+0xa0]`, §3.6) — `CanJoinBattlefieldAsGroup`.
|
||
pub group_queue: bool,
|
||
}
|
||
|
||
/// The reference's 1-based index gate: `dec eax; cmp eax,n; jae bail` — an unsigned compare, so
|
||
/// `0` and every negative fall out in the same branch as `> n`.
|
||
fn slot_index(index: i32, n: usize) -> Option<usize> {
|
||
usize::try_from(index)
|
||
.ok()?
|
||
.checked_sub(1)
|
||
.filter(|&i| i < n)
|
||
}
|
||
|
||
/// The local-player gate `GetBattlefieldInfo` and `GetBattlefieldInstanceInfo` answer zero values
|
||
/// behind (`0x468550` + `0x468460(ecx = 0x10)`: the player object, read at call time) — here the
|
||
/// unit model's `"player"` token, the same answer `UnitExists("player")` gives.
|
||
fn player_exists(model: &Model) -> bool {
|
||
model.unit("player").is_some_and(|u| u.exists)
|
||
}
|
||
|
||
/// `[slot+0x04]` → the status string, through the reference's four-entry jump table with its
|
||
/// `"error"` default (§3.2).
|
||
fn status_text(status: u32) -> &'static str {
|
||
match status {
|
||
0 => "none",
|
||
1 => "queued",
|
||
2 => "confirm",
|
||
3 => "active",
|
||
_ => "error",
|
||
}
|
||
}
|
||
|
||
impl super::UiScript {
|
||
/// Push the instance list. The selection (`[0xb6eba0]`) is left alone — a new list does not
|
||
/// clear it, it only decides whether `GetSelectedBattlefield` still finds it (§3.5).
|
||
pub fn set_battlefield_list(&mut self, list: BattlefieldListView) {
|
||
self.model_mut().battlefield_list = list;
|
||
}
|
||
|
||
/// The world-enter reset's half of this family (§8): the selection cleared with the list.
|
||
pub fn reset_battlefield_selection(&mut self) {
|
||
self.model_mut().battlefield_selected = 0;
|
||
}
|
||
|
||
/// Push the three queue slots (clock-shaped values already reduced) and the instance
|
||
/// expiration (`[0xb6ebb8]`, `deadline − now`, 0 when unset or past) — every frame, since
|
||
/// three of the getters move with the clock.
|
||
pub fn set_battlefield_queue(
|
||
&mut self,
|
||
slots: Vec<BattlefieldQueueSlot>,
|
||
instance_expiration_ms: u32,
|
||
) {
|
||
let mut model = self.model_mut();
|
||
model.battlefield_slots = slots;
|
||
model.battlefield_instance_expiration_ms = instance_expiration_ms;
|
||
}
|
||
|
||
/// `JoinBattlefield` calls since the last drain: `(instance id, 0 = first available; as a
|
||
/// group)`. The app adds the map, the cached battlemaster guid (which picks the opcode) and
|
||
/// the group-size refusal (§5.2).
|
||
pub fn take_battlefield_join_requests(&mut self) -> Vec<(u32, bool)> {
|
||
std::mem::take(&mut self.model_mut().battlefield_join_requests)
|
||
}
|
||
|
||
/// `ShowBattlefieldList` calls that passed their gates since the last drain: the queued
|
||
/// slot's map id, `CMSG_BATTLEFIELD_LIST`'s payload.
|
||
pub fn take_battlefield_list_requests(&mut self) -> Vec<u32> {
|
||
std::mem::take(&mut self.model_mut().battlefield_list_requests)
|
||
}
|
||
}
|
||
|
||
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||
let g = lua.globals();
|
||
|
||
// `GetNumBattlefields()` — 0 args, never raises: the instance count.
|
||
g.set(
|
||
"GetNumBattlefields",
|
||
lua.create_function(|lua, ()| {
|
||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||
Ok(model.battlefield_list.instances.len() as i64)
|
||
})?,
|
||
)?;
|
||
|
||
// `GetBattlefieldInfo()` — 0 args, never raises; ZERO values behind either gate (no player, no
|
||
// map row), else nine (§3.4).
|
||
g.set(
|
||
"GetBattlefieldInfo",
|
||
lua.create_function(|lua, ()| {
|
||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||
let list = &model.battlefield_list;
|
||
let (Some(info), true) = (list.info.as_ref(), player_exists(&model)) else {
|
||
return Ok(MultiValue::new());
|
||
};
|
||
let description = match &info.description {
|
||
Some(d) => Value::String(lua.create_string(d)?),
|
||
None => Value::Nil,
|
||
};
|
||
Ok(MultiValue::from_vec(vec![
|
||
Value::String(lua.create_string(&info.name)?),
|
||
description,
|
||
Value::Number(f64::from(info.min_level)),
|
||
Value::Number(f64::from(info.max_level)),
|
||
Value::Number(f64::from(info.field_16)),
|
||
Value::Number(f64::from(info.field_17)),
|
||
Value::Number(f64::from(info.field_18)),
|
||
Value::Number(f64::from(list.bracket_min)),
|
||
Value::Number(f64::from(list.bracket_max)),
|
||
]))
|
||
})?,
|
||
)?;
|
||
|
||
// `GetBattlefieldInstanceInfo(index)` — raises with GetBattlefieldInfo's usage string (the
|
||
// shipped mislabel, §3.1); no player or out of range → zero values; else the instance id,
|
||
// pushed SIGNED (`fild dword`).
|
||
g.set(
|
||
"GetBattlefieldInstanceInfo",
|
||
lua.create_function(|lua, index: Value| {
|
||
let index = number_arg(lua, index, "Usage: GetBattlefieldInfo(index)")?;
|
||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||
let list = &model.battlefield_list;
|
||
if !player_exists(&model) {
|
||
return Ok(MultiValue::new());
|
||
}
|
||
Ok(match slot_index(index, list.instances.len()) {
|
||
Some(i) => {
|
||
MultiValue::from_vec(vec![Value::Number(f64::from(list.instances[i] as i32))])
|
||
}
|
||
None => MultiValue::new(),
|
||
})
|
||
})?,
|
||
)?;
|
||
|
||
// `JoinBattlefield(index [, asGroup])` — arg 1 shape A, arg 2 the never-raising optional
|
||
// boolean reader; the instance is `index−1 < count ? list[index−1] : 0` (0 = first
|
||
// available, and what an out-of-range index — 0 included — degrades to), §5.2. The refusal
|
||
// leg and the opcode choice are the app's and invisible to Lua.
|
||
g.set(
|
||
"JoinBattlefield",
|
||
lua.create_function(|lua, (index, as_group): (Value, Value)| {
|
||
let index = number_arg(lua, index, "Usage: JoinBattlefield(index)")?;
|
||
let as_group = bool_or_default(Some(&as_group), false);
|
||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||
let instance = slot_index(index, model.battlefield_list.instances.len())
|
||
.map_or(0, |i| model.battlefield_list.instances[i]);
|
||
model.battlefield_join_requests.push((instance, as_group));
|
||
Ok(())
|
||
})?,
|
||
)?;
|
||
|
||
// `CloseBattlefield()` — `xor eax,eax; ret`: nothing, and the stock window calls it on hide.
|
||
g.set("CloseBattlefield", lua.create_function(|_, ()| Ok(()))?)?;
|
||
|
||
// `SetSelectedBattlefield(index)` — stores the instance id at that position, or 0 when out
|
||
// of range (§3.5).
|
||
g.set(
|
||
"SetSelectedBattlefield",
|
||
lua.create_function(|lua, index: Value| {
|
||
let index = number_arg(lua, index, "Usage: SetSelectedBattlefield(index)")?;
|
||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||
model.battlefield_selected = slot_index(index, model.battlefield_list.instances.len())
|
||
.map_or(0, |i| model.battlefield_list.instances[i]);
|
||
Ok(())
|
||
})?,
|
||
)?;
|
||
|
||
// `GetSelectedBattlefield()` — the 1-based position of the stored id in the CURRENT list, or
|
||
// 0 on a miss (which is also what a stored 0 answers: the "first available" row).
|
||
g.set(
|
||
"GetSelectedBattlefield",
|
||
lua.create_function(|lua, ()| {
|
||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||
let selected = model.battlefield_selected;
|
||
Ok(model
|
||
.battlefield_list
|
||
.instances
|
||
.iter()
|
||
.position(|&id| id == selected)
|
||
.map_or(0, |i| i as i64 + 1))
|
||
})?,
|
||
)?;
|
||
|
||
// `GetBattlefieldStatus(index)` — five values on EVERY leg (§3.2): off 1..3 it is
|
||
// `(nil, nil, 0, 0, 0)`, never a raise and never zero values.
|
||
g.set(
|
||
"GetBattlefieldStatus",
|
||
lua.create_function(|lua, index: Value| {
|
||
let index = number_arg(lua, index, "Usage: GetBattlefieldStatus(index)")?;
|
||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||
let Some(slot) = slot_index(index, 3).and_then(|i| model.battlefield_slots.get(i))
|
||
else {
|
||
return Ok(MultiValue::from_vec(vec![
|
||
Value::Nil,
|
||
Value::Nil,
|
||
Value::Number(0.0),
|
||
Value::Number(0.0),
|
||
Value::Number(0.0),
|
||
]));
|
||
};
|
||
let name = match &slot.map_name {
|
||
Some(n) => Value::String(lua.create_string(n)?),
|
||
None => Value::Nil,
|
||
};
|
||
Ok(MultiValue::from_vec(vec![
|
||
Value::String(lua.create_string(status_text(slot.status))?),
|
||
name,
|
||
Value::Number(f64::from(slot.instance_id)),
|
||
Value::Number(f64::from(slot.min_level)),
|
||
Value::Number(f64::from(slot.max_level)),
|
||
]))
|
||
})?,
|
||
)?;
|
||
|
||
// The three per-slot clock-shaped getters (§3.3): shape A index, 0 off 1..3, one number
|
||
// always. The app reduces each against its clock; the values arrive in ms.
|
||
for (name, usage, read) in [
|
||
(
|
||
"GetBattlefieldPortExpiration",
|
||
"Usage: GetBattlefieldPortExpiration(index)",
|
||
(|s: &BattlefieldQueueSlot| s.port_expiration_ms) as fn(&BattlefieldQueueSlot) -> u32,
|
||
),
|
||
(
|
||
"GetBattlefieldEstimatedWaitTime",
|
||
"Usage: GetBattlefieldEstimatedWaitTime(index)",
|
||
|s: &BattlefieldQueueSlot| s.estimated_wait_ms,
|
||
),
|
||
(
|
||
"GetBattlefieldTimeWaited",
|
||
"Usage: GetBattlefieldTimeWaited(index)",
|
||
|s: &BattlefieldQueueSlot| s.time_waited_ms,
|
||
),
|
||
] {
|
||
g.set(
|
||
name,
|
||
lua.create_function(move |lua, index: Value| {
|
||
let index = number_arg(lua, index, usage)?;
|
||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||
Ok(i64::from(
|
||
slot_index(index, 3)
|
||
.and_then(|i| model.battlefield_slots.get(i))
|
||
.map_or(0, read),
|
||
))
|
||
})?,
|
||
)?;
|
||
}
|
||
|
||
// `GetBattlefieldInstanceExpiration()` — 0 args: `[0xb6ebb8] − now`, 0 when unset or past.
|
||
g.set(
|
||
"GetBattlefieldInstanceExpiration",
|
||
lua.create_function(|lua, ()| {
|
||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||
Ok(i64::from(model.battlefield_instance_expiration_ms))
|
||
})?,
|
||
)?;
|
||
|
||
// `ShowBattlefieldList(index)` — shape A; silent off 1..3, on an empty slot, and on any
|
||
// status but "queued"; else the slot's map goes out as `CMSG_BATTLEFIELD_LIST`. No event.
|
||
g.set(
|
||
"ShowBattlefieldList",
|
||
lua.create_function(|lua, index: Value| {
|
||
let index = number_arg(lua, index, "Usage: ShowBattlefieldList(index)")?;
|
||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||
let map_id = slot_index(index, 3)
|
||
.and_then(|i| model.battlefield_slots.get(i))
|
||
.filter(|s| s.map_id != 0 && s.status == 1)
|
||
.map(|s| s.map_id);
|
||
if let Some(map_id) = map_id {
|
||
model.battlefield_list_requests.push(map_id);
|
||
}
|
||
Ok(())
|
||
})?,
|
||
)?;
|
||
|
||
// `CanJoinBattlefieldAsGroup()` — `1` or nil off the listed map's `+0xa0` flag (§3.6). The
|
||
// join-time size check reads a different column and lives with the sender.
|
||
g.set(
|
||
"CanJoinBattlefieldAsGroup",
|
||
lua.create_function(|lua, ()| {
|
||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||
Ok(predicate(model.battlefield_list.group_queue))
|
||
})?,
|
||
)?;
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crate::script::UiScript;
|
||
|
||
/// A VM with a local player — the state every in-world call is made in.
|
||
fn vm() -> UiScript {
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_unit(
|
||
"player",
|
||
Some(crate::script::UnitState {
|
||
exists: true,
|
||
name: Some("Probe".into()),
|
||
..Default::default()
|
||
}),
|
||
);
|
||
s
|
||
}
|
||
|
||
fn list(ids: &[u32]) -> BattlefieldListView {
|
||
BattlefieldListView {
|
||
instances: ids.to_vec(),
|
||
bracket_min: 20,
|
||
bracket_max: 29,
|
||
info: Some(BattlefieldMapInfo {
|
||
name: "Arathi Basin".into(),
|
||
description: Some("The Arathi Basin is …".into()),
|
||
min_level: 20,
|
||
max_level: 60,
|
||
field_16: -1,
|
||
field_17: 0.0,
|
||
field_18: 0.0,
|
||
}),
|
||
group_queue: true,
|
||
}
|
||
}
|
||
|
||
fn slot(map_id: u32, status: u32, instance: u32) -> BattlefieldQueueSlot {
|
||
BattlefieldQueueSlot {
|
||
map_id,
|
||
map_name: Some(format!("Map {map_id}")),
|
||
status,
|
||
instance_id: instance,
|
||
min_level: 20,
|
||
max_level: 29,
|
||
port_expiration_ms: 60_000,
|
||
estimated_wait_ms: 30_000,
|
||
time_waited_ms: 5_000,
|
||
}
|
||
}
|
||
|
||
/// The reference's 1-based gate: 0 and negatives fall out with the too-large ones.
|
||
#[test]
|
||
fn the_index_gate_is_unsigned_and_one_based() {
|
||
assert_eq!(slot_index(1, 3), Some(0));
|
||
assert_eq!(slot_index(3, 3), Some(2));
|
||
assert_eq!(slot_index(4, 3), None);
|
||
assert_eq!(slot_index(0, 3), None);
|
||
assert_eq!(slot_index(-1, 3), None);
|
||
assert_eq!(slot_index(1, 0), None);
|
||
}
|
||
|
||
/// Nine values past both gates, none behind either; the two raw row levels sit at 3 and 4
|
||
/// and the bracket pair at 8 and 9.
|
||
#[test]
|
||
fn get_battlefield_info_answers_nine_or_none() {
|
||
let mut s = vm();
|
||
assert_eq!(
|
||
s.eval::<i64>("return select('#', GetBattlefieldInfo())")
|
||
.unwrap(),
|
||
0,
|
||
"no list at all: the map gate"
|
||
);
|
||
s.set_battlefield_list(list(&[7, 3]));
|
||
s.set_unit("player", None);
|
||
assert_eq!(
|
||
s.eval::<i64>("return select('#', GetBattlefieldInfo())")
|
||
.unwrap(),
|
||
0,
|
||
"no player: the other gate, read at call time"
|
||
);
|
||
assert_eq!(
|
||
s.eval::<i64>("return select('#', GetBattlefieldInstanceInfo(1))")
|
||
.unwrap(),
|
||
0,
|
||
"…and the instance verb's"
|
||
);
|
||
s = vm();
|
||
s.set_battlefield_list(list(&[7, 3]));
|
||
assert_eq!(
|
||
s.eval::<i64>("return select('#', GetBattlefieldInfo())")
|
||
.unwrap(),
|
||
9
|
||
);
|
||
let got = s
|
||
.eval::<String>(
|
||
"local n, d, a, b, c, x, y, lo, hi = GetBattlefieldInfo() \
|
||
return n .. '|' .. a .. '|' .. b .. '|' .. c .. '|' .. lo .. '|' .. hi",
|
||
)
|
||
.unwrap();
|
||
assert_eq!(got, "Arathi Basin|20|60|-1|20|29");
|
||
}
|
||
|
||
/// The instance verb: the wrong usage string, the silent out-of-range leg, the signed push.
|
||
#[test]
|
||
fn get_battlefield_instance_info_raises_with_the_mislabel_and_bails_silently() {
|
||
let mut s = vm();
|
||
s.set_battlefield_list(list(&[7, 0xFFFF_FFFF]));
|
||
assert_eq!(
|
||
s.eval::<i64>("return GetBattlefieldInstanceInfo(1)")
|
||
.unwrap(),
|
||
7
|
||
);
|
||
assert_eq!(
|
||
s.eval::<i64>("return GetBattlefieldInstanceInfo('2')")
|
||
.unwrap(),
|
||
-1,
|
||
"a numeric string passes; the id is pushed signed"
|
||
);
|
||
assert_eq!(
|
||
s.eval::<i64>("return select('#', GetBattlefieldInstanceInfo(3))")
|
||
.unwrap(),
|
||
0
|
||
);
|
||
assert_eq!(
|
||
s.eval::<i64>("return select('#', GetBattlefieldInstanceInfo(0))")
|
||
.unwrap(),
|
||
0
|
||
);
|
||
let err = s
|
||
.run("GetBattlefieldInstanceInfo(nil)")
|
||
.unwrap_err()
|
||
.to_string();
|
||
assert!(
|
||
err.contains("Usage: GetBattlefieldInfo(index)"),
|
||
"the shipped client's own mislabel: {err}"
|
||
);
|
||
assert_eq!(s.eval::<i64>("return GetNumBattlefields()").unwrap(), 2);
|
||
}
|
||
|
||
/// The selection round-trips by VALUE: a reordered list moves it, a shortened one drops it.
|
||
#[test]
|
||
fn the_selection_is_an_instance_id_not_a_position() {
|
||
let mut s = vm();
|
||
s.set_battlefield_list(list(&[7, 3, 11]));
|
||
assert_eq!(s.eval::<i64>("return GetSelectedBattlefield()").unwrap(), 0);
|
||
s.run("SetSelectedBattlefield(2)").unwrap();
|
||
assert_eq!(s.eval::<i64>("return GetSelectedBattlefield()").unwrap(), 2);
|
||
s.set_battlefield_list(list(&[3, 7, 11]));
|
||
assert_eq!(
|
||
s.eval::<i64>("return GetSelectedBattlefield()").unwrap(),
|
||
1,
|
||
"instance 3 moved to the front"
|
||
);
|
||
s.set_battlefield_list(list(&[7, 11]));
|
||
assert_eq!(
|
||
s.eval::<i64>("return GetSelectedBattlefield()").unwrap(),
|
||
0,
|
||
"instance 3 left the list"
|
||
);
|
||
s.run("SetSelectedBattlefield(0)").unwrap();
|
||
assert_eq!(s.eval::<i64>("return GetSelectedBattlefield()").unwrap(), 0);
|
||
s.run("SetSelectedBattlefield(9)").unwrap();
|
||
assert_eq!(
|
||
s.eval::<i64>("return GetSelectedBattlefield()").unwrap(),
|
||
0,
|
||
"out of range stores 0"
|
||
);
|
||
let err = s.run("SetSelectedBattlefield({})").unwrap_err().to_string();
|
||
assert!(
|
||
err.contains("Usage: SetSelectedBattlefield(index)"),
|
||
"{err}"
|
||
);
|
||
}
|
||
|
||
/// `JoinBattlefield`: the instance at the position, 0 for "first available" and for any
|
||
/// out-of-range index, the optional group flag through the never-raising reader.
|
||
#[test]
|
||
fn join_battlefield_resolves_the_instance_and_the_group_flag() {
|
||
let mut s = vm();
|
||
s.set_battlefield_list(list(&[7, 3]));
|
||
s.run("JoinBattlefield(0) JoinBattlefield(2, 1) JoinBattlefield(5, true) JoinBattlefield(1, nil)")
|
||
.unwrap();
|
||
assert_eq!(
|
||
s.take_battlefield_join_requests(),
|
||
vec![(0, false), (3, true), (0, true), (7, false)]
|
||
);
|
||
assert!(s.take_battlefield_join_requests().is_empty(), "drained");
|
||
let err = s.run("JoinBattlefield()").unwrap_err().to_string();
|
||
assert!(err.contains("Usage: JoinBattlefield(index)"), "{err}");
|
||
s.run("CloseBattlefield()").unwrap();
|
||
}
|
||
|
||
/// Five values on every leg of the status verb; the four status strings and the default.
|
||
#[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);
|
||
let n = |s: &mut UiScript, i: &str| {
|
||
s.eval::<i64>(&format!("return select('#', GetBattlefieldStatus({i}))"))
|
||
.unwrap()
|
||
};
|
||
assert_eq!(n(&mut s, "1"), 5);
|
||
assert_eq!(n(&mut s, "0"), 5);
|
||
assert_eq!(n(&mut s, "4"), 5);
|
||
let got = s
|
||
.eval::<String>(
|
||
"local st, name, id, lo, hi = GetBattlefieldStatus(1) \
|
||
return st .. '|' .. name .. '|' .. id .. '|' .. lo .. '|' .. hi",
|
||
)
|
||
.unwrap();
|
||
assert_eq!(got, "queued|Map 489|5|20|29");
|
||
assert_eq!(
|
||
s.eval::<String>("return (GetBattlefieldStatus(2))")
|
||
.unwrap(),
|
||
"none"
|
||
);
|
||
assert_eq!(
|
||
s.eval::<String>("return (GetBattlefieldStatus(3))")
|
||
.unwrap(),
|
||
"error",
|
||
"a status past 3 takes the jump table's default"
|
||
);
|
||
let off = s
|
||
.eval::<String>(
|
||
"local st, name, id, lo, hi = GetBattlefieldStatus(4) \
|
||
return tostring(st) .. '|' .. tostring(name) .. '|' .. id .. lo .. hi",
|
||
)
|
||
.unwrap();
|
||
assert_eq!(off, "nil|nil|000");
|
||
let err = s.run("GetBattlefieldStatus('x')").unwrap_err().to_string();
|
||
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);
|
||
assert!(
|
||
s.eval::<bool>("local _, name = GetBattlefieldStatus(1) return name == nil")
|
||
.unwrap(),
|
||
"no row: a real nil"
|
||
);
|
||
}
|
||
|
||
/// The clock-shaped getters read the pushed ms and answer 0 off the slots; the instance
|
||
/// expiration is the singleton.
|
||
#[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);
|
||
assert_eq!(
|
||
s.eval::<i64>("return GetBattlefieldPortExpiration(1)")
|
||
.unwrap(),
|
||
60_000
|
||
);
|
||
assert_eq!(
|
||
s.eval::<i64>("return GetBattlefieldEstimatedWaitTime(1)")
|
||
.unwrap(),
|
||
30_000
|
||
);
|
||
assert_eq!(
|
||
s.eval::<i64>("return GetBattlefieldTimeWaited(1)").unwrap(),
|
||
5_000
|
||
);
|
||
assert_eq!(
|
||
s.eval::<i64>("return GetBattlefieldTimeWaited(2)").unwrap(),
|
||
0
|
||
);
|
||
assert_eq!(
|
||
s.eval::<i64>("return GetBattlefieldPortExpiration(0)")
|
||
.unwrap(),
|
||
0
|
||
);
|
||
assert_eq!(
|
||
s.eval::<i64>("return GetBattlefieldInstanceExpiration()")
|
||
.unwrap(),
|
||
120_000
|
||
);
|
||
for (call, usage) in [
|
||
(
|
||
"GetBattlefieldPortExpiration()",
|
||
"Usage: GetBattlefieldPortExpiration(index)",
|
||
),
|
||
(
|
||
"GetBattlefieldEstimatedWaitTime(nil)",
|
||
"Usage: GetBattlefieldEstimatedWaitTime(index)",
|
||
),
|
||
(
|
||
"GetBattlefieldTimeWaited({})",
|
||
"Usage: GetBattlefieldTimeWaited(index)",
|
||
),
|
||
] {
|
||
let err = s.run(call).unwrap_err().to_string();
|
||
assert!(err.contains(usage), "{call}: {err}");
|
||
}
|
||
}
|
||
|
||
/// `ShowBattlefieldList` sends only for a queued, non-empty slot; the group predicate is
|
||
/// `1` or nil.
|
||
#[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.run("ShowBattlefieldList(1) ShowBattlefieldList(2) ShowBattlefieldList(3) ShowBattlefieldList(4)")
|
||
.unwrap();
|
||
assert_eq!(s.take_battlefield_list_requests(), vec![489]);
|
||
let err = s.run("ShowBattlefieldList('q')").unwrap_err().to_string();
|
||
assert!(err.contains("Usage: ShowBattlefieldList(index)"), "{err}");
|
||
|
||
assert!(s
|
||
.eval::<bool>("return CanJoinBattlefieldAsGroup() == nil")
|
||
.unwrap());
|
||
s.set_battlefield_list(list(&[]));
|
||
assert_eq!(
|
||
s.eval::<i64>("return CanJoinBattlefieldAsGroup()").unwrap(),
|
||
1
|
||
);
|
||
}
|
||
}
|