veloren_common/
cmd.rs

1use crate::{
2    assets::{self, AssetCombined, Concatenate},
3    combat::GroupTarget,
4    comp::{
5        self, AdminRole as Role, Skill, aura::AuraKindVariant, buff::BuffKind,
6        inventory::item::try_all_item_defs,
7    },
8    generation::try_all_entity_configs,
9    npc, outcome,
10    recipe::RecipeBookManifest,
11    spot::Spot,
12    terrain,
13};
14use common_i18n::Content;
15use hashbrown::{HashMap, HashSet};
16use lazy_static::lazy_static;
17use serde::{Deserialize, Serialize};
18use std::{
19    fmt::{self, Display},
20    str::FromStr,
21};
22use strum::{AsRefStr, EnumIter, EnumString, IntoEnumIterator, VariantNames};
23use tracing::warn;
24
25/// Struct representing a command that a user can run from server chat.
26pub struct ChatCommandData {
27    /// A list of arguments useful for both tab completion and parsing
28    pub args: Vec<ArgumentSpec>,
29    /// The i18n content for the description of the command
30    pub description: Content,
31    /// Whether the command requires administrator permissions.
32    pub needs_role: Option<Role>,
33}
34
35impl ChatCommandData {
36    pub fn new(args: Vec<ArgumentSpec>, description: Content, needs_role: Option<Role>) -> Self {
37        Self {
38            args,
39            description,
40            needs_role,
41        }
42    }
43}
44
45#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
46pub enum KitSpec {
47    Item(String),
48    ModularWeaponSet {
49        tool: comp::tool::ToolKind,
50        material: comp::item::Material,
51        hands: Option<comp::item::tool::Hands>,
52    },
53    ModularWeaponRandom {
54        tool: comp::tool::ToolKind,
55        material: comp::item::Material,
56        hands: Option<comp::item::tool::Hands>,
57    },
58}
59#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
60pub struct KitManifest(pub HashMap<String, Vec<(KitSpec, u32)>>);
61impl assets::Asset for KitManifest {
62    type Loader = assets::RonLoader;
63
64    const EXTENSION: &'static str = "ron";
65}
66impl Concatenate for KitManifest {
67    fn concatenate(self, b: Self) -> Self { KitManifest(self.0.concatenate(b.0)) }
68}
69
70#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
71pub struct SkillPresetManifest(pub HashMap<String, Vec<(Skill, u8)>>);
72impl assets::Asset for SkillPresetManifest {
73    type Loader = assets::RonLoader;
74
75    const EXTENSION: &'static str = "ron";
76}
77impl Concatenate for SkillPresetManifest {
78    fn concatenate(self, b: Self) -> Self { SkillPresetManifest(self.0.concatenate(b.0)) }
79}
80
81pub const KIT_MANIFEST_PATH: &str = "server.manifests.kits";
82pub const PRESET_MANIFEST_PATH: &str = "server.manifests.presets";
83
84/// Enum for all possible area types
85#[derive(Debug, Clone, EnumIter, EnumString, AsRefStr)]
86pub enum AreaKind {
87    #[strum(serialize = "build")]
88    Build,
89    #[strum(serialize = "no_durability")]
90    NoDurability,
91}
92
93lazy_static! {
94    static ref ALIGNMENTS: Vec<String> = ["wild", "enemy", "npc", "pet"]
95        .iter()
96        .map(|s| s.to_string())
97        .collect();
98    static ref SKILL_TREES: Vec<String> = ["general", "sword", "axe", "hammer", "bow", "staff", "sceptre", "mining"]
99        .iter()
100        .map(|s| s.to_string())
101        .collect();
102    /// TODO: Make this use hot-reloading
103    pub static ref ENTITIES: Vec<String> = {
104        let npc_names = &*npc::NPC_NAMES.read();
105
106        // HashSets for deduplication of male, female, etc
107        let mut categories = HashSet::new();
108        let mut species = HashSet::new();
109        for body in comp::Body::iter() {
110            // plugin doesn't seem to be spawnable, yet
111            if matches!(body, comp::Body::Plugin(_)) {
112                continue;
113            }
114
115            if let Some(meta) = npc_names.get_species_meta(&body) {
116                categories.insert(npc_names[&body].keyword.clone());
117                species.insert(meta.keyword.clone());
118            }
119        }
120
121        let mut strings = Vec::new();
122        strings.extend(categories);
123        strings.extend(species);
124
125        strings
126    };
127    static ref AREA_KINDS: Vec<String> = AreaKind::iter().map(|kind| kind.as_ref().to_string()).collect();
128    static ref OBJECTS: Vec<String> = comp::object::ALL_OBJECTS
129        .iter()
130        .map(|o| o.to_string().to_string())
131        .collect();
132    static ref RECIPES: Vec<String> = {
133        let rbm = RecipeBookManifest::load().cloned();
134        rbm.keys().cloned().collect::<Vec<String>>()
135    };
136    static ref TIMES: Vec<String> = [
137        "midnight", "night", "dawn", "morning", "day", "noon", "dusk"
138    ]
139    .iter()
140    .map(|s| s.to_string())
141    .collect();
142
143    static ref WEATHERS: Vec<String> = [
144        "clear", "cloudy", "rain", "wind", "storm"
145    ]
146    .iter()
147    .map(|s| s.to_string())
148    .collect();
149
150    pub static ref BUFF_PARSER: HashMap<String, BuffKind> = {
151        let string_from_buff = |kind| match kind {
152            BuffKind::Burning => "burning",
153            BuffKind::Regeneration => "regeneration",
154            BuffKind::Saturation => "saturation",
155            BuffKind::Bleeding => "bleeding",
156            BuffKind::Cursed => "cursed",
157            BuffKind::Potion => "potion",
158            BuffKind::Agility => "agility",
159            BuffKind::RestingHeal => "resting_heal",
160            BuffKind::EnergyRegen => "energy_regen",
161            BuffKind::IncreaseMaxEnergy => "increase_max_energy",
162            BuffKind::IncreaseMaxHealth => "increase_max_health",
163            BuffKind::Invulnerability => "invulnerability",
164            BuffKind::ProtectingWard => "protecting_ward",
165            BuffKind::Frenzied => "frenzied",
166            BuffKind::Crippled => "crippled",
167            BuffKind::Frozen => "frozen",
168            BuffKind::Wet => "wet",
169            BuffKind::Ensnared => "ensnared",
170            BuffKind::Poisoned => "poisoned",
171            BuffKind::Hastened => "hastened",
172            BuffKind::Fortitude => "fortitude",
173            BuffKind::Parried => "parried",
174            BuffKind::PotionSickness => "potion_sickness",
175            BuffKind::Reckless => "reckless",
176            BuffKind::Polymorphed => "polymorphed",
177            BuffKind::Flame => "flame",
178            BuffKind::Frigid => "frigid",
179            BuffKind::Lifesteal => "lifesteal",
180            // BuffKind::SalamanderAspect => "salamander_aspect",
181            BuffKind::ImminentCritical => "imminent_critical",
182            BuffKind::Fury => "fury",
183            BuffKind::Sunderer => "sunderer",
184            BuffKind::Defiance => "defiance",
185            BuffKind::Bloodfeast => "bloodfeast",
186            BuffKind::Berserk => "berserk",
187            BuffKind::Heatstroke => "heatstroke",
188            BuffKind::ScornfulTaunt => "scornful_taunt",
189            BuffKind::Rooted => "rooted",
190            BuffKind::Winded => "winded",
191            BuffKind::Concussion => "concussion",
192            BuffKind::Staggered => "staggered",
193            BuffKind::Tenacity => "tenacity",
194            BuffKind::Resilience => "resilience",
195        };
196        let mut buff_parser = HashMap::new();
197        for kind in BuffKind::iter() {
198            buff_parser.insert(string_from_buff(kind).to_string(), kind);
199        }
200        buff_parser
201    };
202
203    pub static ref BUFF_PACK: Vec<String> = {
204        let mut buff_pack: Vec<_> = BUFF_PARSER.keys().cloned().collect();
205        // Remove invulnerability as it removes debuffs
206        buff_pack.retain(|kind| kind != "invulnerability");
207        buff_pack
208    };
209
210    static ref BUFFS: Vec<String> = {
211        let mut buff_pack: Vec<String> = BUFF_PARSER.keys().cloned().collect();
212
213        // Add `all` and `clear` as valid command
214        buff_pack.push("all".to_owned());
215        buff_pack.push("clear".to_owned());
216        buff_pack
217    };
218
219    static ref BLOCK_KINDS: Vec<String> = terrain::block::BlockKind::iter()
220        .map(|bk| bk.to_string())
221        .collect();
222
223    static ref SPRITE_KINDS: Vec<String> = terrain::sprite::SPRITE_KINDS
224        .keys()
225        .cloned()
226        .collect();
227
228    static ref OUTCOME_KINDS: Vec<String> = outcome::Outcome::VARIANTS
229        .iter()
230        .map(|s| s.to_string())
231        .collect();
232
233    static ref ROLES: Vec<String> = ["admin", "moderator"].iter().copied().map(Into::into).collect();
234
235    /// List of item's asset specifiers. Useful for tab completing.
236    /// Doesn't cover all items (like modulars), includes "fake" items like
237    /// TagExamples.
238    pub static ref ITEM_SPECS: Vec<String> = {
239        let mut items = try_all_item_defs()
240            .unwrap_or_else(|e| {
241                warn!(?e, "Failed to load item specifiers");
242                Vec::new()
243            });
244        items.sort();
245        items
246    };
247
248    /// List of all entity configs. Useful for tab completing
249    pub static ref ENTITY_CONFIGS: Vec<String> = {
250        try_all_entity_configs()
251            .unwrap_or_else(|e| {
252                warn!(?e, "Failed to load entity configs");
253                Vec::new()
254            })
255    };
256
257    pub static ref KITS: Vec<String> = {
258        let mut kits = if let Ok(kits) = KitManifest::load_and_combine_static(KIT_MANIFEST_PATH) {
259            let mut kits = kits.read().0.keys().cloned().collect::<Vec<String>>();
260            kits.sort();
261            kits
262        } else {
263            Vec::new()
264        };
265        kits.push("all".to_owned());
266
267        kits
268    };
269
270    static ref PRESETS: HashMap<String, Vec<(Skill, u8)>> = {
271        if let Ok(presets) = SkillPresetManifest::load_and_combine_static(PRESET_MANIFEST_PATH) {
272            presets.read().0.clone()
273        } else {
274            warn!("Error while loading presets");
275            HashMap::new()
276        }
277    };
278
279    static ref PRESET_LIST: Vec<String> = {
280        let mut preset_list: Vec<String> = PRESETS.keys().cloned().collect();
281        preset_list.push("clear".to_owned());
282
283        preset_list
284    };
285
286    /// Map from string to a Spot's kind (except RonFile)
287    pub static ref SPOT_PARSER: HashMap<String, Spot> = {
288        let spot_to_string = |kind| match kind {
289            Spot::DwarvenGrave => "dwarven_grave",
290            Spot::SaurokAltar => "saurok_altar",
291            Spot::MyrmidonTemple => "myrmidon_temple",
292            Spot::GnarlingTotem => "gnarling_totem",
293            Spot::WitchHouse => "witch_house",
294            Spot::GnomeSpring => "gnome_spring",
295            Spot::WolfBurrow => "wolf_burrow",
296            Spot::Igloo => "igloo",
297            Spot::LionRock => "lion_rock",
298            Spot::TreeStumpForest => "tree_stump_forest",
299            Spot::DesertBones => "desert_bones",
300            Spot::Arch => "arch",
301            Spot::AirshipCrash => "airship_crash",
302            Spot::FruitTree => "fruit_tree",
303            Spot::Shipwreck => "shipwreck",
304            Spot::Shipwreck2 => "shipwreck2",
305            Spot::FallenTree => "fallen_tree",
306            Spot::GraveSmall => "grave_small",
307            Spot::JungleTemple => "jungle_temple",
308            Spot::SaurokTotem => "saurok_totem",
309            Spot::JungleOutpost => "jungle_outpost",
310            // unused here, but left for completeness
311            Spot::RonFile(props) => &props.base_structures,
312        };
313
314        let mut map = HashMap::new();
315        for spot_kind in Spot::iter() {
316            map.insert(spot_to_string(spot_kind).to_owned(), spot_kind);
317        }
318
319        map
320    };
321
322    pub static ref SPOTS: Vec<String> = {
323        let mut config_spots = crate::spot::RON_SPOT_PROPERTIES
324            .0
325            .iter()
326            .map(|s| s.base_structures.clone())
327            .collect::<Vec<_>>();
328
329        config_spots.extend(SPOT_PARSER.keys().cloned());
330        config_spots
331    };
332}
333
334pub enum EntityTarget {
335    Player(String),
336    RtsimNpc(u64),
337    Uid(crate::uid::Uid),
338}
339
340impl FromStr for EntityTarget {
341    type Err = String;
342
343    fn from_str(s: &str) -> Result<Self, Self::Err> {
344        // NOTE: `@` is an invalid character in usernames, so we can use it here.
345        if let Some((spec, data)) = s.split_once('@') {
346            match spec {
347                "rtsim" => Ok(EntityTarget::RtsimNpc(u64::from_str(data).map_err(
348                    |_| format!("Expected a valid number after 'rtsim@' but found {data}."),
349                )?)),
350                "uid" => Ok(EntityTarget::Uid(
351                    u64::from_str(data)
352                        .map_err(|_| {
353                            format!("Expected a valid number after 'uid@' but found {data}.")
354                        })?
355                        .into(),
356                )),
357                _ => Err(format!(
358                    "Expected either 'rtsim' or 'uid' before '@' but found '{spec}'"
359                )),
360            }
361        } else {
362            Ok(EntityTarget::Player(s.to_string()))
363        }
364    }
365}
366
367// Please keep this sorted alphabetically :-)
368#[derive(Copy, Clone, strum::EnumIter)]
369pub enum ServerChatCommand {
370    Adminify,
371    Airship,
372    Alias,
373    AreaAdd,
374    AreaList,
375    AreaRemove,
376    Aura,
377    Ban,
378    BanIp,
379    BattleMode,
380    BattleModeForce,
381    Body,
382    Buff,
383    Build,
384    Campfire,
385    ClearPersistedTerrain,
386    CreateLocation,
387    DeathEffect,
388    DebugColumn,
389    DebugWays,
390    DeleteLocation,
391    DestroyTethers,
392    DisconnectAllPlayers,
393    Dismount,
394    DropAll,
395    Dummy,
396    Explosion,
397    Faction,
398    GiveItem,
399    Goto,
400    GotoRand,
401    Group,
402    GroupInvite,
403    GroupKick,
404    GroupLeave,
405    GroupPromote,
406    Health,
407    IntoNpc,
408    JoinFaction,
409    Jump,
410    Kick,
411    Kill,
412    KillNpcs,
413    Kit,
414    Lantern,
415    Light,
416    Lightning,
417    Location,
418    MakeBlock,
419    MakeNpc,
420    MakeSprite,
421    MakeVolume,
422    Motd,
423    Mount,
424    Object,
425    Outcome,
426    PermitBuild,
427    Players,
428    Portal,
429    Region,
430    ReloadChunks,
431    RemoveLights,
432    RepairEquipment,
433    ResetRecipes,
434    Respawn,
435    RevokeBuild,
436    RevokeBuildAll,
437    RtsimChunk,
438    RtsimInfo,
439    RtsimNpc,
440    RtsimPurge,
441    RtsimTp,
442    Safezone,
443    Say,
444    Scale,
445    ServerPhysics,
446    SetMotd,
447    SetWaypoint,
448    Ship,
449    Site,
450    SkillPoint,
451    SkillPreset,
452    Spawn,
453    Spot,
454    Sudo,
455    Tell,
456    Tether,
457    Time,
458    TimeScale,
459    Tp,
460    Unban,
461    UnbanIp,
462    Version,
463    WeatherZone,
464    Whitelist,
465    Wiring,
466    World,
467}
468
469impl ServerChatCommand {
470    pub fn data(&self) -> ChatCommandData {
471        use ArgumentSpec::*;
472        use Requirement::*;
473        use Role::*;
474        let cmd = ChatCommandData::new;
475        match self {
476            ServerChatCommand::Adminify => cmd(
477                vec![PlayerName(Required), Enum("role", ROLES.clone(), Optional)],
478                Content::localized("command-adminify-desc"),
479                Some(Admin),
480            ),
481            ServerChatCommand::Airship => cmd(
482                vec![
483                    Enum(
484                        "kind",
485                        comp::ship::ALL_AIRSHIPS
486                            .iter()
487                            .map(|b| format!("{b:?}"))
488                            .collect(),
489                        Optional,
490                    ),
491                    Float("destination_degrees_ccw_of_east", 90.0, Optional),
492                ],
493                Content::localized("command-airship-desc"),
494                Some(Admin),
495            ),
496            ServerChatCommand::Alias => cmd(
497                vec![Any("name", Required)],
498                Content::localized("command-alias-desc"),
499                Some(Moderator),
500            ),
501            ServerChatCommand::Aura => cmd(
502                vec![
503                    Float("aura_radius", 10.0, Required),
504                    Float("aura_duration", 10.0, Optional),
505                    Boolean("new_entity", "true".to_string(), Optional),
506                    Enum("aura_target", GroupTarget::all_options(), Optional),
507                    Enum("aura_kind", AuraKindVariant::all_options(), Required),
508                    Any("aura spec", Optional),
509                ],
510                Content::localized("command-aura-desc"),
511                Some(Admin),
512            ),
513            ServerChatCommand::Buff => cmd(
514                vec![
515                    Enum("buff", BUFFS.clone(), Required),
516                    Float("strength", 0.01, Optional),
517                    Float("duration", 10.0, Optional),
518                    Any("buff data spec", Optional),
519                ],
520                Content::localized("command-buff-desc"),
521                Some(Admin),
522            ),
523            ServerChatCommand::Ban => cmd(
524                vec![
525                    PlayerName(Required),
526                    Boolean("overwrite", "true".to_string(), Optional),
527                    Any("ban duration", Optional),
528                    Message(Optional),
529                ],
530                Content::localized("command-ban-desc"),
531                Some(Moderator),
532            ),
533            ServerChatCommand::BanIp => cmd(
534                vec![
535                    PlayerName(Required),
536                    Boolean("overwrite", "true".to_string(), Optional),
537                    Any("ban duration", Optional),
538                    Message(Optional),
539                ],
540                Content::localized("command-ban-ip-desc"),
541                Some(Moderator),
542            ),
543            #[rustfmt::skip]
544            ServerChatCommand::BattleMode => cmd(
545                vec![Enum(
546                    "battle mode",
547                    vec!["pvp".to_owned(), "pve".to_owned()],
548                    Optional,
549                )],
550                Content::localized("command-battlemode-desc"),
551                None,
552
553            ),
554            ServerChatCommand::IntoNpc => cmd(
555                vec![AssetPath(
556                    "entity_config",
557                    "common.entity.",
558                    ENTITY_CONFIGS.clone(),
559                    Required,
560                )],
561                Content::localized("command-into_npc-desc"),
562                Some(Admin),
563            ),
564            ServerChatCommand::Body => cmd(
565                vec![Enum("body", ENTITIES.clone(), Required)],
566                Content::localized("command-body-desc"),
567                Some(Admin),
568            ),
569            ServerChatCommand::BattleModeForce => cmd(
570                vec![Enum(
571                    "battle mode",
572                    vec!["pvp".to_owned(), "pve".to_owned()],
573                    Required,
574                )],
575                Content::localized("command-battlemode_force-desc"),
576                Some(Admin),
577            ),
578            ServerChatCommand::Build => cmd(vec![], Content::localized("command-build-desc"), None),
579            ServerChatCommand::AreaAdd => cmd(
580                vec![
581                    Any("name", Required),
582                    Enum("kind", AREA_KINDS.clone(), Required),
583                    Integer("xlo", 0, Required),
584                    Integer("xhi", 10, Required),
585                    Integer("ylo", 0, Required),
586                    Integer("yhi", 10, Required),
587                    Integer("zlo", 0, Required),
588                    Integer("zhi", 10, Required),
589                ],
590                Content::localized("command-area_add-desc"),
591                Some(Admin),
592            ),
593            ServerChatCommand::AreaList => cmd(
594                vec![],
595                Content::localized("command-area_list-desc"),
596                Some(Admin),
597            ),
598            ServerChatCommand::AreaRemove => cmd(
599                vec![
600                    Any("name", Required),
601                    Enum("kind", AREA_KINDS.clone(), Required),
602                ],
603                Content::localized("command-area_remove-desc"),
604                Some(Admin),
605            ),
606            ServerChatCommand::Campfire => cmd(
607                vec![],
608                Content::localized("command-campfire-desc"),
609                Some(Admin),
610            ),
611            ServerChatCommand::ClearPersistedTerrain => cmd(
612                vec![Integer("chunk_radius", 6, Required)],
613                Content::localized("command-clear_persisted_terrain-desc"),
614                Some(Admin),
615            ),
616            ServerChatCommand::DeathEffect => cmd(
617                vec![
618                    Enum("death_effect", vec!["transform".to_string()], Required),
619                    // NOTE: I added this for QoL as transform is currently the only death effect
620                    // and takes an asset path, when more on-death effects are added to the command
621                    // remove this.
622                    AssetPath(
623                        "entity_config",
624                        "common.entity.",
625                        ENTITY_CONFIGS.clone(),
626                        Required,
627                    ),
628                ],
629                Content::localized("command-death_effect-dest"),
630                Some(Admin),
631            ),
632            ServerChatCommand::DebugColumn => cmd(
633                vec![Integer("x", 15000, Required), Integer("y", 15000, Required)],
634                Content::localized("command-debug_column-desc"),
635                Some(Admin),
636            ),
637            ServerChatCommand::DebugWays => cmd(
638                vec![Integer("x", 15000, Required), Integer("y", 15000, Required)],
639                Content::localized("command-debug_ways-desc"),
640                Some(Admin),
641            ),
642            ServerChatCommand::DisconnectAllPlayers => cmd(
643                vec![Any("confirm", Required)],
644                Content::localized("command-disconnect_all_players-desc"),
645                Some(Admin),
646            ),
647            ServerChatCommand::DropAll => cmd(
648                vec![],
649                Content::localized("command-dropall-desc"),
650                Some(Moderator),
651            ),
652            ServerChatCommand::Dummy => cmd(
653                vec![],
654                Content::localized("command-dummy-desc"),
655                Some(Admin),
656            ),
657            ServerChatCommand::Explosion => cmd(
658                vec![Float("radius", 5.0, Required)],
659                Content::localized("command-explosion-desc"),
660                Some(Admin),
661            ),
662            ServerChatCommand::Faction => cmd(
663                vec![Message(Optional)],
664                Content::localized("command-faction-desc"),
665                None,
666            ),
667            ServerChatCommand::GiveItem => cmd(
668                vec![
669                    AssetPath("item", "common.items.", ITEM_SPECS.clone(), Required),
670                    Integer("num", 1, Optional),
671                ],
672                Content::localized("command-give_item-desc"),
673                Some(Admin),
674            ),
675            ServerChatCommand::Goto => cmd(
676                vec![
677                    Float("x", 0.0, Required),
678                    Float("y", 0.0, Required),
679                    Float("z", 0.0, Required),
680                    Boolean("Dismount from ship", "true".to_string(), Optional),
681                ],
682                Content::localized("command-goto-desc"),
683                Some(Admin),
684            ),
685            ServerChatCommand::GotoRand => cmd(
686                vec![Boolean("Dismount from ship", "true".to_string(), Optional)],
687                Content::localized("command-goto-rand"),
688                Some(Admin),
689            ),
690            ServerChatCommand::Group => cmd(
691                vec![Message(Optional)],
692                Content::localized("command-group-desc"),
693                None,
694            ),
695            ServerChatCommand::GroupInvite => cmd(
696                vec![PlayerName(Required)],
697                Content::localized("command-group_invite-desc"),
698                None,
699            ),
700            ServerChatCommand::GroupKick => cmd(
701                vec![PlayerName(Required)],
702                Content::localized("command-group_kick-desc"),
703                None,
704            ),
705            ServerChatCommand::GroupLeave => {
706                cmd(vec![], Content::localized("command-group_leave-desc"), None)
707            },
708            ServerChatCommand::GroupPromote => cmd(
709                vec![PlayerName(Required)],
710                Content::localized("command-group_promote-desc"),
711                None,
712            ),
713            ServerChatCommand::Health => cmd(
714                vec![Integer("hp", 100, Required)],
715                Content::localized("command-health-desc"),
716                Some(Admin),
717            ),
718            ServerChatCommand::Respawn => cmd(
719                vec![],
720                Content::localized("command-respawn-desc"),
721                Some(Moderator),
722            ),
723            ServerChatCommand::JoinFaction => cmd(
724                vec![Any("faction", Optional)],
725                Content::localized("command-join_faction-desc"),
726                None,
727            ),
728            ServerChatCommand::Jump => cmd(
729                vec![
730                    Float("x", 0.0, Required),
731                    Float("y", 0.0, Required),
732                    Float("z", 0.0, Required),
733                    Boolean("Dismount from ship", "true".to_string(), Optional),
734                ],
735                Content::localized("command-jump-desc"),
736                Some(Admin),
737            ),
738            ServerChatCommand::Kick => cmd(
739                vec![PlayerName(Required), Message(Optional)],
740                Content::localized("command-kick-desc"),
741                Some(Moderator),
742            ),
743            ServerChatCommand::Kill => cmd(vec![], Content::localized("command-kill-desc"), None),
744            ServerChatCommand::KillNpcs => cmd(
745                vec![Float("radius", 100.0, Optional), Flag("--also-pets")],
746                Content::localized("command-kill_npcs-desc"),
747                Some(Admin),
748            ),
749            ServerChatCommand::Kit => cmd(
750                vec![Enum("kit_name", KITS.to_vec(), Required)],
751                Content::localized("command-kit-desc"),
752                Some(Admin),
753            ),
754            ServerChatCommand::Lantern => cmd(
755                vec![
756                    Float("strength", 5.0, Required),
757                    Float("r", 1.0, Optional),
758                    Float("g", 1.0, Optional),
759                    Float("b", 1.0, Optional),
760                ],
761                Content::localized("command-lantern-desc"),
762                Some(Admin),
763            ),
764            ServerChatCommand::Light => cmd(
765                vec![
766                    Float("r", 1.0, Optional),
767                    Float("g", 1.0, Optional),
768                    Float("b", 1.0, Optional),
769                    Float("x", 0.0, Optional),
770                    Float("y", 0.0, Optional),
771                    Float("z", 0.0, Optional),
772                    Float("strength", 5.0, Optional),
773                ],
774                Content::localized("command-light-desc"),
775                Some(Admin),
776            ),
777            ServerChatCommand::MakeBlock => cmd(
778                vec![
779                    Enum("block", BLOCK_KINDS.clone(), Required),
780                    Integer("r", 255, Optional),
781                    Integer("g", 255, Optional),
782                    Integer("b", 255, Optional),
783                ],
784                Content::localized("command-make_block-desc"),
785                Some(Admin),
786            ),
787            ServerChatCommand::MakeNpc => cmd(
788                vec![
789                    AssetPath(
790                        "entity_config",
791                        "common.entity.",
792                        ENTITY_CONFIGS.clone(),
793                        Required,
794                    ),
795                    Integer("num", 1, Optional),
796                ],
797                Content::localized("command-make_npc-desc"),
798                Some(Admin),
799            ),
800            ServerChatCommand::MakeSprite => cmd(
801                vec![Enum("sprite", SPRITE_KINDS.clone(), Required)],
802                Content::localized("command-make_sprite-desc"),
803                Some(Admin),
804            ),
805            ServerChatCommand::Motd => cmd(vec![], Content::localized("command-motd-desc"), None),
806            ServerChatCommand::Object => cmd(
807                vec![Enum("object", OBJECTS.clone(), Required)],
808                Content::localized("command-object-desc"),
809                Some(Admin),
810            ),
811            ServerChatCommand::Outcome => cmd(
812                vec![Enum("outcome", OUTCOME_KINDS.clone(), Required)],
813                Content::localized("command-outcome-desc"),
814                Some(Admin),
815            ),
816            ServerChatCommand::PermitBuild => cmd(
817                vec![Any("area_name", Required)],
818                Content::localized("command-permit_build-desc"),
819                Some(Admin),
820            ),
821            ServerChatCommand::Players => {
822                cmd(vec![], Content::localized("command-players-desc"), None)
823            },
824            ServerChatCommand::Portal => cmd(
825                vec![
826                    Float("x", 0., Required),
827                    Float("y", 0., Required),
828                    Float("z", 0., Required),
829                    Boolean("requires_no_aggro", "true".to_string(), Optional),
830                    Float("buildup_time", 5., Optional),
831                ],
832                Content::localized("command-portal-desc"),
833                Some(Admin),
834            ),
835            ServerChatCommand::ReloadChunks => cmd(
836                vec![Integer("chunk_radius", 6, Optional)],
837                Content::localized("command-reload_chunks-desc"),
838                Some(Admin),
839            ),
840            ServerChatCommand::ResetRecipes => cmd(
841                vec![],
842                Content::localized("command-reset_recipes-desc"),
843                Some(Admin),
844            ),
845            ServerChatCommand::RemoveLights => cmd(
846                vec![Float("radius", 20.0, Optional)],
847                Content::localized("command-remove_lights-desc"),
848                Some(Admin),
849            ),
850            ServerChatCommand::RevokeBuild => cmd(
851                vec![Any("area_name", Required)],
852                Content::localized("command-revoke_build-desc"),
853                Some(Admin),
854            ),
855            ServerChatCommand::RevokeBuildAll => cmd(
856                vec![],
857                Content::localized("command-revoke_build_all-desc"),
858                Some(Admin),
859            ),
860            ServerChatCommand::Region => cmd(
861                vec![Message(Optional)],
862                Content::localized("command-region-desc"),
863                None,
864            ),
865            ServerChatCommand::Safezone => cmd(
866                vec![Float("range", 100.0, Optional)],
867                Content::localized("command-safezone-desc"),
868                Some(Moderator),
869            ),
870            ServerChatCommand::Say => cmd(
871                vec![Message(Optional)],
872                Content::localized("command-say-desc"),
873                None,
874            ),
875            ServerChatCommand::ServerPhysics => cmd(
876                vec![
877                    PlayerName(Required),
878                    Boolean("enabled", "true".to_string(), Optional),
879                    Message(Optional),
880                ],
881                Content::localized("command-server_physics-desc"),
882                Some(Moderator),
883            ),
884            ServerChatCommand::SetMotd => cmd(
885                vec![Any("locale", Optional), Message(Optional)],
886                Content::localized("command-set_motd-desc"),
887                Some(Admin),
888            ),
889            ServerChatCommand::Ship => cmd(
890                vec![
891                    Enum(
892                        "kind",
893                        comp::ship::ALL_SHIPS
894                            .iter()
895                            .map(|b| format!("{b:?}"))
896                            .collect(),
897                        Optional,
898                    ),
899                    Boolean(
900                        "Whether the ship should be tethered to the target (or its mount)",
901                        "false".to_string(),
902                        Optional,
903                    ),
904                    Float("destination_degrees_ccw_of_east", 90.0, Optional),
905                ],
906                Content::localized("command-ship-desc"),
907                Some(Admin),
908            ),
909            // Uses Message because site names can contain spaces,
910            // which would be assumed to be separators otherwise
911            ServerChatCommand::Site => cmd(
912                vec![
913                    SiteName(Required),
914                    Boolean("Dismount from ship", "true".to_string(), Optional),
915                ],
916                Content::localized("command-site-desc"),
917                Some(Moderator),
918            ),
919            ServerChatCommand::SkillPoint => cmd(
920                vec![
921                    Enum("skill tree", SKILL_TREES.clone(), Required),
922                    Integer("amount", 1, Optional),
923                ],
924                Content::localized("command-skill_point-desc"),
925                Some(Admin),
926            ),
927            ServerChatCommand::SkillPreset => cmd(
928                vec![Enum("preset_name", PRESET_LIST.to_vec(), Required)],
929                Content::localized("command-skill_preset-desc"),
930                Some(Admin),
931            ),
932            ServerChatCommand::Spawn => cmd(
933                vec![
934                    Enum("alignment", ALIGNMENTS.clone(), Required),
935                    Enum("entity", ENTITIES.clone(), Required),
936                    Integer("amount", 1, Optional),
937                    Boolean("ai", "true".to_string(), Optional),
938                    Float("scale", 1.0, Optional),
939                    Boolean("tethered", "false".to_string(), Optional),
940                ],
941                Content::localized("command-spawn-desc"),
942                Some(Admin),
943            ),
944            ServerChatCommand::Spot => cmd(
945                vec![Enum("Spot kind to find", SPOTS.clone(), Required)],
946                Content::localized("command-spot-desc"),
947                Some(Admin),
948            ),
949            ServerChatCommand::Sudo => cmd(
950                vec![EntityTarget(Required), SubCommand],
951                Content::localized("command-sudo-desc"),
952                Some(Moderator),
953            ),
954            ServerChatCommand::Tell => cmd(
955                vec![PlayerName(Required), Message(Optional)],
956                Content::localized("command-tell-desc"),
957                None,
958            ),
959            ServerChatCommand::Time => cmd(
960                vec![Enum("time", TIMES.clone(), Optional)],
961                Content::localized("command-time-desc"),
962                Some(Admin),
963            ),
964            ServerChatCommand::TimeScale => cmd(
965                vec![Float("time scale", 1.0, Optional)],
966                Content::localized("command-time_scale-desc"),
967                Some(Admin),
968            ),
969            ServerChatCommand::Tp => cmd(
970                vec![
971                    EntityTarget(Optional),
972                    Boolean("Dismount from ship", "true".to_string(), Optional),
973                ],
974                Content::localized("command-tp-desc"),
975                Some(Moderator),
976            ),
977            ServerChatCommand::RtsimTp => cmd(
978                vec![
979                    Integer("npc index", 0, Required),
980                    Boolean("Dismount from ship", "true".to_string(), Optional),
981                ],
982                Content::localized("command-rtsim_tp-desc"),
983                Some(Admin),
984            ),
985            ServerChatCommand::RtsimInfo => cmd(
986                vec![Integer("npc index", 0, Required)],
987                Content::localized("command-rtsim_info-desc"),
988                Some(Admin),
989            ),
990            ServerChatCommand::RtsimNpc => cmd(
991                vec![Any("query", Required), Integer("max number", 20, Optional)],
992                Content::localized("command-rtsim_npc-desc"),
993                Some(Admin),
994            ),
995            ServerChatCommand::RtsimPurge => cmd(
996                vec![Boolean(
997                    "whether purging of rtsim data should occur on next startup",
998                    true.to_string(),
999                    Required,
1000                )],
1001                Content::localized("command-rtsim_purge-desc"),
1002                Some(Admin),
1003            ),
1004            ServerChatCommand::RtsimChunk => cmd(
1005                vec![],
1006                Content::localized("command-rtsim_chunk-desc"),
1007                Some(Admin),
1008            ),
1009            ServerChatCommand::Unban => cmd(
1010                vec![PlayerName(Required)],
1011                Content::localized("command-unban-desc"),
1012                Some(Moderator),
1013            ),
1014            ServerChatCommand::UnbanIp => cmd(
1015                vec![PlayerName(Required)],
1016                Content::localized("command-unban-ip-desc"),
1017                Some(Moderator),
1018            ),
1019            ServerChatCommand::Version => {
1020                cmd(vec![], Content::localized("command-version-desc"), None)
1021            },
1022            ServerChatCommand::SetWaypoint => cmd(
1023                vec![],
1024                Content::localized("command-waypoint-desc"),
1025                Some(Admin),
1026            ),
1027            ServerChatCommand::Wiring => cmd(
1028                vec![],
1029                Content::localized("command-wiring-desc"),
1030                Some(Admin),
1031            ),
1032            ServerChatCommand::Whitelist => cmd(
1033                vec![Any("add/remove", Required), PlayerName(Required)],
1034                Content::localized("command-whitelist-desc"),
1035                Some(Moderator),
1036            ),
1037            ServerChatCommand::World => cmd(
1038                vec![Message(Optional)],
1039                Content::localized("command-world-desc"),
1040                None,
1041            ),
1042            ServerChatCommand::MakeVolume => cmd(
1043                vec![Integer("size", 15, Optional)],
1044                Content::localized("command-make_volume-desc"),
1045                Some(Admin),
1046            ),
1047            ServerChatCommand::Location => cmd(
1048                vec![Any("name", Required)],
1049                Content::localized("command-location-desc"),
1050                None,
1051            ),
1052            ServerChatCommand::CreateLocation => cmd(
1053                vec![Any("name", Required)],
1054                Content::localized("command-create_location-desc"),
1055                Some(Moderator),
1056            ),
1057            ServerChatCommand::DeleteLocation => cmd(
1058                vec![Any("name", Required)],
1059                Content::localized("command-delete_location-desc"),
1060                Some(Moderator),
1061            ),
1062            ServerChatCommand::WeatherZone => cmd(
1063                vec![
1064                    Enum("weather kind", WEATHERS.clone(), Required),
1065                    Float("radius", 500.0, Optional),
1066                    Float("time", 300.0, Optional),
1067                ],
1068                Content::localized("command-weather_zone-desc"),
1069                Some(Admin),
1070            ),
1071            ServerChatCommand::Lightning => cmd(
1072                vec![],
1073                Content::localized("command-lightning-desc"),
1074                Some(Admin),
1075            ),
1076            ServerChatCommand::Scale => cmd(
1077                vec![
1078                    Float("factor", 1.0, Required),
1079                    Boolean("reset_mass", true.to_string(), Optional),
1080                ],
1081                Content::localized("command-scale-desc"),
1082                Some(Admin),
1083            ),
1084            ServerChatCommand::RepairEquipment => cmd(
1085                vec![ArgumentSpec::Boolean(
1086                    "repair inventory",
1087                    true.to_string(),
1088                    Optional,
1089                )],
1090                Content::localized("command-repair_equipment-desc"),
1091                Some(Admin),
1092            ),
1093            ServerChatCommand::Tether => cmd(
1094                vec![
1095                    EntityTarget(Required),
1096                    Boolean("automatic length", "true".to_string(), Optional),
1097                ],
1098                Content::localized("command-tether-desc"),
1099                Some(Admin),
1100            ),
1101            ServerChatCommand::DestroyTethers => cmd(
1102                vec![],
1103                Content::localized("command-destroy_tethers-desc"),
1104                Some(Admin),
1105            ),
1106            ServerChatCommand::Mount => cmd(
1107                vec![EntityTarget(Required)],
1108                Content::localized("command-mount-desc"),
1109                Some(Admin),
1110            ),
1111            ServerChatCommand::Dismount => cmd(
1112                vec![EntityTarget(Required)],
1113                Content::localized("command-dismount-desc"),
1114                Some(Admin),
1115            ),
1116        }
1117    }
1118
1119    /// The keyword used to invoke the command, omitting the prefix.
1120    pub fn keyword(&self) -> &'static str {
1121        match self {
1122            ServerChatCommand::Adminify => "adminify",
1123            ServerChatCommand::Airship => "airship",
1124            ServerChatCommand::Alias => "alias",
1125            ServerChatCommand::AreaAdd => "area_add",
1126            ServerChatCommand::AreaList => "area_list",
1127            ServerChatCommand::AreaRemove => "area_remove",
1128            ServerChatCommand::Aura => "aura",
1129            ServerChatCommand::Ban => "ban",
1130            ServerChatCommand::BanIp => "ban_ip",
1131            ServerChatCommand::BattleMode => "battlemode",
1132            ServerChatCommand::BattleModeForce => "battlemode_force",
1133            ServerChatCommand::Body => "body",
1134            ServerChatCommand::Buff => "buff",
1135            ServerChatCommand::Build => "build",
1136            ServerChatCommand::Campfire => "campfire",
1137            ServerChatCommand::ClearPersistedTerrain => "clear_persisted_terrain",
1138            ServerChatCommand::DeathEffect => "death_effect",
1139            ServerChatCommand::DebugColumn => "debug_column",
1140            ServerChatCommand::DebugWays => "debug_ways",
1141            ServerChatCommand::DisconnectAllPlayers => "disconnect_all_players",
1142            ServerChatCommand::DropAll => "dropall",
1143            ServerChatCommand::Dummy => "dummy",
1144            ServerChatCommand::Explosion => "explosion",
1145            ServerChatCommand::Faction => "faction",
1146            ServerChatCommand::GiveItem => "give_item",
1147            ServerChatCommand::Goto => "goto",
1148            ServerChatCommand::GotoRand => "goto_rand",
1149            ServerChatCommand::Group => "group",
1150            ServerChatCommand::GroupInvite => "group_invite",
1151            ServerChatCommand::GroupKick => "group_kick",
1152            ServerChatCommand::GroupLeave => "group_leave",
1153            ServerChatCommand::GroupPromote => "group_promote",
1154            ServerChatCommand::Health => "health",
1155            ServerChatCommand::IntoNpc => "into_npc",
1156            ServerChatCommand::JoinFaction => "join_faction",
1157            ServerChatCommand::Jump => "jump",
1158            ServerChatCommand::Kick => "kick",
1159            ServerChatCommand::Kill => "kill",
1160            ServerChatCommand::KillNpcs => "kill_npcs",
1161            ServerChatCommand::Kit => "kit",
1162            ServerChatCommand::Lantern => "lantern",
1163            ServerChatCommand::Respawn => "respawn",
1164            ServerChatCommand::Light => "light",
1165            ServerChatCommand::MakeBlock => "make_block",
1166            ServerChatCommand::MakeNpc => "make_npc",
1167            ServerChatCommand::MakeSprite => "make_sprite",
1168            ServerChatCommand::Motd => "motd",
1169            ServerChatCommand::Object => "object",
1170            ServerChatCommand::Outcome => "outcome",
1171            ServerChatCommand::PermitBuild => "permit_build",
1172            ServerChatCommand::Players => "players",
1173            ServerChatCommand::Portal => "portal",
1174            ServerChatCommand::ResetRecipes => "reset_recipes",
1175            ServerChatCommand::Region => "region",
1176            ServerChatCommand::ReloadChunks => "reload_chunks",
1177            ServerChatCommand::RemoveLights => "remove_lights",
1178            ServerChatCommand::RevokeBuild => "revoke_build",
1179            ServerChatCommand::RevokeBuildAll => "revoke_build_all",
1180            ServerChatCommand::Safezone => "safezone",
1181            ServerChatCommand::Say => "say",
1182            ServerChatCommand::ServerPhysics => "server_physics",
1183            ServerChatCommand::SetMotd => "set_motd",
1184            ServerChatCommand::Ship => "ship",
1185            ServerChatCommand::Site => "site",
1186            ServerChatCommand::SkillPoint => "skill_point",
1187            ServerChatCommand::SkillPreset => "skill_preset",
1188            ServerChatCommand::Spawn => "spawn",
1189            ServerChatCommand::Spot => "spot",
1190            ServerChatCommand::Sudo => "sudo",
1191            ServerChatCommand::Tell => "tell",
1192            ServerChatCommand::Time => "time",
1193            ServerChatCommand::TimeScale => "time_scale",
1194            ServerChatCommand::Tp => "tp",
1195            ServerChatCommand::RtsimTp => "rtsim_tp",
1196            ServerChatCommand::RtsimInfo => "rtsim_info",
1197            ServerChatCommand::RtsimNpc => "rtsim_npc",
1198            ServerChatCommand::RtsimPurge => "rtsim_purge",
1199            ServerChatCommand::RtsimChunk => "rtsim_chunk",
1200            ServerChatCommand::Unban => "unban",
1201            ServerChatCommand::UnbanIp => "unban_ip",
1202            ServerChatCommand::Version => "version",
1203            ServerChatCommand::SetWaypoint => "set_waypoint",
1204            ServerChatCommand::Wiring => "wiring",
1205            ServerChatCommand::Whitelist => "whitelist",
1206            ServerChatCommand::World => "world",
1207            ServerChatCommand::MakeVolume => "make_volume",
1208            ServerChatCommand::Location => "location",
1209            ServerChatCommand::CreateLocation => "create_location",
1210            ServerChatCommand::DeleteLocation => "delete_location",
1211            ServerChatCommand::WeatherZone => "weather_zone",
1212            ServerChatCommand::Lightning => "lightning",
1213            ServerChatCommand::Scale => "scale",
1214            ServerChatCommand::RepairEquipment => "repair_equipment",
1215            ServerChatCommand::Tether => "tether",
1216            ServerChatCommand::DestroyTethers => "destroy_tethers",
1217            ServerChatCommand::Mount => "mount",
1218            ServerChatCommand::Dismount => "dismount",
1219        }
1220    }
1221
1222    /// The short keyword used to invoke the command, omitting the leading '/'.
1223    /// Returns None if the command doesn't have a short keyword
1224    pub fn short_keyword(&self) -> Option<&'static str> {
1225        Some(match self {
1226            ServerChatCommand::Faction => "f",
1227            ServerChatCommand::Group => "g",
1228            ServerChatCommand::Region => "r",
1229            ServerChatCommand::Say => "s",
1230            ServerChatCommand::Tell => "t",
1231            ServerChatCommand::World => "w",
1232            _ => return None,
1233        })
1234    }
1235
1236    /// Produce an iterator over all the available commands
1237    pub fn iter() -> impl Iterator<Item = Self> + Clone { <Self as IntoEnumIterator>::iter() }
1238
1239    /// A message that explains what the command does
1240    pub fn help_content(&self) -> Content {
1241        let data = self.data();
1242
1243        let usage = std::iter::once(format!("/{}", self.keyword()))
1244            .chain(data.args.iter().map(|arg| arg.usage_string()))
1245            .collect::<Vec<_>>()
1246            .join(" ");
1247
1248        Content::localized_with_args("command-help-template", [
1249            ("usage", Content::Plain(usage)),
1250            ("description", data.description),
1251        ])
1252    }
1253
1254    /// Produce an iterator that first goes over all the short keywords
1255    /// and their associated commands and then iterates over all the normal
1256    /// keywords with their associated commands
1257    pub fn iter_with_keywords() -> impl Iterator<Item = (&'static str, Self)> {
1258        Self::iter()
1259        // Go through all the shortcuts first
1260        .filter_map(|c| c.short_keyword().map(|s| (s, c)))
1261        .chain(Self::iter().map(|c| (c.keyword(), c)))
1262    }
1263
1264    pub fn needs_role(&self) -> Option<comp::AdminRole> { self.data().needs_role }
1265
1266    /// Returns a format string for parsing arguments with scan_fmt
1267    pub fn arg_fmt(&self) -> String {
1268        self.data()
1269            .args
1270            .iter()
1271            .map(|arg| match arg {
1272                ArgumentSpec::PlayerName(_) => "{}",
1273                ArgumentSpec::EntityTarget(_) => "{}",
1274                ArgumentSpec::SiteName(_) => "{/.*/}",
1275                ArgumentSpec::Float(_, _, _) => "{}",
1276                ArgumentSpec::Integer(_, _, _) => "{d}",
1277                ArgumentSpec::Any(_, _) => "{}",
1278                ArgumentSpec::Command(_) => "{}",
1279                ArgumentSpec::Message(_) => "{/.*/}",
1280                ArgumentSpec::SubCommand => "{} {/.*/}",
1281                ArgumentSpec::Enum(_, _, _) => "{}",
1282                ArgumentSpec::AssetPath(_, _, _, _) => "{}",
1283                ArgumentSpec::Boolean(_, _, _) => "{}",
1284                ArgumentSpec::Flag(_) => "{}",
1285            })
1286            .collect::<Vec<_>>()
1287            .join(" ")
1288    }
1289}
1290
1291impl Display for ServerChatCommand {
1292    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
1293        write!(f, "{}", self.keyword())
1294    }
1295}
1296
1297impl FromStr for ServerChatCommand {
1298    type Err = ();
1299
1300    fn from_str(keyword: &str) -> Result<ServerChatCommand, ()> {
1301        Self::iter()
1302        // Go through all the shortcuts first
1303        .filter_map(|c| c.short_keyword().map(|s| (s, c)))
1304        .chain(Self::iter().map(|c| (c.keyword(), c)))
1305            // Find command with matching string as keyword
1306            .find_map(|(kwd, command)| (kwd == keyword).then_some(command))
1307            // Return error if not found
1308            .ok_or(())
1309    }
1310}
1311
1312#[derive(Eq, PartialEq, Debug, Clone, Copy)]
1313pub enum Requirement {
1314    Required,
1315    Optional,
1316}
1317
1318/// Representation for chat command arguments
1319pub enum ArgumentSpec {
1320    /// The argument refers to a player by alias
1321    PlayerName(Requirement),
1322    /// The arguments refers to an entity in some way.
1323    EntityTarget(Requirement),
1324    // The argument refers to a site, by name.
1325    SiteName(Requirement),
1326    /// The argument is a float. The associated values are
1327    /// * label
1328    /// * suggested tab-completion
1329    /// * whether it's optional
1330    Float(&'static str, f32, Requirement),
1331    /// The argument is an integer. The associated values are
1332    /// * label
1333    /// * suggested tab-completion
1334    /// * whether it's optional
1335    Integer(&'static str, i32, Requirement),
1336    /// The argument is any string that doesn't contain spaces
1337    Any(&'static str, Requirement),
1338    /// The argument is a command name (such as in /help)
1339    Command(Requirement),
1340    /// This is the final argument, consuming all characters until the end of
1341    /// input.
1342    Message(Requirement),
1343    /// This command is followed by another command (such as in /sudo)
1344    SubCommand,
1345    /// The argument is likely an enum. The associated values are
1346    /// * label
1347    /// * Predefined string completions
1348    /// * whether it's optional
1349    Enum(&'static str, Vec<String>, Requirement),
1350    /// The argument is an asset path. The associated values are
1351    /// * label
1352    /// * Path prefix shared by all assets
1353    /// * List of all asset paths as strings for completion
1354    /// * whether it's optional
1355    AssetPath(&'static str, &'static str, Vec<String>, Requirement),
1356    /// The argument is likely a boolean. The associated values are
1357    /// * label
1358    /// * suggested tab-completion
1359    /// * whether it's optional
1360    Boolean(&'static str, String, Requirement),
1361    /// The argument is a flag that enables or disables a feature.
1362    Flag(&'static str),
1363}
1364
1365impl ArgumentSpec {
1366    pub fn usage_string(&self) -> String {
1367        match self {
1368            ArgumentSpec::PlayerName(req) => {
1369                if &Requirement::Required == req {
1370                    "<player>".to_string()
1371                } else {
1372                    "[player]".to_string()
1373                }
1374            },
1375            ArgumentSpec::EntityTarget(req) => {
1376                if &Requirement::Required == req {
1377                    "<entity>".to_string()
1378                } else {
1379                    "[entity]".to_string()
1380                }
1381            },
1382            ArgumentSpec::SiteName(req) => {
1383                if &Requirement::Required == req {
1384                    "<site>".to_string()
1385                } else {
1386                    "[site]".to_string()
1387                }
1388            },
1389            ArgumentSpec::Float(label, _, req) => {
1390                if &Requirement::Required == req {
1391                    format!("<{}>", label)
1392                } else {
1393                    format!("[{}]", label)
1394                }
1395            },
1396            ArgumentSpec::Integer(label, _, req) => {
1397                if &Requirement::Required == req {
1398                    format!("<{}>", label)
1399                } else {
1400                    format!("[{}]", label)
1401                }
1402            },
1403            ArgumentSpec::Any(label, req) => {
1404                if &Requirement::Required == req {
1405                    format!("<{}>", label)
1406                } else {
1407                    format!("[{}]", label)
1408                }
1409            },
1410            ArgumentSpec::Command(req) => {
1411                if &Requirement::Required == req {
1412                    "<[/]command>".to_string()
1413                } else {
1414                    "[[/]command]".to_string()
1415                }
1416            },
1417            ArgumentSpec::Message(req) => {
1418                if &Requirement::Required == req {
1419                    "<message>".to_string()
1420                } else {
1421                    "[message]".to_string()
1422                }
1423            },
1424            ArgumentSpec::SubCommand => "<[/]command> [args...]".to_string(),
1425            ArgumentSpec::Enum(label, _, req) => {
1426                if &Requirement::Required == req {
1427                    format!("<{}>", label)
1428                } else {
1429                    format!("[{}]", label)
1430                }
1431            },
1432            ArgumentSpec::AssetPath(label, _, _, req) => {
1433                if &Requirement::Required == req {
1434                    format!("<{}>", label)
1435                } else {
1436                    format!("[{}]", label)
1437                }
1438            },
1439            ArgumentSpec::Boolean(label, _, req) => {
1440                if &Requirement::Required == req {
1441                    format!("<{}>", label)
1442                } else {
1443                    format!("[{}]", label)
1444                }
1445            },
1446            ArgumentSpec::Flag(label) => {
1447                format!("[{}]", label)
1448            },
1449        }
1450    }
1451
1452    pub fn requirement(&self) -> Requirement {
1453        match self {
1454            ArgumentSpec::PlayerName(r)
1455            | ArgumentSpec::EntityTarget(r)
1456            | ArgumentSpec::SiteName(r)
1457            | ArgumentSpec::Float(_, _, r)
1458            | ArgumentSpec::Integer(_, _, r)
1459            | ArgumentSpec::Any(_, r)
1460            | ArgumentSpec::Command(r)
1461            | ArgumentSpec::Message(r)
1462            | ArgumentSpec::Enum(_, _, r)
1463            | ArgumentSpec::AssetPath(_, _, _, r)
1464            | ArgumentSpec::Boolean(_, _, r) => *r,
1465            ArgumentSpec::Flag(_) => Requirement::Optional,
1466            ArgumentSpec::SubCommand => Requirement::Required,
1467        }
1468    }
1469}
1470
1471pub trait CommandEnumArg: FromStr {
1472    fn all_options() -> Vec<String>;
1473}
1474
1475macro_rules! impl_from_to_str_cmd {
1476    ($enum:ident, ($($attribute:ident => $str:expr),*)) => {
1477        impl std::str::FromStr for $enum {
1478            type Err = String;
1479
1480            fn from_str(s: &str) -> Result<Self, Self::Err> {
1481                match s {
1482                    $(
1483                        $str => Ok($enum::$attribute),
1484                    )*
1485                    s => Err(format!("Invalid variant: {s}")),
1486                }
1487            }
1488        }
1489
1490        impl $crate::cmd::CommandEnumArg for $enum {
1491            fn all_options() -> Vec<String> {
1492                vec![$($str.to_string()),*]
1493            }
1494        }
1495    }
1496}
1497
1498impl_from_to_str_cmd!(AuraKindVariant, (
1499    Buff => "buff",
1500    FriendlyFire => "friendly_fire",
1501    ForcePvP => "force_pvp"
1502));
1503
1504impl_from_to_str_cmd!(GroupTarget, (
1505    InGroup => "in_group",
1506    OutOfGroup => "out_of_group",
1507    All => "all"
1508));
1509
1510/// Parse a series of command arguments into values, including collecting all
1511/// trailing arguments.
1512#[macro_export]
1513macro_rules! parse_cmd_args {
1514    ($args:expr, $($t:ty),* $(, ..$tail:ty)? $(,)?) => {
1515        {
1516            let mut args = $args.into_iter().peekable();
1517            (
1518                // We only consume the input argument when parsing is successful. If this fails, we
1519                // will then attempt to parse it as the next argument type. This is done regardless
1520                // of whether the argument is optional because that information is not available
1521                // here. Nevertheless, if the caller only precedes to use the parsed arguments when
1522                // all required arguments parse successfully to `Some(val)` this should not create
1523                // any unexpected behavior.
1524                //
1525                // This does mean that optional arguments will be included in the trailing args or
1526                // that one optional arg could be interpreted as another, if the user makes a
1527                // mistake that causes an optional arg to fail to parse. But there is no way to
1528                // discern this in the current model with the optional args and trailing arg being
1529                // solely position based.
1530                $({
1531                    let parsed = args.peek().and_then(|s| s.parse::<$t>().ok());
1532                    // Consume successfully parsed arg.
1533                    if parsed.is_some() { args.next(); }
1534                    parsed
1535                }),*
1536                $(, args.map(|s| s.to_string()).collect::<$tail>())?
1537            )
1538        }
1539    };
1540}
1541
1542#[cfg(test)]
1543mod tests {
1544    use super::*;
1545    use crate::comp::Item;
1546
1547    #[test]
1548    fn verify_cmd_list_sorted() {
1549        let mut list = ServerChatCommand::iter()
1550            .map(|c| c.keyword())
1551            .collect::<Vec<_>>();
1552
1553        // Vec::is_sorted is unstable, so we do it the hard way
1554        let list2 = list.clone();
1555        list.sort_unstable();
1556        assert_eq!(list, list2);
1557    }
1558
1559    #[test]
1560    fn test_loading_skill_presets() {
1561        SkillPresetManifest::load_expect_combined_static(PRESET_MANIFEST_PATH);
1562    }
1563
1564    #[test]
1565    fn test_load_kits() {
1566        let kits = KitManifest::load_expect_combined_static(KIT_MANIFEST_PATH).read();
1567        let mut rng = rand::thread_rng();
1568        for kit in kits.0.values() {
1569            for (item_id, _) in kit.iter() {
1570                match item_id {
1571                    KitSpec::Item(item_id) => {
1572                        Item::new_from_asset_expect(item_id);
1573                    },
1574                    KitSpec::ModularWeaponSet {
1575                        tool,
1576                        material,
1577                        hands,
1578                    } => {
1579                        comp::item::modular::generate_weapons(*tool, *material, *hands)
1580                            .unwrap_or_else(|_| {
1581                                panic!(
1582                                    "Failed to synthesize a modular {tool:?} set made of \
1583                                     {material:?}."
1584                                )
1585                            });
1586                    },
1587                    KitSpec::ModularWeaponRandom {
1588                        tool,
1589                        material,
1590                        hands,
1591                    } => {
1592                        comp::item::modular::random_weapon(*tool, *material, *hands, &mut rng)
1593                            .unwrap_or_else(|_| {
1594                                panic!(
1595                                    "Failed to synthesize a random {hands:?}-handed modular \
1596                                     {tool:?} made of {material:?}."
1597                                )
1598                            });
1599                    },
1600                }
1601            }
1602        }
1603    }
1604}