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