use std::marker::PhantomData;
use bevy_asset::{Asset, AssetLoader, Handle, LoadContext, io::Reader};
use bevy_ecs::bundle::Bundle;
use bevy_ecs::component::Component;
use bevy_reflect::TypePath;
use brink_format::{EffectRowEntry, LineEntry};
use brink_runtime::{FlowInstance, Program, RuntimeError, World};
use crate::line_tables::BrinkLocale;
#[derive(Asset, TypePath)]
pub struct ProgramAsset {
pub program: Program,
pub initial_context: World,
pub effect_rows: Vec<EffectRowEntry>,
}
#[derive(Asset, TypePath)]
pub struct LineTablesAsset {
pub tables: Vec<Vec<LineEntry>>,
}
#[derive(Asset, TypePath)]
pub struct BrinkStoryAsset {
pub program: Handle<ProgramAsset>,
pub line_tables: Handle<LineTablesAsset>,
}
#[derive(Default, TypePath)]
pub struct InkbLoader;
#[derive(Debug, thiserror::Error)]
pub enum InkbLoaderError {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("invalid .inkb: {0:?}")]
Decode(brink_format::DecodeError),
#[error("link error: {0}")]
Link(#[from] RuntimeError),
}
impl From<brink_format::DecodeError> for InkbLoaderError {
fn from(err: brink_format::DecodeError) -> Self {
Self::Decode(err)
}
}
impl AssetLoader for InkbLoader {
type Asset = BrinkStoryAsset;
type Settings = ();
type Error = InkbLoaderError;
async fn load(
&self,
reader: &mut dyn Reader,
_settings: &Self::Settings,
load_context: &mut LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
let story_data = brink_format::read_inkb(&bytes)?;
let (program, tables) = brink_runtime::link(&story_data)?;
Ok(emit_story_assets(
load_context,
program,
tables,
story_data.effect_rows,
))
}
fn extensions(&self) -> &[&str] {
&["inkb"]
}
}
pub(crate) fn fresh_context(program: &Program) -> World {
let (_, context) = FlowInstance::new_at_root(program);
context
}
pub(crate) fn emit_story_assets(
load_context: &mut LoadContext<'_>,
program: Program,
tables: Vec<Vec<LineEntry>>,
effect_rows: Vec<EffectRowEntry>,
) -> BrinkStoryAsset {
let initial_context = fresh_context(&program);
let program = load_context.add_labeled_asset(
"program".to_string(),
ProgramAsset {
program,
initial_context,
effect_rows,
},
);
let line_tables =
load_context.add_labeled_asset("line_tables".to_string(), LineTablesAsset { tables });
BrinkStoryAsset {
program,
line_tables,
}
}
#[derive(Component)]
pub struct BrinkProgram<M: Send + Sync + 'static = ()> {
pub handle: Handle<ProgramAsset>,
_marker: PhantomData<fn() -> M>,
}
impl<M: Send + Sync + 'static> BrinkProgram<M> {
#[must_use]
pub fn new(handle: Handle<ProgramAsset>) -> Self {
Self {
handle,
_marker: PhantomData,
}
}
}
#[derive(Bundle)]
pub struct BrinkStory<M: Send + Sync + 'static = ()> {
pub program: BrinkProgram<M>,
pub locale: BrinkLocale<M>,
}
impl<M: Send + Sync + 'static> BrinkStory<M> {
#[must_use]
pub fn new(program: Handle<ProgramAsset>, line_tables: Handle<LineTablesAsset>) -> Self {
Self {
program: BrinkProgram::new(program),
locale: BrinkLocale::new(line_tables),
}
}
}
#[cfg(test)]
mod fresh_context_tests {
use crate::test_support::compile_test_story;
#[test]
fn fresh_context_picks_up_var_defaults() {
let source = "VAR score = 42\n=== start ===\nHello.\n* [Continue] -> END\n";
let (program, tables, ctx) = compile_test_story(source);
let mut score_value = None;
for slot in 0..program.global_count() {
if program.global_name(slot) == Some("score") {
score_value = Some(ctx.globals[slot as usize].clone());
}
}
assert!(score_value.is_some(), "score global should exist");
assert!(
matches!(score_value.unwrap(), brink_format::Value::Int(42)),
"score should be 42 from the VAR default"
);
assert!(!tables.is_empty(), "compiled story should have line tables");
}
}