Skip to main content

steel_core/command/execution/
runtime.rs

1#![cfg_attr(
2    not(test),
3    expect(
4        dead_code,
5        reason = "custom runtime variants are reserved for future keyed command integrations"
6    )
7)]
8
9use std::sync::Arc;
10
11use crate::command::brigadier::{
12    CommandContext, CommandNodeBuilder, CommandRedirectTarget, CommandRuntime, CommandSyntaxError,
13    ContextChain,
14};
15use steel_registry::damage_type::DamageTypeRef;
16use steel_registry::{
17    enchantment::EnchantmentRef, entity_type::EntityTypeRef, item_stack::ItemStack,
18    timeline::TimelineRef, world_clock::WorldClockRef,
19};
20use steel_utils::{DowncastType, Identifier, nbt::NbtPath, translations, types::GameType};
21use text_components::TextComponent;
22
23use super::{
24    BiomeOrTag, BlockPredicate, ChainModifiers, CommandResultSuspension, CommandSource,
25    Coordinates, ExecutionCommandSource, ExecutionControl, GameProfileArgument, IntRange,
26    ItemPredicate, PermissionGroupName, ScoreHolderArgument, ScoreHolderWildcard,
27    SteelArgumentType, StructureOrTagKey, WorldArgument,
28    argument::{
29        ComponentValue, CoordinateAxes, DomainValue, EnchantmentValue, EntityTypeValue,
30        GameModeValue, IdentifierValue, ItemStackValue, NbtPathValue, ObjectiveValue,
31        SteelArgumentValue, TimeValue, TimelineValue, WorldClockValue,
32    },
33    selector::EntitySelector,
34};
35use crate::command::execution::argument::DamageTypeValue;
36use crate::{
37    chunk::heightmap::HeightmapType,
38    entity::{EntityAnchor, SharedEntity},
39    permission::{PermissionMetadataExpression, PermissionRuleExpression},
40    player::Player,
41    scoreboard::ScoreHolder,
42};
43
44/// Runtime model interpreted by Steel's tick-owned command scheduler.
45pub(crate) struct SteelCommandRuntime;
46
47pub(crate) type SteelCommandContext<S> = CommandContext<S, SteelCommandRuntime>;
48pub(crate) type SteelContextChain<S> = ContextChain<S, SteelCommandRuntime>;
49
50type StandardExecutor<S> =
51    dyn Fn(&SteelCommandContext<S>) -> Result<i32, CommandSyntaxError> + Send + Sync;
52type SuspendedExecutor<S> = dyn Fn(&SteelCommandContext<S>) -> Result<Box<dyn CommandResultSuspension>, CommandSyntaxError>
53    + Send
54    + Sync;
55type StandardModifier<S> =
56    dyn Fn(&SteelCommandContext<S>) -> Result<Vec<S>, CommandSyntaxError> + Send + Sync;
57
58/// A terminal executor stored in a Steel command graph.
59pub(crate) enum SteelExecutor<S>
60where
61    S: ExecutionCommandSource,
62{
63    Standard(Box<StandardExecutor<S>>),
64    Suspended(Box<SuspendedExecutor<S>>),
65    Custom(Arc<dyn CustomCommandExecutor<S>>),
66}
67
68/// A redirect modifier stored in a Steel command graph.
69pub(crate) enum SteelModifier<S>
70where
71    S: ExecutionCommandSource,
72{
73    Standard(Box<StandardModifier<S>>),
74    Custom(Arc<dyn CustomModifierExecutor<S>>),
75}
76
77/// Special terminal behavior that controls command frames or queues more work.
78pub(crate) trait CustomCommandExecutor<S>: Send + Sync
79where
80    S: ExecutionCommandSource,
81{
82    fn run(
83        &self,
84        source: Arc<S>,
85        chain: &SteelContextChain<S>,
86        modifiers: ChainModifiers,
87        control: &mut ExecutionControl<'_, S>,
88    );
89}
90
91/// Special redirect behavior that controls command frames or queues more work.
92pub(crate) trait CustomModifierExecutor<S>: Send + Sync
93where
94    S: ExecutionCommandSource,
95{
96    fn apply(
97        &self,
98        original_source: Arc<S>,
99        sources: Vec<Arc<S>>,
100        chain: &SteelContextChain<S>,
101        modifiers: ChainModifiers,
102        control: &mut ExecutionControl<'_, S>,
103    );
104}
105
106impl<S> CommandRuntime<S> for SteelCommandRuntime
107where
108    S: ExecutionCommandSource,
109{
110    type Argument = SteelArgumentType;
111    type ArgumentValue = SteelArgumentValue;
112    type Executor = SteelExecutor<S>;
113    type Modifier = SteelModifier<S>;
114}
115
116/// Creates a literal backed by Steel's runtime model.
117pub(crate) fn literal<S>(name: impl Into<Box<str>>) -> CommandNodeBuilder<S, SteelCommandRuntime>
118where
119    S: ExecutionCommandSource,
120{
121    CommandNodeBuilder::literal(name)
122}
123
124/// Creates an argument backed by Steel's runtime model.
125pub(crate) fn argument<S>(
126    name: impl Into<Box<str>>,
127    argument_type: impl Into<SteelArgumentType>,
128) -> CommandNodeBuilder<S, SteelCommandRuntime>
129where
130    S: ExecutionCommandSource,
131{
132    CommandNodeBuilder::argument(name, argument_type.into())
133}
134
135impl<S> CommandNodeBuilder<S, SteelCommandRuntime>
136where
137    S: ExecutionCommandSource,
138{
139    /// Attaches an ordinary synchronous executor.
140    #[must_use]
141    pub(crate) fn executes(
142        self,
143        executor: impl Fn(&SteelCommandContext<S>) -> Result<i32, CommandSyntaxError>
144        + Send
145        + Sync
146        + 'static,
147    ) -> Self {
148        self.executes_with_executor(Arc::new(SteelExecutor::Standard(Box::new(executor))))
149    }
150
151    /// Attaches an ordinary executor whose command result is produced across ticks.
152    #[must_use]
153    pub(crate) fn executes_suspended<T>(
154        self,
155        executor: impl Fn(&SteelCommandContext<S>) -> Result<T, CommandSyntaxError>
156        + Send
157        + Sync
158        + 'static,
159    ) -> Self
160    where
161        T: CommandResultSuspension,
162    {
163        let executor = move |context: &SteelCommandContext<S>| {
164            executor(context)
165                .map(|suspension| Box::new(suspension) as Box<dyn CommandResultSuspension>)
166        };
167        self.executes_with_executor(Arc::new(SteelExecutor::Suspended(Box::new(executor))))
168    }
169
170    /// Attaches an internal executor with frame and queue control.
171    #[must_use]
172    pub(crate) fn executes_custom(self, executor: impl CustomCommandExecutor<S> + 'static) -> Self {
173        self.executes_with_executor(Arc::new(SteelExecutor::Custom(Arc::new(executor))))
174    }
175
176    /// Redirects parsing and transforms the source once before continuing.
177    #[must_use]
178    pub(crate) fn redirects_with(
179        self,
180        target: impl Into<CommandRedirectTarget>,
181        modifier: impl Fn(&SteelCommandContext<S>) -> Result<S, CommandSyntaxError>
182        + Send
183        + Sync
184        + 'static,
185    ) -> Self {
186        let modifier = SteelModifier::Standard(Box::new(move |context| {
187            modifier(context).map(|source| vec![source])
188        }));
189        self.redirects_with_modifier(target, Arc::new(modifier), false)
190    }
191
192    /// Redirects parsing and expands one source into zero or more sources.
193    #[must_use]
194    pub(crate) fn forks(
195        self,
196        target: impl Into<CommandRedirectTarget>,
197        modifier: impl Fn(&SteelCommandContext<S>) -> Result<Vec<S>, CommandSyntaxError>
198        + Send
199        + Sync
200        + 'static,
201    ) -> Self {
202        self.redirects_with_modifier(
203            target,
204            Arc::new(SteelModifier::Standard(Box::new(modifier))),
205            true,
206        )
207    }
208
209    /// Redirects with an internal modifier that controls frames or queued work.
210    #[must_use]
211    pub(crate) fn redirects_custom(
212        self,
213        target: impl Into<CommandRedirectTarget>,
214        modifier: impl CustomModifierExecutor<S> + 'static,
215        forks: bool,
216    ) -> Self {
217        self.redirects_with_modifier(
218            target,
219            Arc::new(SteelModifier::Custom(Arc::new(modifier))),
220            forks,
221        )
222    }
223}
224
225impl<S> SteelCommandContext<S>
226where
227    S: ExecutionCommandSource,
228{
229    fn typed_argument<T: DowncastType>(&self, name: &str) -> Option<&T> {
230        self.argument(name)?.downcast_ref::<T>()
231    }
232
233    /// Returns a parsed Minecraft time argument in ticks.
234    pub(crate) fn time(&self, name: &str) -> Option<i32> {
235        self.typed_argument::<TimeValue>(name).map(|value| value.0)
236    }
237
238    /// Returns a parsed coordinate expression without resolving it early.
239    pub(crate) fn coordinates(&self, name: &str) -> Option<Coordinates> {
240        self.typed_argument::<Coordinates>(name).copied()
241    }
242
243    /// Returns a parsed entity position anchor.
244    pub(crate) fn entity_anchor(&self, name: &str) -> Option<EntityAnchor> {
245        self.typed_argument::<EntityAnchor>(name).copied()
246    }
247
248    pub(crate) fn swizzle(&self, name: &str) -> Option<CoordinateAxes> {
249        self.typed_argument::<CoordinateAxes>(name).copied()
250    }
251
252    pub(crate) fn heightmap(&self, name: &str) -> Option<HeightmapType> {
253        self.typed_argument::<HeightmapType>(name).copied()
254    }
255
256    pub(crate) fn score_holder_argument(&self, name: &str) -> Option<&ScoreHolderArgument> {
257        self.typed_argument(name)
258    }
259
260    pub(crate) fn objective_name(&self, name: &str) -> Option<&str> {
261        self.typed_argument::<ObjectiveValue>(name)
262            .map(|value| value.0.as_ref())
263    }
264
265    pub(crate) fn int_range(&self, name: &str) -> Option<IntRange> {
266        self.typed_argument::<IntRange>(name).copied()
267    }
268
269    pub(crate) fn biome_or_tag(&self, name: &str) -> Option<&BiomeOrTag> {
270        self.typed_argument(name)
271    }
272
273    pub(crate) fn structure_or_tag_key(&self, name: &str) -> Option<&StructureOrTagKey> {
274        self.typed_argument(name)
275    }
276
277    pub(crate) fn block_predicate(&self, name: &str) -> Option<&BlockPredicate> {
278        self.typed_argument(name)
279    }
280
281    /// Returns a configured Steel domain name.
282    pub(crate) fn domain(&self, name: &str) -> Option<&str> {
283        self.typed_argument::<DomainValue>(name)
284            .map(|value| value.0.as_ref())
285    }
286
287    pub(crate) fn world_argument(&self, name: &str) -> Option<&WorldArgument> {
288        self.typed_argument(name)
289    }
290
291    /// Returns a parsed vanilla game mode.
292    pub(crate) fn game_mode(&self, name: &str) -> Option<GameType> {
293        self.typed_argument::<GameModeValue>(name)
294            .map(|value| value.0)
295    }
296
297    pub(crate) fn entity_type(&self, name: &str) -> Option<EntityTypeRef> {
298        self.typed_argument::<EntityTypeValue>(name)
299            .map(|value| value.0)
300    }
301
302    pub(crate) fn enchantment(&self, name: &str) -> Option<EnchantmentRef> {
303        self.typed_argument::<EnchantmentValue>(name)
304            .map(|value| value.0)
305    }
306
307    pub(crate) fn damage_type(&self, name: &str) -> Option<DamageTypeRef> {
308        self.typed_argument::<DamageTypeValue>(name)
309            .map(|value| value.0)
310    }
311
312    pub(crate) fn item_stack(&self, name: &str) -> Option<&ItemStack> {
313        self.typed_argument::<ItemStackValue>(name)
314            .map(|value| &value.0)
315    }
316
317    pub(crate) fn item_predicate(&self, name: &str) -> Option<&ItemPredicate> {
318        self.typed_argument(name)
319    }
320
321    pub(crate) fn text_component(&self, name: &str) -> Option<&TextComponent> {
322        self.typed_argument::<ComponentValue>(name)
323            .map(|value| &value.0)
324    }
325
326    pub(crate) fn nbt_path(&self, name: &str) -> Option<&NbtPath> {
327        self.typed_argument::<NbtPathValue>(name)
328            .map(|value| &value.0)
329    }
330
331    pub(crate) fn identifier(&self, name: &str) -> Option<&Identifier> {
332        self.typed_argument::<IdentifierValue>(name)
333            .map(|value| &value.0)
334    }
335
336    pub(crate) fn world_clock(&self, name: &str) -> Option<WorldClockRef> {
337        self.typed_argument::<WorldClockValue>(name)
338            .map(|value| value.0)
339    }
340
341    pub(crate) fn timeline(&self, name: &str) -> Option<TimelineRef> {
342        self.typed_argument::<TimelineValue>(name)
343            .map(|value| value.0)
344    }
345
346    pub(crate) fn entity_selector(&self, name: &str) -> Option<&EntitySelector> {
347        self.typed_argument(name)
348    }
349
350    pub(crate) fn game_profile_argument(&self, name: &str) -> Option<&GameProfileArgument> {
351        self.typed_argument(name)
352    }
353
354    pub(crate) fn permission_rule_expression(
355        &self,
356        name: &str,
357    ) -> Option<&PermissionRuleExpression> {
358        self.typed_argument(name)
359    }
360
361    pub(crate) fn permission_metadata_expression(
362        &self,
363        name: &str,
364    ) -> Option<&PermissionMetadataExpression> {
365        self.typed_argument(name)
366    }
367
368    pub(crate) fn permission_group(&self, name: &str) -> Option<&PermissionGroupName> {
369        self.typed_argument(name)
370    }
371}
372
373impl SteelCommandContext<CommandSource> {
374    pub(crate) fn score_holders(
375        &self,
376        name: &str,
377        wildcard: ScoreHolderWildcard,
378    ) -> Result<Vec<ScoreHolder>, CommandSyntaxError> {
379        let holders = self
380            .score_holder_argument(name)
381            .ok_or_else(|| missing_score_holder_argument(name))?
382            .resolve(self.source(), wildcard)?;
383        if holders.is_empty() {
384            Err(CommandSyntaxError::dynamic(TextComponent::from(
385                &translations::ARGUMENT_SCORE_HOLDER_EMPTY,
386            )))
387        } else {
388            Ok(holders)
389        }
390    }
391
392    pub(crate) fn score_holder(&self, name: &str) -> Result<ScoreHolder, CommandSyntaxError> {
393        let mut holders = self.score_holders(name, ScoreHolderWildcard::Empty)?;
394        Ok(holders.remove(0))
395    }
396
397    pub(crate) fn optional_entities(
398        &self,
399        name: &str,
400    ) -> Result<Vec<SharedEntity>, CommandSyntaxError> {
401        self.entity_selector(name)
402            .ok_or_else(|| missing_selector_argument(name))?
403            .find_entities(self.source())
404    }
405
406    pub(crate) fn entities(&self, name: &str) -> Result<Vec<SharedEntity>, CommandSyntaxError> {
407        let entities = self.optional_entities(name)?;
408        if entities.is_empty() {
409            Err(CommandSyntaxError::dynamic(TextComponent::from(
410                &translations::ARGUMENT_ENTITY_NOTFOUND_ENTITY,
411            )))
412        } else {
413            Ok(entities)
414        }
415    }
416
417    pub(crate) fn entity(&self, name: &str) -> Result<SharedEntity, CommandSyntaxError> {
418        let mut entities = self.entities(name)?;
419        if entities.len() != 1 {
420            return Err(CommandSyntaxError::dynamic(TextComponent::from(
421                &translations::ARGUMENT_ENTITY_TOOMANY,
422            )));
423        }
424        Ok(entities.remove(0))
425    }
426
427    pub(crate) fn optional_players(
428        &self,
429        name: &str,
430    ) -> Result<Vec<Arc<Player>>, CommandSyntaxError> {
431        self.entity_selector(name)
432            .ok_or_else(|| missing_selector_argument(name))?
433            .find_players(self.source())
434    }
435
436    pub(crate) fn players(&self, name: &str) -> Result<Vec<Arc<Player>>, CommandSyntaxError> {
437        let players = self.optional_players(name)?;
438        if players.is_empty() {
439            Err(CommandSyntaxError::dynamic(TextComponent::from(
440                &translations::ARGUMENT_ENTITY_NOTFOUND_PLAYER,
441            )))
442        } else {
443            Ok(players)
444        }
445    }
446
447    pub(crate) fn player(&self, name: &str) -> Result<Arc<Player>, CommandSyntaxError> {
448        let mut players = self.players(name)?;
449        if players.len() != 1 {
450            return Err(CommandSyntaxError::dynamic(TextComponent::from(
451                &translations::ARGUMENT_PLAYER_TOOMANY,
452            )));
453        }
454        Ok(players.remove(0))
455    }
456}
457
458fn missing_selector_argument(name: &str) -> CommandSyntaxError {
459    CommandSyntaxError::dynamic(format!(
460        "Parsed selector for {name} is missing from the command context"
461    ))
462}
463
464fn missing_score_holder_argument(name: &str) -> CommandSyntaxError {
465    CommandSyntaxError::dynamic(format!(
466        "Parsed score holder for {name} is missing from the command context"
467    ))
468}