//! nampower's explicit spell queue: the three slots behind `QueueSpellByName`, `QueueScript` and //! `ChannelStopCastingNextTick`. //! //! **What this is, and what it deliberately is not.** Upstream's queue has two halves. One is the //! Lua surface — "remember this spell and send it when the client will accept it" — and that is //! what is built here. The other intercepts **every ordinary press**, so a keypress during a cast //! is held rather than refused; that half is governed by `NP_QueueCastTimeSpells`, //! `NP_QueueInstantSpells`, `NP_QueueTargetingSpells` and friends, and it is **not** built. It is //! not a binding: it changes how every key in the game feels, it is the one piece whose bugs are //! double-casts and dropped casts, and it is not something to switch on as a side effect of //! porting three function names. Those CVars stay stored-only in `cvars.rs`, which says so at //! each row. //! //! So: a spell reaches a slot only because Lua asked, and leaves it the moment the client would //! accept that press anyway. //! //! **Three slots, because upstream has three and its own event says so.** `SPELL_QUEUE_EVENT`'s //! first argument is `0` on-swing queued, `1` on-swing popped, `2` normal queued, `3` normal //! popped, `4` non-GCD queued, `5` non-GCD popped — so the queue is keyed by what *kind* of cast a //! spell is, and one slot per kind is the shape a consumer reads. The kind comes off the spell's //! own `Spell.dbc` row, the same three tests the send path makes. use std::time::{Duration, Instant}; use bevy::prelude::*; /// `SPELL_QUEUE_EVENT`'s first argument. Numbers, not an enum, because they cross to Lua as /// numbers and an addon compares them against these literals. pub(crate) mod queue_event { pub(crate) const ON_SWING_QUEUED: i64 = 0; pub(crate) const ON_SWING_POPPED: i64 = 1; pub(crate) const NORMAL_QUEUED: i64 = 2; pub(crate) const NORMAL_POPPED: i64 = 3; pub(crate) const NON_GCD_QUEUED: i64 = 4; pub(crate) const NON_GCD_POPPED: i64 = 5; } /// Which slot a spell belongs in, from its own `Spell.dbc` row. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum Kind { /// `Attributes & 0x404`: Heroic Strike, Cleave, Raptor Strike. OnSwing, /// `StartRecoveryTime == 0`: it costs no global cooldown, so it need not wait for one. NonGcd, /// Everything else. Normal, } impl Kind { /// The same three tests the send path makes, in the same order: the on-next-swing attribute /// first (it outranks everything, since such a spell never occupies the cast slot at all), /// then the GCD pair. pub(crate) fn of(spell: Option<&benilla_formats::SpellDisplay>) -> Self { match spell { Some(d) if d.on_next_swing() => Kind::OnSwing, Some(d) if d.start_recovery_ms == 0 => Kind::NonGcd, // An uncatalogued spell is Normal, which is the slot that waits for the most: a queue // that fires something early is worse than one that fires it late. _ => Kind::Normal, } } fn queued_code(self) -> i64 { match self { Kind::OnSwing => queue_event::ON_SWING_QUEUED, Kind::NonGcd => queue_event::NON_GCD_QUEUED, Kind::Normal => queue_event::NORMAL_QUEUED, } } fn popped_code(self) -> i64 { match self { Kind::OnSwing => queue_event::ON_SWING_POPPED, Kind::NonGcd => queue_event::NON_GCD_POPPED, Kind::Normal => queue_event::NORMAL_POPPED, } } } /// One occupied slot. #[derive(Clone, Copy, Debug, PartialEq, Eq)] struct Slot { spell_id: u32, /// Where the window is measured from: the latest guard end this entry has been told about. /// /// **It ratchets forward**, in [`SpellQueue::expire`], while something is still guarding. /// A fixed `expires` stamped at queue time looks right and has a hole in it: a cast pushed /// back twice (`SMSG_SPELL_DELAYED`) ends after the window a press made at its start was /// given, so the entry would time out while the thing it is waiting for is still running and /// the press would vanish with no packet and no error. The same hole swallows a press held /// through a cast whose global cooldown then outlasts the window. base: Instant, /// How long past [`Self::base`] this entry survives: one of nampower's four windows, picked /// by what guarded the press (`np_press.rs`'s `window_for`). window: Duration, /// The earliest [`SpellQueue::pop`] will take it. The moment it was queued for an ordinary /// entry, which is to say no constraint at all; later only for a retry /// (`NP_RetryServerRejectedSpells`), where the point is to not re-send inside the same /// millisecond the server just refused. not_before: Instant, } impl Slot { /// Whether the window has run out, `base` having already been brought up to date. fn expired(&self, now: Instant) -> bool { now >= self.base + self.window } } /// One queued script, with the priority that orders it. #[derive(Clone, Debug, PartialEq, Eq)] struct Script { priority: i64, chunk: String, expires: Instant, } /// What the press queue did in the reporting period, for the one line it prints at `info`. /// /// **Every other line this half of the client writes is `debug!`, which the default filter drops** /// — so without this a whole session says nothing about whether a press was held, popped, dropped /// or refused, and the only evidence left is the player's own combat log. The period is long and /// the line is skipped when nothing moved, so an idle session stays quiet. #[derive(Default)] pub(crate) struct Tally { /// Presses held by [`super::np_press::PressRules::admit`]. pub(crate) held: u32, /// Held presses that reached the wire. pub(crate) sent: u32, /// Held presses that gave up: the window ran out, or the spell was cold with /// `NP_QueueSpellsOnCooldown` off. The failure mode a player cannot describe. pub(crate) dropped: u32, /// Casts re-sent after a server refusal (`NP_RetryServerRejectedSpells`). pub(crate) retried: u32, /// Presses the rules declined to hold while something still guarded — each one is an error /// line the player read. A high count beside a low `held` is the queue not doing its job. pub(crate) fell_through: u32, /// Presses that ended a channel instead (`NP_InterruptChannelsOutsideQueueWindow`). pub(crate) interrupted: u32, /// When the period began; `None` until the first event. since: Option, } impl Tally { fn moved(&self) -> bool { self.held | self.sent | self.dropped | self.retried | self.fell_through | self.interrupted != 0 } } /// The queue: one slot per [`Kind`], plus the scripts. /// /// `pub(crate)` fields are avoided on purpose — every mutation goes through [`Self::queue`] or /// [`Self::pop`], so the `SPELL_QUEUE_EVENT` rows cannot be forgotten by a caller that writes a /// slot directly. #[derive(Resource)] pub(crate) struct SpellQueue { on_swing: Option, non_gcd: Option, normal: Option, scripts: Vec