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}