bevy_socratic 0.0.1

A bevy plugin for dialog
Documentation
use std::str::FromStr;

use bevy::{
    asset::{Asset, AssetLoader, LoadContext, LoadedAsset},
    prelude::*,
    utils::BoxedFuture,
};
use serde::de::DeserializeOwned;
use socratic::DialogState;

pub struct SocraticPlugin<AssetWrapper, State>(std::marker::PhantomData<(AssetWrapper, State)>);

impl<W, S> Default for SocraticPlugin<W, S> {
    fn default() -> Self {
        Self(std::marker::PhantomData)
    }
}

impl<AssetWrapper, State> Plugin for SocraticPlugin<AssetWrapper, State>
where
    State: BevyDialogState + Default + Send + Sync + 'static,
    State::DoAction: FromStr + DeserializeOwned + std::fmt::Debug,
    State::IF: FromStr + DeserializeOwned,
    State::Interpolation: FromStr + DeserializeOwned,
    <State::IF as FromStr>::Err: Sync + Send + std::fmt::Debug + std::fmt::Display,
    <State::DoAction as FromStr>::Err: Sync + Send + std::fmt::Debug + std::fmt::Display,
    <State::Interpolation as FromStr>::Err: Sync + Send + std::fmt::Debug + std::fmt::Display,
    AssetWrapper: Asset
        + std::ops::Deref<Target = socratic::Dialog<State::DoAction, State::IF, State::Interpolation>>
        + std::ops::DerefMut
        + From<socratic::Dialog<State::DoAction, State::IF, State::Interpolation>>,
{
    fn build(&self, app: &mut App) {
        app.add_asset::<AssetWrapper>()
            .init_asset_loader::<CBORDialogLoader<AssetWrapper, State>>()
            .init_asset_loader::<DialogLoader<AssetWrapper, State>>()
            .add_event::<DialogInputEvent<AssetWrapper>>()
            .add_event::<DialogOutputEvent<AssetWrapper>>()
            .add_system(handle_events::<AssetWrapper, State>);
    }
}

pub trait BevyDialogState {
    type EmitAction: Send + Sync + 'static;
    type DoAction;
    type IF;
    type Interpolation;

    fn do_action(&mut self, events: &mut EventWriter<Self::EmitAction>, command: &Self::DoAction);
    fn check_condition(&self, cond: &Self::IF) -> bool;
    fn interpolate(&self, command: &Self::Interpolation) -> String;
}

struct StateWrapper<'a, 'b, 'c, BDS: BevyDialogState>(
    &'a mut EventWriter<'b, 'c, BDS::EmitAction>,
    &'a mut BDS,
);

impl<'a, 'b, 'c, BDS: BevyDialogState> DialogState for StateWrapper<'a, 'b, 'c, BDS> {
    type DoAction = BDS::DoAction;
    type IF = BDS::IF;
    type Interpolation = BDS::Interpolation;

    fn do_action(&mut self, command: &Self::DoAction) {
        self.1.do_action(self.0, command);
    }

    fn check_condition(&self, cond: &Self::IF) -> bool {
        self.1.check_condition(cond)
    }

    fn interpolate(&self, command: &Self::Interpolation) -> String {
        self.1.interpolate(command)
    }
}

pub struct CBORDialogLoader<Wrapper, State>(std::marker::PhantomData<(Wrapper, State)>);
impl<W, S> Default for CBORDialogLoader<W, S> {
    fn default() -> Self {
        Self(std::marker::PhantomData)
    }
}

impl<S, W> AssetLoader for CBORDialogLoader<W, S>
where
    S: BevyDialogState + Send + Sync + 'static,
    S::DoAction: DeserializeOwned,
    S::IF: DeserializeOwned,
    S::Interpolation: DeserializeOwned,
    W: Asset + From<socratic::Dialog<S::DoAction, S::IF, S::Interpolation>>,
{
    fn load<'a>(
        &'a self,
        bytes: &'a [u8],
        load_context: &'a mut LoadContext,
    ) -> BoxedFuture<'a, anyhow::Result<()>> {
        Box::pin(async move {
            load_context.set_default_asset(LoadedAsset::new(W::from(
                socratic::Dialog::packed_from_reader(bytes)?,
            )));
            Ok(())
        })
    }

    fn extensions(&self) -> &[&str] {
        &["csdlg"]
    }
}

