Pet cooldowns flush at each packet's place, as 1.12.1's do (#2421)
In 1.12.1 the pet's cooldown list (list 1) flushes at each packet's own place. `PET_BAR_UPDATE_COOLDOWN` fires from five sites: `0x6e2e8e`, `0x6e3080`, `0x6e8601`, `0x6e95b9` and `0x6e971c`. The pet getters query that list live (`GetPetActionCooldown` at `0x4be03c`, plus the pet book and bar at `0x4b40dd` and `0x4e6cf7`). benilla flushed the pet's list once a frame and read it from the frame's final state. Now the pet's list logs and replays like the player's (#2414), on its own lane in packet order, and the three getters read it live. A failed `CAST_RESULT` fires its GCD-clear pair (`0x6e167d`) before the pop (`0x6e2218`), so the cast slot and the targeting word change only at the pop. The tests cover two pet packets in one frame, a player packet then a pet packet, and the GCD-clear pair seeing the cast still held. Each fails without its piece. Part of #2217 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -369,3 +369,94 @@ fn a_failed_cast_result_pops_the_slot_at_its_place_in_the_drain() {
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// A failed `SMSG_CAST_RESULT`'s report clears the GCD the send armed and fires its pair
|
||||
/// (`0x6e1d89` → `0x6e1630`, the pair at `0x6e167d`) before it pops the slot (`0x6e2218`): the
|
||||
/// pair's `SPELL_UPDATE_COOLDOWN` reads Heal still in flight, and the pop's
|
||||
/// `CURRENT_SPELL_CAST_CHANGED` and `SPELLCAST_FAILED` after it read it popped.
|
||||
#[test]
|
||||
fn a_failed_results_gcd_clear_reads_the_cast_still_in_flight() {
|
||||
let mut c = client();
|
||||
c.app.insert_resource(crate::ui_action::Spells {
|
||||
catalog: SpellCatalog::from_displays(HashMap::from([(
|
||||
HEAL,
|
||||
SpellDisplay {
|
||||
id: HEAL,
|
||||
name: "Heal".into(),
|
||||
icon: Some("Interface\\Icons\\Spell_Holy_Heal".into()),
|
||||
start_recovery_category: 133,
|
||||
start_recovery_ms: 1500,
|
||||
..Default::default()
|
||||
},
|
||||
)])),
|
||||
..crate::ui_action::Spells::empty_for_tests()
|
||||
});
|
||||
c.frame(vec![]);
|
||||
c.seen(&[]);
|
||||
cast_heal(&mut c);
|
||||
c.frame(vec![vec![SessionEvent::CastResult {
|
||||
spell_id: HEAL,
|
||||
success: false,
|
||||
reason: Some(0x0b),
|
||||
arg: None,
|
||||
}]]);
|
||||
assert_eq!(
|
||||
c.seen(&[
|
||||
"SPELL_UPDATE_COOLDOWN",
|
||||
"CURRENT_SPELL_CAST_CHANGED",
|
||||
"SPELLCAST_FAILED",
|
||||
]),
|
||||
[
|
||||
"SPELL_UPDATE_COOLDOWN:1:nil",
|
||||
"CURRENT_SPELL_CAST_CHANGED:nil:nil",
|
||||
"SPELLCAST_FAILED:nil:nil",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// `CancelCast 0x6e4940` drops the targeting word (`0x6e4955`) where it pops the slot
|
||||
/// (`0x6e49d6`), at the failed report's pop (`0x6e2218`), after the GCD clear's pair
|
||||
/// (`0x6e167d`): with the cursor up for Heal, the pair's `SPELL_UPDATE_COOLDOWN` reads the slot and
|
||||
/// the word both still up.
|
||||
#[test]
|
||||
fn a_failed_results_gcd_clear_reads_the_targeting_word_still_up() {
|
||||
let mut c = client();
|
||||
let heal = SpellDisplay {
|
||||
id: HEAL,
|
||||
name: "Heal".into(),
|
||||
start_recovery_category: 133,
|
||||
start_recovery_ms: 1500,
|
||||
..Default::default()
|
||||
};
|
||||
let world = c.app.world_mut();
|
||||
world.resource_mut::<crate::spell::Cooldowns>().start_gcd(
|
||||
HEAL,
|
||||
&heal,
|
||||
std::time::Instant::now(),
|
||||
&crate::spell::SpellModifiers::default(),
|
||||
);
|
||||
world.resource_mut::<crate::spell::SpellTargeting>().enter(
|
||||
HEAL,
|
||||
crate::spell::CastCommit::Spell,
|
||||
0x0002,
|
||||
);
|
||||
c.frame(vec![]);
|
||||
c.seen(&[]);
|
||||
c.frame(vec![vec![SessionEvent::CastResult {
|
||||
spell_id: HEAL,
|
||||
success: false,
|
||||
reason: Some(0x0b),
|
||||
arg: None,
|
||||
}]]);
|
||||
let seen = c.seen(&["SPELL_UPDATE_COOLDOWN", "CURRENT_SPELL_CAST_CHANGED"]);
|
||||
assert_eq!(
|
||||
seen.first().map(String::as_str),
|
||||
Some("SPELL_UPDATE_COOLDOWN:1:1"),
|
||||
"{seen:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
seen.last().map(String::as_str),
|
||||
Some("CURRENT_SPELL_CAST_CHANGED:nil:nil"),
|
||||
"{seen:?}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -75,6 +75,11 @@ impl Client {
|
||||
|
||||
/// The two spells' rows and the potion's spell.
|
||||
fn spells() -> crate::ui_action::Spells {
|
||||
spells_with(&[])
|
||||
}
|
||||
|
||||
/// [`spells`] and the rows of `more`.
|
||||
fn spells_with(more: &[u32]) -> crate::ui_action::Spells {
|
||||
let row = |id: u32| {
|
||||
(
|
||||
id,
|
||||
@@ -87,11 +92,13 @@ fn spells() -> crate::ui_action::Spells {
|
||||
)
|
||||
};
|
||||
crate::ui_action::Spells {
|
||||
catalog: SpellCatalog::from_displays(HashMap::from([
|
||||
row(FROST_NOVA),
|
||||
row(BLINK),
|
||||
row(POTION_SPELL),
|
||||
])),
|
||||
catalog: SpellCatalog::from_displays(
|
||||
[FROST_NOVA, BLINK, POTION_SPELL]
|
||||
.iter()
|
||||
.chain(more)
|
||||
.map(|&id| row(id))
|
||||
.collect::<HashMap<_, _>>(),
|
||||
),
|
||||
..crate::ui_action::Spells::empty_for_tests()
|
||||
}
|
||||
}
|
||||
@@ -401,3 +408,91 @@ fn a_worn_items_cooldown_reads_live_inside_its_packet() {
|
||||
assert_eq!(c.eval::<f64>(live, chunk), 30.0, "live {live}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The pet's list read inside the call, through the book and the bar: `GetSpellCooldown(id,
|
||||
/// "pet")` queries list 1 (`0x4b40dd` → `0x4b410f` → `0x6e2ea0`), and `GetActionCooldown` on a
|
||||
/// slot whose macro is bound to the pet's book likewise (`0x4e6cf7` → `0x4e6d0b`). Two pet
|
||||
/// `SMSG_SPELL_COOLDOWN`s in one frame: the first packet's `SPELL_UPDATE_COOLDOWN` reads Bite cold
|
||||
/// through both, the second's reads it running.
|
||||
#[test]
|
||||
fn the_pet_book_and_a_pet_bound_slot_read_the_pets_list_inside_their_packet() {
|
||||
use benilla_protocol::messages::{PetActionEntry, ACTION_KIND_MACRO, PET_ACT_ENABLED};
|
||||
const PET: u64 = 0xF130_0000_0000_0042;
|
||||
const GROWL: u32 = 2649;
|
||||
const BITE: u32 = 17253;
|
||||
let mut c = client();
|
||||
let world = c.app.world_mut();
|
||||
world.insert_resource(spells_with(&[GROWL, BITE]));
|
||||
let entry = |id: u32| PetActionEntry::from(id | (u32::from(PET_ACT_ENABLED) << 24));
|
||||
{
|
||||
let mut bar = world.resource_mut::<crate::ui_pet::PetBar>();
|
||||
bar.spells.pet_guid = PET;
|
||||
bar.spells.bar[0] = entry(GROWL);
|
||||
bar.spells.bar[1] = entry(BITE);
|
||||
bar.spells.spells = vec![entry(GROWL), entry(BITE)];
|
||||
}
|
||||
let pet = world
|
||||
.spawn((
|
||||
crate::net::Guid(PET),
|
||||
Transform::default(),
|
||||
crate::net::ObjectStore(ObjectFields::from_pairs(&[
|
||||
(OBJECT_TYPE, 0x9),
|
||||
(22, 100),
|
||||
(12, ME as u32),
|
||||
(13, (ME >> 32) as u32),
|
||||
])),
|
||||
))
|
||||
.id();
|
||||
world
|
||||
.resource_mut::<crate::net::GuidIndex>()
|
||||
.0
|
||||
.insert(PET, pet);
|
||||
{
|
||||
let mut slots = world.resource_mut::<crate::ui_action::ActionSlots>();
|
||||
slots.buttons.insert(
|
||||
3,
|
||||
ActionButton {
|
||||
slot: 3,
|
||||
action: 1,
|
||||
kind: ACTION_KIND_MACRO,
|
||||
},
|
||||
);
|
||||
slots.dirty = true;
|
||||
}
|
||||
c.frame(vec![]);
|
||||
c.frame(vec![]);
|
||||
c.app
|
||||
.world_mut()
|
||||
.resource_mut::<crate::ui_macro::MacroBoundSpells>()
|
||||
.0
|
||||
.insert(
|
||||
1,
|
||||
crate::ui_macro::Bound {
|
||||
spell: crate::ui_macro::BoundSpell::Spell(BITE),
|
||||
pet_book: true,
|
||||
},
|
||||
);
|
||||
c.frame(vec![]);
|
||||
c.eval::<()>(
|
||||
false,
|
||||
r#"local g = CreateFrame("Frame")
|
||||
g:RegisterEvent("SPELL_UPDATE_COOLDOWN")
|
||||
g:SetScript("OnEvent", function()
|
||||
local _, a = GetSpellCooldown(1, "pet")
|
||||
local _, b = GetSpellCooldown(2, "pet")
|
||||
local _, s = GetActionCooldown(4)
|
||||
table.insert(Seen, "PET:" .. tostring(a) .. ":" .. tostring(b) .. ":" .. tostring(s))
|
||||
end)"#,
|
||||
);
|
||||
c.seen(&[]);
|
||||
let pet_cooldown = |spell: u32, ms: u32| SessionEvent::SpellCooldowns {
|
||||
caster: PET,
|
||||
cooldowns: vec![(spell, ms)],
|
||||
};
|
||||
c.frame(vec![
|
||||
vec![pet_cooldown(GROWL, 3_000)],
|
||||
vec![pet_cooldown(BITE, 4_000)],
|
||||
]);
|
||||
// The book sorts Bite ahead of Growl.
|
||||
assert_eq!(c.seen(&["PET"]), ["PET:0:3:0", "PET:4:3:4"]);
|
||||
}
|
||||
|
||||
@@ -400,7 +400,6 @@ const HOST_HELD_UNREAD: &[&str] = &[
|
||||
"::ui_loot::AutoLootByte",
|
||||
"::ui_loot::LootLatch",
|
||||
"::ui_merchant::MerchantOpen",
|
||||
"::ui_pet::PetBar",
|
||||
"::ui_petition::GuildRegistrarState",
|
||||
"::ui_petition::PetitionRecords",
|
||||
"::ui_session::latch::InteractLatch",
|
||||
@@ -554,7 +553,6 @@ const READS_FRAME_FINAL: &[(&str, &[&str])] = &[
|
||||
"spell::mods::SpellModifiers", "target::Selection", "ui_action::PlayerActions"]),
|
||||
("::ui_pet::bar::feed_pet_bar", &["net::GuidIndex", "net::ObjectStore", "net::SelfGuid",
|
||||
"ui_pet::PetAttackLatch", "ui_pet::PetBar"]),
|
||||
("::ui_pet::bar::fire_pet_cooldown_events", &["ui_pet::PetBar"]),
|
||||
// The pet's snapshot reads its store as of the drain's place (`ui_unit::views`); its name
|
||||
// reads as the frame left it.
|
||||
("::ui_pet::unit::feed_pet_unit", &["names::NameCache", "net::GuidIndex", "net::ObjectStore",
|
||||
@@ -758,7 +756,6 @@ const READ_ONLY_TO_HANDLERS: &[&str] = &[
|
||||
"::BattlefieldQueue",
|
||||
"::DuelState",
|
||||
"::SummonState",
|
||||
"::PetBar",
|
||||
"::QuestLog",
|
||||
"::UiClock",
|
||||
"::InteractNpc",
|
||||
@@ -780,7 +777,7 @@ const READ_ONLY_TO_HANDLERS: &[&str] = &[
|
||||
];
|
||||
|
||||
/// Every lane's queue, by type: what a handler fills there fires in the packets' order.
|
||||
fn lane_types() -> [TypeId; 16] {
|
||||
fn lane_types() -> [TypeId; 17] {
|
||||
[
|
||||
TypeId::of::<crate::ui_items::feed::BagView>(),
|
||||
// The views lane: what each packet changed in the stores.
|
||||
@@ -802,8 +799,10 @@ fn lane_types() -> [TypeId; 16] {
|
||||
TypeId::of::<crate::target::Deaths>(),
|
||||
// The item-text lane: the page-text packets' reads, stamped at their packets.
|
||||
TypeId::of::<crate::ui_item_text::ItemTextLane>(),
|
||||
// The cooldown lane: the player's list, which logs its writes and flushes at their places.
|
||||
// The cooldown lanes: the player's list and the pet's, which log their writes and flushes
|
||||
// at their places.
|
||||
TypeId::of::<crate::spell::Cooldowns>(),
|
||||
TypeId::of::<crate::ui_pet::PetBar>(),
|
||||
// The cast views lane: the view each packet left, stamped ahead of the packet.
|
||||
TypeId::of::<crate::ui_cast::CastViews>(),
|
||||
]
|
||||
@@ -811,7 +810,7 @@ fn lane_types() -> [TypeId; 16] {
|
||||
|
||||
/// [`lane_types`] by the lane that drains each: a queue is stripped only from its own lane's row,
|
||||
/// so another lane's read of the same type shows as the state it is.
|
||||
fn lane_queue_owners() -> [(&'static str, TypeId); 16] {
|
||||
fn lane_queue_owners() -> [(&'static str, TypeId); 17] {
|
||||
[
|
||||
("bag view", TypeId::of::<crate::ui_items::feed::BagView>()),
|
||||
("unit views", TypeId::of::<crate::ui_unit::views::ViewLog>()),
|
||||
@@ -840,6 +839,7 @@ fn lane_queue_owners() -> [(&'static str, TypeId); 16] {
|
||||
TypeId::of::<crate::ui_item_text::ItemTextLane>(),
|
||||
),
|
||||
("cooldowns", TypeId::of::<crate::spell::Cooldowns>()),
|
||||
("pet cooldowns", TypeId::of::<crate::ui_pet::PetBar>()),
|
||||
("cast views", TypeId::of::<crate::ui_cast::CastViews>()),
|
||||
]
|
||||
}
|
||||
@@ -1006,6 +1006,7 @@ fn the_drain_runs_after_the_feeds_and_before_the_tick() {
|
||||
"list rebuilds",
|
||||
"loot",
|
||||
"party",
|
||||
"pet cooldowns",
|
||||
"unit views"
|
||||
]
|
||||
);
|
||||
|
||||
@@ -153,14 +153,16 @@ impl CooldownInfo {
|
||||
|
||||
/// One cooldown list, the client's `SpellHistory`: the player's is this resource (`0xcecaec`), the
|
||||
/// pet's is [`crate::ui_pet::PetBar::cooldowns`] (`0xcecb04`), which the pet bar, the pet book and
|
||||
/// the pet's `SMSG_SPELL_GO` leg read and write. The player's list signals at each flush site in
|
||||
/// its place on the cooldown lane ([`log`]); the pet's, which keeps no log, through its counters,
|
||||
/// once a frame (`ui_pet::bar::fire_pet_cooldown_events`).
|
||||
/// the pet's `SMSG_SPELL_GO` leg read and write. Each signals at each flush site in its place on
|
||||
/// its lane ([`log`]).
|
||||
#[derive(Resource, Default)]
|
||||
pub(crate) struct Cooldowns {
|
||||
records: Vec<Record>,
|
||||
/// The frame's writes and signals, which the drain replays (the player's list alone).
|
||||
/// The frame's writes and signals, which the drain replays.
|
||||
log: CooldownLog,
|
||||
/// The pet's list (`0xcecb04`, the list index 1 every site tests): its sites end in the pet's
|
||||
/// flush `0x4bce90` where the player's call the bags' or nothing.
|
||||
pet: bool,
|
||||
/// Bumped by [`Self::flush`], `0x4b31b0`. Only counts up, across [`Self::clear_silent`] too,
|
||||
/// so a watcher reads a reset as no mutation.
|
||||
pub(crate) generation: u64,
|
||||
@@ -209,10 +211,10 @@ struct PendingItem {
|
||||
}
|
||||
|
||||
impl Cooldowns {
|
||||
/// A list that keeps no log, the pet's: its watcher reads its counters once a frame.
|
||||
pub(crate) fn unlogged() -> Self {
|
||||
/// The pet's list.
|
||||
pub(crate) fn pet() -> Self {
|
||||
Self {
|
||||
log: CooldownLog::new(false),
|
||||
pet: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -223,9 +225,7 @@ impl Cooldowns {
|
||||
write.apply(&mut self.records);
|
||||
}
|
||||
|
||||
/// `0x4b31b0`, the cooldown pair: `ACTIONBAR_UPDATE_COOLDOWN` and `SPELL_UPDATE_COOLDOWN`. On
|
||||
/// the pet's list its watcher adds `PET_BAR_UPDATE_COOLDOWN`, the `0x4bce90` each pet site
|
||||
/// calls after it.
|
||||
/// `0x4b31b0`, the cooldown pair: `ACTIONBAR_UPDATE_COOLDOWN` and `SPELL_UPDATE_COOLDOWN`.
|
||||
pub(crate) fn flush(&mut self) {
|
||||
self.generation = self.generation.wrapping_add(1);
|
||||
self.log.signalled(Signal::Pair);
|
||||
@@ -236,12 +236,27 @@ impl Cooldowns {
|
||||
self.log.signalled(Signal::Usable);
|
||||
}
|
||||
|
||||
/// `0x4f93d0`, `BAG_UPDATE_COOLDOWN`, on the player's list; on the pet's, its flush
|
||||
/// `0x4bce90` in its place (`0x6e3080`, `0x6e971c`).
|
||||
fn flush_bags(&mut self) {
|
||||
self.bag_flushes = self.bag_flushes.wrapping_add(1);
|
||||
self.log.signalled(Signal::Bags);
|
||||
if self.pet {
|
||||
self.log.signalled(Signal::Pet);
|
||||
} else {
|
||||
self.bag_flushes = self.bag_flushes.wrapping_add(1);
|
||||
self.log.signalled(Signal::Bags);
|
||||
}
|
||||
}
|
||||
|
||||
/// `0x4b31b0` then `0x4f93d0`, the pair every item leg calls.
|
||||
/// `0x4bce90`, `PET_BAR_UPDATE_COOLDOWN`, the tail of a site that calls the bags' for neither
|
||||
/// list: the pet's alone (`0x6e2e8e`, `0x6e8601`, `0x6e95b9`).
|
||||
fn flush_pet(&mut self) {
|
||||
if self.pet {
|
||||
self.log.signalled(Signal::Pet);
|
||||
}
|
||||
}
|
||||
|
||||
/// `0x4b31b0` then `0x4f93d0`, the pair every item leg calls, and the cheat's (`0x6e9712`,
|
||||
/// then `0x6e9721` or the pet's flush at `0x6e971c`).
|
||||
pub(crate) fn flush_item_leg(&mut self) {
|
||||
self.flush();
|
||||
self.flush_bags();
|
||||
@@ -249,7 +264,7 @@ impl Cooldowns {
|
||||
|
||||
/// `0x6e3050`'s tail after its walk, whether or not a record matched: the pair (`0x6e3071`),
|
||||
/// the usable refresh (`0x6e3076`), then the bags for the player (`0x6e3089`) or the pet's flush
|
||||
/// for the pet (`0x6e3080`), which reads [`Self::generation`].
|
||||
/// for the pet (`0x6e3080`).
|
||||
fn flush_records(&mut self) {
|
||||
self.flush();
|
||||
self.refresh_usable();
|
||||
@@ -418,7 +433,10 @@ impl Cooldowns {
|
||||
0,
|
||||
0,
|
||||
);
|
||||
// The self leg's pair (`0x6e849d`), or the pet leg's, then its flush (`0x6e85fc`,
|
||||
// `0x6e8601`).
|
||||
self.flush();
|
||||
self.flush_pet();
|
||||
}
|
||||
|
||||
/// The go's item leg when the item's record names the spell (`0x6e8511`-`0x6e8566`), then
|
||||
@@ -642,6 +660,8 @@ impl Cooldowns {
|
||||
if spell.cooldown_on_event() {
|
||||
self.refresh_usable();
|
||||
}
|
||||
// The pet's list's flush last (`0x6e2e8a`-`0x6e2e8e`).
|
||||
self.flush_pet();
|
||||
}
|
||||
|
||||
/// `ClearGlobalCooldown 0x6e1630`, on a failed cast: every record of the spell matches
|
||||
@@ -736,6 +756,8 @@ impl Cooldowns {
|
||||
}
|
||||
}
|
||||
self.flush();
|
||||
// The pet's list's flush (`0x6e95b5`-`0x6e95b9`).
|
||||
self.flush_pet();
|
||||
}
|
||||
|
||||
/// One `SMSG_SPELL_COOLDOWN` entry, started at the packet:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! The cooldown getters' query at the call, the call host's half of
|
||||
//! [`benilla_ui::script::CooldownQuery`]: the player's list as of the drain's place (the cooldown
|
||||
//! lane replays it packet by packet, [`super::log`]), each item found as of there too
|
||||
//! [`benilla_ui::script::CooldownQuery`]: the player's list and the pet's as of the drain's place
|
||||
//! (their cooldown lanes replay them packet by packet, [`super::log`]), each item found as of there too
|
||||
//! ([`crate::ui_unit::views::ViewStores`]), each answer derived as the feeds derive their
|
||||
//! snapshots.
|
||||
|
||||
@@ -16,6 +16,7 @@ use crate::query_cache::Ask;
|
||||
#[derive(SystemParam)]
|
||||
pub(crate) struct CooldownLive<'w, 's> {
|
||||
cooldowns: Option<Res<'w, Cooldowns>>,
|
||||
pet: Option<Res<'w, crate::ui_pet::PetBar>>,
|
||||
spells: Option<Res<'w, crate::ui_action::Spells>>,
|
||||
items: Option<Res<'w, crate::items::Items>>,
|
||||
/// The frame's clock pair every pushed start is mapped through, as the action feed last read
|
||||
@@ -29,9 +30,24 @@ impl CooldownLive<'_, '_> {
|
||||
/// `query`'s answer as of here; `None` for an item object not held then, and in a harness
|
||||
/// without the list, the items or the clock.
|
||||
pub(crate) fn read(&self, query: CooldownQuery) -> Option<Option<CooldownTriple>> {
|
||||
let (cooldowns, items) = (self.cooldowns.as_deref()?, self.items.as_deref()?);
|
||||
let (anchor, ui_now) = self.clock.as_deref()?.clock()?;
|
||||
let catalog = self.spells.as_deref().map(|s| &s.catalog);
|
||||
// `0x6e2ea0` on list 1 with item 0, as the pet bar's feed reads it.
|
||||
if let CooldownQuery::PetSpell(spell) = query {
|
||||
return Some(
|
||||
self.pet
|
||||
.as_deref()?
|
||||
.cooldowns
|
||||
.info(
|
||||
spell,
|
||||
0,
|
||||
catalog.and_then(|c| c.get(spell)),
|
||||
std::time::Instant::now(),
|
||||
)
|
||||
.ui_triple(anchor, ui_now),
|
||||
);
|
||||
}
|
||||
let (cooldowns, items) = (self.cooldowns.as_deref()?, self.items.as_deref()?);
|
||||
let objects = self.stores.item_objects(&self.objects);
|
||||
match query {
|
||||
// `0x6e2ea0`: the spell with item 0, as the bar's and the book's pushes read it.
|
||||
@@ -82,6 +98,7 @@ impl CooldownLive<'_, '_> {
|
||||
.flatten(),
|
||||
)
|
||||
}
|
||||
CooldownQuery::PetSpell(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
//! The player's list as each packet left it. In the reference each cooldown packet's handler
|
||||
//! writes the list and signals inside itself (`0x6e9460` adds its records, then `0x4b31b0` at
|
||||
//! `0x6e95b0`), and the getters query the list inside their calls (`0x6e2ea0` → `0x6e13e0`), so a
|
||||
//! listener reads its own packet's records. Here every handler of the frame writes the list before
|
||||
//! any listener runs, so each write is logged with its stamp, as are the flushes' signals
|
||||
//! ([`Signal`]); the cooldown lane (`ui_action::cooldown_lane`) rewinds the list to the frame's
|
||||
//! start and replays the log in stamp order among the other lanes, firing each signal at its
|
||||
//! place. A write made during the drain, a listener's cast arming its global cooldown, lands at
|
||||
//! Each list as each packet left it, the player's and the pet's. In the reference each cooldown
|
||||
//! packet's handler writes its list and signals inside itself (`0x6e9460` adds its records, then
|
||||
//! `0x4b31b0` at `0x6e95b0`, and for the pet's list `0x4bce90` at `0x6e95b9`), and the getters
|
||||
//! query the list inside their calls (`0x6e2ea0` → `0x6e13e0`, the pet's through `0x4be03c`), so
|
||||
//! a listener reads its own packet's records. Here every handler of the frame writes the lists
|
||||
//! before any listener runs, so each write is logged with its stamp, as are the flushes' signals
|
||||
//! ([`Signal`]); each list's lane (`ui_action::cooldown_lane`) rewinds it to the frame's start and
|
||||
//! replays its log in stamp order among the other lanes, firing each signal at its place. A write made during the drain, a listener's cast arming its global cooldown, lands at
|
||||
//! its own place and is not logged; its signal is, stamped under the item firing, and fires
|
||||
//! straight after it.
|
||||
|
||||
@@ -85,6 +85,8 @@ pub(crate) enum Signal {
|
||||
Usable,
|
||||
/// `0x4f93d0`: `BAG_UPDATE_COOLDOWN`.
|
||||
Bags,
|
||||
/// `0x4bce90`: `PET_BAR_UPDATE_COOLDOWN`, the pet's list's tail in place of the bags.
|
||||
Pet,
|
||||
}
|
||||
|
||||
/// One logged item.
|
||||
@@ -94,13 +96,10 @@ enum Entry {
|
||||
Signal(Signal),
|
||||
}
|
||||
|
||||
/// A list's log of the frame's writes and signals ([`super::Cooldowns`]), which only the
|
||||
/// player's list keeps: the pet's list signals its own watcher once a frame
|
||||
/// (`crate::ui_pet::bar`).
|
||||
#[derive(Debug)]
|
||||
/// A list's log of the frame's writes and signals ([`super::Cooldowns`]), the player's and the
|
||||
/// pet's alike.
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct CooldownLog {
|
||||
/// The list keeps the log.
|
||||
on: bool,
|
||||
/// The records before the log's first write.
|
||||
start: Option<Vec<Record>>,
|
||||
/// The writes and signals not yet replayed, in stamp order.
|
||||
@@ -109,24 +108,7 @@ pub(crate) struct CooldownLog {
|
||||
replaying: bool,
|
||||
}
|
||||
|
||||
impl Default for CooldownLog {
|
||||
/// The player's list's: one that keeps.
|
||||
fn default() -> Self {
|
||||
Self::new(true)
|
||||
}
|
||||
}
|
||||
|
||||
impl CooldownLog {
|
||||
/// A log that keeps the list's writes and signals, or one that keeps nothing.
|
||||
pub(super) fn new(on: bool) -> Self {
|
||||
Self {
|
||||
on,
|
||||
start: None,
|
||||
entries: VecDeque::new(),
|
||||
replaying: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert `entry` at `at` behind everything stamped before it: one pushed while an item fires
|
||||
/// is stamped under that item and goes ahead of the later ones.
|
||||
fn push(&mut self, at: Stamp, entry: Entry) {
|
||||
@@ -136,7 +118,7 @@ impl CooldownLog {
|
||||
|
||||
/// Log `write`, made on `records`, unless the drain is replaying.
|
||||
pub(super) fn wrote(&mut self, records: &[Record], write: &Write) {
|
||||
if !self.on || self.replaying {
|
||||
if self.replaying {
|
||||
return;
|
||||
}
|
||||
if self.start.is_none() {
|
||||
@@ -147,9 +129,7 @@ impl CooldownLog {
|
||||
|
||||
/// Log `signal`, the replay's included: a listener's flush fires after its cause.
|
||||
pub(super) fn signalled(&mut self, signal: Signal) {
|
||||
if self.on {
|
||||
self.push(crate::lanes::stamp(), Entry::Signal(signal));
|
||||
}
|
||||
self.push(crate::lanes::stamp(), Entry::Signal(signal));
|
||||
}
|
||||
|
||||
/// The lane's head: the rewind, at the origin, while the log holds what the drain has not
|
||||
|
||||
@@ -397,11 +397,12 @@ fn an_awaited_record_flushes_its_running_cooldown_once_it_lands() {
|
||||
/// the pair (`0x6e95b0`); `SMSG_ITEM_COOLDOWN` the pair and the bags (`0x6e9656`, `0x6e965b`);
|
||||
/// the event and the clear the pair, the usable refresh and the bags (`0x6e3071`, `0x6e3076`,
|
||||
/// `0x6e3089`); the cheat the pair and the bags (`0x6e9712`, `0x6e9721`); a held global cooldown
|
||||
/// the pair and the refresh (`0x6e2e77`, `0x6e2e85`). The pet's list logs nothing: its watcher
|
||||
/// reads its counters.
|
||||
/// the pair and the refresh (`0x6e2e77`, `0x6e2e85`). The pet's list ends each site in its flush
|
||||
/// `0x4bce90`, in the bags' place or after the pair and the refresh (`0x6e95b9`, `0x6e3080`,
|
||||
/// `0x6e971c`, `0x6e2e8e`, the GO's pet leg `0x6e8601`).
|
||||
#[test]
|
||||
fn each_flush_site_logs_its_signals_in_the_references_order() {
|
||||
use super::Signal::{Bags, Pair, Usable};
|
||||
use super::Signal::{Bags, Pair, Pet, Usable};
|
||||
let now = Instant::now();
|
||||
let mods = SpellModifiers::default();
|
||||
let mut cds = Cooldowns::default();
|
||||
@@ -432,11 +433,35 @@ fn each_flush_site_logs_its_signals_in_the_references_order() {
|
||||
site(&|c| c.start_gcd(1784, &held, now, &mods)),
|
||||
[Pair, Usable]
|
||||
);
|
||||
let mut pet = Cooldowns::unlogged();
|
||||
pet.apply_spell_cooldowns([(122, 0, Some(&nova))], &mods, CategoryPad::Skip, now);
|
||||
pet.wipe();
|
||||
assert!(pet.logged_signals().is_empty());
|
||||
assert_eq!(pet.generation, 2, "its watcher's counter moves");
|
||||
let mut pet = Cooldowns::pet();
|
||||
let mut pet_site = |f: &dyn Fn(&mut Cooldowns)| {
|
||||
let before = pet.logged_signals().len();
|
||||
f(&mut pet);
|
||||
pet.logged_signals()[before..].to_vec()
|
||||
};
|
||||
assert_eq!(
|
||||
pet_site(&|c| c.apply_spell_cooldowns(
|
||||
[(122, 0, Some(&nova))],
|
||||
&mods,
|
||||
CategoryPad::Skip,
|
||||
now
|
||||
)),
|
||||
[Pair, Pet]
|
||||
);
|
||||
assert_eq!(
|
||||
pet_site(&|c| c.cooldown_event(122, now)),
|
||||
[Pair, Usable, Pet]
|
||||
);
|
||||
assert_eq!(pet_site(&|c| c.clear_spell(122)), [Pair, Usable, Pet]);
|
||||
assert_eq!(pet_site(&|c| c.wipe()), [Pair, Pet]);
|
||||
assert_eq!(
|
||||
pet_site(&|c| c.start_gcd(1784, &held, now, &mods)),
|
||||
[Pair, Usable, Pet]
|
||||
);
|
||||
assert_eq!(
|
||||
pet_site(&|c| c.start_spell(122, &nova, 0, now, None)),
|
||||
[Pair, Pet]
|
||||
);
|
||||
}
|
||||
|
||||
/// The drain rewinds the list to the frame's start and replays each write at its stamp: a read
|
||||
|
||||
@@ -274,6 +274,11 @@ impl CastCore<'_> {
|
||||
}
|
||||
match self.slot() {
|
||||
Some(slot) if slot.spell_id() == spell_id => {
|
||||
// The server's report pops the slot here (`0x6e2218`), after the GCD clear's pair
|
||||
// its packet signalled (`0x6e167d`), which reads the cast still in flight.
|
||||
if from_server {
|
||||
crate::ui_cast::slot_moves_here();
|
||||
}
|
||||
self.cancel(true, !from_server, reason);
|
||||
}
|
||||
Some(_) => {}
|
||||
|
||||
@@ -1,51 +1,80 @@
|
||||
//! The cooldown lane: the player's list replayed packet by packet, and each flush's events fired
|
||||
//! at its place among the other lanes' items ([`crate::lanes`]). In the reference each cooldown
|
||||
//! packet's handler writes the list and signals inside itself (`SMSG_SPELL_COOLDOWN` `0x6e9460`
|
||||
//! calls `0x4b31b0` at `0x6e95b0`; `SMSG_ITEM_COOLDOWN` `0x6e95d0` calls `0x4b31b0` and `0x4f93d0`
|
||||
//! at `0x6e9656` and `0x6e965b`; the event and the clear call `0x6e3050`, which signals the pair,
|
||||
//! the usable refresh and the bags at `0x6e3071`, `0x6e3076` and `0x6e3089`; the cheat calls
|
||||
//! `0x6e9700`), so its listeners read its own records through the getters' queries (`0x6e2ea0`,
|
||||
//! `0x6e2ed0`). The list logs its writes and its signals as the handlers make them
|
||||
//! ([`crate::spell::Cooldowns`]); the lane rewinds it to the frame's start, replays each write at
|
||||
//! its stamp, and fires each signal there with the host lent.
|
||||
//! The cooldown lanes: each list replayed packet by packet, and each flush's events fired at its
|
||||
//! place among the other lanes' items ([`crate::lanes`]). In the reference each cooldown packet's
|
||||
//! handler writes its list and signals inside itself (`SMSG_SPELL_COOLDOWN` `0x6e9460` calls
|
||||
//! `0x4b31b0` at `0x6e95b0`, and for the pet's list `0x4bce90` at `0x6e95b9`; `SMSG_ITEM_COOLDOWN`
|
||||
//! `0x6e95d0` calls `0x4b31b0` and `0x4f93d0` at `0x6e9656` and `0x6e965b`; the event and the
|
||||
//! clear call `0x6e3050`, which signals the pair, the usable refresh, then the bags or the pet's
|
||||
//! flush at `0x6e3071`, `0x6e3076` and `0x6e3089` or `0x6e3080`; the cheat calls `0x6e9700`, the
|
||||
//! pair, then the bags or the pet's flush), so its listeners read its own records through the
|
||||
//! getters' queries (`0x6e2ea0`, `0x6e2ed0`; the pet bar's `0x6e2ea0` on list 1 at `0x4be03c`).
|
||||
//! Each list logs its writes and its signals as the handlers make them
|
||||
//! ([`crate::spell::Cooldowns`]); its lane rewinds it to the frame's start, replays each write at
|
||||
//! its stamp, and fires each signal there with the host lent. The player's list is its resource,
|
||||
//! the pet's [`crate::ui_pet::PetBar::cooldowns`].
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
use crate::lanes::{LaneApp, Until};
|
||||
use crate::spell::{Cooldowns, Signal};
|
||||
use crate::ui_pet::PetBar;
|
||||
|
||||
/// The lane, its end and the drop under the exact complement of the drain's gate.
|
||||
/// The resource that holds a cooldown list.
|
||||
pub(crate) trait HoldsList: Resource {
|
||||
fn list_mut(&mut self) -> &mut Cooldowns;
|
||||
}
|
||||
|
||||
impl HoldsList for Cooldowns {
|
||||
fn list_mut(&mut self) -> &mut Cooldowns {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl HoldsList for PetBar {
|
||||
fn list_mut(&mut self) -> &mut Cooldowns {
|
||||
&mut self.cooldowns
|
||||
}
|
||||
}
|
||||
|
||||
/// The two lanes, the player's ahead of the pet's at a tie, their ends and the drops under the
|
||||
/// exact complement of the drain's gate.
|
||||
pub(super) fn add_cooldown_lane(app: &mut App) {
|
||||
app.init_resource::<Cooldowns>()
|
||||
.add_lane(
|
||||
"cooldowns",
|
||||
|world| world.get_resource::<Cooldowns>()?.lane_head(),
|
||||
fire_cooldowns,
|
||||
fire_cooldowns::<Cooldowns>,
|
||||
None,
|
||||
)
|
||||
.add_drain_end(end_cooldowns)
|
||||
.add_lane(
|
||||
"pet cooldowns",
|
||||
|world| world.get_resource::<PetBar>()?.cooldowns.lane_head(),
|
||||
fire_cooldowns::<PetBar>,
|
||||
None,
|
||||
)
|
||||
.add_drain_end(end_cooldowns::<Cooldowns>)
|
||||
.add_drain_end(end_cooldowns::<PetBar>)
|
||||
.add_systems(
|
||||
Update,
|
||||
drop_cooldown_log
|
||||
(drop_cooldown_log::<Cooldowns>, drop_cooldown_log::<PetBar>)
|
||||
.in_set(crate::ui_script::UiFeed)
|
||||
.run_if(not(crate::ui_script::ingame_ui_up)),
|
||||
);
|
||||
}
|
||||
|
||||
/// The lane's firing: the list rewound to the frame's start, once per drain, at the origin and
|
||||
/// A lane's firing: the list rewound to the frame's start, once per drain, at the origin and
|
||||
/// alone, so every other lane's start is pushed before a signal fires; then each write the drain
|
||||
/// admits replayed and each signal fired at its place with the host lent, the usable refresh
|
||||
/// through the action states ([`crate::script_calls::VmWith::engine_usable_refresh`]). It stops
|
||||
/// after a signal whose firing stamped anything, so what that pushed fires next.
|
||||
fn fire_cooldowns(
|
||||
fn fire_cooldowns<R: HoldsList>(
|
||||
In(until): In<Until>,
|
||||
mut vm: crate::script_calls::VmWith<ResMut<'static, Cooldowns>>,
|
||||
mut vm: crate::script_calls::VmWith<ResMut<'static, R>>,
|
||||
) {
|
||||
if vm.own().rewind() {
|
||||
if vm.own().list_mut().rewind() {
|
||||
return;
|
||||
}
|
||||
loop {
|
||||
let Some((at, signal)) = vm.own().replay(until) else {
|
||||
let Some((at, signal)) = vm.own().list_mut().replay(until) else {
|
||||
return;
|
||||
};
|
||||
let ((), moved) = crate::lanes::firing(at, || match signal {
|
||||
@@ -61,6 +90,11 @@ fn fire_cooldowns(
|
||||
script.fire_event("BAG_UPDATE_COOLDOWN", vec![]);
|
||||
}
|
||||
}),
|
||||
Signal::Pet => vm.run(|script| {
|
||||
if let Some(script) = script {
|
||||
script.fire_event("PET_BAR_UPDATE_COOLDOWN", vec![]);
|
||||
}
|
||||
}),
|
||||
});
|
||||
if moved {
|
||||
return;
|
||||
@@ -69,18 +103,20 @@ fn fire_cooldowns(
|
||||
}
|
||||
|
||||
/// The drain's end, however it ended: the list as the frame left it.
|
||||
fn end_cooldowns(world: &mut World) {
|
||||
if let Some(mut cooldowns) = world.get_resource_mut::<Cooldowns>() {
|
||||
cooldowns.end_replay();
|
||||
fn end_cooldowns<R: HoldsList>(world: &mut World) {
|
||||
if let Some(mut holder) = world.get_resource_mut::<R>() {
|
||||
holder.list_mut().end_replay();
|
||||
}
|
||||
}
|
||||
|
||||
/// What the packets wrote while no in-game interface is up, left unreplayed: no VM hears it.
|
||||
fn drop_cooldown_log(mut cooldowns: ResMut<Cooldowns>) {
|
||||
cooldowns.drop_log();
|
||||
fn drop_cooldown_log<R: HoldsList>(holder: Option<ResMut<R>>) {
|
||||
if let Some(mut holder) = holder {
|
||||
holder.list_mut().drop_log();
|
||||
}
|
||||
}
|
||||
|
||||
/// The lane fired alone with no host, for a test that drives the list without the drain: each
|
||||
/// The player's lane fired alone with no host, for a test that drives the list without the drain: each
|
||||
/// signal fired on the VM, the usable refresh run on the action states directly
|
||||
/// ([`super::run_watcher_refresh_once`]), then the drain's end.
|
||||
#[cfg(test)]
|
||||
@@ -102,5 +138,5 @@ pub(crate) fn fire_cooldown_lane(world: &mut World) {
|
||||
script.fire_event("BAG_UPDATE_COOLDOWN", vec![]);
|
||||
}
|
||||
}
|
||||
end_cooldowns(world);
|
||||
end_cooldowns::<Cooldowns>(world);
|
||||
}
|
||||
|
||||
@@ -52,10 +52,8 @@ pub(crate) mod toggle;
|
||||
mod unlearn;
|
||||
mod weapon_icon;
|
||||
|
||||
/// Every feed that pushes cooldown triples runs `.before` this set: the pet list's flush,
|
||||
/// `ui_pet::bar::fire_pet_cooldown_events`, runs `.after` it and fires the pet's events
|
||||
/// synchronously, so a triple pushed later goes unread until the next cooldown change. The player's
|
||||
/// list fires on the cooldown lane, after every feed ([`cooldown_lane`]).
|
||||
/// Every feed that pushes cooldown triples runs `.before` this set. Both lists' flushes fire on
|
||||
/// their cooldown lanes, after every feed ([`cooldown_lane`]).
|
||||
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub(crate) struct CooldownEvents;
|
||||
|
||||
|
||||
@@ -197,6 +197,7 @@ struct PetBookLeg<'a> {
|
||||
fn pet_book_state(leg: PetBookLeg) -> ActionState {
|
||||
let mut st = ActionState {
|
||||
is_attack: leg.d.is_melee_auto_attack(),
|
||||
pet_spell: Some(leg.spell),
|
||||
..Default::default()
|
||||
};
|
||||
if let (Some(player), Some(pet)) = (leg.player, leg.pet) {
|
||||
@@ -364,6 +365,12 @@ impl ActionStateMemory {
|
||||
pub(crate) fn clock(&self) -> Option<(Instant, f64)> {
|
||||
self.clock
|
||||
}
|
||||
|
||||
/// The pair a feed would have read, for a rig that runs no action feed.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn set_clock(&mut self, clock: (Instant, f64)) {
|
||||
self.clock = Some(clock);
|
||||
}
|
||||
}
|
||||
|
||||
/// One pass over the slots: when it runs, and which of the refreshes it is.
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::ui_action::{CastFail, CastFailLines, MessageSink, Shown, Spells};
|
||||
use crate::ui_unit::UnitFeed;
|
||||
|
||||
mod views;
|
||||
pub(crate) use views::CastViews;
|
||||
pub(crate) use views::{slot_moves_here, CastViews};
|
||||
|
||||
/// One edge of our own cast, queued by the spell's packet handlers, its reports and its cancels.
|
||||
pub(crate) enum CastBarEdge {
|
||||
|
||||
@@ -134,6 +134,22 @@ impl CastViews {
|
||||
self.shown = Some(view);
|
||||
}
|
||||
|
||||
/// `view` logged at `at`, when it differs from the one before it.
|
||||
fn log(&mut self, at: Stamp, view: CastView) {
|
||||
let Some(last) = self.last else {
|
||||
return;
|
||||
};
|
||||
if last == view {
|
||||
return;
|
||||
}
|
||||
if self.entries.is_empty() && !self.started && self.start.is_none() {
|
||||
self.start = Some(last);
|
||||
}
|
||||
let i = self.entries.partition_point(|(s, _)| *s < at);
|
||||
self.entries.insert(i, (at, view));
|
||||
self.last = Some(view);
|
||||
}
|
||||
|
||||
/// A logged `view` into the VM, with the fields the drain's calls moved as they stand live.
|
||||
fn replay(&mut self, script: &mut UiScript, view: CastView) {
|
||||
let view = self.called.over(view, self.live.unwrap_or(view));
|
||||
@@ -159,9 +175,25 @@ fn live_view(world: &mut World) -> Option<CastView> {
|
||||
view
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
/// Where the packet being handled moves the in-flight slot, past what it raised before
|
||||
/// ([`slot_moves_here`]).
|
||||
static SLOT_MOVES: std::cell::Cell<Option<Stamp>> = const { std::cell::Cell::new(None) };
|
||||
}
|
||||
|
||||
/// The packet being handled moves the in-flight slot here, after what its handler raised so far:
|
||||
/// a failed `SMSG_CAST_RESULT`'s report fires the GCD clear's pair (`0x6e1d89` → `0x6e167d`)
|
||||
/// before it pops the slot (`0x6e2218`), so that pair's listeners read the cast still in flight.
|
||||
/// The packet's view then lands in two steps: everything but the slot and the targeting word,
|
||||
/// which `CancelCast 0x6e4940` drops with it (`0x6e4955`), ahead of the packet, and those here.
|
||||
pub(crate) fn slot_moves_here() {
|
||||
SLOT_MOVES.set(Some(crate::lanes::stamp()));
|
||||
}
|
||||
|
||||
/// The net stage's start: the view as the frame's input left it, which the first packet that
|
||||
/// moves it is compared with.
|
||||
fn packets_begin(world: &mut World) {
|
||||
SLOT_MOVES.set(None);
|
||||
let view = live_view(world);
|
||||
if let Some(mut views) = world.get_resource_mut::<CastViews>() {
|
||||
views.last = view;
|
||||
@@ -169,8 +201,10 @@ fn packets_begin(world: &mut World) {
|
||||
}
|
||||
|
||||
/// A packet handled: its view logged at `at`, the stamp taken ahead of its handler, when it
|
||||
/// differs from the one before it.
|
||||
/// differs from the one before it; with its slot at the stamp its handler moved it, when it
|
||||
/// named one ([`slot_moves_here`]).
|
||||
fn packet_end(world: &mut World, at: Stamp) {
|
||||
let moves = SLOT_MOVES.take().filter(|s| *s > at);
|
||||
let Some(view) = live_view(world) else {
|
||||
return;
|
||||
};
|
||||
@@ -180,15 +214,20 @@ fn packet_end(world: &mut World, at: Stamp) {
|
||||
let Some(last) = views.last else {
|
||||
return;
|
||||
};
|
||||
if last == view {
|
||||
return;
|
||||
if let Some(moves) = moves {
|
||||
// `CancelCast 0x6e4940` drops the targeting word (`0x6e4955`) and pops the slot
|
||||
// (`0x6e49d6`) at the pop's place.
|
||||
let ahead = CastView {
|
||||
slot: last.slot,
|
||||
item: last.item,
|
||||
targeting: last.targeting,
|
||||
..view
|
||||
};
|
||||
views.log(at, ahead);
|
||||
views.log(moves, view);
|
||||
} else {
|
||||
views.log(at, view);
|
||||
}
|
||||
if views.entries.is_empty() && !views.started && views.start.is_none() {
|
||||
views.start = Some(last);
|
||||
}
|
||||
let i = views.entries.partition_point(|(s, _)| *s < at);
|
||||
views.entries.insert(i, (at, view));
|
||||
views.last = Some(view);
|
||||
}
|
||||
|
||||
/// The lane's head: its start, at the origin, while the frame logged a view; then its next entry.
|
||||
|
||||
@@ -24,7 +24,7 @@ pub(super) struct PetBarMemory {
|
||||
slots: Vec<PetActionView>,
|
||||
state: Option<u32>,
|
||||
/// The ten cooldown triples as last pushed, so a cooldown alone is pushed with no event: its
|
||||
/// events are [`fire_pet_cooldown_events`]'s.
|
||||
/// events are the list's flush, on the pet's cooldown lane (`crate::ui_action::cooldown_lane`).
|
||||
cooldowns: Vec<Option<(i64, u32, bool)>>,
|
||||
}
|
||||
|
||||
@@ -222,8 +222,8 @@ pub(crate) fn active_aura_press(
|
||||
}
|
||||
|
||||
/// Push the ten slot views on a change, and `PET_BAR_UPDATE` when the bar itself moved. A change
|
||||
/// of the cooldowns alone is pushed without an event: the pet's cooldown events fire off the
|
||||
/// list's generation, so a natural expiry fires none, as in the reference.
|
||||
/// of the cooldowns alone is pushed without an event: the pet's cooldown events are its list's
|
||||
/// flushes, so a natural expiry fires none, as in the reference.
|
||||
pub(super) fn feed_pet_bar(
|
||||
mut vm: crate::script_calls::VmWith<(
|
||||
Res<'static, PetBar>,
|
||||
@@ -362,51 +362,3 @@ pub(super) fn feed_pet_bar(
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// The flush behind every mutation of the pet's cooldown list: `0x4b31b0` fires
|
||||
/// `ACTIONBAR_UPDATE_COOLDOWN` (`0x4e5c60`, event `0xd8`) then `SPELL_UPDATE_COOLDOWN` (`0x106`),
|
||||
/// and `0x4bce90` fires `PET_BAR_UPDATE_COOLDOWN` (`0x162`). Its callers are the pet's
|
||||
/// `SMSG_SPELL_GO` leg (`0x6e85fc`), `StartGlobalCooldown` for the list (`0x6e2e77`, `0x6e2e8e`),
|
||||
/// `SMSG_SPELL_COOLDOWN` for the pet's guid (`0x6e95b0`, `0x6e95b9`), `SMSG_COOLDOWN_EVENT` and
|
||||
/// `SMSG_CLEAR_COOLDOWN` (`0x6e3071`, `0x6e3080`) and `SMSG_COOLDOWN_CHEAT` (`0x6e9712`,
|
||||
/// `0x6e971c`); each is a change of [`Cooldowns::generation`](crate::spell::Cooldowns), and one
|
||||
/// frame's changes fire it once. Between the two, the list's own usable refresh `0x4b31c0`
|
||||
/// (`0x6e2e85`, `0x6e3076`) signals `SPELL_UPDATE_USABLE` on a change of
|
||||
/// [`Cooldowns::usable_refreshes`](crate::spell::Cooldowns). `SMSG_PET_SPELLS` seeds without it,
|
||||
/// so the events never fire for a rebuild. Runs after
|
||||
/// [`CooldownEvents`](crate::ui_action::CooldownEvents): every feed that pushes a pet cooldown,
|
||||
/// the bar's and the book's, has pushed by then, so the handlers read the new list.
|
||||
pub(super) fn fire_pet_cooldown_events(
|
||||
mut vm: crate::script_calls::VmWith<Res<'static, PetBar>>,
|
||||
mut memory: Local<
|
||||
crate::ui_script::VmMemo<(crate::ui_script::gate::Watch, crate::ui_script::gate::Watch)>,
|
||||
>,
|
||||
) {
|
||||
let (generation, usable_refreshes) = {
|
||||
let bar = vm.own();
|
||||
(bar.cooldowns.generation, bar.cooldowns.usable_refreshes)
|
||||
};
|
||||
vm.run(|script| {
|
||||
let Some(script) = script else {
|
||||
return;
|
||||
};
|
||||
// A new VM starts from the list as it stands: the reference flushes on a mutation, not at
|
||||
// load.
|
||||
let ((flushes, refreshes), fresh) = memory.get_reset(script);
|
||||
let flushed = flushes.moved(generation);
|
||||
let refreshed = refreshes.moved(usable_refreshes);
|
||||
if fresh || !(flushed || refreshed) {
|
||||
return;
|
||||
}
|
||||
if flushed {
|
||||
script.fire_event("ACTIONBAR_UPDATE_COOLDOWN", vec![]);
|
||||
script.fire_event("SPELL_UPDATE_COOLDOWN", vec![]);
|
||||
}
|
||||
if refreshed {
|
||||
script.fire_event("SPELL_UPDATE_USABLE", vec![]);
|
||||
}
|
||||
if flushed {
|
||||
script.fire_event("PET_BAR_UPDATE_COOLDOWN", vec![]);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -358,9 +358,8 @@ impl PetPress<'_, '_> {
|
||||
/// The spell arm's tail (`0x4bd367`-`0x4bd36e`) for a press past the usability gate whose
|
||||
/// `Spell.dbc` record resolved and that neither cancels an aura nor takes the attempt: it calls
|
||||
/// `StartGlobalCooldown 0x6e2de0(spellId, 1)` before the send (`0x4bd444`), the spell's own
|
||||
/// `StartRecovery*` pair, under op 21, into the pet's list (`0xcecaec + 0x18`). The insert moves
|
||||
/// the list's generation, which carries the flush that follows it (`0x6e2e77`, `0x6e2e8e`) to
|
||||
/// `bar::fire_pet_cooldown_events`.
|
||||
/// `StartRecovery*` pair, under op 21, into the pet's list (`0xcecaec + 0x18`), and the flush
|
||||
/// that follows it (`0x6e2e77`, `0x6e2e8e`), at its place on the pet's cooldown lane.
|
||||
///
|
||||
/// The pet's GCD starts in three places, and this arm is one. A `SMSG_SPELL_COOLDOWN` addressed
|
||||
/// to the pet's guid inserts the same pair for every entry whose spell lacks `Attributes` bit 25
|
||||
|
||||
@@ -232,20 +232,62 @@ fn the_pets_event_and_clear_refresh_the_usable_state_inside_the_flush() {
|
||||
assert_eq!(rig.frame(), FLUSH, "the cheat on an empty list");
|
||||
}
|
||||
|
||||
/// The frame's mutations are one flush for the frame, as the player's list has them.
|
||||
/// Each packet's handler flushes inside itself (`0x6e95b0`, `0x6e95b9`): two pet cooldown packets
|
||||
/// in one frame fire the flush twice, and each `PET_BAR_UPDATE_COOLDOWN` listener's
|
||||
/// `GetPetActionCooldown` (`0x4be03c` → `0x6e2ea0` on the pet's list) reads the list as its own
|
||||
/// packet left it: Claw cold to the first, running to the second.
|
||||
#[test]
|
||||
fn several_mutations_in_a_frame_fire_the_flush_once() {
|
||||
fn each_pet_cooldown_packet_flushes_and_reads_its_own_list() {
|
||||
let mut rig = rig(
|
||||
vec![(GROWL, growl()), (CLAW, claw())],
|
||||
&[GROWL, CLAW],
|
||||
Some(summoned_pet()),
|
||||
);
|
||||
ui(&mut rig)
|
||||
.run(
|
||||
"READ = {} local f = CreateFrame('Frame') f:RegisterEvent('PET_BAR_UPDATE_COOLDOWN') \
|
||||
f:SetScript('OnEvent', function() local _, a = GetPetActionCooldown(1) \
|
||||
local _, b = GetPetActionCooldown(2) \
|
||||
table.insert(READ, tostring(a) .. ':' .. tostring(b)) end)",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
rig.receive(vec![
|
||||
SessionEvent::SpellCooldowns {
|
||||
caster: PET,
|
||||
cooldowns: vec![(GROWL, 3000)],
|
||||
},
|
||||
SessionEvent::SpellCooldowns {
|
||||
caster: PET,
|
||||
cooldowns: vec![(CLAW, 4000)],
|
||||
},
|
||||
]);
|
||||
|
||||
assert_eq!(rig.frame(), [FLUSH, FLUSH].concat());
|
||||
assert_eq!(
|
||||
ui(&mut rig).eval::<Vec<String>>("return READ").unwrap(),
|
||||
["3:0", "3:4"]
|
||||
);
|
||||
}
|
||||
|
||||
/// The pet's flush falls in packet order with the player's: a player's `SMSG_SPELL_COOLDOWN`
|
||||
/// ahead of the pet's signals its pair first, then the pet's packet its pair and its flush.
|
||||
#[test]
|
||||
fn the_pets_flush_falls_in_packet_order_with_the_players() {
|
||||
let mut rig = growl_rig();
|
||||
|
||||
rig.receive(vec![
|
||||
go(PET, GROWL),
|
||||
SessionEvent::SpellCooldowns {
|
||||
caster: ME,
|
||||
cooldowns: vec![(GROWL, 3000)],
|
||||
},
|
||||
SessionEvent::SpellCooldowns {
|
||||
caster: PET,
|
||||
cooldowns: vec![(GROWL, 3000)],
|
||||
},
|
||||
]);
|
||||
|
||||
assert_eq!(rig.frame(), FLUSH);
|
||||
assert_eq!(rig.frame(), [&FLUSH[..2], &FLUSH[..]].concat());
|
||||
}
|
||||
|
||||
/// `SMSG_PET_SPELLS` seeds the list through `0x6e2c60`, which flushes nothing, and ends in
|
||||
@@ -350,60 +392,6 @@ fn a_new_vm_is_not_flushed_for_a_list_that_already_moved() {
|
||||
assert_eq!(rig.frame(), FLUSH, "the next mutation is a flush");
|
||||
}
|
||||
|
||||
/// The flush reads what the feeds pushed: it runs after the bar's feed and the book's, which
|
||||
/// push the pet's cooldown triples the handlers ask for, and (declared through the cooldown
|
||||
/// events) after the player's own flush. Skips where bevy carries no system names.
|
||||
#[test]
|
||||
fn the_flush_runs_after_every_feed_that_pushes_a_pet_cooldown() {
|
||||
use crate::game_plugins::schedule_tests::{census, headless_client, SyncPoints};
|
||||
use bevy::prelude::Update;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
let mut app = headless_client();
|
||||
let c = census(&mut app, Update, SyncPoints::Declared);
|
||||
let named = |needle: &str| {
|
||||
let hits: Vec<_> = c
|
||||
.systems
|
||||
.iter()
|
||||
.filter(|(_, s)| s.name.ends_with(needle))
|
||||
.map(|(k, _)| *k)
|
||||
.collect();
|
||||
(!hits.is_empty()).then(|| {
|
||||
assert_eq!(hits.len(), 1, "one system named {needle}");
|
||||
hits[0]
|
||||
})
|
||||
};
|
||||
let Some(fire) = named("::fire_pet_cooldown_events") else {
|
||||
eprintln!("skipped: this build carries no system names");
|
||||
return;
|
||||
};
|
||||
let mut after: HashMap<_, Vec<_>> = HashMap::new();
|
||||
for (a, b) in &c.dependencies {
|
||||
after.entry(*a).or_default().push(*b);
|
||||
}
|
||||
let runs_before = |first| {
|
||||
let (mut seen, mut stack) = (HashSet::new(), vec![first]);
|
||||
while let Some(n) = stack.pop() {
|
||||
if n == fire {
|
||||
return true;
|
||||
}
|
||||
if seen.insert(n) {
|
||||
stack.extend(after.get(&n).into_iter().flatten().copied());
|
||||
}
|
||||
}
|
||||
false
|
||||
};
|
||||
for feed in [
|
||||
"::feed_pet_bar",
|
||||
"::feed_pet_book",
|
||||
"::feed_action_state",
|
||||
"::fire_action_events",
|
||||
] {
|
||||
let key = named(feed).unwrap_or_else(|| panic!("{feed} is scheduled"));
|
||||
assert!(runs_before(key), "{feed} runs before the pet's flush");
|
||||
}
|
||||
}
|
||||
|
||||
/// The stock spellbook (`SpellBookFrame.lua:209`-`231`): a `SpellButton` redraws on
|
||||
/// `SPELL_UPDATE_COOLDOWN`, and on `PET_BAR_UPDATE` while the pet tab is open, and never hears
|
||||
/// `PET_BAR_UPDATE_COOLDOWN`. So the GCD a pet-bar press starts sweeps the pet tab's button
|
||||
@@ -567,7 +555,7 @@ fn an_inserts_prune_fires_only_the_inserts_own_flush() {
|
||||
for _ in 0..5 {
|
||||
rig.press(1);
|
||||
}
|
||||
assert_eq!(rig.frame(), FLUSH, "the presses' own flush");
|
||||
assert_eq!(rig.frame(), FLUSH.repeat(5), "each press's own flush");
|
||||
let held = rig.generation();
|
||||
let_the_gcds_run_out();
|
||||
let pressed = Instant::now();
|
||||
@@ -611,7 +599,11 @@ fn an_insert_changes_no_bar_read_of_the_records_that_stay() {
|
||||
for _ in 0..5 {
|
||||
rig.press(1);
|
||||
}
|
||||
assert_eq!(rig.frame(), FLUSH);
|
||||
assert_eq!(
|
||||
rig.frame(),
|
||||
FLUSH.repeat(7),
|
||||
"the go, the packet and each press"
|
||||
);
|
||||
let (running, parked_read) = (rig.cooldown(2), rig.cooldown(3));
|
||||
assert_eq!(running.1, 5.0, "Growl sweeps its 5 s");
|
||||
assert!(
|
||||
|
||||
@@ -25,7 +25,7 @@ mod usable;
|
||||
// The predicate `0x4bcf70` over what the caller already holds, for the action bar's pet legs.
|
||||
pub(crate) use usable::actions_usable as pet_actions_usable;
|
||||
|
||||
use bar::{feed_pet_bar, fire_pet_cooldown_events};
|
||||
use bar::feed_pet_bar;
|
||||
// A pet bar press and the pet's other verbs, applied in call order by `crate::script_calls`.
|
||||
pub(crate) use drain::call_pet_off;
|
||||
pub(crate) use drain::{cast_pet_press, PetPress, PetStep};
|
||||
@@ -60,8 +60,8 @@ pub(crate) struct PetBar {
|
||||
pub(crate) spells: PetSpells,
|
||||
/// The pet's own cooldowns, the list at `0xcecb04`. Seeded from `SMSG_PET_SPELLS`, topped up
|
||||
/// by `SMSG_SPELL_COOLDOWN` for the pet's guid, by the pet's own `SMSG_SPELL_GO` and by a
|
||||
/// press's GCD. Each mutation but the seed fires the flush, off [`Cooldowns::generation`]
|
||||
/// (`bar::fire_pet_cooldown_events`), once a frame: it keeps no log.
|
||||
/// press's GCD. Each mutation but the seed fires the flush at its place, on the pet's cooldown
|
||||
/// lane (`crate::ui_action::cooldown_lane`).
|
||||
pub(crate) cooldowns: Cooldowns,
|
||||
/// `PET_BAR_UPDATE` signals, counted. Both state writes signal unconditionally (`0x4bc940`,
|
||||
/// `0x4bc960`), and `OnClick`'s `SetChecked(0)` needs that repaint to relight a press on the
|
||||
@@ -94,11 +94,11 @@ impl PetAttackLatch {
|
||||
}
|
||||
|
||||
impl Default for PetBar {
|
||||
/// No bar, and the pet's list, which keeps no log ([`Cooldowns::unlogged`]).
|
||||
/// No bar, and the pet's list ([`Cooldowns::pet`]).
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
spells: PetSpells::default(),
|
||||
cooldowns: Cooldowns::unlogged(),
|
||||
cooldowns: Cooldowns::pet(),
|
||||
bar_signals: 0,
|
||||
book_sorts: 0,
|
||||
expires: None,
|
||||
@@ -177,15 +177,11 @@ impl Plugin for UiPetPlugin {
|
||||
.before(feed_pet_bar),
|
||||
// After the pet snapshot: these feeds' events reach Lua at once, and their
|
||||
// handlers read `HasPetUI()`, which `crate::ui_pet_stats` pushes. The bar pushes
|
||||
// its cooldown triples before the cooldown events and the pet's flush fires after
|
||||
// them, so the buttons it wakes read the new list.
|
||||
// its cooldown triples before the cooldown events, as every cooldown feed does.
|
||||
feed_pet_bar
|
||||
.in_set(UnitFeed)
|
||||
.after(crate::ui_pet_stats::PetSnapshot)
|
||||
.before(CooldownEvents),
|
||||
fire_pet_cooldown_events
|
||||
.in_set(UnitFeed)
|
||||
.after(CooldownEvents),
|
||||
feed_pet_unit
|
||||
.in_set(UnitFeed)
|
||||
.in_set(crate::ui_unit::field_events::FieldEventPushes)
|
||||
|
||||
@@ -20,7 +20,7 @@ use crate::net::{ClientCommand, Guid, GuidIndex, NetCommands, ObjectStore, SelfG
|
||||
use crate::spell::Cooldowns;
|
||||
use crate::ui_action::Spells;
|
||||
|
||||
use super::bar::{feed_pet_bar, fire_pet_cooldown_events};
|
||||
use super::bar::feed_pet_bar;
|
||||
use super::drain::PetPress;
|
||||
use super::PetBar;
|
||||
|
||||
@@ -178,14 +178,14 @@ pub(super) fn rig(
|
||||
}
|
||||
world.insert_non_send_resource(listening_vm());
|
||||
world.init_resource::<super::PetBarFeed>();
|
||||
// The order the plugin gives them: the feeds that push a pet cooldown, then the flush.
|
||||
// The order the plugin gives them: the feeds that push a pet cooldown; the flush is the pet's
|
||||
// cooldown lane's, in the drain.
|
||||
let mut feed = Schedule::default();
|
||||
feed.add_systems(
|
||||
(
|
||||
super::drain::arm_owed_pet_gcds,
|
||||
crate::ui_pet_book::feed_pet_book,
|
||||
feed_pet_bar,
|
||||
fire_pet_cooldown_events,
|
||||
)
|
||||
.chain(),
|
||||
);
|
||||
@@ -204,6 +204,15 @@ impl Rig {
|
||||
/// The bar's feeds for one frame, then the lanes' drain, and the events they fired.
|
||||
pub(super) fn frame(&mut self) -> Vec<String> {
|
||||
self.feed.run(self.app.world_mut());
|
||||
// The frame's clock pair, as the action feed hands it to the call host's cooldown reads.
|
||||
let clock = {
|
||||
let c = self.app.world().resource::<crate::ui_script::UiClock>();
|
||||
(c.anchor, c.ui_now)
|
||||
};
|
||||
self.app
|
||||
.world_mut()
|
||||
.resource_mut::<crate::ui_action::ActionStateMemory>()
|
||||
.set_clock(clock);
|
||||
crate::lanes::fire_in_order(self.app.world_mut());
|
||||
let mut script = self
|
||||
.app
|
||||
|
||||
@@ -112,8 +112,8 @@ pub(crate) fn feed_pet_book(
|
||||
// The push signals nothing: the book's `SPELLS_CHANGED` is its packet's sort
|
||||
// (`0x4bdadb` → `0x4b2fd0`, `crate::ui_pet`'s handler), on the notice lane after this
|
||||
// push. A cooldown repaints the pet page through the flush's `SPELL_UPDATE_COOLDOWN`
|
||||
// (`SpellBookFrame.lua:209`, `221`), which `ui_pet`'s `fire_pet_cooldown_events` fires
|
||||
// after this feed's push; autocast, through `PET_BAR_UPDATE` (`227-231`).
|
||||
// (`SpellBookFrame.lua:209`, `221`), which the pet's cooldown lane fires after every
|
||||
// feed; autocast, through `PET_BAR_UPDATE` (`227-231`).
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -101,6 +101,9 @@ pub struct ActionState {
|
||||
/// The entry an item slot holds, which `IsCurrentAction`'s item arm compares with the in-flight
|
||||
/// cast's item (`0x4e53fc`-`0x4e5428`); `None` for any other slot.
|
||||
pub item: Option<u32>,
|
||||
/// The pet's book spell a macro slot is bound to (`0x4e5a50`'s pet flag), whose cooldown is
|
||||
/// queried on the pet's list (`0x4e6d04` → `0x6e2ea0`); `None` for any other slot.
|
||||
pub pet_spell: Option<u32>,
|
||||
/// `(start_ms, duration_ms, enabled)`, the start absolute on the `GetTime` clock, so a running
|
||||
/// cooldown re-pushes the same triple and a re-arm a new one (`0x6e13e0` returns the start).
|
||||
pub cooldown: Option<(i64, u32, bool)>,
|
||||
@@ -649,8 +652,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// a slot that reaches no query, an empty one included, answers `(0, 0, 0)`, and one that
|
||||
// reaches it with nothing running `(0, 0, 1)`, so a later `CooldownFrame_SetTimer` replays no
|
||||
// sweep and no finish flash. The query reads the list inside the call (`0x4e6ca0`: `0x6e2ea0`
|
||||
// for a spell at `0x4e6d0b`, `0x6e2ed0` for an item at `0x4e6ce9`), through the lent host; a slot bound to the
|
||||
// pet's book reads the pet's list off the snapshot.
|
||||
// for a spell at `0x4e6d0b`, `0x6e2ed0` for an item at `0x4e6ce9`), through the lent host; a
|
||||
// slot bound to the pet's book queries the pet's list, the resolve's flag in `edx`
|
||||
// (`0x4e6cf7` → `0x4e6d04`).
|
||||
g.set(
|
||||
"GetActionCooldown",
|
||||
lua.create_function(|lua, action: Value| {
|
||||
@@ -666,10 +670,11 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
else {
|
||||
return Ok((0.0, 0.0, 0));
|
||||
};
|
||||
let query = match (state.state.spell, state.state.item) {
|
||||
(Some(spell), _) => Some(super::CooldownQuery::Spell(spell)),
|
||||
(None, Some(entry)) => Some(super::CooldownQuery::Item(entry)),
|
||||
(None, None) => None,
|
||||
let query = match (state.state.spell, state.state.item, state.state.pet_spell) {
|
||||
(Some(spell), _, _) => Some(super::CooldownQuery::Spell(spell)),
|
||||
(None, Some(entry), _) => Some(super::CooldownQuery::Item(entry)),
|
||||
(None, None, Some(spell)) => Some(super::CooldownQuery::PetSpell(spell)),
|
||||
(None, None, None) => None,
|
||||
};
|
||||
(query, state.cooldown)
|
||||
};
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
//! The cooldown getters' query of the player's list inside the call. `GetActionCooldown`
|
||||
//! (`0x4e6ed0` → `0x4e6ca0`), `GetSpellCooldown` (`0x4b40a0`), `GetContainerItemCooldown`
|
||||
//! (`0x4f99b0`) and `GetInventoryItemCooldown` (`0x4c8a60`) ask the list as it stands
|
||||
//! (`GetSpellCooldown 0x6e2ea0` and `GetItemCooldown 0x6e2ed0`, both through `GetCooldownInfo
|
||||
//! 0x6e13e0`), so a listener reads its own packet's records. Through the lent host when one is
|
||||
//! The cooldown getters' query of a list inside the call. `GetActionCooldown` (`0x4e6ed0` →
|
||||
//! `0x4e6ca0`), `GetSpellCooldown` (`0x4b40a0`), `GetContainerItemCooldown` (`0x4f99b0`) and
|
||||
//! `GetInventoryItemCooldown` (`0x4c8a60`) ask the player's list as it stands, and
|
||||
//! `GetPetActionCooldown` (`0x4bdfa0`) the pet's (`GetSpellCooldown 0x6e2ea0` and `GetItemCooldown
|
||||
//! 0x6e2ed0`, both through `GetCooldownInfo 0x6e13e0`), so a listener reads its own packet's
|
||||
//! records. Through the lent host when one is
|
||||
//! free ([`crate::script::CallHost::read_cooldown`]); with none, the frame's snapshot answers.
|
||||
|
||||
use mlua::Lua;
|
||||
|
||||
/// What a getter asks of the player's list.
|
||||
/// What a getter asks of a list.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum CooldownQuery {
|
||||
/// `0x6e2ea0` on the player's list: the spell, with item 0.
|
||||
@@ -21,6 +22,8 @@ pub enum CooldownQuery {
|
||||
/// `0x6e2ed0` for the item the active player wears in descriptor slot `slot0`, the paper
|
||||
/// doll's (`0x4c8b3a`), likewise.
|
||||
Worn(u8),
|
||||
/// `0x6e2ea0` on the pet's list (`edx = 1`, `0x4be037`): a pet bar slot's spell.
|
||||
PetSpell(u32),
|
||||
}
|
||||
|
||||
/// `(start_ms on the GetTime clock, duration_ms, enabled)`, the shape the snapshots push.
|
||||
|
||||
@@ -504,8 +504,20 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// `is-number` else `Usage: GetPetActionCooldown(index)` (`0x8478d8`, `0x4bdfb7`).
|
||||
let i = binding_abi::index_arg32(lua, i, "Usage: GetPetActionCooldown(index)")?;
|
||||
let now = crate::script::clock::now(lua);
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
Ok(match slot_at(&model, i).and_then(|s| s.cooldown) {
|
||||
let (spell, snapshot) = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
let slot = slot_at(&model, i);
|
||||
(
|
||||
slot.and_then(|s| s.view.spell_id),
|
||||
slot.and_then(|s| s.cooldown),
|
||||
)
|
||||
};
|
||||
// A spell slot's query reads the pet's list inside the call (`0x4be03c` →
|
||||
// `0x6e2ea0`), through the lent host; with none, the frame's snapshot answers.
|
||||
let live = spell.and_then(|spell| {
|
||||
super::cooldown_read::read_live(lua, super::CooldownQuery::PetSpell(spell))
|
||||
});
|
||||
Ok(match live.unwrap_or(snapshot) {
|
||||
Some((start, duration, enabled)) if start + duration > now || !enabled => (
|
||||
crate::script::clock::held_start(start, enabled, now),
|
||||
duration,
|
||||
|
||||
@@ -454,28 +454,30 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
|
||||
// GetSpellCooldown(id, bookType) -> start, duration, enable in `GetTime` seconds, as
|
||||
// `GetActionCooldown`: enable 0 for an on-hold cooldown; an expired or empty one answers the
|
||||
// reference's no-cooldown `(0, 0, 1)`, so a re-feed cannot replay the finish flash. A player's
|
||||
// book slot queries the list inside the call (`0x4b410f` → `0x6e2ea0`), through the lent host;
|
||||
// the pet's book reads the pet's list off the snapshot.
|
||||
// reference's no-cooldown `(0, 0, 1)`, so a re-feed cannot replay the finish flash. The slot
|
||||
// queries its book's list inside the call (`0x4b410f` → `0x6e2ea0`, the book type in `edx`
|
||||
// from `0x4b40dd`), through the lent host.
|
||||
g.set(
|
||||
"GetSpellCooldown",
|
||||
lua.create_function(|lua, (id, book_type): (Value, Value)| {
|
||||
let (id, book_type) = spell_slot_args(id, book_type, "GetSpellCooldown")?;
|
||||
let now = crate::script::clock::now(lua);
|
||||
let (spell, snapshot) = {
|
||||
let (query, snapshot) = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
let slot = book_slot(&model, id, &book_type);
|
||||
let spell = slot
|
||||
.filter(|_| !is_pet_book(&book_type))
|
||||
.map(|s| s.spell_id);
|
||||
let query = slot.map(|s| {
|
||||
if is_pet_book(&book_type) {
|
||||
super::CooldownQuery::PetSpell(s.spell_id)
|
||||
} else {
|
||||
super::CooldownQuery::Spell(s.spell_id)
|
||||
}
|
||||
});
|
||||
let snapshot = slot
|
||||
.and_then(|s| s.cooldown)
|
||||
.map(super::cooldown_read::seconds);
|
||||
(spell, snapshot)
|
||||
(query, snapshot)
|
||||
};
|
||||
let live = spell.and_then(|spell| {
|
||||
super::cooldown_read::read_live(lua, super::CooldownQuery::Spell(spell))
|
||||
});
|
||||
let live = query.and_then(|q| super::cooldown_read::read_live(lua, q));
|
||||
Ok(match live.unwrap_or(snapshot) {
|
||||
Some((start, duration, enabled)) if start + duration > now || !enabled => (
|
||||
crate::script::clock::held_start(start, enabled, now),
|
||||
|
||||
@@ -30,6 +30,7 @@ fn action_state_bindings_answer_the_reference_conventions() {
|
||||
is_attack: false,
|
||||
equipped: false,
|
||||
item: None,
|
||||
pet_spell: None,
|
||||
// 4 s remaining of a 10 s cooldown, running: started at GetTime 94.
|
||||
cooldown: Some((94_000, 10_000, true)),
|
||||
no_cooldown_query: false,
|
||||
|
||||
+1
-1
@@ -700,7 +700,7 @@ Each entry is the first sentence of a `Deviation:` comment, linking to where it
|
||||
|
||||
#### script
|
||||
|
||||
- An out-of-range slot answers nil, because the reference indexes `0xbc6980` with no bounds check and reads the memory beside its 120 slots. ([`src/script/action.rs:414`](../crates/benilla-ui/src/script/action.rs#L414))
|
||||
- An out-of-range slot answers nil, because the reference indexes `0xbc6980` with no bounds check and reads the memory beside its 120 slots. ([`src/script/action.rs:417`](../crates/benilla-ui/src/script/action.rs#L417))
|
||||
- The files the handler writes beside the install (`<Name>.pub` with the key, `<Name>.url`; `0x51dce0`, `0x51dde0`) are not written, as the install is read-only; the key is never read either, as there is no signature to verify with it. ([`src/script/addon.rs:207`](../crates/benilla-ui/src/script/addon.rs#L207))
|
||||
- The reference fails silently; we report it, as a player needs to know, and hold the file so the shutdown write keeps it. ([`src/script/addon.rs:785`](../crates/benilla-ui/src/script/addon.rs#L785))
|
||||
- The reference drops a send that resolves to PARTY while party slot 0's guid (`0xbc6f48`) is zero (`0x49fa9f`-`0x49faab`); benilla sends it, because `PartyState` is empty until the first `SMSG_GROUP_LIST` and the gate would swallow a real broadcast. vmangos drops a group-less party line anyway (`ChatHandler.cpp:476`). ([`src/script/addon_message.rs:7`](../crates/benilla-ui/src/script/addon_message.rs#L7))
|
||||
|
||||
Reference in New Issue
Block a user