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::OwlTalon => "owl_talon",
182            BuffKind::HeavyNock => "heavy_nock",
183            BuffKind::Heartseeker => "heartseeker",
184            BuffKind::EagleEye => "eagle_eye",
185            BuffKind::Chilled => "chilled",
186            BuffKind::ArdentHunter => "ardent_hunter",
187            BuffKind::ArdentHunted => "ardent_hunted",
188            BuffKind::SepticShot => "septic_shot",
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(u64),
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(u64::from_str(data).map_err(
342                    |_| format!("Expected a valid number after 'rtsim@' but found {data}."),
343                )?)),
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![Integer("chunk_radius", 6, Optional)],
870                Content::localized("command-reload_chunks-desc"),
871                Some(Admin),
872            ),
873            ServerChatCommand::ResetRecipes => cmd(
874                vec![],
875                Content::localized("command-reset_recipes-desc"),
876                Some(Admin),
877            ),
878            ServerChatCommand::RemoveLights => cmd(
879                vec![Float("radius", 20.0, Optional)],
880                Content::localized("command-remove_lights-desc"),
881                Some(Admin),
882            ),
883            ServerChatCommand::RevokeBuild => cmd(
884                vec![Any("area_name", Required)],
885                Content::localized("command-revoke_build-desc"),
886                Some(Admin),
887            ),
888            ServerChatCommand::RevokeBuildAll => cmd(
889                vec![],
890                Content::localized("command-revoke_build_all-desc"),
891                Some(Admin),
892            ),
893            ServerChatCommand::Region => cmd(
894                vec![Message(Optional)],
895                Content::localized("command-region-desc"),
896                None,
897            ),
898            ServerChatCommand::Safezone => cmd(
899                vec![Float("range", 100.0, Optional)],
900                Content::localized("command-safezone-desc"),
901                Some(Moderator),
902            ),
903            ServerChatCommand::Say => cmd(
904                vec![Message(Optional)],
905                Content::localized("command-say-desc"),
906                None,
907            ),
908            ServerChatCommand::ServerPhysics => cmd(
909                vec![
910                    PlayerName(Required),
911                    Boolean("enabled", "true".to_string(), Optional),
912                    Message(Optional),
913                ],
914                Content::localized("command-server_physics-desc"),
915                Some(Moderator),
916            ),
917            ServerChatCommand::SetMotd => cmd(
918                vec![Any("locale", Optional), Message(Optional)],
919                Content::localized("command-set_motd-desc"),
920                Some(Admin),
921            ),
922            ServerChatCommand::SetBodyType => cmd(
923                vec![
924                    Enum(
925                        "body type",
926                        vec!["Female".to_string(), "Male".to_string()],
927                        Required,
928                    ),
929                    Boolean("permanent", "false".to_string(), Requirement::Optional),
930                ],
931                Content::localized("command-set_body_type-desc"),
932                Some(Admin),
933            ),
934            ServerChatCommand::Ship => cmd(
935                vec![
936                    Enum(
937                        "kind",
938                        comp::ship::ALL_SHIPS
939                            .iter()
940                            .map(|b| format!("{b:?}"))
941                            .collect(),
942                        Optional,
943                    ),
944                    Boolean(
945                        "Whether the ship should be tethered to the target (or its mount)",
946                        "false".to_string(),
947                        Optional,
948                    ),
949                    Float("destination_degrees_ccw_of_east", 90.0, Optional),
950                ],
951                Content::localized("command-ship-desc"),
952                Some(Admin),
953            ),
954            // Uses Message because site names can contain spaces,
955            // which would be assumed to be separators otherwise
956            ServerChatCommand::Site => cmd(
957                vec![
958                    SiteName(Required),
959                    Boolean("Dismount from ship", "true".to_string(), Optional),
960                ],
961                Content::localized("command-site-desc"),
962                Some(Moderator),
963            ),
964            ServerChatCommand::SkillPoint => cmd(
965                vec![
966                    Enum("skill tree", SKILL_TREES.clone(), Required),
967                    Integer("amount", 1, Optional),
968                ],
969                Content::localized("command-skill_point-desc"),
970                Some(Admin),
971            ),
972            ServerChatCommand::SkillPreset => cmd(
973                vec![Enum("preset_name", PRESET_LIST.to_vec(), Required)],
974                Content::localized("command-skill_preset-desc"),
975                Some(Admin),
976            ),
977            ServerChatCommand::Spawn => cmd(
978                vec![
979                    Enum("alignment", ALIGNMENTS.clone(), Required),
980                    Enum("entity", ENTITIES.clone(), Required),
981                    Integer("amount", 1, Optional),
982                    Boolean("ai", "true".to_string(), Optional),
983                    Float("scale", 1.0, Optional),
984                    Boolean("tethered", "false".to_string(), Optional),
985                ],
986                Content::localized("command-spawn-desc"),
987                Some(Admin),
988            ),
989            ServerChatCommand::Spot => cmd(
990                vec![Enum("Spot kind to find", SPOTS.clone(), Required)],
991                Content::localized("command-spot-desc"),
992                Some(Admin),
993            ),
994            ServerChatCommand::Sudo => cmd(
995                vec![EntityTarget(Required), SubCommand],
996                Content::localized("command-sudo-desc"),
997                Some(Moderator),
998            ),
999            ServerChatCommand::Tell => cmd(
1000                vec![PlayerName(Required), Message(Optional)],
1001                Content::localized("command-tell-desc"),
1002                None,
1003            ),
1004            ServerChatCommand::Time => cmd(
1005                vec![Enum("time", TIMES.clone(), Optional)],
1006                Content::localized("command-time-desc"),
1007                Some(Admin),
1008            ),
1009            ServerChatCommand::TimeScale => cmd(
1010                vec![Float("time scale", 1.0, Optional)],
1011                Content::localized("command-time_scale-desc"),
1012                Some(Admin),
1013            ),
1014            ServerChatCommand::Tp => cmd(
1015                vec![
1016                    EntityTarget(Optional),
1017                    Boolean("Dismount from ship", "true".to_string(), Optional),
1018                ],
1019                Content::localized("command-tp-desc"),
1020                Some(Moderator),
1021            ),
1022            ServerChatCommand::RtsimTp => cmd(
1023                vec![
1024                    Integer("npc index", 0, Required),
1025                    Boolean("Dismount from ship", "true".to_string(), Optional),
1026                ],
1027                Content::localized("command-rtsim_tp-desc"),
1028                Some(Admin),
1029            ),
1030            ServerChatCommand::RtsimInfo => cmd(
1031                vec![Integer("npc index", 0, Required)],
1032                Content::localized("command-rtsim_info-desc"),
1033                Some(Admin),
1034            ),
1035            ServerChatCommand::RtsimNpc => cmd(
1036                vec![Any("query", Required), Integer("max number", 20, Optional)],
1037                Content::localized("command-rtsim_npc-desc"),
1038                Some(Admin),
1039            ),
1040            ServerChatCommand::RtsimPurge => cmd(
1041                vec![Boolean(
1042                    "whether purging of rtsim data should occur on next startup",
1043                    true.to_string(),
1044                    Required,
1045                )],
1046                Content::localized("command-rtsim_purge-desc"),
1047                Some(Admin),
1048            ),
1049            ServerChatCommand::RtsimChunk => cmd(
1050                vec![],
1051                Content::localized("command-rtsim_chunk-desc"),
1052                Some(Admin),
1053            ),
1054            ServerChatCommand::Unban => cmd(
1055                vec![PlayerName(Required)],
1056                Content::localized("command-unban-desc"),
1057                Some(Moderator),
1058            ),
1059            ServerChatCommand::UnbanIp => cmd(
1060                vec![PlayerName(Required)],
1061                Content::localized("command-unban-ip-desc"),
1062                Some(Moderator),
1063            ),
1064            ServerChatCommand::Version => {
1065                cmd(vec![], Content::localized("command-version-desc"), None)
1066            },
1067            ServerChatCommand::SetWaypoint => cmd(
1068                vec![],
1069                Content::localized("command-waypoint-desc"),
1070                Some(Admin),
1071            ),
1072            ServerChatCommand::Wiring => cmd(
1073                vec![],
1074                Content::localized("command-wiring-desc"),
1075                Some(Admin),
1076            ),
1077            ServerChatCommand::Whitelist => cmd(
1078                vec![Any("add/remove", Required), PlayerName(Required)],
1079                Content::localized("command-whitelist-desc"),
1080                Some(Moderator),
1081            ),
1082            ServerChatCommand::World => cmd(
1083                vec![Message(Optional)],
1084                Content::localized("command-world-desc"),
1085                None,
1086            ),
1087            ServerChatCommand::MakeVolume => cmd(
1088                vec![Integer("size", 15, Optional)],
1089                Content::localized("command-make_volume-desc"),
1090                Some(Admin),
1091            ),
1092            ServerChatCommand::Location => cmd(
1093                vec![Any("name", Required)],
1094                Content::localized("command-location-desc"),
1095                None,
1096            ),
1097            ServerChatCommand::CreateLocation => cmd(
1098                vec![Any("name", Required)],
1099                Content::localized("command-create_location-desc"),
1100                Some(Moderator),
1101            ),
1102            ServerChatCommand::DeleteLocation => cmd(
1103                vec![Any("name", Required)],
1104                Content::localized("command-delete_location-desc"),
1105                Some(Moderator),
1106            ),
1107            ServerChatCommand::WeatherZone => cmd(
1108                vec![
1109                    Enum("weather kind", WEATHERS.clone(), Required),
1110                    Float("radius", 500.0, Optional),
1111                    Float("time", 300.0, Optional),
1112                ],
1113                Content::localized("command-weather_zone-desc"),
1114                Some(Admin),
1115            ),
1116            ServerChatCommand::Lightning => cmd(
1117                vec![],
1118                Content::localized("command-lightning-desc"),
1119                Some(Admin),
1120            ),
1121            ServerChatCommand::Scale => cmd(
1122                vec![
1123                    Float("factor", 1.0, Required),
1124                    Boolean("reset_mass", true.to_string(), Optional),
1125                ],
1126                Content::localized("command-scale-desc"),
1127                Some(Admin),
1128            ),
1129            ServerChatCommand::RepairEquipment => cmd(
1130                vec![ArgumentSpec::Boolean(
1131                    "repair inventory",
1132                    true.to_string(),
1133                    Optional,
1134                )],
1135                Content::localized("command-repair_equipment-desc"),
1136                Some(Admin),
1137            ),
1138            ServerChatCommand::Tether => cmd(
1139                vec![
1140                    EntityTarget(Required),
1141                    Boolean("automatic length", "true".to_string(), Optional),
1142                ],
1143                Content::localized("command-tether-desc"),
1144                Some(Admin),
1145            ),
1146            ServerChatCommand::DestroyTethers => cmd(
1147                vec![],
1148                Content::localized("command-destroy_tethers-desc"),
1149                Some(Admin),
1150            ),
1151            ServerChatCommand::Mount => cmd(
1152                vec![EntityTarget(Required)],
1153                Content::localized("command-mount-desc"),
1154                Some(Admin),
1155            ),
1156            ServerChatCommand::Dismount => cmd(
1157                vec![EntityTarget(Required)],
1158                Content::localized("command-dismount-desc"),
1159                Some(Admin),
1160            ),
1161        }
1162    }
1163
1164    /// The keyword used to invoke the command, omitting the prefix.
1165    pub fn keyword(&self) -> &'static str {
1166        match self {
1167            ServerChatCommand::Adminify => "adminify",
1168            ServerChatCommand::Airship => "airship",
1169            ServerChatCommand::Alias => "alias",
1170            ServerChatCommand::AreaAdd => "area_add",
1171            ServerChatCommand::AreaList => "area_list",
1172            ServerChatCommand::AreaRemove => "area_remove",
1173            ServerChatCommand::Aura => "aura",
1174            ServerChatCommand::Ban => "ban",
1175            ServerChatCommand::BanIp => "ban_ip",
1176            ServerChatCommand::BanLog => "ban_log",
1177            ServerChatCommand::BattleMode => "battlemode",
1178            ServerChatCommand::BattleModeForce => "battlemode_force",
1179            ServerChatCommand::Body => "body",
1180            ServerChatCommand::Buff => "buff",
1181            ServerChatCommand::Build => "build",
1182            ServerChatCommand::Campfire => "campfire",
1183            ServerChatCommand::ClearPersistedTerrain => "clear_persisted_terrain",
1184            ServerChatCommand::DeathEffect => "death_effect",
1185            ServerChatCommand::DebugColumn => "debug_column",
1186            ServerChatCommand::DebugWays => "debug_ways",
1187            ServerChatCommand::DisconnectAllPlayers => "disconnect_all_players",
1188            ServerChatCommand::DropAll => "dropall",
1189            ServerChatCommand::Dummy => "dummy",
1190            ServerChatCommand::Explosion => "explosion",
1191            ServerChatCommand::Faction => "faction",
1192            ServerChatCommand::GiveItem => "give_item",
1193            ServerChatCommand::Gizmos => "gizmos",
1194            ServerChatCommand::GizmosRange => "gizmos_range",
1195            ServerChatCommand::Goto => "goto",
1196            ServerChatCommand::GotoRand => "goto_rand",
1197            ServerChatCommand::Group => "group",
1198            ServerChatCommand::GroupInvite => "group_invite",
1199            ServerChatCommand::GroupKick => "group_kick",
1200            ServerChatCommand::GroupLeave => "group_leave",
1201            ServerChatCommand::GroupPromote => "group_promote",
1202            ServerChatCommand::Health => "health",
1203            ServerChatCommand::IntoNpc => "into_npc",
1204            ServerChatCommand::JoinFaction => "join_faction",
1205            ServerChatCommand::Jump => "jump",
1206            ServerChatCommand::Kick => "kick",
1207            ServerChatCommand::Kill => "kill",
1208            ServerChatCommand::KillNpcs => "kill_npcs",
1209            ServerChatCommand::Kit => "kit",
1210            ServerChatCommand::Lantern => "lantern",
1211            ServerChatCommand::Respawn => "respawn",
1212            ServerChatCommand::Light => "light",
1213            ServerChatCommand::MakeBlock => "make_block",
1214            ServerChatCommand::MakeNpc => "make_npc",
1215            ServerChatCommand::MakeSprite => "make_sprite",
1216            ServerChatCommand::Motd => "motd",
1217            ServerChatCommand::Object => "object",
1218            ServerChatCommand::Outcome => "outcome",
1219            ServerChatCommand::PermitBuild => "permit_build",
1220            ServerChatCommand::Players => "players",
1221            ServerChatCommand::Poise => "poise",
1222            ServerChatCommand::Portal => "portal",
1223            ServerChatCommand::ResetRecipes => "reset_recipes",
1224            ServerChatCommand::Region => "region",
1225            ServerChatCommand::ReloadChunks => "reload_chunks",
1226            ServerChatCommand::RemoveLights => "remove_lights",
1227            ServerChatCommand::RevokeBuild => "revoke_build",
1228            ServerChatCommand::RevokeBuildAll => "revoke_build_all",
1229            ServerChatCommand::Safezone => "safezone",
1230            ServerChatCommand::Say => "say",
1231            ServerChatCommand::ServerPhysics => "server_physics",
1232            ServerChatCommand::SetMotd => "set_motd",
1233            ServerChatCommand::SetBodyType => "set_body_type",
1234            ServerChatCommand::Ship => "ship",
1235            ServerChatCommand::Site => "site",
1236            ServerChatCommand::SkillPoint => "skill_point",
1237            ServerChatCommand::SkillPreset => "skill_preset",
1238            ServerChatCommand::Spawn => "spawn",
1239            ServerChatCommand::Spot => "spot",
1240            ServerChatCommand::Sudo => "sudo",
1241            ServerChatCommand::Tell => "tell",
1242            ServerChatCommand::Time => "time",
1243            ServerChatCommand::TimeScale => "time_scale",
1244            ServerChatCommand::Tp => "tp",
1245            ServerChatCommand::RtsimTp => "rtsim_tp",
1246            ServerChatCommand::RtsimInfo => "rtsim_info",
1247            ServerChatCommand::RtsimNpc => "rtsim_npc",
1248            ServerChatCommand::RtsimPurge => "rtsim_purge",
1249            ServerChatCommand::RtsimChunk => "rtsim_chunk",
1250            ServerChatCommand::Unban => "unban",
1251            ServerChatCommand::UnbanIp => "unban_ip",
1252            ServerChatCommand::Version => "version",
1253            ServerChatCommand::SetWaypoint => "set_waypoint",
1254            ServerChatCommand::Wiring => "wiring",
1255            ServerChatCommand::Whitelist => "whitelist",
1256            ServerChatCommand::World => "world",
1257            ServerChatCommand::MakeVolume => "make_volume",
1258            ServerChatCommand::Location => "location",
1259            ServerChatCommand::CreateLocation => "create_location",
1260            ServerChatCommand::DeleteLocation => "delete_location",
1261            ServerChatCommand::WeatherZone => "weather_zone",
1262            ServerChatCommand::Lightning => "lightning",
1263            ServerChatCommand::Scale => "scale",
1264            ServerChatCommand::RepairEquipment => "repair_equipment",
1265            ServerChatCommand::Tether => "tether",
1266            ServerChatCommand::DestroyTethers => "destroy_tethers",
1267            ServerChatCommand::Mount => "mount",
1268            ServerChatCommand::Dismount => "dismount",
1269        }
1270    }
1271
1272    /// The short keyword used to invoke the command, omitting the leading '/'.
1273    /// Returns None if the command doesn't have a short keyword
1274    pub fn short_keyword(&self) -> Option<&'static str> {
1275        Some(match self {
1276            ServerChatCommand::Faction => "f",
1277            ServerChatCommand::Group => "g",
1278            ServerChatCommand::Region => "r",
1279            ServerChatCommand::Say => "s",
1280            ServerChatCommand::Tell => "t",
1281            ServerChatCommand::World => "w",
1282            _ => return None,
1283        })
1284    }
1285
1286    /// Produce an iterator over all the available commands
1287    pub fn iter() -> impl Iterator<Item = Self> + Clone { <Self as IntoEnumIterator>::iter() }
1288
1289    /// A message that explains what the command does
1290    pub fn help_content(&self) -> Content {
1291        let data = self.data();
1292
1293        let usage = std::iter::once(format!("/{}", self.keyword()))
1294            .chain(data.args.iter().map(|arg| arg.usage_string()))
1295            .collect::<Vec<_>>()
1296            .join(" ");
1297
1298        Content::localized_with_args("command-help-template", [
1299            ("usage", Content::Plain(usage)),
1300            ("description", data.description),
1301        ])
1302    }
1303
1304    /// Produce an iterator that first goes over all the short keywords
1305    /// and their associated commands and then iterates over all the normal
1306    /// keywords with their associated commands
1307    pub fn iter_with_keywords() -> impl Iterator<Item = (&'static str, Self)> {
1308        Self::iter()
1309        // Go through all the shortcuts first
1310        .filter_map(|c| c.short_keyword().map(|s| (s, c)))
1311        .chain(Self::iter().map(|c| (c.keyword(), c)))
1312    }
1313
1314    pub fn needs_role(&self) -> Option<comp::AdminRole> { self.data().needs_role }
1315
1316    /// Returns a format string for parsing arguments with scan_fmt
1317    pub fn arg_fmt(&self) -> String {
1318        self.data()
1319            .args
1320            .iter()
1321            .map(|arg| match arg {
1322                ArgumentSpec::PlayerName(_) => "{}",
1323                ArgumentSpec::EntityTarget(_) => "{}",
1324                ArgumentSpec::SiteName(_) => "{/.*/}",
1325                ArgumentSpec::Float(_, _, _) => "{}",
1326                ArgumentSpec::Integer(_, _, _) => "{d}",
1327                ArgumentSpec::Any(_, _) => "{}",
1328                ArgumentSpec::Command(_) => "{}",
1329                ArgumentSpec::Message(_) => "{/.*/}",
1330                ArgumentSpec::SubCommand => "{} {/.*/}",
1331                ArgumentSpec::Enum(_, _, _) => "{}",
1332                ArgumentSpec::AssetPath(_, _, _, _) => "{}",
1333                ArgumentSpec::Boolean(_, _, _) => "{}",
1334                ArgumentSpec::Flag(_) => "{}",
1335            })
1336            .collect::<Vec<_>>()
1337            .join(" ")
1338    }
1339}
1340
1341impl Display for ServerChatCommand {
1342    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
1343        write!(f, "{}", self.keyword())
1344    }
1345}
1346
1347impl FromStr for ServerChatCommand {
1348    type Err = ();
1349
1350    fn from_str(keyword: &str) -> Result<ServerChatCommand, ()> {
1351        Self::iter()
1352        // Go through all the shortcuts first
1353        .filter_map(|c| c.short_keyword().map(|s| (s, c)))
1354        .chain(Self::iter().map(|c| (c.keyword(), c)))
1355            // Find command with matching string as keyword
1356            .find_map(|(kwd, command)| (kwd == keyword).then_some(command))
1357            // Return error if not found
1358            .ok_or(())
1359    }
1360}
1361
1362#[derive(Eq, PartialEq, Debug, Clone, Copy)]
1363pub enum Requirement {
1364    Required,
1365    Optional,
1366}
1367
1368/// Representation for chat command arguments
1369pub enum ArgumentSpec {
1370    /// The argument refers to a player by alias
1371    PlayerName(Requirement),
1372    /// The arguments refers to an entity in some way.
1373    EntityTarget(Requirement),
1374    // The argument refers to a site, by name.
1375    SiteName(Requirement),
1376    /// The argument is a float. The associated values are
1377    /// * label
1378    /// * suggested tab-completion
1379    /// * whether it's optional
1380    Float(&'static str, f32, Requirement),
1381    /// The argument is an integer. The associated values are
1382    /// * label
1383    /// * suggested tab-completion
1384    /// * whether it's optional
1385    Integer(&'static str, i32, Requirement),
1386    /// The argument is any string that doesn't contain spaces
1387    Any(&'static str, Requirement),
1388    /// The argument is a command name (such as in /help)
1389    Command(Requirement),
1390    /// This is the final argument, consuming all characters until the end of
1391    /// input.
1392    Message(Requirement),
1393    /// This command is followed by another command (such as in /sudo)
1394    SubCommand,
1395    /// The argument is likely an enum. The associated values are
1396    /// * label
1397    /// * Predefined string completions
1398    /// * whether it's optional
1399    Enum(&'static str, Vec<String>, Requirement),
1400    /// The argument is an asset path. The associated values are
1401    /// * label
1402    /// * Path prefix shared by all assets
1403    /// * List of all asset paths as strings for completion
1404    /// * whether it's optional
1405    AssetPath(&'static str, &'static str, Vec<String>, Requirement),
1406    /// The argument is likely a boolean. The associated values are
1407    /// * label
1408    /// * suggested tab-completion
1409    /// * whether it's optional
1410    Boolean(&'static str, String, Requirement),
1411    /// The argument is a flag that enables or disables a feature.
1412    Flag(&'static str),
1413}
1414
1415impl ArgumentSpec {
1416    pub fn usage_string(&self) -> String {
1417        match self {
1418            ArgumentSpec::PlayerName(req) => {
1419                if &Requirement::Required == req {
1420                    "<player>".to_string()
1421                } else {
1422                    "[player]".to_string()
1423                }
1424            },
1425            ArgumentSpec::EntityTarget(req) => {
1426                if &Requirement::Required == req {
1427                    "<entity>".to_string()
1428                } else {
1429                    "[entity]".to_string()
1430                }
1431            },
1432            ArgumentSpec::SiteName(req) => {
1433                if &Requirement::Required == req {
1434                    "<site>".to_string()
1435                } else {
1436                    "[site]".to_string()
1437                }
1438            },
1439            ArgumentSpec::Float(label, _, req) => {
1440                if &Requirement::Required == req {
1441                    format!("<{}>", label)
1442                } else {
1443                    format!("[{}]", label)
1444                }
1445            },
1446            ArgumentSpec::Integer(label, _, req) => {
1447                if &Requirement::Required == req {
1448                    format!("<{}>", label)
1449                } else {
1450                    format!("[{}]", label)
1451                }
1452            },
1453            ArgumentSpec::Any(label, req) => {
1454                if &Requirement::Required == req {
1455                    format!("<{}>", label)
1456                } else {
1457                    format!("[{}]", label)
1458                }
1459            },
1460            ArgumentSpec::Command(req) => {
1461                if &Requirement::Required == req {
1462                    "<[/]command>".to_string()
1463                } else {
1464                    "[[/]command]".to_string()
1465                }
1466            },
1467            ArgumentSpec::Message(req) => {
1468                if &Requirement::Required == req {
1469                    "<message>".to_string()
1470                } else {
1471                    "[message]".to_string()
1472                }
1473            },
1474            ArgumentSpec::SubCommand => "<[/]command> [args...]".to_string(),
1475            ArgumentSpec::Enum(label, _, req) => {
1476                if &Requirement::Required == req {
1477                    format!("<{}>", label)
1478                } else {
1479                    format!("[{}]", label)
1480                }
1481            },
1482            ArgumentSpec::AssetPath(label, _, _, req) => {
1483                if &Requirement::Required == req {
1484                    format!("<{}>", label)
1485                } else {
1486                    format!("[{}]", label)
1487                }
1488            },
1489            ArgumentSpec::Boolean(label, _, req) => {
1490                if &Requirement::Required == req {
1491                    format!("<{}>", label)
1492                } else {
1493                    format!("[{}]", label)
1494                }
1495            },
1496            ArgumentSpec::Flag(label) => {
1497                format!("[{}]", label)
1498            },
1499        }
1500    }
1501
1502    pub fn requirement(&self) -> Requirement {
1503        match self {
1504            ArgumentSpec::PlayerName(r)
1505            | ArgumentSpec::EntityTarget(r)
1506            | ArgumentSpec::SiteName(r)
1507            | ArgumentSpec::Float(_, _, r)
1508            | ArgumentSpec::Integer(_, _, r)
1509            | ArgumentSpec::Any(_, r)
1510            | ArgumentSpec::Command(r)
1511            | ArgumentSpec::Message(r)
1512            | ArgumentSpec::Enum(_, _, r)
1513            | ArgumentSpec::AssetPath(_, _, _, r)
1514            | ArgumentSpec::Boolean(_, _, r) => *r,
1515            ArgumentSpec::Flag(_) => Requirement::Optional,
1516            ArgumentSpec::SubCommand => Requirement::Required,
1517        }
1518    }
1519}
1520
1521pub trait CommandEnumArg: FromStr {
1522    fn all_options() -> Vec<String>;
1523}
1524
1525macro_rules! impl_from_to_str_cmd {
1526    ($enum:ident, ($($attribute:ident => $str:expr),*)) => {
1527        impl std::str::FromStr for $enum {
1528            type Err = String;
1529
1530            fn from_str(s: &str) -> Result<Self, Self::Err> {
1531                match s {
1532                    $(
1533                        $str => Ok($enum::$attribute),
1534                    )*
1535                    s => Err(format!("Invalid variant: {s}")),
1536                }
1537            }
1538        }
1539
1540        impl $crate::cmd::CommandEnumArg for $enum {
1541            fn all_options() -> Vec<String> {
1542                vec![$($str.to_string()),*]
1543            }
1544        }
1545    }
1546}
1547
1548impl_from_to_str_cmd!(AuraKindVariant, (
1549    Buff => "buff",
1550    FriendlyFire => "friendly_fire",
1551    ForcePvP => "force_pvp"
1552));
1553
1554impl_from_to_str_cmd!(GroupTarget, (
1555    InGroup => "in_group",
1556    OutOfGroup => "out_of_group",
1557    All => "all"
1558));
1559
1560/// Parse a series of command arguments into values, including collecting all
1561/// trailing arguments.
1562#[macro_export]
1563macro_rules! parse_cmd_args {
1564    ($args:expr, $($t:ty),* $(, ..$tail:ty)? $(,)?) => {
1565        {
1566            let mut args = $args.into_iter().peekable();
1567            (
1568                // We only consume the input argument when parsing is successful. If this fails, we
1569                // will then attempt to parse it as the next argument type. This is done regardless
1570                // of whether the argument is optional because that information is not available
1571                // here. Nevertheless, if the caller only precedes to use the parsed arguments when
1572                // all required arguments parse successfully to `Some(val)` this should not create
1573                // any unexpected behavior.
1574                //
1575                // This does mean that optional arguments will be included in the trailing args or
1576                // that one optional arg could be interpreted as another, if the user makes a
1577                // mistake that causes an optional arg to fail to parse. But there is no way to
1578                // discern this in the current model with the optional args and trailing arg being
1579                // solely position based.
1580                $({
1581                    let parsed = args.peek().and_then(|s| s.parse::<$t>().ok());
1582                    // Consume successfully parsed arg.
1583                    if parsed.is_some() { args.next(); }
1584                    parsed
1585                }),*
1586                $(, args.map(|s| s.to_string()).collect::<$tail>())?
1587            )
1588        }
1589    };
1590}
1591
1592#[cfg(test)]
1593mod tests {
1594    use super::*;
1595    use crate::comp::Item;
1596
1597    #[test]
1598    fn verify_cmd_list_sorted() {
1599        let mut list = ServerChatCommand::iter()
1600            .map(|c| c.keyword())
1601            .collect::<Vec<_>>();
1602
1603        // Vec::is_sorted is unstable, so we do it the hard way
1604        let list2 = list.clone();
1605        list.sort_unstable();
1606        assert_eq!(list, list2);
1607    }
1608
1609    #[test]
1610    fn test_loading_skill_presets() {
1611        SkillPresetManifest::load_expect_combined_static(PRESET_MANIFEST_PATH);
1612    }
1613
1614    #[test]
1615    fn test_load_kits() {
1616        let kits = KitManifest::load_expect_combined_static(KIT_MANIFEST_PATH).read();
1617        let mut rng = rand::rng();
1618        for kit in kits.0.values() {
1619            for (item_id, _) in kit.iter() {
1620                match item_id {
1621                    KitSpec::Item(item_id) => {
1622                        Item::new_from_asset_expect(item_id);
1623                    },
1624                    KitSpec::ModularWeaponSet {
1625                        tool,
1626                        material,
1627                        hands,
1628                    } => {
1629                        comp::item::modular::generate_weapons(*tool, *material, *hands)
1630                            .unwrap_or_else(|_| {
1631                                panic!(
1632                                    "Failed to synthesize a modular {tool:?} set made of \
1633                                     {material:?}."
1634                                )
1635                            });
1636                    },
1637                    KitSpec::ModularWeaponRandom {
1638                        tool,
1639                        material,
1640                        hands,
1641                    } => {
1642                        comp::item::modular::random_weapon(*tool, *material, *hands, &mut rng)
1643                            .unwrap_or_else(|_| {
1644                                panic!(
1645                                    "Failed to synthesize a random {hands:?}-handed modular \
1646                                     {tool:?} made of {material:?}."
1647                                )
1648                            });
1649                    },
1650                }
1651            }
1652        }
1653    }
1654}