pub struct DialogLoader<Wrapper, State>(std::marker::PhantomData<(Wrapper, State)>);
impl<W, S> Default for DialogLoader<W, S> {
    fn default() -> Self {
        Self(std::marker::PhantomData)
    }
}

impl<S, W> AssetLoader for DialogLoader<W, S>
where
    S: BevyDialogState + Send + Sync + 'static,
    W: Asset + From<socratic::Dialog<S::DoAction, S::IF, S::Interpolation>>,
    S::DoAction: FromStr,
    S::IF: FromStr,
    S::Interpolation: FromStr,
    <S::IF as FromStr>::Err: Sync + Send + std::fmt::Debug + std::fmt::Display,
    <S::DoAction as FromStr>::Err: Sync + Send + std::fmt::Debug + std::fmt::Display,
    <S::Interpolation as FromStr>::Err: Sync + Send + std::fmt::Debug + std::fmt::Display,
{
    fn load<'a>(
        &'a self,
        bytes: &'a [u8],
        load_context: &'a mut LoadContext,
    ) -> BoxedFuture<'a, anyhow::Result<()>> {
        Box::pin(async move {
            load_context.set_default_asset(LoadedAsset::new(W::from(
                socratic::Dialog::parse_from_reader(bytes)?,
            )));
            Ok(())
        })
    }

    fn extensions(&self) -> &[&str] {
        &["sdlg"]
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DialogInputEvent<AssetWrapper: Asset> {
    Begin(Handle<AssetWrapper>, String),
    Next(Handle<AssetWrapper>, socratic::DialogIndex),
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DialogOutputEvent<AssetWrapper: Asset> {
    DialogLine(
        Handle<AssetWrapper>,
        socratic::DialogItem,
        socratic::DialogIndex,
    ),
    DialogEnd(Handle<AssetWrapper>),
}

fn handle_events<Wrapper, State>(
    mut input_events: EventReader<DialogInputEvent<Wrapper>>,
    mut output_events: EventWriter<DialogOutputEvent<Wrapper>>,
    mut emit_events: EventWriter<State::EmitAction>,
    assets: Res<Assets<Wrapper>>,
    mut state: ResMut<State>,
) where
    Wrapper: Asset
        + std::ops::Deref<Target = socratic::Dialog<State::DoAction, State::IF, State::Interpolation>>
        + std::ops::DerefMut,
    State: BevyDialogState + Sync + Send + 'static,
    State::DoAction: std::fmt::Debug,
{
    for event in input_events.iter() {
        match event {
            DialogInputEvent::Begin(handle, section) => {
                let dialog = assets.get(handle).expect("invalid dialog handle");
                let mut wrapped_state = StateWrapper(&mut emit_events, state.as_mut());
                if let Some((line, index)) = dialog.begin(section, &mut wrapped_state) {
                    info!("Beginning {line}");
                    output_events.send(DialogOutputEvent::DialogLine(
                        handle.clone_weak(),
                        line,
                        index,
                    ));
                } else {
                    error!("Failed to begin!");
                }
            }
            DialogInputEvent::Next(handle, index) => {
                let dialog = assets.get(handle).expect("invalid dialog handle");
                let mut wrapped_state = StateWrapper(&mut emit_events, state.as_mut());
                if let Some((line, index)) = dialog.get(index.clone(), &mut wrapped_state) {
                    info!("Next {line}");
                    output_events.send(DialogOutputEvent::DialogLine(
                        handle.clone_weak(),
                        line,
                        index,
                    ));
                } else {
                    output_events.send(DialogOutputEvent::DialogEnd(handle.clone_weak()));
                }
            }
        }
    }
}