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