Skip to main content

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