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