veloren_common/
cmd.rs

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