veloren_common/
cmd.rs

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