use std::mem;
use bevy_app::prelude::*;
use bevy_ecs::message::Messages;
use bevy_ecs::prelude::*;
use bevy_ecs::system::SystemId;
use bevy_log::warn;
use bevy_reflect::prelude::*;
use rig_core::effect::Decider;
use rig_ecs::agent::{Notice, NoticeLevel};
use rig_ecs::inbox::{DeliveryMode, Incoming, Recalled};
use rig_ecs::stage::{
AgentPass, Decision, Effect, ForAgent, IncomingMessages, Outcome, ReadyToStart, Stage,
};
#[derive(Clone, Debug)]
pub struct CommandArgs {
pub agent: Entity,
pub args: String,
pub mode: DeliveryMode,
}
#[derive(Component, Reflect, Clone, Debug)]
#[reflect(Component, Clone, Debug)]
pub struct SlashCommand {
pub help: String,
}
pub struct CommandPlugin;
impl Plugin for CommandPlugin {
fn build(&self, app: &mut App) {
runner(app);
}
}
#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct CommandSystems;
#[derive(Resource)]
struct Runner;
fn runner(app: &mut App) {
if app.world().contains_resource::<Runner>() {
return;
}
app.insert_resource(Runner)
.configure_sets(AgentPass, CommandSystems.in_set(Stage::Gate))
.add_systems(AgentPass, run_typed.in_set(CommandSystems))
.add_observer(on_run_command);
}
fn run_typed(
mut typed: Query<
(&Effect, &Incoming, &ForAgent, &mut Decision),
(ReadyToStart, IncomingMessages),
>,
mut commands: Commands,
) {
let mut lines: Vec<_> = typed
.iter_mut()
.filter_map(|(effect, message, agent, decision)| {
Some((
effect.id(),
message.command()?,
message.mode,
agent.0,
decision,
))
})
.collect();
lines.sort_unstable_by_key(|(id, ..)| *id);
for (_, line, mode, agent, mut decision) in lines {
if decision.decide(Decider::plugin("rig-harness"), Outcome::custom("command")) {
commands.trigger(RunCommand {
mode,
..RunCommand::new(agent, line)
});
}
}
}
pub trait AppCommandsExt {
fn add_command<M>(
&mut self,
name: &str,
help: &str,
system: impl IntoSystem<In<CommandArgs>, (), M> + 'static,
) -> &mut Self;
}
impl AppCommandsExt for App {
fn add_command<M>(
&mut self,
name: &str,
help: &str,
system: impl IntoSystem<In<CommandArgs>, (), M> + 'static,
) -> &mut Self {
runner(self);
let name = format!("/{name}");
if find(self.world_mut(), &name).is_some() {
warn!("command not registered: {name} exists; insert `Disabled` on it to replace it");
return self;
}
let system = self.register_system(system);
let help = help.to_owned();
let command = (Name::new(name), SlashCommand { help });
self.world_mut().entity_mut(system.entity()).insert(command);
self
}
}
fn find(world: &mut World, name: &str) -> Option<Entity> {
world
.query_filtered::<(Entity, &Name), With<SlashCommand>>()
.iter(world)
.find(|(_, command)| command.as_str() == name)
.map(|(entity, _)| entity)
}
#[derive(EntityEvent, Reflect, Clone, Debug)]
#[reflect(Event, Clone, Debug)]
pub struct RunCommand {
pub entity: Entity,
pub line: String,
pub mode: DeliveryMode,
}
impl RunCommand {
pub fn new(agent: Entity, line: impl Into<String>) -> Self {
Self {
entity: agent,
line: line.into(),
mode: DeliveryMode::Steer,
}
}
}
fn on_run_command(run: On<RunCommand>, mut commands: Commands) {
let (agent, line, mode) = (run.entity, run.line.clone(), run.mode);
commands.queue(move |world: &mut World| run_command(world, agent, line, mode));
}
fn run_command(world: &mut World, agent: Entity, line: String, mode: DeliveryMode) {
let typed = line.trim();
let (name, args) = typed.split_once(char::is_whitespace).unwrap_or((typed, ""));
let name = format!("/{name}");
let refusals = match find(world, &name) {
None => vec![format!("Unknown command {name}.")],
Some(system) => {
let earlier = swap_notices(world, Messages::default());
let ran = world.run_system_with(
SystemId::<In<CommandArgs>>::from_entity(system),
CommandArgs {
agent,
args: args.trim().to_owned(),
mode,
},
);
let mut reply = swap_notices(world, earlier);
let mut refusals: Vec<String> =
ran.err().map(|ran| ran.to_string()).into_iter().collect();
for notice in reply.drain() {
let refusal = notice.level == NoticeLevel::Error
&& notice.agent.is_none_or(|about| about == agent);
if refusal {
refusals.push(notice.text);
} else {
world.write_message(notice);
}
}
refusals
}
};
if !refusals.is_empty() {
world.write_message(Recalled {
agent,
text: format!("/{line}"),
why: Some(refusals.join(" ")),
});
}
}
fn swap_notices(world: &mut World, notices: Messages<Notice>) -> Messages<Notice> {
match world.get_resource_mut::<Messages<Notice>>() {
Some(mut current) => mem::replace(&mut *current, notices),
None => notices,
}
}
#[cfg(test)]
mod tests;