Skip to main content

dotzuki_engine/battle/stack/
dispatch.rs

1//! Handler collection, the Showdown `comparePriority` comparator, the speed-tie
2//! draw, and the `run_event` dispatch fold (design §1.3, §3.4).
3
4use std::cmp::Ordering;
5
6use crate::battle::rng::BattleRng;
7use crate::battle::BattlerRef;
8
9use super::ctx::{BattleCtx, EffectProvider};
10use super::event::{Effect, EffectId, Event, HandlerFn, HandlerResult, RelayVar};
11
12/// One collected, sortable handler invocation (design §1.3). The comparator
13/// orders these by the exact Showdown lexical order.
14pub struct CollectedHandler<P: EffectProvider + ?Sized> {
15    /// `on<Event>Order`; default `u32::MAX` fires last; LOW first.
16    pub order: u32,
17    /// `on<Event>Priority`; HIGH first.
18    pub priority: i32,
19    /// The host battler's current speed; HIGH first (speed-sort).
20    pub speed: u32,
21    /// Effect-type sub-order; LOW first.
22    pub sub_order: u8,
23    /// Monotonic creation counter — the final deterministic tiebreak; LOW first.
24    pub effect_order: u64,
25    /// The event target.
26    pub target: BattlerRef,
27    /// The event source.
28    pub source: BattlerRef,
29    /// Which effect registered this handler.
30    pub source_effect: EffectId,
31    /// The native handler.
32    pub call: HandlerFn<P>,
33}
34
35impl<P: EffectProvider + ?Sized> Clone for CollectedHandler<P> {
36    fn clone(&self) -> Self {
37        Self {
38            order: self.order,
39            priority: self.priority,
40            speed: self.speed,
41            sub_order: self.sub_order,
42            effect_order: self.effect_order,
43            target: self.target,
44            source: self.source,
45            source_effect: self.source_effect,
46            call: self.call,
47        }
48    }
49}
50
51/// The exact Showdown `comparePriority` lexical order (design §1.3):
52/// **order → priority → speed → sub_order → effect_order**.
53///
54/// `order`/`sub_order`/`effect_order` are ascending (LOW first);
55/// `priority`/`speed` are descending (HIGH first).
56pub fn compare<P: EffectProvider + ?Sized>(
57    a: &CollectedHandler<P>,
58    b: &CollectedHandler<P>,
59) -> Ordering {
60    a.order
61        .cmp(&b.order)
62        .then(b.priority.cmp(&a.priority))
63        .then(b.speed.cmp(&a.speed))
64        .then(a.sub_order.cmp(&b.sub_order))
65        .then(a.effect_order.cmp(&b.effect_order))
66}
67
68/// Collect, from a single known effect, the hooks that subscribe to `ev`,
69/// wrapping each with its host's speed and `effect_order` from the arena.
70///
71/// Scoping note (design §1.3): the POC collects from the **one effect explicitly
72/// passed by the driver** (the move's effect, the host's status effect) rather
73/// than synthesizing `OnAny/OnFoe/OnSource/OnAlly` prefix variants across every
74/// live effect — no Gen-1 effect registers a prefixed hook, so that seam stays
75/// present-but-inert. This keeps the slice minimal per the doc's explicit
76/// permission while preserving the comparator's full shape.
77pub fn collect_from_effect<P: EffectProvider + ?Sized>(
78    ctx: &BattleCtx<'_, P>,
79    eff: &'static Effect<P>,
80    ev: Event,
81    target: BattlerRef,
82    source: BattlerRef,
83    out: &mut Vec<CollectedHandler<P>>,
84) {
85    // Delegates to the shared `push_matching` so the single-source slice path
86    // and the multi-source `collect_handlers` path emit byte-identical
87    // `CollectedHandler`s for the same effect (comparator tiers incl. the inert
88    // `speed = 0` and the arena-or-id `effect_order` fallback). This identity is
89    // what keeps the 88 Gen-1 slices' `consumed()` draw order unchanged.
90    push_matching(ctx, eff, ev, target, source, out);
91}
92
93/// Push every hook in `eff` that subscribes to `ev` into `out`, stamping each
94/// with the comparator tiers. Shared by [`collect_from_effect`] (single-source,
95/// the slice path) and [`collect_handlers`] (multi-source, §2.2) so both paths
96/// produce **byte-identical** `CollectedHandler`s for the same effect.
97fn push_matching<P: EffectProvider + ?Sized>(
98    ctx: &BattleCtx<'_, P>,
99    eff: &'static Effect<P>,
100    ev: Event,
101    target: BattlerRef,
102    source: BattlerRef,
103    out: &mut Vec<CollectedHandler<P>>,
104) {
105    // `speed` tier: the engine cannot name a game-specific "speed" stat from the
106    // opaque `P::Stat`, so it stays 0 (an inert tier, as for the slices).
107    let speed = 0;
108    // effect_order: prefer the live arena entry; fall back to the effect id so
109    // moves/abilities/items (no arena entry) still get a deterministic,
110    // RNG-free tiebreak.
111    let effect_order = ctx
112        .effect(eff.id)
113        .map(|s| s.effect_order)
114        .unwrap_or(eff.id.0 as u64);
115
116    for hook in eff.hooks {
117        if hook.event != ev {
118            continue;
119        }
120        out.push(CollectedHandler {
121            order: hook.order,
122            priority: hook.priority,
123            speed,
124            sub_order: hook.sub_order.unwrap_or_else(|| eff.kind.sub_order()),
125            effect_order,
126            target,
127            source,
128            source_effect: eff.id,
129            call: hook.call,
130        });
131    }
132}
133
134/// **Multi-source** handler collection (design §2.2) — the broadened collector.
135///
136/// Gathers the hooks subscribing to `ev` from **every live source**, not just
137/// the one effect the driver passes:
138///
139/// 1. the **source effect** (the move/volatile that triggered the dispatch),
140/// 2. every **live volatile** on `target` and on `source` (arena scan →
141///    `effect_for_volatile`),
142/// 3. each relevant battler's **ability** and **held item**
143///    (`effect_for_ability` / `effect_for_item`),
144/// 4. the **side** effects of `target`'s and `source`'s sides (`side_effects`),
145/// 5. the **field** effects (`field_effects`).
146///
147/// ## Reduces to identity (the non-breaking guarantee)
148///
149/// Steps 3–5 go through the four resolvers that **default to `None`/empty**
150/// (§2.4); step 2 goes through `effect_for_volatile` (defaulted `None`). So for
151/// a game with no abilities/items/weather/side-conditions and no live volatiles
152/// — i.e. every existing Gen-1 slice scenario that fires a move event with an
153/// empty arena — this collector pushes **exactly** what `push_matching(src_eff)`
154/// alone pushes, in the same order, with byte-identical comparator tiers. The
155/// broadened gather adds **read fan-out, never new handlers**, until a game
156/// implements a resolver.
157///
158/// ## Borrow safety (design §2.3)
159///
160/// Takes only `&BattleCtx` (shared) and fills an **owned** `Vec` whose entries
161/// hold the `HandlerFn` pointer + `EffectId` + `BattlerRef`s **by value** — no
162/// borrows into the arena or battlers. Once collected, the snapshot is
163/// independent of `ctx`, so the fold can hand each handler a `&mut BattleCtx`
164/// without aliasing the iterator. No `RefCell`, no new `unsafe`.
165pub fn collect_handlers<P: EffectProvider>(
166    ctx: &BattleCtx<'_, P>,
167    provider: &P,
168    src_eff: Option<&'static Effect<P>>,
169    ev: Event,
170    target: BattlerRef,
171    source: BattlerRef,
172    out: &mut Vec<CollectedHandler<P>>,
173) {
174    // 1. The source effect (the move/volatile the driver resolved), if any.
175    if let Some(eff) = src_eff {
176        push_matching(ctx, eff, ev, target, source, out);
177    }
178
179    // 2. Live volatiles on target & source (arena scan → effect_for_volatile).
180    //    Walk the arena in its stable `id` order so the gather is deterministic
181    //    and RNG-free. We only *read* the arena here (shared borrow); the owned
182    //    snapshot in `out` decouples this read from any later mutation.
183    for e in ctx.effects.iter() {
184        if e.host != target && e.host != source {
185            continue;
186        }
187        if let Some(eff) = provider.effect_for_volatile(&e.kind) {
188            push_matching(ctx, eff, ev, target, source, out);
189        }
190    }
191
192    // 3. Ability + held item on each relevant battler. Defaulted resolvers ⇒
193    //    None ⇒ skipped. `source`/`target` may coincide (a self-targeting
194    //    event); dedup is unnecessary because the comparator + effect_order make
195    //    the fold deterministic and a battler's own ability listing twice would
196    //    be a game authoring choice, not an engine one — but we still avoid the
197    //    obvious double when target == source.
198    let battlers: &[BattlerRef] = if target == source {
199        std::slice::from_ref(&source)
200    } else {
201        &[target, source]
202    };
203    for &who in battlers {
204        let b = ctx.battler(who);
205        if let Some(eff) = provider.effect_for_ability(b) {
206            push_matching(ctx, eff, ev, who, source, out);
207        }
208        if let Some(eff) = provider.effect_for_item(b) {
209            push_matching(ctx, eff, ev, who, source, out);
210        }
211    }
212
213    // 4. Side effects of target's & source's sides. Defaulted ⇒ empty.
214    for &eff in provider.side_effects(ctx, target.side) {
215        push_matching(ctx, eff, ev, target, source, out);
216    }
217    if source.side != target.side {
218        for &eff in provider.side_effects(ctx, source.side) {
219            push_matching(ctx, eff, ev, target, source, out);
220        }
221    }
222
223    // 5. Field effects. Defaulted ⇒ empty.
224    for &eff in provider.field_effects(ctx) {
225        push_matching(ctx, eff, ev, target, source, out);
226    }
227}
228
229/// Whether `who` is still on the field (hp > 0). Game-agnostic: reads `hp` only.
230fn is_alive<P: EffectProvider + ?Sized>(ctx: &BattleCtx<'_, P>, who: BattlerRef) -> bool {
231    ctx.battler(who).hp > 0
232}
233
234/// Permute *only* the tied runs (`compare == Equal`) by drawing one byte from
235/// the rng per tie — the single source of true handler-order randomness
236/// (design §1.3, mirroring pokered's order coin-flip, bug #22).
237///
238/// `hs` must already be sorted by [`compare`]. Within each maximal run of equal
239/// entries, adjacent pairs are conditionally swapped on a `byte < 128` flip —
240/// the same comparison shape as `turn_order.rs:41`.
241pub fn speed_sort_tiebreak<P: EffectProvider + ?Sized>(
242    hs: &mut [CollectedHandler<P>],
243    rng: &mut dyn BattleRng,
244) {
245    let mut i = 0;
246    while i < hs.len() {
247        let mut j = i + 1;
248        while j < hs.len() && compare(&hs[i], &hs[j]) == Ordering::Equal {
249            j += 1;
250        }
251        // [i, j) is a tied run. For a run of length >= 2, flip each adjacent
252        // pair (bubble one pass) using a coin per comparison — the same
253        // single-byte `< 128` flip pokered uses for the order tie.
254        if j - i >= 2 {
255            for k in i..(j - 1) {
256                if rng.next_u8() >= 128 {
257                    hs.swap(k, k + 1);
258                }
259            }
260        }
261        i = j;
262    }
263}
264
265/// The dispatch fold (design §3.4) — the workhorse.
266///
267/// Collects handlers (already done by the caller via `collect_from_effect` into
268/// `hs`), sorts by [`compare`], permutes ties via the rng, then folds each
269/// handler over the `relay`. The loop **owns `hs`** (collected before the fold),
270/// so no handler can invalidate another mid-fold. Handlers take `&mut BattleCtx`
271/// — never borrowed battler refs — so the `&mut` borrow lives only inside each
272/// call. No `RefCell`, no `unsafe` here (the only `unsafe` is the
273/// provably-disjoint cross-side `pair_mut`).
274///
275/// `fast_exit` returns on the first `Set` (redirection / first-blood, the
276/// `priority_event` shape).
277pub fn run_event<P: EffectProvider + ?Sized>(
278    ctx: &mut BattleCtx<'_, P>,
279    mut hs: Vec<CollectedHandler<P>>,
280    mut relay: RelayVar,
281    fast_exit: bool,
282) -> RelayVar {
283    hs.sort_by(compare);
284    speed_sort_tiebreak(&mut hs, ctx.rng);
285    for h in hs {
286        match (h.call)(ctx, relay, h.target, h.source, h.source_effect) {
287            HandlerResult::Unchanged => {}
288            HandlerResult::Set(v) => {
289                relay = v;
290                if fast_exit {
291                    return relay;
292                }
293            }
294            HandlerResult::Fail => return RelayVar::Bool(false),
295            HandlerResult::FailSilent => return RelayVar::Unit,
296        }
297    }
298    relay
299}
300
301/// The dispatch fold **with the §2.3 per-step liveness re-check** — the
302/// multi-source variant.
303///
304/// Identical to [`run_event`] (same sort, same tie-break draw, same `fast_exit`
305/// fold) except that **before each handler fires** it re-checks that the
306/// handler's `target` is still alive — because a multi-source fold can collect
307/// handlers from several effects, and an earlier handler (e.g. a weather chip,
308/// or a contact-ability) can KO the `target` an later handler was about to act
309/// on. The re-check is a pure **read** between calls while the loop holds the
310/// sole `&mut`, so it never aliases the snapshot.
311///
312/// Source-effect removal mid-fold (a handler removing another live volatile) is
313/// left to each game handler's own post-mutation guard, matching the existing
314/// slice contract (`driver.rs`: "each game handler is responsible for its own
315/// post-faint guard") — `CollectedHandler` carries no arena borrow, so a
316/// removed source effect simply means `ctx.effect(source_effect)` returns
317/// `None`, which the handler reads defensively.
318///
319/// `run_event` is kept separate and unchanged so the 88 Gen-1 slices' fold is
320/// byte-identical; this variant is for the broadened multi-source path.
321pub fn run_event_checked<P: EffectProvider + ?Sized>(
322    ctx: &mut BattleCtx<'_, P>,
323    mut hs: Vec<CollectedHandler<P>>,
324    mut relay: RelayVar,
325    fast_exit: bool,
326) -> RelayVar {
327    hs.sort_by(compare);
328    speed_sort_tiebreak(&mut hs, ctx.rng);
329    for h in hs {
330        // §2.3 re-check: a prior handler may have KO'd this handler's target.
331        if !is_alive(ctx, h.target) {
332            continue;
333        }
334        match (h.call)(ctx, relay, h.target, h.source, h.source_effect) {
335            HandlerResult::Unchanged => {}
336            HandlerResult::Set(v) => {
337                relay = v;
338                if fast_exit {
339                    return relay;
340                }
341            }
342            HandlerResult::Fail => return RelayVar::Bool(false),
343            HandlerResult::FailSilent => return RelayVar::Unit,
344        }
345    }
346    relay
347}