use std::collections::BTreeSet;
use std::marker::PhantomData;
use bevy_ecs::change_detection::Tick;
use bevy_ecs::resource::Resource;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct WorldDelta {
globals: BTreeSet<u32>,
bookkeeping: bool,
}
impl WorldDelta {
#[must_use]
pub fn globals(&self) -> &BTreeSet<u32> {
&self.globals
}
#[must_use]
pub fn touched_bookkeeping(&self) -> bool {
self.bookkeeping
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.globals.is_empty() && !self.bookkeeping
}
pub(crate) fn note_global(&mut self, idx: u32) {
self.globals.insert(idx);
}
pub(crate) fn note_bookkeeping(&mut self) {
self.bookkeeping = true;
}
pub(crate) fn absorb(&mut self, other: &WorldDelta) {
self.globals.extend(other.globals.iter().copied());
self.bookkeeping |= other.bookkeeping;
}
}
#[derive(Resource, Debug)]
pub struct BrinkWorldDelta<M: Send + Sync + 'static = ()> {
delta: WorldDelta,
recorded: bool,
foreign: bool,
recorded_tick: Option<Tick>,
_marker: PhantomData<fn() -> M>,
}
impl<M: Send + Sync + 'static> Default for BrinkWorldDelta<M> {
fn default() -> Self {
Self {
delta: WorldDelta::default(),
recorded: false,
foreign: false,
recorded_tick: None,
_marker: PhantomData,
}
}
}
impl<M: Send + Sync + 'static> BrinkWorldDelta<M> {
pub(crate) fn record(&mut self, delta: &WorldDelta, globals_tick: Option<Tick>) {
self.delta.absorb(delta);
self.recorded = true;
self.recorded_tick = globals_tick;
}
pub(crate) fn record_condition_evaluation(&mut self) {
self.delta.note_bookkeeping();
self.recorded = true;
}
pub(crate) fn note_foreign(&mut self) {
self.foreign = true;
}
pub(crate) fn drain(
&mut self,
globals_last_changed: Option<Tick>,
globals_changed: bool,
) -> Option<WorldDelta> {
let complete = self.recorded
&& !self.foreign
&& (!globals_changed || globals_last_changed == self.recorded_tick);
let delta = std::mem::take(&mut self.delta);
self.recorded = false;
self.foreign = false;
self.recorded_tick = None;
complete.then_some(delta)
}
#[must_use]
pub fn pending(&self) -> &WorldDelta {
&self.delta
}
}
#[cfg(test)]
mod tests {
use super::*;
const APPLY_TICK: Tick = Tick::new(7);
fn drain_agreeing(ledger: &mut BrinkWorldDelta<()>) -> Option<WorldDelta> {
ledger.drain(Some(APPLY_TICK), true)
}
#[test]
fn drain_reports_none_until_a_turn_is_recorded() {
let mut ledger = BrinkWorldDelta::<()>::default();
assert!(
drain_agreeing(&mut ledger).is_none(),
"a ledger no driver has recorded into explains nothing — the caller must stay \
conservative"
);
}
#[test]
fn drain_reports_the_recorded_cells_and_resets() {
let mut ledger = BrinkWorldDelta::<()>::default();
let mut turn = WorldDelta::default();
turn.note_global(3);
turn.note_bookkeeping();
ledger.record(&turn, Some(APPLY_TICK));
let drained = drain_agreeing(&mut ledger).expect("a recorded turn is a complete account");
assert!(drained.globals().contains(&3));
assert!(drained.touched_bookkeeping());
assert!(
drain_agreeing(&mut ledger).is_none(),
"draining resets the ledger — the next window starts clean and unattributed"
);
}
#[test]
fn drain_accumulates_every_turn_between_two_drains() {
let mut ledger = BrinkWorldDelta::<()>::default();
let mut first = WorldDelta::default();
first.note_global(1);
ledger.record(&first, Some(Tick::new(3)));
let mut second = WorldDelta::default();
second.note_global(2);
ledger.record(&second, Some(APPLY_TICK));
let drained = drain_agreeing(&mut ledger).expect("recorded");
assert!(
drained.globals().contains(&1) && drained.globals().contains(&2),
"a wake pass that skipped a frame must still see the earlier turn's cells"
);
}
#[test]
fn a_foreign_write_makes_the_window_unattributable() {
let mut ledger = BrinkWorldDelta::<()>::default();
ledger.note_foreign();
let mut turn = WorldDelta::default();
turn.note_global(1);
ledger.record(&turn, Some(APPLY_TICK));
assert!(
drain_agreeing(&mut ledger).is_none(),
"a write landing before the Apply (host system, serial driver) must force the \
conservative path — never under-report"
);
assert!(
drain_agreeing(&mut ledger).is_none(),
"the foreign flag clears on drain, but the window is then unrecorded again"
);
}
#[test]
fn a_write_after_the_apply_makes_the_window_unattributable() {
let mut ledger = BrinkWorldDelta::<()>::default();
let mut turn = WorldDelta::default();
turn.note_bookkeeping();
ledger.record(&turn, Some(APPLY_TICK));
assert!(
ledger.drain(Some(Tick::new(9)), true).is_none(),
"a live change tick past the one the Apply recorded means somebody wrote after it \
— the half of the window the batch driver's own entry check cannot see"
);
}
#[test]
fn an_unchanged_resource_has_nothing_for_the_recorded_tick_to_disagree_with() {
let mut ledger = BrinkWorldDelta::<()>::default();
ledger.record_condition_evaluation();
let drained = ledger
.drain(Some(Tick::new(99)), false)
.expect("nothing moved the resource, so the recorded tick cannot be stale");
assert!(
drained.touched_bookkeeping(),
"a condition evaluation's bookkeeping residue survives even though it \
deliberately leaves no change tick behind"
);
}
#[test]
fn an_empty_delta_is_distinguishable_from_an_unattributed_one() {
let mut ledger = BrinkWorldDelta::<()>::default();
ledger.record(&WorldDelta::default(), Some(APPLY_TICK));
let drained = drain_agreeing(&mut ledger).expect("recorded, even though it wrote nothing");
assert!(
drained.is_empty(),
"a turn that wrote nothing is a complete account of *no* change — not a reason \
to re-check every parked policy"
);
}
}