The quest log counts party members on each quest, as 1.12.1 does (#824)
The quest log never showed the `[n]` count of party members on a quest, because `IsUnitOnQuest` was a stub that always answered nil. 1.12.1 (`0x4dfe10`) checks both arguments first. It reads the row's quest id (`0x4df150`; a header or out-of-range row answers nil before the unit is resolved), resolves the token, and admits only you or one of the four party slots, by guid (`0x4e7f70`). It then needs the unit held as a player and scans the 20 `PLAYER_QUEST_LOG` ids for that quest. The answer is 1 or nil. The ids reach party members on the wire. `UNIT_QUEST_LOG_CHANGED` (`0x51bc63` → `0x51bd90`) fires once whenever any slot of a unit's window changes, so an open log recounts. Unit snapshots now carry the window, the binding follows those steps, and the event fires. Tests cover each path and run the stock `QuestLogFrame.xml` to a `[2]` row; they fail against the stub. Fixes #818 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
80b0848628
commit
63e917965d
@@ -1693,3 +1693,115 @@ fn the_stock_title_width_cap_bites_and_is_sticky_per_row_button() {
|
||||
"the capped box survives the row going untagged (the ref's own 275 reset never fires)"
|
||||
);
|
||||
}
|
||||
|
||||
// ── The group-mates count ──
|
||||
|
||||
/// A party member held with `ids` in their quest-log window, named `name`.
|
||||
fn mate_on(guid: u64, name: &str, ids: &[u32]) -> benilla_ui::script::UnitState {
|
||||
let mut quest_log = [[0; 3]; 20];
|
||||
for (slot, &id) in quest_log.iter_mut().zip(ids) {
|
||||
*slot = [id, 0, 0];
|
||||
}
|
||||
benilla_ui::script::UnitState {
|
||||
exists: true,
|
||||
has_object: true,
|
||||
is_player: true,
|
||||
guid,
|
||||
name: Some(name.into()),
|
||||
quest_log,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Each quest row counts the party members on it as `[n]` (`QuestLogFrame.lua:168-183`), through
|
||||
/// `IsUnitOnQuest(questIndex, "partyN")`; the row tooltip names them (`:515-541`), and a mate's
|
||||
/// `UNIT_QUEST_LOG_CHANGED` recounts (`:69-72`).
|
||||
#[test]
|
||||
fn quest_rows_count_the_party_members_on_each_quest() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml"); // our scroll kits
|
||||
|
||||
s.set_quest_log(eight_entries());
|
||||
s.set_party(party(2));
|
||||
s.set_unit("party1", Some(mate_on(0x300, "Mate0", &[1, 3])));
|
||||
s.set_unit("party2", Some(mate_on(0x301, "Mate1", &[1])));
|
||||
s.run("ToggleQuestLog()").unwrap();
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
|
||||
let mates = |s: &mut UiScript, row: u32| {
|
||||
s.eval::<String>(&format!(
|
||||
"return QuestLogTitle{row}GroupMates:GetText() or ''"
|
||||
))
|
||||
.unwrap()
|
||||
};
|
||||
assert_eq!(mates(&mut s, 1), "[2]", "both mates are on quest 1");
|
||||
assert_eq!(mates(&mut s, 2), "", "nobody is on quest 2");
|
||||
assert_eq!(mates(&mut s, 3), "[1]", "party1 is on quest 3");
|
||||
|
||||
// The row tooltip, as `QuestLogTitleButton_OnEnter` builds it for row 1, then row 2.
|
||||
let line = |s: &mut UiScript, n: u32| {
|
||||
s.eval::<String>(&format!("return GameTooltipTextLeft{n}:GetText() or ''"))
|
||||
.unwrap()
|
||||
};
|
||||
s.run("this = QuestLogTitle1 QuestLog_UpdatePartyInfoTooltip()")
|
||||
.unwrap();
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
assert_eq!(line(&mut s, 1), "Quest 1");
|
||||
assert!(s
|
||||
.eval::<bool>(
|
||||
"return GameTooltipTextLeft2:GetText() == \
|
||||
HIGHLIGHT_FONT_COLOR_CODE..PARTY_QUEST_STATUS_ON..FONT_COLOR_CODE_CLOSE"
|
||||
)
|
||||
.unwrap());
|
||||
assert!(line(&mut s, 3).contains("Mate0"), "{}", line(&mut s, 3));
|
||||
assert!(line(&mut s, 4).contains("Mate1"), "{}", line(&mut s, 4));
|
||||
s.run("this = QuestLogTitle2 QuestLog_UpdatePartyInfoTooltip()")
|
||||
.unwrap();
|
||||
assert!(s
|
||||
.eval::<bool>(
|
||||
"return GameTooltipTextLeft2:GetText() == \
|
||||
HIGHLIGHT_FONT_COLOR_CODE..PARTY_QUEST_STATUS_NONE..FONT_COLOR_CODE_CLOSE"
|
||||
)
|
||||
.unwrap());
|
||||
|
||||
// party2 takes quest 3: their quest-log event recounts the open log.
|
||||
s.set_unit("party2", Some(mate_on(0x301, "Mate1", &[1, 3])));
|
||||
s.fire_event(
|
||||
"UNIT_QUEST_LOG_CHANGED",
|
||||
vec![benilla_ui::script::ScriptValue::Str("party2".into())],
|
||||
);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
assert_eq!(mates(&mut s, 3), "[2]");
|
||||
|
||||
// Out of range, a mate counts for nothing, whatever their last window held (`0x468460` finds
|
||||
// no object).
|
||||
s.set_unit(
|
||||
"party1",
|
||||
Some(benilla_ui::script::UnitState {
|
||||
has_object: false,
|
||||
..mate_on(0x300, "Mate0", &[1, 3])
|
||||
}),
|
||||
);
|
||||
s.run("QuestLog_Update()").unwrap();
|
||||
assert_eq!(mates(&mut s, 1), "[1]");
|
||||
assert_eq!(mates(&mut s, 3), "[1]");
|
||||
}
|
||||
|
||||
@@ -1937,12 +1937,6 @@ mod tests {
|
||||
),
|
||||
("SYSMSG", "UIErrorsFrame.lua"),
|
||||
("UNIT_DEFENSE", "PetPaperDollFrame.lua"),
|
||||
(
|
||||
"UNIT_QUEST_LOG_CHANGED",
|
||||
"QuestLogFrame.lua — a party member's quest-log fields changing (the reference \
|
||||
fires it off the unit's PLAYER_QUEST_LOG_* descriptor updates); benilla's unit \
|
||||
feed does not derive it yet",
|
||||
),
|
||||
(
|
||||
"UNIT_MODEL_CHANGED",
|
||||
"four files — the paperdoll model refresh",
|
||||
|
||||
@@ -929,6 +929,13 @@ pub(crate) fn snapshot(
|
||||
pvp_team: store.0.unit_race().map_or(-1, race_pvp_team),
|
||||
// `PLAYER_BYTES_3` byte 2, the city-protector title (`PVP_MEDAL<n>`), unset by vmangos.
|
||||
pvp_medal: store.0.player_pvp_medal().unwrap_or(0),
|
||||
// The quest-log window, off a player alone: `IsUnitOnQuest` looks the guid up with
|
||||
// TYPEMASK_PLAYER (`0x4dfe97`) before it reads `[obj+0xe68]`.
|
||||
quest_log: if matches!(store.0.object_type(), Some(ObjectType::Player)) {
|
||||
store.0.player_quest_log_window()
|
||||
} else {
|
||||
Default::default()
|
||||
},
|
||||
// `is_player` and the creature-record fields come from [`enrich_unit`].
|
||||
..Default::default()
|
||||
}
|
||||
@@ -1145,6 +1152,15 @@ pub(crate) fn fire_transitions(
|
||||
if edges.moved(cur.guid, benilla_protocol::field::FIELD_UNIT_DYNAMIC_FLAGS) {
|
||||
script.fire_event("UNIT_DYNAMIC_FLAGS", vec![tok()]);
|
||||
}
|
||||
// `UNIT_QUEST_LOG_CHANGED` (id 522), the player-window watch over the quest-log slots
|
||||
// (`0x51bc63`: offset `0x28`, length `0xf0`, callback `0x51bd90`): one compare over the 60
|
||||
// dwords, so one event however many moved. A group mate's ids are all of it on the wire.
|
||||
let log = benilla_protocol::field::FIELD_PLAYER_QUEST_LOG_1_1;
|
||||
if (log..log + 3 * u16::from(benilla_protocol::messages::PLAYER_QUEST_LOG_SLOTS))
|
||||
.any(|i| edges.moved(cur.guid, i))
|
||||
{
|
||||
script.fire_event("UNIT_QUEST_LOG_CHANGED", vec![tok()]);
|
||||
}
|
||||
// 1.12 names the power events per resource (`UNIT_MANA`, `UNIT_MAXRAGE`, …;
|
||||
// `UnitFrame.lua:190-199`), and `power_token` yields the suffix.
|
||||
if changed(|u| u64::from(u.power)) {
|
||||
@@ -2579,6 +2595,46 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// `UNIT_QUEST_LOG_CHANGED` (id 522) is the player-window watch over the 60 quest-log dwords
|
||||
/// (`0x51bc63`: offset `0x28`, length `0xf0`, callback `0x51bd90`): any of them moving fires it
|
||||
/// once with the token, and a dword either side does not.
|
||||
#[test]
|
||||
fn a_quest_log_change_fires_unit_quest_log_changed_with_the_token() {
|
||||
let fired = |prev: &UnitState, cur: &UnitState, edges: &[(u64, u16)]| -> Vec<String> {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.run(
|
||||
r#"
|
||||
SEEN = {}
|
||||
local f = CreateFrame("Frame")
|
||||
f:RegisterEvent("UNIT_QUEST_LOG_CHANGED")
|
||||
f:SetScript("OnEvent", function() table.insert(SEEN, event .. ":" .. arg1) end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
fire_transitions(&mut s, "party1", Some(prev), cur, &FieldEdges::of(edges));
|
||||
s.eval::<Vec<String>>("return SEEN").unwrap()
|
||||
};
|
||||
const MATE: u64 = 0x300;
|
||||
const LOG: u16 = benilla_protocol::field::FIELD_PLAYER_QUEST_LOG_1_1;
|
||||
let before = UnitState {
|
||||
exists: true,
|
||||
has_object: true,
|
||||
guid: MATE,
|
||||
..Default::default()
|
||||
};
|
||||
let mut after = before.clone();
|
||||
after.quest_log[4][0] = 783;
|
||||
let once = vec!["UNIT_QUEST_LOG_CHANGED:party1".to_string()];
|
||||
assert_eq!(fired(&before, &after, &[(MATE, LOG + 12)]), once);
|
||||
// One watch over the window: two slots moving in one pass is one event.
|
||||
assert_eq!(
|
||||
fired(&before, &after, &[(MATE, LOG), (MATE, LOG + 59)]),
|
||||
once
|
||||
);
|
||||
assert!(fired(&before, &after, &[(MATE, LOG - 1), (MATE, LOG + 60)]).is_empty());
|
||||
assert!(fired(&before, &after, &[(MATE + 1, LOG + 12)]).is_empty());
|
||||
}
|
||||
|
||||
/// LOST going down, GAINED going up (boot is in control); far sight on every change.
|
||||
#[test]
|
||||
fn the_control_and_far_sight_edges_fire_once_each_way() {
|
||||
@@ -3153,6 +3209,42 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// The quest-log window rides a player's snapshot whole, and a non-player's is all zero, as
|
||||
/// `IsUnitOnQuest`'s lookup is TYPEMASK_PLAYER (`0x4dfe97`).
|
||||
#[test]
|
||||
fn a_players_snapshot_carries_its_quest_log_window_and_a_creatures_none() {
|
||||
use benilla_protocol::messages::ObjectFields;
|
||||
|
||||
const OBJECT_TYPE: u16 = 2;
|
||||
const QUEST_LOG_1_1: u16 = 198;
|
||||
let log = [
|
||||
(QUEST_LOG_1_1, 783),
|
||||
(QUEST_LOG_1_1 + 1, 0x0100_0003),
|
||||
(QUEST_LOG_1_1 + 57, 7),
|
||||
];
|
||||
let snap = |type_bits: u32| {
|
||||
snapshot(
|
||||
&ObjectStore(ObjectFields::from_pairs(
|
||||
&[log.as_slice(), &[(OBJECT_TYPE, type_bits)]].concat(),
|
||||
)),
|
||||
0,
|
||||
None,
|
||||
0,
|
||||
None,
|
||||
Default::default(),
|
||||
)
|
||||
};
|
||||
let player = snap(0x19);
|
||||
assert_eq!(player.quest_log[0], [783, 0x0100_0003, 0]);
|
||||
assert_eq!(player.quest_log[19], [7, 0, 0]);
|
||||
assert_eq!(player.quest_log[1], [0, 0, 0]);
|
||||
assert_eq!(
|
||||
snap(0x09).quest_log,
|
||||
[[0; 3]; 20],
|
||||
"a creature has no PLAYER block"
|
||||
);
|
||||
}
|
||||
|
||||
/// The rank getter's two gates (`0x605620`), through `enrich_unit`'s wiring.
|
||||
#[test]
|
||||
fn the_rank_gate_zeroes_a_pet_or_charm() {
|
||||
|
||||
@@ -202,6 +202,17 @@ impl ObjectFields {
|
||||
timer,
|
||||
})
|
||||
}
|
||||
/// The whole `PLAYER_QUEST_LOG` window, `[id, count-state, timer]` per slot as the wire left
|
||||
/// it, 0 where absent: a group mate's carries the ids alone, the one dword per slot vmangos
|
||||
/// flags `GROUP_ONLY` (`UpdateFields_1_12_1.h:128-129`).
|
||||
pub fn player_quest_log_window(&self) -> [[u32; 3]; PLAYER_QUEST_LOG_SLOTS as usize] {
|
||||
std::array::from_fn(|slot| {
|
||||
std::array::from_fn(|k| {
|
||||
self.get_u32(FIELD_PLAYER_QUEST_LOG_1_1 + 3 * slot as u16 + k as u16)
|
||||
.unwrap_or(0)
|
||||
})
|
||||
})
|
||||
}
|
||||
/// `PLAYER_EXPLORED_ZONES_1 + i`: bitset dword `i`; bit `n` is `AreaTable` explore flag `n`.
|
||||
pub fn player_explored_zone_slot(&self, i: u16) -> u32 {
|
||||
if i >= PLAYER_EXPLORED_ZONES_SLOTS {
|
||||
|
||||
@@ -135,4 +135,17 @@ mod tests {
|
||||
let f = ObjectFields::from_pairs(&[(198, 0), (199, 0), (200, 0)]);
|
||||
assert_eq!(f.player_quest_log(0).map(|s| s.quest_id), Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quest_log_window_is_every_slot_raw_and_zero_where_absent() {
|
||||
// A group mate's view: ids alone in slots 0 and 19, the private dwords never sent; slot 2
|
||||
// carries a count-state dword, kept as sent rather than unpacked.
|
||||
let f = ObjectFields::from_pairs(&[(198, 783), (198 + 57, 7), (205, 0x0100_0005)]);
|
||||
let w = f.player_quest_log_window();
|
||||
assert_eq!(w.len(), usize::from(PLAYER_QUEST_LOG_SLOTS));
|
||||
assert_eq!(w[0], [783, 0, 0]);
|
||||
assert_eq!(w[1], [0, 0, 0]);
|
||||
assert_eq!(w[2], [0, 0x0100_0005, 0]);
|
||||
assert_eq!(w[19], [7, 0, 0]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
//! does (`0x4dfb50` copies the selection `0xbb7480`, a quest id, to `0xbb7484`), so a re-index
|
||||
//! before the popup's Yes cannot retarget them.
|
||||
//!
|
||||
//! Not built: `IsUnitOnQuest` and `GetAbandonQuestItems` answer nil; the first needs the party
|
||||
//! members' quest logs.
|
||||
//! `IsUnitOnQuest` reads a unit's quest-log window off its snapshot ([`super::UnitState`]).
|
||||
//!
|
||||
//! Not built: `GetAbandonQuestItems` answers nil.
|
||||
|
||||
use mlua::{Lua, MultiValue, Value};
|
||||
|
||||
@@ -598,9 +599,45 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
super::quest::reward_spell_returns(lua, spell)
|
||||
})?,
|
||||
)?;
|
||||
// `IsUnitOnQuest(index, "unit")` (`0x4dfe10`): 1 when the unit's quest log holds row
|
||||
// `index`'s quest, else nil; the stock rows count `[n]` mates (`QuestLogFrame.lua:175`).
|
||||
g.set(
|
||||
"IsUnitOnQuest",
|
||||
lua.create_function(|_, (_q, _unit): (Value, Value)| Ok(flag(false)))?,
|
||||
lua.create_function(|lua, (index, token): (Value, Value)| {
|
||||
const USAGE: &str = "Usage: IsUnitOnQuest(index, \"unit\")";
|
||||
// Both are checked before either is read: `lua_isnumber` (`0x4dfe1e`), `lua_isstring`
|
||||
// (`0x4dfe32`); the index is `_ftol`'d (`0x4dfe4d`).
|
||||
let n = number_arg(lua, index, USAGE)?;
|
||||
let token = super::binding_abi::string_arg(lua, token, USAGE)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
// The row's quest id (`0x4df150`): 0 for a header or a row past the list, which
|
||||
// answers nil before the token is resolved (`0x4dfe5e`).
|
||||
let quest_id = usize::try_from(n)
|
||||
.ok()
|
||||
.and_then(|n| n.checked_sub(1))
|
||||
.and_then(|row| model.quest_log.entries.get(row))
|
||||
.filter(|e| !e.is_header)
|
||||
.map_or(0, |e| e.quest_id);
|
||||
if quest_id == 0 {
|
||||
return Ok(Value::Nil);
|
||||
}
|
||||
// The token's guid (`0x515970`), which raises on an unknown token.
|
||||
super::unit::check_unit_token(&Some(token.clone()))?;
|
||||
let Some(unit) = model.unit(&token) else {
|
||||
return Ok(Value::Nil);
|
||||
};
|
||||
// The active player or one of the four party slots, by guid (`0x4e7f70`): a raid mate
|
||||
// outside our subgroup, a pet or a stranger answers nil.
|
||||
let me = model.unit("player").map_or(0, |p| p.guid);
|
||||
let grouped = unit.guid != 0
|
||||
&& (unit.guid == me || model.party.members.iter().any(|m| m.guid == unit.guid));
|
||||
// Held as a player (`0x468460`, typemask `0x10`) and not deactivated (`0x614ea0`, a
|
||||
// state the app's despawn never leaves held), then the 20 slots' ids, stride 12
|
||||
// (`0x4dfebd`-`0x4dfed7`); a non-player's window is all zero.
|
||||
Ok(flag(
|
||||
grouped && unit.has_object && unit.quest_log.iter().any(|s| s[0] == quest_id),
|
||||
))
|
||||
})?,
|
||||
)?;
|
||||
// ── The party share and the quest watch ──────────────────────────────────────────────────────
|
||||
/// The selected row, which the share verbs read.
|
||||
@@ -1412,4 +1449,197 @@ mod tests {
|
||||
"a header is not a quest"
|
||||
);
|
||||
}
|
||||
|
||||
// ── IsUnitOnQuest (`0x4dfe10`) ──
|
||||
|
||||
const ME: u64 = 0x10;
|
||||
const MATE: u64 = 0x300;
|
||||
const FAR_MATE: u64 = 0x301;
|
||||
|
||||
/// A held unit whose quest-log window holds `ids`, one per slot from the last slot down, so the
|
||||
/// scan must reach slot 19.
|
||||
fn held(guid: u64, ids: &[u32]) -> crate::script::UnitState {
|
||||
let mut quest_log = [[0; 3]; 20];
|
||||
for (slot, &id) in quest_log.iter_mut().rev().zip(ids) {
|
||||
*slot = [id, 0, 0];
|
||||
}
|
||||
crate::script::UnitState {
|
||||
exists: true,
|
||||
has_object: true,
|
||||
is_player: true,
|
||||
guid,
|
||||
quest_log,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// A header, then quest 783 (row 2) and quest 7 (row 3). We hold both; `party1` holds 783 and
|
||||
/// `party2` is out of range. `target` is `party1`; `mouseover` a stranger and `raid7` a raid
|
||||
/// mate outside our subgroup, both holding 783.
|
||||
fn on_quest_world() -> UiScript {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
let mut state = two_quests();
|
||||
state.entries.insert(
|
||||
0,
|
||||
QuestLogEntryView {
|
||||
title: "Elwynn Forest".into(),
|
||||
is_header: true,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
s.set_quest_log(state);
|
||||
s.set_party(crate::script::PartyState {
|
||||
members: vec![
|
||||
crate::script::PartyMemberInfo {
|
||||
name: "Mate".into(),
|
||||
guid: MATE,
|
||||
},
|
||||
crate::script::PartyMemberInfo {
|
||||
name: "Far".into(),
|
||||
guid: FAR_MATE,
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
});
|
||||
s.set_unit("player", Some(held(ME, &[7, 783])));
|
||||
s.set_unit("party1", Some(held(MATE, &[783])));
|
||||
s.set_unit(
|
||||
"party2",
|
||||
Some(crate::script::UnitState {
|
||||
exists: true,
|
||||
is_player: true,
|
||||
guid: FAR_MATE,
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
s.set_unit("target", Some(held(MATE, &[783])));
|
||||
s.set_unit("mouseover", Some(held(0x500, &[783])));
|
||||
s.set_unit("raid7", Some(held(0x700, &[783])));
|
||||
s.set_unit(
|
||||
"pet",
|
||||
Some(crate::script::UnitState {
|
||||
exists: true,
|
||||
has_object: true,
|
||||
guid: 0xF140_0000_0000_0001,
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
s
|
||||
}
|
||||
|
||||
/// The number 1 when the unit's log holds the row's quest (`0x4dfef5` pushes 1.0), else nil
|
||||
/// (`0x4dfedb`), never a boolean.
|
||||
#[test]
|
||||
fn is_unit_on_quest_answers_one_for_the_player_and_a_held_party_member_on_it() {
|
||||
let mut s = on_quest_world();
|
||||
let on = |s: &mut UiScript, call: &str| -> Option<i64> {
|
||||
s.eval::<Option<i64>>(&format!("return {call}")).unwrap()
|
||||
};
|
||||
assert!(s
|
||||
.eval::<bool>("return type(IsUnitOnQuest(2, 'party1')) == 'number'")
|
||||
.unwrap());
|
||||
assert_eq!(on(&mut s, "IsUnitOnQuest(2, 'party1')"), Some(1));
|
||||
assert_eq!(on(&mut s, "IsUnitOnQuest(3, 'party1')"), None, "not on 7");
|
||||
assert_eq!(on(&mut s, "IsUnitOnQuest(2, 'player')"), Some(1));
|
||||
assert_eq!(on(&mut s, "IsUnitOnQuest(3, 'player')"), Some(1));
|
||||
assert_eq!(
|
||||
on(&mut s, "IsUnitOnQuest(2, 'PARTY1')"),
|
||||
Some(1),
|
||||
"tokens fold case"
|
||||
);
|
||||
// The guid decides, not the token: `target` names party1 (`0x4e7f70`'s slot compare).
|
||||
assert_eq!(on(&mut s, "IsUnitOnQuest(2, 'target')"), Some(1));
|
||||
}
|
||||
|
||||
/// `0x4e7f70` admits the active player and the four party slots alone, and `0x468460` needs
|
||||
/// the object held: every other unit answers nil whatever its log holds.
|
||||
#[test]
|
||||
fn is_unit_on_quest_is_nil_off_the_party_or_out_of_range() {
|
||||
let mut s = on_quest_world();
|
||||
for token in [
|
||||
"party2", // in the party, not held
|
||||
"party3", // no such slot
|
||||
"mouseover", // held, on the quest, not grouped
|
||||
"raid7", // a raid mate outside our subgroup
|
||||
"pet", // not a player, no quest log
|
||||
"", // names nobody
|
||||
] {
|
||||
assert!(
|
||||
s.eval::<bool>(&format!("return IsUnitOnQuest(2, '{token}') == nil"))
|
||||
.unwrap(),
|
||||
"{token:?} answers nil"
|
||||
);
|
||||
}
|
||||
// The party stops at four slots of guids: a member who left reads nil off a stale log.
|
||||
s.set_party(crate::script::PartyState::default());
|
||||
assert!(s
|
||||
.eval::<bool>("return IsUnitOnQuest(2, 'target') == nil")
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
/// The index is `_ftol`'d and 1-based (`0x4dfe4d`, `dec` at `0x4dfe52`); a header or a row
|
||||
/// past the list has no quest id (`0x4df150`) and answers nil before the token is read, so an
|
||||
/// unknown token raises only on a quest row (`0x515c14`).
|
||||
#[test]
|
||||
fn is_unit_on_quest_reads_the_row_before_the_token() {
|
||||
let s = on_quest_world();
|
||||
for index in ["1", "0", "-1", "4", "99", "-0.5"] {
|
||||
assert!(
|
||||
s.eval::<bool>(&format!("return IsUnitOnQuest({index}, 'party1') == nil"))
|
||||
.unwrap(),
|
||||
"index {index}"
|
||||
);
|
||||
assert!(
|
||||
s.eval::<bool>(&format!("return IsUnitOnQuest({index}, 'bogus') == nil"))
|
||||
.unwrap(),
|
||||
"index {index} answers before the token is resolved"
|
||||
);
|
||||
}
|
||||
assert!(s
|
||||
.eval::<bool>("return IsUnitOnQuest(2.9, 'party1') == 1")
|
||||
.unwrap());
|
||||
assert!(s
|
||||
.eval::<bool>("return IsUnitOnQuest('2', 'party1') == 1")
|
||||
.unwrap());
|
||||
for (token, shown) in [("'bogus'", "bogus"), ("5", "5")] {
|
||||
let e = format!(
|
||||
"{:?}",
|
||||
s.eval::<mlua::Value>(&format!("return IsUnitOnQuest(2, {token})"))
|
||||
.unwrap_err()
|
||||
);
|
||||
assert!(
|
||||
e.contains(&format!("Unknown unit name: {shown}")),
|
||||
"{token}: {e}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// `lua_isnumber` on 1 and `lua_isstring` on 2, both ahead of any read (`0x4dfe1e`,
|
||||
/// `0x4dfe32`), raise the one Usage line (`0x84b564`), an out-of-range index included.
|
||||
#[test]
|
||||
fn is_unit_on_quest_raises_usage_on_a_bad_argument() {
|
||||
let s = on_quest_world();
|
||||
for args in [
|
||||
"",
|
||||
"2",
|
||||
"99",
|
||||
"nil, 'party1'",
|
||||
"'x', 'party1'",
|
||||
"{}, 'party1'",
|
||||
"2, nil",
|
||||
"2, {}",
|
||||
"2, true",
|
||||
] {
|
||||
let e = format!(
|
||||
"{:?}",
|
||||
s.eval::<mlua::Value>(&format!("return IsUnitOnQuest({args})"))
|
||||
.unwrap_err()
|
||||
);
|
||||
assert!(
|
||||
e.contains("Usage: IsUnitOnQuest(index, \\\"unit\\\")")
|
||||
|| e.contains("Usage: IsUnitOnQuest(index, \"unit\")"),
|
||||
"({args}) must raise Usage:, got {e}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,6 +200,11 @@ pub struct UnitState {
|
||||
/// `UnitAffectingCombat` (`0x517e10`): `UNIT_FIELD_FLAGS` bit `0x00080000`, for every token
|
||||
/// including `"player"`; the reference has no separate player combat flag.
|
||||
pub in_combat: bool,
|
||||
/// A held player's `PLAYER_QUEST_LOG` window, `[quest id, count-state, timer]` per slot, all
|
||||
/// zero off a player: the 20 slots of 12 bytes at `[player+0xe68]+0x28` that `IsUnitOnQuest`
|
||||
/// scans (`0x4dfebd`) and the `UNIT_QUEST_LOG_CHANGED` watch compares (`0x51bc63`). Whole, so
|
||||
/// a change anywhere in it is a snapshot change; a group mate's carries the ids alone.
|
||||
pub quest_log: [[u32; 3]; 20],
|
||||
}
|
||||
|
||||
/// The name of a unit whose name is not yet known, as the reference's `UnitName` (`0x517220`) and
|
||||
|
||||
Reference in New Issue
Block a user