use std::fmt::Display;
use bevy::{
ecs::message::{MessageCursor, Messages},
platform::collections::HashMap,
prelude::*,
};
use crate::{
call::{
CurrentDialogueCall,
args::{DialogueArgs, FromDialogueArgs},
},
message::{DialogueCommand, ResumeDialogue, ResumeInput},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CommandFlow {
Immediate,
Blocking,
}
pub trait IntoCommandFlow {
fn into_flow(self) -> CommandFlow;
}
impl IntoCommandFlow for () {
fn into_flow(self) -> CommandFlow {
CommandFlow::Immediate
}
}
impl IntoCommandFlow for CommandFlow {
fn into_flow(self) -> CommandFlow {
self
}
}
impl<V, E> IntoCommandFlow for Result<V, E>
where
V: IntoCommandFlow,
E: Display,
{
fn into_flow(self) -> CommandFlow {
match self {
Ok(val) => val.into_flow(),
Err(err) => {
error!("{err}");
CommandFlow::Immediate
}
}
}
}
type CommandRunner = Box<dyn Fn(&mut World, DialogueArgs) -> CommandFlow + Send + Sync>;
#[derive(Resource, Default)]
pub(crate) struct DialogueCommandRegistry {
commands: HashMap<String, CommandRunner>,
}
pub trait RepliqueCommand {
const NAME: &'static str;
const DOC: &'static str;
fn register(self, app: &mut App);
}
pub trait DialogueCommandAppExt {
fn add_dialogue_command<S>(&mut self, system: S) -> &mut Self
where
S: RepliqueCommand;
fn add_dialogue_command_named<T, O, M>(
&mut self,
name: impl Into<String>,
system: impl IntoSystem<In<T>, O, M> + 'static,
) -> &mut Self
where
T: FromDialogueArgs + Send + Sync + 'static,
O: IntoCommandFlow + 'static;
}
impl DialogueCommandAppExt for App {
fn add_dialogue_command_named<T, O, M>(
&mut self,
name: impl Into<String>,
system: impl IntoSystem<In<T>, O, M> + 'static,
) -> &mut Self
where
T: FromDialogueArgs + Send + Sync + 'static,
O: IntoCommandFlow + 'static,
{
let name = name.into();
let world = self.world_mut();
let id = world.register_system(system);
let label = name.clone();
let run: CommandRunner = Box::new(move |world, args| {
let input = match T::from_dialogue_args(args) {
Ok(input) => input,
Err(err) => {
error!("dialogue command `{label}`: {err}");
return CommandFlow::Immediate;
}
};
match world.run_system_with(id, input) {
Ok(out) => out.into_flow(),
Err(err) => {
error!("dialogue command `{label}` failed: {err}");
CommandFlow::Immediate
}
}
});
world
.get_resource_or_insert_with(DialogueCommandRegistry::default)
.commands
.insert(name, run);
self
}
fn add_dialogue_command<S>(&mut self, system: S) -> &mut Self
where
S: RepliqueCommand,
{
system.register(self);
self
}
}
pub(crate) fn run_dialogue_commands(
world: &mut World,
mut cursor: Local<MessageCursor<DialogueCommand>>,
) {
let pending: Vec<DialogueCommand> = {
let Some(messages) = world.get_resource::<Messages<DialogueCommand>>() else {
return;
};
cursor.read(messages).cloned().collect()
};
if pending.is_empty() || !world.contains_resource::<DialogueCommandRegistry>() {
return;
}
world.resource_scope(|world, registry: Mut<DialogueCommandRegistry>| {
for command in pending {
let flow = match registry.commands.get(&command.name) {
Some(func) => {
world.insert_resource(CurrentDialogueCall(command.token));
let flow = func(world, DialogueArgs(command.args));
world.remove_resource::<CurrentDialogueCall>();
flow
}
None => {
error!("dialogue command {}: not found in registry", command.name);
CommandFlow::Immediate
}
};
let waits = match (command.awaited, flow) {
(true, CommandFlow::Blocking) => true,
(false, CommandFlow::Blocking) => {
warn!(
"dialogue command `{}` asked to block, but the line does not `await` it: the dialogue carries on",
command.name
);
false
}
(_, CommandFlow::Immediate) => false,
};
if !waits {
world.write_message(ResumeDialogue {
token: command.token,
input: ResumeInput::Advance,
});
}
}
});
}
#[cfg(test)]
mod tests {
use bevy::asset::AssetPlugin;
use replique::dialogue::Value;
use super::*;
use crate::{
call::{DialogueCall, DialogueToken},
plugin::RepliquePlugin,
};
#[derive(Resource, Default, Debug, PartialEq)]
struct Ran(Vec<String>);
fn app() -> App {
let mut app = App::new();
app.add_plugins((MinimalPlugins, AssetPlugin::default(), RepliquePlugin))
.init_resource::<Ran>();
app
}
fn spawn_runner(app: &mut App) -> Entity {
app.world_mut().spawn_empty().id()
}
fn call(app: &mut App, runner: Entity, name: &str, args: Vec<Value>) {
call_with(app, runner, name, args, false);
}
fn call_awaited(app: &mut App, runner: Entity, name: &str, args: Vec<Value>) {
call_with(app, runner, name, args, true);
}
fn call_with(app: &mut App, runner: Entity, name: &str, args: Vec<Value>, awaited: bool) {
app.world_mut().write_message(DialogueCommand {
token: DialogueToken::new(0, runner),
name: name.to_string(),
args,
awaited,
});
app.update();
}
fn ran(app: &App) -> &[String] {
&app.world().resource::<Ran>().0
}
fn resumed(app: &App) -> Vec<Entity> {
app.world()
.resource::<Messages<ResumeDialogue>>()
.iter_current_update_messages()
.map(|resume| resume.token.runner())
.collect()
}
#[test]
fn a_command_takes_its_arguments_already_typed() {
fn play(In((sound, volume, loops)): In<(String, f32, bool)>, mut ran: ResMut<Ran>) {
ran.0.push(format!("{sound} {volume} {loops}"));
}
let mut app = app();
app.add_dialogue_command_named("play", play);
let runner = spawn_runner(&mut app);
call(
&mut app,
runner,
"play",
vec![
Value::String("bell".into()),
Value::Float(0.5),
Value::Bool(true),
],
);
assert_eq!(ran(&app), ["bell 0.5 true"]);
}
#[test]
fn a_command_that_does_not_fit_its_signature_is_skipped_but_resumes() {
fn play(In(_): In<(String, f32)>, mut ran: ResMut<Ran>) {
ran.0.push("ran".into());
}
let mut app = app();
app.add_dialogue_command_named("play", play);
let runner = spawn_runner(&mut app);
call(&mut app, runner, "play", vec![Value::Bool(true)]);
assert_eq!(ran(&app), [] as [String; 0]);
assert_eq!(resumed(&app), [runner]);
}
#[test]
fn raw_arguments_stay_reachable() {
fn any(In(args): In<DialogueArgs>, mut ran: ResMut<Ran>) {
ran.0.push(format!("{}", args.0.len()));
}
let mut app = app();
app.add_dialogue_command_named("any", any);
let runner = spawn_runner(&mut app);
call(&mut app, runner, "any", vec![Value::Bool(true)]);
assert_eq!(ran(&app), ["1".to_string()]);
}
#[test]
fn a_registered_command_resumes_its_runner() {
fn noop(In(()): In<()>) {}
let mut app = app();
app.add_dialogue_command_named("noop", noop);
let runner = spawn_runner(&mut app);
call(&mut app, runner, "noop", vec![]);
assert_eq!(resumed(&app), [runner]);
}
#[test]
fn an_unregistered_command_resumes_tis_runner() {
let mut app = app();
let runner = spawn_runner(&mut app);
call(&mut app, runner, "unknown", vec![]);
assert_eq!(ran(&app), [] as [String; 0]);
assert_eq!(resumed(&app), [runner]);
}
#[test]
fn a_command_sees_the_whole_world() {
#[derive(Component)]
struct Spawned;
fn spawn(In(()): In<()>, mut commands: Commands) {
commands.spawn(Spawned);
}
let mut app = app();
app.add_dialogue_command_named("spawn", spawn);
let runner = spawn_runner(&mut app);
call(&mut app, runner, "spawn", vec![]);
assert_eq!(
app.world_mut()
.query::<&Spawned>()
.iter(app.world())
.count(),
1
);
}
#[test]
fn an_awaited_blocking_command_leaves_its_runner_suspended() {
fn play_anim(In(()): In<()>, mut ran: ResMut<Ran>) -> CommandFlow {
ran.0.push("started".into());
CommandFlow::Blocking
}
let mut app = app();
app.add_dialogue_command_named("play_anim", play_anim);
let runner = spawn_runner(&mut app);
call_awaited(&mut app, runner, "play_anim", vec![]);
assert_eq!(ran(&app), ["started"]);
assert_eq!(resumed(&app), [] as [Entity; 0]);
}
#[test]
fn a_blocking_command_that_is_not_awaited_resumes_anyway() {
fn play_anim(In(()): In<()>) -> CommandFlow {
CommandFlow::Blocking
}
let mut app = app();
app.add_dialogue_command_named("play_anim", play_anim);
let runner = spawn_runner(&mut app);
call(&mut app, runner, "play_anim", vec![]);
assert_eq!(resumed(&app), [runner]);
}
#[test]
fn an_awaited_immediate_command_resumes_its_runner() {
fn set_flag(In(()): In<()>) -> CommandFlow {
CommandFlow::Immediate
}
let mut app = app();
app.add_dialogue_command_named("set_flag", set_flag);
let runner = spawn_runner(&mut app);
call_awaited(&mut app, runner, "set_flag", vec![]);
assert_eq!(resumed(&app), [runner]);
}
#[test]
fn a_command_without_a_flow_is_immediate_even_when_awaited() {
fn noop(In(()): In<()>) {}
let mut app = app();
app.add_dialogue_command_named("noop", noop);
let runner = spawn_runner(&mut app);
call_awaited(&mut app, runner, "noop", vec![]);
assert_eq!(resumed(&app), [runner]);
}
#[test]
fn an_awaited_command_that_fails_resumes_rather_than_freezing() {
fn play_anim(In(()): In<()>) -> Result<CommandFlow, String> {
Err("no such animation".into())
}
let mut app = app();
app.add_dialogue_command_named("play_anim", play_anim);
let runner = spawn_runner(&mut app);
call_awaited(&mut app, runner, "play_anim", vec![]);
assert_eq!(resumed(&app), [runner]);
}
#[test]
fn an_awaited_command_with_bad_arguments_resumes() {
fn play_anim(In(_): In<(String, f32)>) -> CommandFlow {
CommandFlow::Blocking
}
let mut app = app();
app.add_dialogue_command_named("play_anim", play_anim);
let runner = spawn_runner(&mut app);
call_awaited(&mut app, runner, "play_anim", vec![Value::Bool(true)]);
assert_eq!(resumed(&app), [runner]);
}
#[test]
fn a_command_reads_the_runner_it_was_called_from() {
fn who(In(()): In<()>, call: DialogueCall, mut ran: ResMut<Ran>) {
ran.0.push(format!("{:?}", call.runner()));
}
let mut app = app();
app.add_dialogue_command_named("who", who);
let runner = spawn_runner(&mut app);
call(&mut app, runner, "who", vec![]);
assert_eq!(ran(&app), [format!("{runner:?}")]);
assert!(!app.world().contains_resource::<CurrentDialogueCall>());
}
#[test]
fn registering_a_name_twice_keeps_the_last_system() {
fn first(In(()): In<()>, mut ran: ResMut<Ran>) {
ran.0.push("first".into());
}
fn second(In(()): In<()>, mut ran: ResMut<Ran>) {
ran.0.push("second".into());
}
let mut app = app();
app.add_dialogue_command_named("play", first);
app.add_dialogue_command_named("play", second);
let runner = spawn_runner(&mut app);
call(&mut app, runner, "play", vec![]);
assert_eq!(ran(&app), ["second"]);
assert_eq!(
app.world()
.resource::<DialogueCommandRegistry>()
.commands
.len(),
1
);
}
}