use std::marker::PhantomData;
use bevy_asset::{AssetEvent, Assets, Handle};
use bevy_ecs::component::Component;
use bevy_ecs::entity::Entity;
use bevy_ecs::message::MessageReader;
use bevy_ecs::system::{Commands, Query, Res};
use bevy_log::{info, warn};
use brink_format::LineEntry;
use brink_runtime::{Context, FallbackHandler, FastRng, FlowInstance, Line, RuntimeError};
use crate::asset::{BrinkProgram, BrinkStoryAsset, LineTablesAsset, ProgramAsset};
use crate::event::BrinkFlowReset;
use crate::flow::BrinkFlow;
use crate::globals::BrinkContext;
use crate::request::FlowStart;
#[derive(Component)]
pub struct BrinkReplayLog<M: Send + Sync + 'static = ()> {
pub start_context: Context,
pub start: FlowStart,
pub story: Handle<BrinkStoryAsset>,
pub choices_made: Vec<usize>,
_marker: PhantomData<fn() -> M>,
}
impl<M: Send + Sync + 'static> BrinkReplayLog<M> {
pub(crate) fn new(
start_context: Context,
start: FlowStart,
story: Handle<BrinkStoryAsset>,
) -> Self {
Self {
start_context,
start,
story,
choices_made: Vec::new(),
_marker: PhantomData,
}
}
}
#[expect(
clippy::needless_pass_by_value,
clippy::type_complexity,
reason = "bevy systems take Res/Query by value and have complex query tuples"
)]
pub fn replay_on_reload<M: Send + Sync + 'static>(
mut events: MessageReader<AssetEvent<ProgramAsset>>,
mut flows: Query<(
Entity,
&mut BrinkFlow<M>,
&BrinkProgram<M>,
&mut BrinkContext<M>,
&mut BrinkReplayLog<M>,
)>,
programs: Res<Assets<ProgramAsset>>,
stories: Res<Assets<BrinkStoryAsset>>,
line_tables_assets: Res<Assets<LineTablesAsset>>,
mut commands: Commands,
) {
let mut any_modified = false;
for event in events.read() {
if matches!(event, AssetEvent::Modified { .. }) {
any_modified = true;
}
}
if !any_modified {
return;
}
for (entity, mut flow, brink_program, mut context, log) in &mut flows {
let Some(program_asset) = programs.get(&brink_program.handle) else {
continue;
};
let line_tables: &[Vec<LineEntry>] = match stories
.get(&log.story)
.and_then(|bundle| line_tables_assets.get(&bundle.line_tables))
{
Some(lt_asset) => <_asset.tables,
None => continue,
};
commands.trigger(BrinkFlowReset::<M>::new(entity));
let new_flow_result = match &log.start {
FlowStart::Root => Some(FlowInstance::new_at_root(&program_asset.program)),
FlowStart::Address(name) => program_asset
.program
.find_address(name)
.map(|(idx, _)| FlowInstance::new_at(&program_asset.program, idx)),
};
let Some((new_flow, _fresh_ctx)) = new_flow_result else {
warn!(
"replay: knot '{:?}' missing in reloaded program; entity {entity:?} will start at root",
log.start
);
let (root_flow, _) = FlowInstance::new_at_root(&program_asset.program);
commands
.entity(entity)
.insert(BrinkFlow::<M>::new(root_flow));
continue;
};
context.inner = log.start_context.clone();
flow.inner = new_flow;
let mut replay_failed = false;
for (i, &choice_idx) in log.choices_made.iter().enumerate() {
if let Err(err) = step_to_next_choices(
&mut flow.inner,
&program_asset.program,
line_tables,
&mut context.inner,
) {
warn!(
"replay: failed to reach choice point {i} for entity {entity:?}: {err}; \
stopping replay"
);
replay_failed = true;
break;
}
if let Err(err) = flow.inner.choose(&mut context.inner, choice_idx) {
warn!(
"replay: choose({choice_idx}) at step {i} for entity {entity:?}: {err}; \
stopping replay"
);
replay_failed = true;
break;
}
}
if replay_failed {
continue;
}
match flow.advance_until_terminal(
&program_asset.program,
line_tables,
&mut context.inner,
&FallbackHandler,
entity,
&mut commands,
) {
Ok(_) => {
info!(
"replay: rebuilt flow on entity {entity:?} from start={:?} +{} choice(s)",
log.start,
log.choices_made.len()
);
}
Err(err) => {
warn!("replay: advance after replay failed on entity {entity:?}: {err}");
}
}
}
}
fn step_to_next_choices(
flow: &mut FlowInstance,
program: &brink_runtime::Program,
line_tables: &[Vec<LineEntry>],
context: &mut Context,
) -> Result<(), RuntimeError> {
const STEP_LIMIT: usize = 10_000;
for _ in 0..STEP_LIMIT {
let line = flow.step_single_line::<FastRng>(
program,
line_tables,
context,
&FallbackHandler,
None,
)?;
match line {
Line::Choices { .. } | Line::Done { .. } | Line::End { .. } => return Ok(()),
Line::Text { .. } => {}
}
}
Err(RuntimeError::StepLimitExceeded(STEP_LIMIT as u64))
}