1use std::mem;
19
20use bevy_app::prelude::*;
21use bevy_ecs::message::Messages;
22use bevy_ecs::prelude::*;
23use bevy_ecs::system::SystemId;
24use bevy_log::warn;
25use bevy_reflect::prelude::*;
26use rig_core::effect::Decider;
27use rig_ecs::agent::{Notice, NoticeLevel};
28use rig_ecs::inbox::{DeliveryMode, Incoming, Recalled};
29use rig_ecs::stage::{
30 AgentPass, Decision, Effect, ForAgent, IncomingMessages, Outcome, ReadyToStart, Stage,
31};
32
33#[derive(Clone, Debug)]
36pub struct CommandArgs {
37 pub agent: Entity,
39 pub args: String,
41 pub mode: DeliveryMode,
44}
45
46#[derive(Component, Reflect, Clone, Debug)]
49#[reflect(Component, Clone, Debug)]
50pub struct SlashCommand {
51 pub help: String,
53}
54
55pub struct CommandPlugin;
58
59impl Plugin for CommandPlugin {
60 fn build(&self, app: &mut App) {
61 runner(app);
62 }
63}
64
65#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
67pub struct CommandSystems;
68
69#[derive(Resource)]
71struct Runner;
72
73fn runner(app: &mut App) {
75 if app.world().contains_resource::<Runner>() {
76 return;
77 }
78 app.insert_resource(Runner)
79 .configure_sets(AgentPass, CommandSystems.in_set(Stage::Gate))
80 .add_systems(AgentPass, run_typed.in_set(CommandSystems))
81 .add_observer(on_run_command);
82}
83
84fn run_typed(
87 mut typed: Query<
88 (&Effect, &Incoming, &ForAgent, &mut Decision),
89 (ReadyToStart, IncomingMessages),
90 >,
91 mut commands: Commands,
92) {
93 let mut lines: Vec<_> = typed
94 .iter_mut()
95 .filter_map(|(effect, message, agent, decision)| {
96 Some((
97 effect.id(),
98 message.command()?,
99 message.mode,
100 agent.0,
101 decision,
102 ))
103 })
104 .collect();
105 lines.sort_unstable_by_key(|(id, ..)| *id);
106 for (_, line, mode, agent, mut decision) in lines {
107 if decision.decide(Decider::plugin("rig-harness"), Outcome::custom("command")) {
108 commands.trigger(RunCommand {
109 mode,
110 ..RunCommand::new(agent, line)
111 });
112 }
113 }
114}
115
116pub trait AppCommandsExt {
121 fn add_command<M>(
124 &mut self,
125 name: &str,
126 help: &str,
127 system: impl IntoSystem<In<CommandArgs>, (), M> + 'static,
128 ) -> &mut Self;
129}
130
131impl AppCommandsExt for App {
132 fn add_command<M>(
133 &mut self,
134 name: &str,
135 help: &str,
136 system: impl IntoSystem<In<CommandArgs>, (), M> + 'static,
137 ) -> &mut Self {
138 runner(self);
139 let name = format!("/{name}");
140 if find(self.world_mut(), &name).is_some() {
141 warn!("command not registered: {name} exists; insert `Disabled` on it to replace it");
142 return self;
143 }
144 let system = self.register_system(system);
145 let help = help.to_owned();
146 let command = (Name::new(name), SlashCommand { help });
147 self.world_mut().entity_mut(system.entity()).insert(command);
148 self
149 }
150}
151
152fn find(world: &mut World, name: &str) -> Option<Entity> {
154 world
155 .query_filtered::<(Entity, &Name), With<SlashCommand>>()
156 .iter(world)
157 .find(|(_, command)| command.as_str() == name)
158 .map(|(entity, _)| entity)
159}
160
161#[derive(EntityEvent, Reflect, Clone, Debug)]
164#[reflect(Event, Clone, Debug)]
165pub struct RunCommand {
166 pub entity: Entity,
168 pub line: String,
170 pub mode: DeliveryMode,
173}
174
175impl RunCommand {
176 pub fn new(agent: Entity, line: impl Into<String>) -> Self {
178 Self {
179 entity: agent,
180 line: line.into(),
181 mode: DeliveryMode::Steer,
182 }
183 }
184}
185
186fn on_run_command(run: On<RunCommand>, mut commands: Commands) {
188 let (agent, line, mode) = (run.entity, run.line.clone(), run.mode);
189 commands.queue(move |world: &mut World| run_command(world, agent, line, mode));
190}
191
192fn run_command(world: &mut World, agent: Entity, line: String, mode: DeliveryMode) {
195 let typed = line.trim();
196 let (name, args) = typed.split_once(char::is_whitespace).unwrap_or((typed, ""));
197 let name = format!("/{name}");
198 let refusals = match find(world, &name) {
199 None => vec![format!("Unknown command {name}.")],
200 Some(system) => {
201 let earlier = swap_notices(world, Messages::default());
202 let ran = world.run_system_with(
203 SystemId::<In<CommandArgs>>::from_entity(system),
204 CommandArgs {
205 agent,
206 args: args.trim().to_owned(),
207 mode,
208 },
209 );
210 let mut reply = swap_notices(world, earlier);
211 let mut refusals: Vec<String> =
212 ran.err().map(|ran| ran.to_string()).into_iter().collect();
213 for notice in reply.drain() {
214 let refusal = notice.level == NoticeLevel::Error
215 && notice.agent.is_none_or(|about| about == agent);
216 if refusal {
217 refusals.push(notice.text);
218 } else {
219 world.write_message(notice);
220 }
221 }
222 refusals
223 }
224 };
225 if !refusals.is_empty() {
226 world.write_message(Recalled {
227 agent,
228 text: format!("/{line}"),
229 why: Some(refusals.join(" ")),
230 });
231 }
232}
233
234fn swap_notices(world: &mut World, notices: Messages<Notice>) -> Messages<Notice> {
236 match world.get_resource_mut::<Messages<Notice>>() {
237 Some(mut current) => mem::replace(&mut *current, notices),
238 None => notices,
239 }
240}
241
242#[cfg(test)]
243mod tests;