diff --git a/kiln-core/src/script.rs b/kiln-core/src/script.rs index 5ff7b96..bc18dea 100644 --- a/kiln-core/src/script.rs +++ b/kiln-core/src/script.rs @@ -327,21 +327,30 @@ impl ScriptHost { /// Promotes ready actions from object `i`'s output queue onto the board queue. /// - /// While the object's ready timer is running, nothing is pulled. Otherwise the - /// leading run of zero-cost actions is promoted, followed by at most one timed - /// action (which arms the timer and ends the pump). The board-queue guard keeps a - /// second pump in the same tick from double-promoting. + /// While the object's ready timer is running, nothing is pulled. While a timed + /// action for this object is already in the board queue, nothing is pulled (a + /// second timed action would race it). Otherwise the leading run of zero-cost + /// actions is promoted, followed by at most one timed action (which arms the timer + /// and ends the pump). All queued free actions therefore land in the board queue + /// in the same pump call as the next timed action — they apply atomically in the + /// same `resolve()`. fn pump(&mut self, i: usize) { let oid = self.objects[i].object_id; - // Don't re-promote if this object already has actions awaiting resolution. - if self.board_queue.borrow().iter().any(|ba| ba.source == oid) { + + // A running cooldown blocks all further pulls (timed or not). + if self.objects[i].ready_timer > 0.0 { return; } + + // Don't promote while a timed action for this object is already in the board + // queue and awaiting resolution — a second timed action would race it. + if self.board_queue.borrow().iter() + .any(|ba| ba.source == oid && ba.action.time_cost() > 0.0) + { + return; + } + loop { - // A running cooldown blocks all further pulls (timed or not). - if self.objects[i].ready_timer > 0.0 { - break; - } let queue = self.objects[i].output_queue.clone(); let cost = match queue.borrow().front() { Some(action) => action.time_cost(),