initiative_core/app/command/
tutorial.rs

1use super::CommandType;
2use crate::app::{
3    AppCommand, AppMeta, Autocomplete, AutocompleteSuggestion, Command, CommandAlias,
4    CommandMatches, ContextAwareParse, Runnable,
5};
6use crate::reference::{ItemCategory, ReferenceCommand, Spell};
7use crate::storage::{Change, Record, StorageCommand};
8use crate::time::TimeCommand;
9use crate::utils::CaseInsensitiveStr;
10use crate::world::npc::{Age, Ethnicity, Gender, NpcData, Species};
11use crate::world::thing::ThingData;
12use crate::world::{ParsedThing, WorldCommand};
13use async_trait::async_trait;
14use std::fmt;
15use uuid::Uuid;
16
17/// An enum representing each possible state of the tutorial. The Introduction variant is mapped to
18/// the `tutorial` command, while each other variant is registered as a [`CommandAlias`] upon
19/// completion of the previous step.
20///
21/// **What's up with the data fields?** There is some dynamically generated content that gets
22/// carried along from one tutorial step to the next. In order for the "Deleting Things" step to
23/// drop the name of the randomly generated inn, it needs to be persisted through the entire
24/// process.
25///
26/// While the tutorial is active, *every* user input is interpreted as a [`TutorialCommand`] by
27/// registering a [`CommandAlias::StrictWildcard`]. [`TutorialCommand::run`] then re-parses the user
28/// input, displaying the result in all cases. If the input parsed to the correct command, it
29/// advances the tutorial to the next step and appends its own output to the end of the command
30/// output. If the user did something different, the command takes effect anyway, and a brief note
31/// about the tutorial still being active is appended to the output.
32#[derive(Clone, Debug, Eq, PartialEq)]
33pub enum TutorialCommand {
34    Introduction,
35    GeneratingLocations,
36    SavingLocations,
37    GeneratingCharacters {
38        inn: ThingRef,
39    },
40    GeneratingAlternatives {
41        inn: ThingRef,
42    },
43    ViewingAlternatives {
44        inn: ThingRef,
45        npc_name: String,
46    },
47    EditingCharacters {
48        inn: ThingRef,
49        npc: ThingRef,
50    },
51    TheJournal {
52        inn: ThingRef,
53        npc: ThingRef,
54    },
55    LoadingFromJournal {
56        inn: ThingRef,
57        npc: ThingRef,
58    },
59    SrdReference {
60        inn: ThingRef,
61        npc: ThingRef,
62    },
63    SrdReferenceLists {
64        inn: ThingRef,
65        npc: ThingRef,
66    },
67    RollingDice {
68        inn: ThingRef,
69        npc: ThingRef,
70    },
71    DeletingThings {
72        inn: ThingRef,
73        npc: ThingRef,
74    },
75    AdvancingTime {
76        inn: ThingRef,
77        npc: ThingRef,
78    },
79    CheckingTheTime {
80        inn: ThingRef,
81        npc: ThingRef,
82    },
83    Conclusion {
84        inn: ThingRef,
85        npc: ThingRef,
86    },
87
88    Cancel {
89        inn: Option<ThingRef>,
90        npc: Option<ThingRef>,
91    },
92    Resume,
93    Restart {
94        inn: Option<ThingRef>,
95        npc: Option<ThingRef>,
96    },
97}
98
99#[derive(Clone, Debug, Eq, PartialEq)]
100pub struct ThingRef {
101    uuid: Uuid,
102    name: String,
103}
104
105impl TutorialCommand {
106    /// Generate the output to be displayed to the user when invoking [`TutorialCommand::run`]. This
107    /// is done in a separate method because it can be invoked in two ways: by satisfying a
108    /// tutorial step and advancing to the next step, and by running the `resume` command to get a
109    /// reminder prompt indicating what the user is supposed to do.
110    ///
111    /// **Very counterintuitively**, there is an off-by-one state going on here. The `resume`
112    /// command doesn't have access to the *current* step, only the *next* step, which is
113    /// registered as an alias and monitoring the app state until its prompt is satisfied.
114    /// [`Self::Resume`], then, runs on that registered alias, while the various registered
115    /// variants work around this limitation by running the `output` method of the alias after
116    /// registration.
117    ///
118    /// That was the reasoning, anyhow. Whether or not it was a good decision is left to the
119    /// judgement of the reader.
120    fn output(
121        &self,
122        command_output: Option<Result<String, String>>,
123        app_meta: &mut AppMeta,
124    ) -> Result<String, String> {
125        let is_ok = if let Some(r) = &command_output {
126            r.is_ok()
127        } else {
128            true
129        };
130
131        let mut output = command_output
132            .unwrap_or_else(|| Ok(String::new()))
133            .unwrap_or_else(|e| e);
134        if !output.is_empty() {
135            output.push_str("\n\n#");
136        }
137
138        match self {
139            Self::Introduction | Self::Cancel { .. } | Self::Resume | Self::Restart { .. } => {}
140            Self::GeneratingLocations => {
141                app_meta.command_aliases.insert(CommandAlias::literal(
142                    "next",
143                    "continue the tutorial",
144                    Self::GeneratingLocations.into(),
145                ));
146
147                output.push_str(include_str!("../../../../data/tutorial/00-introduction.md"));
148            }
149            Self::SavingLocations => output.push_str(include_str!(
150                "../../../../data/tutorial/01-generating-locations.md"
151            )),
152            Self::GeneratingCharacters { inn } => {
153                app_meta.command_aliases.insert(CommandAlias::literal(
154                    "save",
155                    format!("save {}", inn),
156                    StorageCommand::Save {
157                        name: inn.to_string(),
158                    }
159                    .into(),
160                ));
161
162                output.push_str(&format!(
163                    include_str!("../../../../data/tutorial/02-saving-locations.md"),
164                    inn_name = inn,
165                ));
166            }
167            Self::GeneratingAlternatives { .. } => output.push_str(include_str!(
168                "../../../../data/tutorial/03-generating-characters.md"
169            )),
170            Self::ViewingAlternatives { npc_name, .. } => {
171                let thing_data: ThingData = NpcData {
172                    species: Species::Human.into(),
173                    ethnicity: Ethnicity::Human.into(),
174                    age: Age::Adult.into(),
175                    gender: Gender::Feminine.into(),
176                    ..Default::default()
177                }
178                .into();
179
180                app_meta.command_aliases.insert(CommandAlias::literal(
181                    "more",
182                    format!("create {}", thing_data.display_description()),
183                    WorldCommand::CreateMultiple { thing_data }.into(),
184                ));
185
186                output.push_str(&format!(
187                    include_str!("../../../../data/tutorial/04-generating-alternatives.md"),
188                    npc_name = npc_name,
189                ));
190            }
191            Self::EditingCharacters { npc, .. } => {
192                app_meta.command_aliases.insert(CommandAlias::literal(
193                    "2",
194                    format!("load {}", npc),
195                    StorageCommand::Load {
196                        name: npc.to_string(),
197                    }
198                    .into(),
199                ));
200
201                output.push_str(&format!(
202                    include_str!("../../../../data/tutorial/05-viewing-alternatives.md"),
203                    npc_name = npc,
204                ));
205            }
206            Self::TheJournal { npc, .. } => {
207                app_meta.command_aliases.insert(CommandAlias::literal(
208                    "save",
209                    format!("save {}", npc),
210                    StorageCommand::Save {
211                        name: npc.to_string(),
212                    }
213                    .into(),
214                ));
215
216                output.push_str(&format!(
217                    include_str!("../../../../data/tutorial/06-editing-characters.md"),
218                    npc_name = npc,
219                ));
220            }
221            Self::LoadingFromJournal { inn, .. } => output.push_str(&format!(
222                include_str!("../../../../data/tutorial/07-the-journal.md"),
223                inn_name = inn,
224            )),
225            Self::SrdReference { npc, .. } => output.push_str(&format!(
226                include_str!("../../../../data/tutorial/08-loading-from-journal.md"),
227                npc_name = npc,
228            )),
229            Self::SrdReferenceLists { .. } => output.push_str(include_str!(
230                "../../../../data/tutorial/09-srd-reference.md"
231            )),
232            Self::RollingDice { inn, npc } => output.push_str(&format!(
233                include_str!("../../../../data/tutorial/10-srd-reference-lists.md"),
234                inn_name = inn,
235                npc_name = npc,
236            )),
237            Self::DeletingThings { npc, .. } => output.push_str(&format!(
238                include_str!("../../../../data/tutorial/11-rolling-dice.md"),
239                npc_name = npc,
240            )),
241            Self::AdvancingTime { inn, npc } => output.push_str(&format!(
242                include_str!("../../../../data/tutorial/12-deleting-things.md"),
243                inn_name = inn,
244                npc_name = npc,
245            )),
246            Self::CheckingTheTime { .. } => output.push_str(include_str!(
247                "../../../../data/tutorial/13-advancing-time.md"
248            )),
249            Self::Conclusion { .. } => output.push_str(include_str!(
250                "../../../../data/tutorial/14-checking-the-time.md"
251            )),
252        }
253
254        if is_ok {
255            Ok(output)
256        } else {
257            Err(output)
258        }
259    }
260
261    /// Extract the inn reference from the enum variant, if present.
262    fn inn(&self) -> Option<ThingRef> {
263        match self {
264            Self::Introduction
265            | Self::GeneratingLocations
266            | Self::SavingLocations
267            | Self::Resume => None,
268
269            Self::GeneratingCharacters { inn }
270            | Self::GeneratingAlternatives { inn }
271            | Self::ViewingAlternatives { inn, .. }
272            | Self::EditingCharacters { inn, .. }
273            | Self::TheJournal { inn, .. }
274            | Self::LoadingFromJournal { inn, .. }
275            | Self::SrdReference { inn, .. }
276            | Self::SrdReferenceLists { inn, .. }
277            | Self::RollingDice { inn, .. }
278            | Self::DeletingThings { inn, .. }
279            | Self::AdvancingTime { inn, .. }
280            | Self::CheckingTheTime { inn, .. }
281            | Self::Conclusion { inn, .. }
282            | Self::Cancel { inn: Some(inn), .. }
283            | Self::Restart { inn: Some(inn), .. } => Some(inn.clone()),
284
285            Self::Cancel { inn: None, .. } | Self::Restart { inn: None, .. } => None,
286        }
287    }
288
289    /// Extract the NPC reference from the enum variant, if present.
290    fn npc(&self) -> Option<ThingRef> {
291        match self {
292            Self::Introduction
293            | Self::GeneratingLocations
294            | Self::SavingLocations
295            | Self::Resume
296            | Self::GeneratingCharacters { .. }
297            | Self::GeneratingAlternatives { .. }
298            | Self::ViewingAlternatives { .. } => None,
299
300            Self::EditingCharacters { npc, .. }
301            | Self::TheJournal { npc, .. }
302            | Self::LoadingFromJournal { npc, .. }
303            | Self::SrdReference { npc, .. }
304            | Self::SrdReferenceLists { npc, .. }
305            | Self::RollingDice { npc, .. }
306            | Self::DeletingThings { npc, .. }
307            | Self::AdvancingTime { npc, .. }
308            | Self::CheckingTheTime { npc, .. }
309            | Self::Conclusion { npc, .. }
310            | Self::Cancel { npc: Some(npc), .. }
311            | Self::Restart { npc: Some(npc), .. } => Some(npc.clone()),
312
313            Self::Cancel { npc: None, .. } | Self::Restart { npc: None, .. } => None,
314        }
315    }
316
317    /// Is this the command that is required to advance to the next step of the tutorial? This is
318    /// determined not by a string match but by validating the parsed result, eg. `time` and `now`
319    /// are equally recognized for the CheckingTheTime step because they both parse to
320    /// `CommandType::Time(TimeCommand::Now)`.
321    fn is_correct_command(&self, command: Option<&CommandType>) -> bool {
322        match self {
323            Self::Cancel { .. } | Self::Resume => false,
324            Self::Introduction | Self::Restart { .. } => true,
325            Self::GeneratingLocations => matches!(
326                command,
327                Some(CommandType::Tutorial(Self::GeneratingLocations))
328            ),
329            Self::SavingLocations => {
330                if let Some(CommandType::World(WorldCommand::Create { parsed_thing_data })) =
331                    command
332                {
333                    parsed_thing_data.thing_data
334                        == "inn".parse::<ParsedThing<ThingData>>().unwrap().thing_data
335                } else {
336                    false
337                }
338            }
339            Self::GeneratingCharacters { inn } => {
340                if let Some(CommandType::Storage(StorageCommand::Save { name })) = command {
341                    name.eq_ci(&inn.name)
342                } else {
343                    false
344                }
345            }
346            Self::GeneratingAlternatives { .. } => {
347                if let Some(CommandType::World(WorldCommand::Create {
348                    parsed_thing_data:
349                        ParsedThing {
350                            thing_data,
351                            unknown_words: _,
352                            word_count: _,
353                        },
354                })) = command
355                {
356                    thing_data.npc_data()
357                        == Some(&NpcData {
358                            species: Species::Human.into(),
359                            ethnicity: Ethnicity::Human.into(),
360                            age: Age::Adult.into(),
361                            gender: Gender::Feminine.into(),
362                            ..Default::default()
363                        })
364                } else {
365                    false
366                }
367            }
368            Self::ViewingAlternatives { .. } => {
369                if let Some(CommandType::World(WorldCommand::CreateMultiple { thing_data })) =
370                    command
371                {
372                    thing_data.npc_data()
373                        == Some(&NpcData {
374                            species: Species::Human.into(),
375                            ethnicity: Ethnicity::Human.into(),
376                            age: Age::Adult.into(),
377                            gender: Gender::Feminine.into(),
378                            ..Default::default()
379                        })
380                } else {
381                    false
382                }
383            }
384            Self::EditingCharacters { npc, .. } => {
385                if let Some(CommandType::Storage(StorageCommand::Load { name })) = command {
386                    name.eq_ci(&npc.name)
387                } else {
388                    false
389                }
390            }
391            Self::TheJournal { npc, .. } => {
392                if let Some(CommandType::World(WorldCommand::Edit {
393                    name,
394                    parsed_diff:
395                        ParsedThing {
396                            thing_data,
397                            unknown_words: _,
398                            word_count: _,
399                        },
400                })) = command
401                {
402                    name.eq_ci(&npc.name)
403                        && thing_data.npc_data()
404                            == Some(&NpcData {
405                                species: Species::HalfElf.into(),
406                                ..Default::default()
407                            })
408                } else {
409                    false
410                }
411            }
412            Self::LoadingFromJournal { .. } => {
413                matches!(command, Some(CommandType::Storage(StorageCommand::Journal)))
414            }
415            Self::SrdReference { npc, .. } => {
416                if let Some(CommandType::Storage(StorageCommand::Load { name })) = command {
417                    name.eq_ci(&npc.name)
418                } else {
419                    false
420                }
421            }
422            Self::SrdReferenceLists { .. } => {
423                matches!(
424                    command,
425                    Some(CommandType::Reference(ReferenceCommand::Spell(
426                        Spell::Fireball
427                    ))),
428                )
429            }
430            Self::RollingDice { .. } => {
431                matches!(
432                    command,
433                    Some(CommandType::Reference(ReferenceCommand::ItemCategory(
434                        ItemCategory::Weapon
435                    ))),
436                )
437            }
438            Self::DeletingThings { .. } => {
439                matches!(command, Some(CommandType::App(AppCommand::Roll(_))))
440            }
441            Self::AdvancingTime { inn, .. } => {
442                if let Some(CommandType::Storage(StorageCommand::Delete { name })) = command {
443                    name.eq_ci(&inn.name)
444                } else {
445                    false
446                }
447            }
448            Self::CheckingTheTime { .. } => {
449                matches!(command, Some(CommandType::Time(TimeCommand::Add { .. })))
450            }
451            Self::Conclusion { .. } => matches!(command, Some(CommandType::Time(TimeCommand::Now))),
452        }
453    }
454}
455
456#[async_trait(?Send)]
457impl Runnable for TutorialCommand {
458    async fn run(self, input: &str, app_meta: &mut AppMeta) -> Result<String, String> {
459        let input_command = Command::parse_input_irrefutable(input, app_meta).await;
460
461        if let Some(CommandType::Tutorial(
462            TutorialCommand::Cancel { inn, npc } | TutorialCommand::Restart { inn, npc },
463        )) = input_command.get_type()
464        {
465            if let Some(inn) = inn {
466                app_meta
467                    .repository
468                    .modify(Change::Delete {
469                        uuid: inn.uuid,
470                        name: inn.name.clone(),
471                    })
472                    .await
473                    .ok();
474            }
475
476            if let Some(npc) = npc {
477                app_meta
478                    .repository
479                    .modify(Change::Delete {
480                        uuid: npc.uuid,
481                        name: npc.name.clone(),
482                    })
483                    .await
484                    .ok();
485            }
486        }
487
488        app_meta.command_aliases.clear();
489
490        let (result, next_command) = if self.is_correct_command(input_command.get_type()) {
491            match self {
492                Self::Cancel { .. } | Self::Resume => unreachable!(),
493                Self::Introduction | Self::Restart { .. } => {
494                    let next = Self::GeneratingLocations;
495                    (next.output(None, app_meta), Some(next))
496                }
497                Self::GeneratingLocations => {
498                    let next = Self::SavingLocations;
499                    (next.output(None, app_meta), Some(next))
500                }
501                Self::SavingLocations => {
502                    let command_output = match input_command.run(input, app_meta).await {
503                        Ok(output) => {
504                            let inn_name = output
505                                .lines()
506                                .find(|s| s.starts_with('#'))
507                                .unwrap()
508                                .trim_start_matches(&[' ', '#'][..]);
509
510                            let Record { thing: inn, .. } =
511                                app_meta.repository.get_by_name(inn_name).await.unwrap();
512
513                            Ok((
514                                ThingRef {
515                                    name: inn.name().to_string(),
516                                    uuid: inn.uuid,
517                                },
518                                output,
519                            ))
520                        }
521                        Err(e) => Err(e),
522                    };
523
524                    match command_output {
525                        Ok((inn, output)) => {
526                            let next = Self::GeneratingCharacters { inn };
527                            (next.output(Some(Ok(output)), app_meta), Some(next))
528                        }
529                        Err(e) => (Err(e), Some(self)),
530                    }
531                }
532                Self::GeneratingCharacters { inn } => {
533                    let next = Self::GeneratingAlternatives { inn };
534
535                    (
536                        next.output(Some(input_command.run(input, app_meta).await), app_meta),
537                        Some(next),
538                    )
539                }
540                Self::GeneratingAlternatives { inn } => {
541                    let command_output = match input_command.run(input, app_meta).await {
542                        Ok(output) => {
543                            let npc_name = output
544                                .lines()
545                                .find(|s| s.starts_with('#'))
546                                .unwrap()
547                                .trim_start_matches(&[' ', '#'][..])
548                                .to_string();
549
550                            Ok((npc_name, output))
551                        }
552                        Err(e) => Err(e),
553                    };
554
555                    match command_output {
556                        Ok((npc_name, output)) => {
557                            let next = Self::ViewingAlternatives { inn, npc_name };
558                            (next.output(Some(Ok(output)), app_meta), Some(next))
559                        }
560                        Err(e) => (Err(e), Some(Self::GeneratingAlternatives { inn })),
561                    }
562                }
563                Self::ViewingAlternatives { inn, npc_name } => {
564                    let command_output = input_command.run(input, app_meta).await;
565
566                    if let Ok(output) = command_output {
567                        if let Some(new_npc_name) = output
568                            .lines()
569                            .find(|s| s.starts_with("~2~"))
570                            .and_then(|s| s.find('(').map(|i| (i, s)))
571                            .map(|(i, s)| s[10..i - 2].to_string())
572                        {
573                            let Record { thing: new_npc, .. } = app_meta
574                                .repository
575                                .get_by_name(&new_npc_name)
576                                .await
577                                .unwrap();
578
579                            let new_npc = ThingRef {
580                                name: new_npc_name,
581                                uuid: new_npc.uuid,
582                            };
583                            let next = Self::EditingCharacters { npc: new_npc, inn };
584
585                            (next.output(Some(Ok(output)), app_meta), Some(next))
586                        } else {
587                            (
588                                Ok(output),
589                                Some(Self::ViewingAlternatives { inn, npc_name }),
590                            )
591                        }
592                    } else {
593                        (
594                            command_output,
595                            Some(Self::ViewingAlternatives { inn, npc_name }),
596                        )
597                    }
598                }
599                Self::EditingCharacters { inn, npc } => {
600                    let command_output = input_command.run(input, app_meta).await;
601
602                    if let Ok(output) = command_output {
603                        let next = Self::TheJournal { inn, npc };
604
605                        (next.output(Some(Ok(output)), app_meta), Some(next))
606                    } else {
607                        (command_output, Some(Self::EditingCharacters { inn, npc }))
608                    }
609                }
610                Self::TheJournal { inn, npc } => {
611                    let next = Self::LoadingFromJournal { inn, npc };
612
613                    (
614                        next.output(Some(input_command.run(input, app_meta).await), app_meta),
615                        Some(next),
616                    )
617                }
618                Self::LoadingFromJournal { inn, npc } => {
619                    let next = Self::SrdReference { inn, npc };
620
621                    (
622                        next.output(Some(input_command.run(input, app_meta).await), app_meta),
623                        Some(next),
624                    )
625                }
626                Self::SrdReference { inn, npc } => {
627                    let next = Self::SrdReferenceLists { inn, npc };
628
629                    (
630                        next.output(Some(input_command.run(input, app_meta).await), app_meta),
631                        Some(next),
632                    )
633                }
634                Self::SrdReferenceLists { inn, npc } => {
635                    let next = Self::RollingDice { inn, npc };
636
637                    (
638                        next.output(Some(input_command.run(input, app_meta).await), app_meta),
639                        Some(next),
640                    )
641                }
642                Self::RollingDice { inn, npc } => {
643                    let next = Self::DeletingThings { inn, npc };
644
645                    (
646                        next.output(Some(input_command.run(input, app_meta).await), app_meta),
647                        Some(next),
648                    )
649                }
650                Self::DeletingThings { inn, npc } => {
651                    let next = Self::AdvancingTime { inn, npc };
652
653                    (
654                        next.output(Some(input_command.run(input, app_meta).await), app_meta),
655                        Some(next),
656                    )
657                }
658                Self::AdvancingTime { inn, npc, .. } => {
659                    let next = Self::CheckingTheTime { inn, npc };
660
661                    (
662                        next.output(Some(input_command.run(input, app_meta).await), app_meta),
663                        Some(next),
664                    )
665                }
666                Self::CheckingTheTime { inn, npc } => {
667                    let next = Self::Conclusion { inn, npc };
668
669                    (
670                        next.output(Some(input_command.run(input, app_meta).await), app_meta),
671                        Some(next),
672                    )
673                }
674                Self::Conclusion { inn, npc } => {
675                    app_meta
676                        .repository
677                        .modify(Change::Delete {
678                            name: inn.name,
679                            uuid: inn.uuid,
680                        })
681                        .await
682                        .ok();
683                    app_meta
684                        .repository
685                        .modify(Change::Delete {
686                            name: npc.name,
687                            uuid: npc.uuid,
688                        })
689                        .await
690                        .ok();
691
692                    (
693                        input_command.run(input, app_meta).await.map(|mut output| {
694                            output.push_str("\n\n#");
695                            output.push_str(include_str!(
696                                "../../../../data/tutorial/99-conclusion.md"
697                            ));
698                            output
699                        }),
700                        None,
701                    )
702                }
703            }
704        } else if let Some(CommandType::Tutorial(TutorialCommand::Cancel { .. })) =
705            input_command.get_type()
706        {
707            (
708                Ok(include_str!("../../../../data/tutorial/xx-cancelled.md").to_string()),
709                None,
710            )
711        } else if let Some(CommandType::Tutorial(TutorialCommand::Resume)) =
712            input_command.get_type()
713        {
714            (self.output(None, app_meta), Some(self))
715        } else {
716            let result = {
717                let f = |mut s: String| {
718                    if !s.is_empty() {
719                        s.push_str("\n\n#");
720                    }
721                    s.push_str(include_str!("../../../../data/tutorial/xx-still-active.md"));
722                    s
723                };
724
725                if !matches!(
726                    input_command.get_type(),
727                    Some(CommandType::Tutorial(TutorialCommand::Introduction))
728                ) {
729                    input_command.run(input, app_meta).await.map(f).map_err(f)
730                } else {
731                    Ok(f(String::new()))
732                }
733            };
734
735            app_meta.command_aliases.insert(CommandAlias::literal(
736                "resume",
737                "return to the tutorial",
738                Self::Resume.into(),
739            ));
740
741            app_meta.command_aliases.insert(CommandAlias::literal(
742                "restart",
743                "restart the tutorial",
744                Self::Restart {
745                    inn: self.inn(),
746                    npc: self.npc(),
747                }
748                .into(),
749            ));
750
751            (result, Some(self))
752        };
753
754        if let Some(command) = next_command {
755            app_meta.command_aliases.insert(CommandAlias::literal(
756                "cancel",
757                "cancel the tutorial",
758                Self::Cancel {
759                    inn: command.inn(),
760                    npc: command.npc(),
761                }
762                .into(),
763            ));
764
765            app_meta
766                .command_aliases
767                .insert(CommandAlias::strict_wildcard(command.into()));
768        }
769
770        result
771    }
772}
773
774#[async_trait(?Send)]
775impl ContextAwareParse for TutorialCommand {
776    async fn parse_input(input: &str, _app_meta: &AppMeta) -> CommandMatches<Self> {
777        if input.eq_ci("tutorial") {
778            CommandMatches::new_canonical(TutorialCommand::Introduction)
779        } else {
780            CommandMatches::default()
781        }
782    }
783}
784
785#[async_trait(?Send)]
786impl Autocomplete for TutorialCommand {
787    async fn autocomplete(input: &str, _app_meta: &AppMeta) -> Vec<AutocompleteSuggestion> {
788        if "tutorial".starts_with_ci(input) {
789            vec![AutocompleteSuggestion::new(
790                "tutorial",
791                "feature walkthrough",
792            )]
793        } else {
794            Vec::new()
795        }
796    }
797}
798
799impl fmt::Display for TutorialCommand {
800    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
801        match self {
802            Self::Introduction => write!(f, "tutorial"),
803            _ => Ok(()),
804        }
805    }
806}
807
808impl fmt::Display for ThingRef {
809    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
810        write!(f, "{}", self.name)
811    }
812}