diff --git a/src/components.rs b/src/components.rs index cc19482..37adbe7 100644 --- a/src/components.rs +++ b/src/components.rs @@ -6,18 +6,21 @@ use bevy::prelude::*; #[derive(Component)] pub struct NoRollback; -/// Added to every entity, for tracking which frame they were last synced to a snapshot -/// Deduct `last_snapshot_frame` from the current frame to determine how many frames this -/// entity is predicted ahead for. -#[derive(Component)] +/// Added to every entity for metrics +#[derive(Component, Debug)] pub struct TimewarpStatus { + /// Deduct `last_snapshot_frame` from the current frame to determine how many frames this + /// entity is predicted ahead for. last_snapshot_frame: FrameNumber, + /// Incremented when an update to this entity caused a rollback to be requested + rollback_triggers: u32, } impl TimewarpStatus { pub fn new(last_snapshot_frame: FrameNumber) -> Self { Self { last_snapshot_frame, + rollback_triggers: 0, } } /// returns the frame of the most recent snapshot, @@ -25,9 +28,17 @@ impl TimewarpStatus { pub fn last_snap_frame(&self) -> FrameNumber { self.last_snapshot_frame } + /// how many times did this entity cause a rollback to be requested + pub fn rollback_triggers(&self) -> u32 { + self.rollback_triggers + } + pub fn set_snapped_at(&mut self, frame: FrameNumber) { self.last_snapshot_frame = self.last_snapshot_frame.max(frame); } + pub fn increment_rollback_triggers(&mut self) { + self.rollback_triggers += 1; + } } /// Used when you want to insert a component T, but for an older frame. @@ -57,11 +68,11 @@ impl InsertComponentAtFrame { /// I use this for blueprints. The blueprint assembly function runs during rollback and /// adds the various timewarp-registered (and other) components to the entity during rollback. #[derive(Component, Debug)] -pub struct AssembleBlueprintAtFrame { +pub struct AssembleBlueprintAtFrame { pub component: T, pub frame: FrameNumber, } -impl AssembleBlueprintAtFrame { +impl AssembleBlueprintAtFrame { pub fn new(frame: FrameNumber, component: T) -> Self { Self { component, frame } } @@ -162,7 +173,12 @@ impl ComponentHistory { entity: &Entity, ) -> Result<(), TimewarpError> { trace!("CH.Insert {entity:?} {frame} = {val:?}"); - self.values.insert(frame, val) + // TODO this is inefficient atm + self.values.insert(frame, val)?; + if !self.alive_at_frame(frame) { + self.report_birth_at_frame(frame); + } + Ok(()) } /// removes values buffered for this frame, and greater frames. @@ -190,6 +206,9 @@ impl ComponentHistory { self.values.get(frame).is_some(), "No stored component value when reporting birth @ {frame}" ); + if self.alive_ranges.last().unwrap().1 == Some(frame) { + return; + } self.alive_ranges.push((frame, None)); } pub fn report_death_at_frame(&mut self, frame: FrameNumber) { @@ -202,16 +221,18 @@ impl ComponentHistory { return; } - trace!( - "component death @ {frame} {:?} {:?}", - std::any::type_name::(), - self.alive_ranges - ); - assert!( self.alive_at_frame(frame), "Can't report death of component not alive" ); + if self.alive_ranges.last().unwrap().1 == Some(frame) { + return; + } self.alive_ranges.last_mut().unwrap().1 = Some(frame); + trace!( + "component death @ {frame} {:?} --> {:?}", + std::any::type_name::(), + self.alive_ranges + ); } } diff --git a/src/systems/postfix_components.rs b/src/systems/postfix_components.rs index b83c743..48c1b2e 100644 --- a/src/systems/postfix_components.rs +++ b/src/systems/postfix_components.rs @@ -142,6 +142,9 @@ pub(crate) fn record_component_birth( game_clock: Res, rb: Option>, ) { + return; + // no. implied by inserting values to CH! + // during rollback, components are removed and readded. // but we don't want to log the same as outside of rollback, we want to ignore. // however this system still runs, so that the Added filters update their markers diff --git a/src/systems/prefix_blueprints.rs b/src/systems/prefix_blueprints.rs index 9c19360..107ff33 100644 --- a/src/systems/prefix_blueprints.rs +++ b/src/systems/prefix_blueprints.rs @@ -10,7 +10,7 @@ use bevy::prelude::*; /// Blueprint components stay wrapped up until their target frame, then we unwrap them /// so the assembly systems can decorate them with various other components at that frame. -pub(crate) fn unwrap_blueprints_at_target_frame( +pub(crate) fn unwrap_blueprints_at_target_frame( q: Query<(Entity, &AssembleBlueprintAtFrame)>, mut commands: Commands, game_clock: Res, diff --git a/src/systems/prefix_not_in_rollback.rs b/src/systems/prefix_not_in_rollback.rs index b6e22df..2d1569d 100644 --- a/src/systems/prefix_not_in_rollback.rs +++ b/src/systems/prefix_not_in_rollback.rs @@ -1,3 +1,5 @@ +use std::cmp::Ordering; + /* NOTE: Timewarp Prefix Systems run at the top of FixedUpdate: * RIGHT BEFORE THE GameClock IS INCREMENTED. @@ -8,16 +10,6 @@ use crate::prelude::*; use bevy::prelude::*; -/// Don't insert ICAFs if the SS exists, use the SS. -/// can probably support this later, but keeps things simpler for now. -pub(crate) fn detect_misuse_of_icaf( - q: Query<(Entity, &ServerSnapshot, &InsertComponentAtFrame)>, -) { - for (e, _ss, icaf) in q.iter() { - panic!("ICAF and SS exist on {e:?} {icaf:?}"); - } -} - /// If a new snapshot was added to SS, we may need to initiate a rollback pub(crate) fn apply_snapshots_and_maybe_rollback( mut q: Query< @@ -73,23 +65,23 @@ pub(crate) fn apply_snapshots_and_maybe_rollback( match comp_hist.insert(snap_frame, comp_from_snapshot.clone(), &entity) { Ok(()) => (), Err(err) => { + rb_stats.range_faults += 1; // probably FrameTooOld. - error!( + panic!( "{err:?} {entity:?} apply_snapshots_and_maybe_rollback({}) - skipping", comp_hist.type_name() ); - rb_stats.range_faults += 1; // we can't rollback to this // this is bad. - continue; + // continue; } } - if !comp_hist.alive_at_frame(snap_frame) { - info!("Setting liveness for {snap_frame} {entity:?} {comp_from_snapshot:?} "); - comp_hist.report_birth_at_frame(snap_frame); - assert!(comp_hist.at_frame(snap_frame).is_some()); - } + // if !comp_hist.alive_at_frame(snap_frame) { + // info!("Setting liveness for {snap_frame} {entity:?} {comp_from_snapshot:?} "); + // comp_hist.report_birth_at_frame(snap_frame); + // assert!(comp_hist.at_frame(snap_frame).is_some()); + // } if snap_frame < **game_clock { debug!( @@ -102,27 +94,39 @@ pub(crate) fn apply_snapshots_and_maybe_rollback( rb_ev.send(RollbackRequest::resimulate_this_frame_onwards( snap_frame + 1, )); + tw_status.increment_rollback_triggers(); } } } -/// Move ICAF data to the SS. +/// Move ICAF data to the SS and add SS, because it's missing. /// /// if an ICAF was inserted, we may need to rollback. /// -pub(crate) fn unpack_icafs_and_maybe_rollback< +pub(crate) fn unpack_icafs_adding_tw_components< T: TimewarpComponent, const CORRECTION_LOGGING: bool, >( - q: Query<(Entity, &InsertComponentAtFrame), Added>>, + mut q: Query< + ( + Entity, + &InsertComponentAtFrame, + Option<&mut TimewarpStatus>, + ), + ( + Added>, + Without, + Without>, + Without>, + ), + >, mut commands: Commands, timewarp_config: Res, game_clock: Res, mut rb_ev: ResMut>, ) { - for (e, icaf) in q.iter() { + for (e, icaf, opt_twstatus) in q.iter_mut() { // insert the timewarp components - let tw_status = TimewarpStatus::new(icaf.frame); let mut ch = ComponentHistory::::with_capacity( timewarp_config.rollback_window as usize, icaf.frame, @@ -137,37 +141,132 @@ pub(crate) fn unpack_icafs_and_maybe_rollback< ServerSnapshot::::with_capacity(timewarp_config.rollback_window as usize * 60); ss.insert(icaf.frame, icaf.component.clone()).unwrap(); // (this will be applied in the ApplyComponents set next) - commands - .entity(e) - .insert((tw_status, ch, ss)) - .remove::>(); - - // if frames match, we want it inserted this frame but not rolled back - if icaf.frame == **game_clock { - // info!("Inserting latecomer in trigger icafs: {e:?} {icaf:?}"); - commands.entity(e).insert(icaf.component.clone()); - continue; + + match icaf.frame.cmp(&game_clock.frame()) { + // if frames match, we want it inserted this frame but not rolled back + // since it has arrived just in time. + Ordering::Equal => { + if let Some(mut tw_status) = opt_twstatus { + tw_status.set_snapped_at(icaf.frame); + } else { + let mut tw_status = TimewarpStatus::new(icaf.frame); + tw_status.set_snapped_at(icaf.frame); + commands.entity(e).insert(tw_status); + } + commands + .entity(e) + .insert((ch, ss, icaf.component.clone())) + .remove::>(); + } + // needs insertion in the past, so request a rollback. + Ordering::Less => { + debug!( + "{e:?} Requesting rolllback when unpacking: {icaf:?} rb to {}", + icaf.frame + 1 + ); + if let Some(mut tw_status) = opt_twstatus { + tw_status.increment_rollback_triggers(); + tw_status.set_snapped_at(icaf.frame); + } else { + let mut tw_status = TimewarpStatus::new(icaf.frame); + tw_status.increment_rollback_triggers(); + tw_status.set_snapped_at(icaf.frame); + commands.entity(e).insert(tw_status); + } + commands + .entity(e) + .insert((ch, ss)) + .remove::>(); + rb_ev.send(RollbackRequest::resimulate_this_frame_onwards( + icaf.frame + 1, + )); + } + Ordering::Greater => { + // clients are supposed to be ahead, so we don't really expect to get updates for + // future frames. We'll store it but can't rollback to future. + commands + .entity(e) + .insert((ch, ss)) + .remove::>(); + } } + } +} - if icaf.frame < **game_clock { - // trigger a rollback using the frame we just added authoritative values for - debug!( - "{e:?} trigger_rollback_when_icaf_added {icaf:?} requesting rb to {}", - icaf.frame + 1 - ); - rb_ev.send(RollbackRequest::resimulate_this_frame_onwards( - icaf.frame + 1, - )); +/// Move ICAF data to the existing SS +/// +/// if an ICAF was inserted, we may need to rollback. +/// +pub(crate) fn unpack_icafs_into_tw_components< + T: TimewarpComponent, + const CORRECTION_LOGGING: bool, +>( + mut q: Query< + ( + Entity, + &InsertComponentAtFrame, + &mut ServerSnapshot, + &mut ComponentHistory, + &mut TimewarpStatus, + ), + (Added>, Without), + >, + mut commands: Commands, + game_clock: Res, + mut rb_ev: ResMut>, +) { + for (e, icaf, mut ss, mut ch, mut tw_status) in q.iter_mut() { + ch.insert(icaf.frame, icaf.component.clone(), &e) + .expect("Couldn't insert ICAF to CH"); + ss.insert(icaf.frame, icaf.component.clone()) + .expect("Couldn't insert ICAF to SS"); + + info!("Alive ranges for {icaf:?} = {:?}", ch.alive_ranges); + + match icaf.frame.cmp(&game_clock.frame()) { + // if frames match, we want it inserted this frame but not rolled back + // since it has arrived just in time. + Ordering::Equal => { + commands + .entity(e) + .insert(icaf.component.clone()) + .remove::>(); + } + // needs insertion in the past, so request a rollback. + Ordering::Less => { + debug!( + "{e:?} Requesting rolllback when unpacking: {icaf:?} rb to {}", + icaf.frame + 1 + ); + tw_status.increment_rollback_triggers(); + commands.entity(e).remove::>(); + rb_ev.send(RollbackRequest::resimulate_this_frame_onwards( + icaf.frame + 1, + )); + } + Ordering::Greater => { + // clients are supposed to be ahead, so we don't really expect to get updates for + // future frames. We'll store it but can't rollback to future. + commands.entity(e).remove::>(); + } } } } -pub(crate) fn request_rollback_for_blueprints( - q: Query<(Entity, &AssembleBlueprintAtFrame), Added>>, +pub(crate) fn request_rollback_for_blueprints( + mut q: Query< + ( + Entity, + &AssembleBlueprintAtFrame, + Option<&mut TimewarpStatus>, + ), + Added>, + >, game_clock: Res, mut rb_ev: ResMut>, + mut commands: Commands, ) { - for (entity, abaf) in q.iter() { + for (entity, abaf, opt_twstatus) in q.iter_mut() { let snap_frame = abaf.frame; // if frames == match, we want it inserted this frame but not rolled back. // don't do this here, the blueprint unpacking fn does this even during rollback. @@ -176,6 +275,13 @@ pub(crate) fn request_rollback_for_blueprints( debug!( "{game_clock:?} {entity:?} Requesting rollback for blueprint with snap_frame:{snap_frame} - {abaf:?}" ); + if let Some(mut tws) = opt_twstatus { + tws.increment_rollback_triggers(); + } else { + let mut tws = TimewarpStatus::new(snap_frame); + tws.increment_rollback_triggers(); + commands.entity(entity).insert(tws); + } rb_ev.send(RollbackRequest::resimulate_this_frame_onwards( snap_frame + 1, )); diff --git a/src/systems/prefix_start_rollback.rs b/src/systems/prefix_start_rollback.rs index dfda512..34e8242 100644 --- a/src/systems/prefix_start_rollback.rs +++ b/src/systems/prefix_start_rollback.rs @@ -21,35 +21,25 @@ pub(crate) fn rollback_initiated( mut rb_stats: ResMut, timewarp_config: Res, ) { - // during syncing large worlds, we kinda have to skip impossible rollbacks until things catch up - // TODO revise comment: // if we're trying to roll back further than our configured rollback window, // all sorts of things will fail spectacularly, so i'm just going to panic for now. // i think the way to handle this is in the game, if you get an update from the past older // than the window that you can't afford to ignore, like a reliable spawn message, then - // perhaps modify the spawn frame to the oldest allowable frame within the window, - // and rely on snapshots to sort you out. + // deal with it and don't tell timewarp. if rb.range.end - rb.range.start >= timewarp_config.rollback_window { panic!( "⚠️⚠️⚠️ Attempted to rollback further than rollback_window: {rb:?} @ {:?}", game_clock.frame() ); - // error!("⚠️⚠️⚠️ Ignoring this rollback request. 🛼 "); - // TODO this isn't really safe - what if there was an ICAF or ABAF and then it never - // gets unpacked because it was outside the window. - // perhaps we need to mark the RB as "desperate", rollback to the oldest frame, - // and unpack anything destined for an even older (oob) frame that go around. - // at least unpack the ABAF ones, maybe don't care about SS in a desperate rollback. - // commands.remove_resource::(); - // return; } // save original period for restoration after rollback completion rb.original_period = Some(fx.period); rb_stats.num_rollbacks += 1; + let depth = rb.range.end - rb.range.start + 1; // we wind clock back 1 past first resim frame, so we can load in data for the frame prior // so we go into our first resim frame with components in the correct state. let reset_game_clock_to = rb.range.start.saturating_sub(1); - info!("🛼 ROLLBACK RESOURCE ADDED (rb#{}), reseting game clock from {game_clock:?}-->{reset_game_clock_to} rb:{rb:?}", + info!("🛼 ROLLBACK RESOURCE ADDED (rb#{} depth={depth}), reseting game clock from {game_clock:?}-->{reset_game_clock_to} rb:{rb:?}", rb_stats.num_rollbacks); // make fixed-update ticks free, ie fast-forward the simulation at max speed fx.period = Duration::ZERO; diff --git a/src/traits.rs b/src/traits.rs index 7eb31d9..c0b4ec2 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -82,10 +82,6 @@ impl TimewarpTraits for App { .in_set(TimewarpPrefixSet::First), ); } - self.add_systems( - schedule.clone(), // TODO RJRJR move to _first file? - prefix_not_in_rollback::detect_misuse_of_icaf::.in_set(TimewarpPrefixSet::First), - ); self.add_systems( schedule.clone(), // TODO RJRJ MOVE FILE prefix_first::record_component_death:: @@ -101,8 +97,8 @@ impl TimewarpTraits for App { self.add_systems( schedule.clone(), ( - prefix_not_in_rollback::detect_misuse_of_icaf::, - prefix_not_in_rollback::unpack_icafs_and_maybe_rollback::, + prefix_not_in_rollback::unpack_icafs_into_tw_components::, + prefix_not_in_rollback::unpack_icafs_adding_tw_components::, prefix_not_in_rollback::apply_snapshots_and_maybe_rollback::, ) .before(prefix_not_in_rollback::consolidate_rollback_requests) diff --git a/tests/component_add_and_remove.rs b/tests/component_add_and_remove.rs index 91aa73f..4d7bdd7 100644 --- a/tests/component_add_and_remove.rs +++ b/tests/component_add_and_remove.rs @@ -1,4 +1,4 @@ -use bevy::prelude::*; +use bevy::{ecs::query::Has, prelude::*}; use bevy_timewarp::prelude::*; mod test_utils; @@ -20,7 +20,18 @@ fn take_damage(mut q: Query<(Entity, &mut Enemy, &EntName, Option<&Shield>)>) { } } -fn log_all(game_clock: Res, q: Query<(Entity, &Enemy, &EntName, Option<&Shield>)>) { +fn log_all( + game_clock: Res, + q: Query<( + Entity, + &Enemy, + &EntName, + Option<&Shield>, + Option<&TimewarpStatus>, + Has>, + Has>, + )>, +) { for tuple in q.iter() { info!("f:{:?} {tuple:?}", game_clock.frame()); } @@ -156,7 +167,7 @@ fn component_add_and_remove() { // .entity_mut(e1) // .insert(InsertComponentAtFrame::::new(8, new_shield)); - tick(&mut app); // frame 10 + tick(&mut app); // frame 10 - rb assert_eq!( app.world diff --git a/tests/despawning.rs b/tests/despawning.rs index 6043766..d7c435f 100644 --- a/tests/despawning.rs +++ b/tests/despawning.rs @@ -170,8 +170,8 @@ fn despawn_revival_during_rollback() { ); // generate a rollback that should revive the component temporarily - let mut ss_e2 = app.world.get_mut::>(e1).unwrap(); - ss_e2.insert(2, Enemy { health: 100 }).unwrap(); + let mut ss_e1 = app.world.get_mut::>(e1).unwrap(); + ss_e1.insert(2, Enemy { health: 100 }).unwrap(); tick(&mut app); // frame 5 -- 1 of rollback_window until despawn