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::Amnesia => "amnesia",
193            BuffKind::OffBalance => "off_balance",
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    BanLog,
381    BattleMode,
382    BattleModeForce,
383    Body,
384    Buff,
385    Build,
386    Campfire,
387    ClearPersistedTerrain,
388    CreateLocation,
389    DeathEffect,
390    DebugColumn,
391    DebugWays,
392    DeleteLocation,
393    DestroyTethers,
394    DisconnectAllPlayers,
395    Dismount,
396    DropAll,
397    Dummy,
398    Explosion,
399    Faction,
400    GiveItem,
401    Gizmos,
402    GizmosRange,
403    Goto,
404    GotoRand,
405    Group,
406    GroupInvite,
407    GroupKick,
408    GroupLeave,
409    GroupPromote,
410    Health,
411    IntoNpc,
412    JoinFaction,
413    Jump,
414    Kick,
415    Kill,
416    KillNpcs,
417    Kit,
418    Lantern,
419    Light,
420    Lightning,
421    Location,
422    MakeBlock,
423    MakeNpc,
424    MakeSprite,
425    MakeVolume,
426    Motd,
427    Mount,
428    Object,
429    Outcome,
430    PermitBuild,
431    Players,
432    Poise,
433    Portal,
434    Region,
435    ReloadChunks,
436    RemoveLights,
437    RepairEquipment,
438    ResetRecipes,
439    Respawn,
440    RevokeBuild,
441    RevokeBuildAll,
442    RtsimChunk,
443    RtsimInfo,
444    RtsimNpc,
445    RtsimPurge,
446    RtsimTp,
447    Safezone,
448    Say,
449    Scale,
450    ServerPhysics,
451    SetBodyType,
452    SetMotd,
453    SetWaypoint,
454    Ship,
455    Site,
456    SkillPoint,
457    SkillPreset,
458    Spawn,
459    Spot,
460    Sudo,
461    Tell,
462    Tether,
463    Time,
464    TimeScale,
465    Tp,
466    Unban,
467    UnbanIp,
468    Version,
469    WeatherZone,
470    Whitelist,
471    Wiring,
472    World,
473}
474
475impl ServerChatCommand {
476    pub fn data(&self) -> ChatCommandData {
477        use ArgumentSpec::*;
478        use Requirement::*;
479        use Role::*;
480        let cmd = ChatCommandData::new;
481        match self {
482            ServerChatCommand::Adminify => cmd(
483                vec![PlayerName(Required), Enum("role", ROLES.clone(), Optional)],
484                Content::localized("command-adminify-desc"),
485                Some(Admin),
486            ),
487            ServerChatCommand::Airship => cmd(
488                vec![
489                    Enum(
490                        "kind",
491                        comp::ship::ALL_AIRSHIPS
492                            .iter()
493                            .map(|b| format!("{b:?}"))
494                            .collect(),
495                        Optional,
496                    ),
497                    Float("destination_degrees_ccw_of_east", 90.0, Optional),
498                ],
499                Content::localized("command-airship-desc"),
500                Some(Admin),
501            ),
502            ServerChatCommand::Alias => cmd(
503                vec![Any("name", Required)],
504                Content::localized("command-alias-desc"),
505                Some(Moderator),
506            ),
507            ServerChatCommand::Aura => cmd(
508                vec![
509                    Float("aura_radius", 10.0, Required),
510                    Float("aura_duration", 10.0, Optional),
511                    Boolean("new_entity", "true".to_string(), Optional),
512                    Enum("aura_target", GroupTarget::all_options(), Optional),
513                    Enum("aura_kind", AuraKindVariant::all_options(), Required),
514                    Any("aura spec", Optional),
515                ],
516                Content::localized("command-aura-desc"),
517                Some(Admin),
518            ),
519            ServerChatCommand::Buff => cmd(
520                vec![
521                    Enum("buff", BUFFS.clone(), Required),
522                    Float("strength", 0.01, Optional),
523                    Float("duration", 10.0, Optional),
524                    Any("buff data spec", Optional),
525                ],
526                Content::localized("command-buff-desc"),
527                Some(Admin),
528            ),
529            ServerChatCommand::Ban => cmd(
530                vec![
531                    PlayerName(Required),
532                    Boolean("overwrite", "true".to_string(), Optional),
533                    Any("ban duration", Optional),
534                    Message(Optional),
535                ],
536                Content::localized("command-ban-desc"),
537                Some(Moderator),
538            ),
539            ServerChatCommand::BanIp => cmd(
540                vec![
541                    PlayerName(Required),
542                    Boolean("overwrite", "true".to_string(), Optional),
543                    Any("ban duration", Optional),
544                    Message(Optional),
545                ],
546                Content::localized("command-ban-ip-desc"),
547                Some(Moderator),
548            ),
549            ServerChatCommand::BanLog => cmd(
550                vec![PlayerName(Required), Integer("max entries", 10, Optional)],
551                Content::localized("command-ban-ip-desc"),
552                Some(Moderator),
553            ),
554            #[rustfmt::skip]
555            ServerChatCommand::BattleMode => cmd(
556                vec![Enum(
557                    "battle mode",
558                    vec!["pvp".to_owned(), "pve".to_owned()],
559                    Optional,
560                )],
561                Content::localized("command-battlemode-desc"),
562                None,
563
564            ),
565            ServerChatCommand::IntoNpc => cmd(
566                vec![AssetPath(
567                    "entity_config",
568                    "common.entity.",
569                    ENTITY_CONFIGS.clone(),
570                    Required,
571                )],
572                Content::localized("command-into_npc-desc"),
573                Some(Admin),
574            ),
575            ServerChatCommand::Body => cmd(
576                vec![Enum("body", ENTITIES.clone(), Required)],
577                Content::localized("command-body-desc"),
578                Some(Admin),
579            ),
580            ServerChatCommand::BattleModeForce => cmd(
581                vec![Enum(
582                    "battle mode",
583                    vec!["pvp".to_owned(), "pve".to_owned()],
584                    Required,
585                )],
586                Content::localized("command-battlemode_force-desc"),
587                Some(Admin),
588            ),
589            ServerChatCommand::Build => cmd(vec![], Content::localized("command-build-desc"), None),
590            ServerChatCommand::AreaAdd => cmd(
591                vec![
592                    Any("name", Required),
593                    Enum("kind", AREA_KINDS.clone(), Required),
594                    Integer("xlo", 0, Required),
595                    Integer("xhi", 10, Required),
596                    Integer("ylo", 0, Required),
597                    Integer("yhi", 10, Required),
598                    Integer("zlo", 0, Required),
599                    Integer("zhi", 10, Required),
600                ],
601                Content::localized("command-area_add-desc"),
602                Some(Admin),
603            ),
604            ServerChatCommand::AreaList => cmd(
605                vec![],
606                Content::localized("command-area_list-desc"),
607                Some(Admin),
608            ),
609            ServerChatCommand::AreaRemove => cmd(
610                vec![
611                    Any("name", Required),
612                    Enum("kind", AREA_KINDS.clone(), Required),
613                ],
614                Content::localized("command-area_remove-desc"),
615                Some(Admin),
616            ),
617            ServerChatCommand::Campfire => cmd(
618                vec![],
619                Content::localized("command-campfire-desc"),
620                Some(Admin),
621            ),
622            ServerChatCommand::ClearPersistedTerrain => cmd(
623                vec![Integer("chunk_radius", 6, Required)],
624                Content::localized("command-clear_persisted_terrain-desc"),
625                Some(Admin),
626            ),
627            ServerChatCommand::DeathEffect => cmd(
628                vec![
629                    Enum("death_effect", vec!["transform".to_string()], Required),
630                    // NOTE: I added this for QoL as transform is currently the only death effect
631                    // and takes an asset path, when more on-death effects are added to the command
632                    // remove this.
633                    AssetPath(
634                        "entity_config",
635                        "common.entity.",
636                        ENTITY_CONFIGS.clone(),
637                        Required,
638                    ),
639                ],
640                Content::localized("command-death_effect-dest"),
641                Some(Admin),
642            ),
643            ServerChatCommand::DebugColumn => cmd(
644                vec![Integer("x", 15000, Required), Integer("y", 15000, Required)],
645                Content::localized("command-debug_column-desc"),
646                Some(Admin),
647            ),
648            ServerChatCommand::DebugWays => cmd(
649                vec![Integer("x", 15000, Required), Integer("y", 15000, Required)],
650                Content::localized("command-debug_ways-desc"),
651                Some(Admin),
652            ),
653            ServerChatCommand::DisconnectAllPlayers => cmd(
654                vec![Any("confirm", Required)],
655                Content::localized("command-disconnect_all_players-desc"),
656                Some(Admin),
657            ),
658            ServerChatCommand::DropAll => cmd(
659                vec![],
660                Content::localized("command-dropall-desc"),
661                Some(Moderator),
662            ),
663            ServerChatCommand::Dummy => cmd(
664                vec![],
665                Content::localized("command-dummy-desc"),
666                Some(Admin),
667            ),
668            ServerChatCommand::Explosion => cmd(
669                vec![Float("radius", 5.0, Required)],
670                Content::localized("command-explosion-desc"),
671                Some(Admin),
672            ),
673            ServerChatCommand::Faction => cmd(
674                vec![Message(Optional)],
675                Content::localized("command-faction-desc"),
676                None,
677            ),
678            ServerChatCommand::GiveItem => cmd(
679                vec![
680                    AssetPath("item", "common.items.", ITEM_SPECS.clone(), Required),
681                    Integer("num", 1, Optional),
682                ],
683                Content::localized("command-give_item-desc"),
684                Some(Admin),
685            ),
686            ServerChatCommand::Gizmos => cmd(
687                vec![
688                    Enum(
689                        "kind",
690                        ["All".to_string(), "None".to_string()]
691                            .into_iter()
692                            .chain(
693                                comp::gizmos::GizmoSubscription::iter()
694                                    .map(|kind| kind.to_string()),
695                            )
696                            .collect(),
697                        Required,
698                    ),
699                    EntityTarget(Optional),
700                ],
701                Content::localized("command-gizmos-desc"),
702                Some(Admin),
703            ),
704            ServerChatCommand::GizmosRange => cmd(
705                vec![Float("range", 32.0, Required)],
706                Content::localized("command-gizmos_range-desc"),
707                Some(Admin),
708            ),
709            ServerChatCommand::Goto => cmd(
710                vec![
711                    Float("x", 0.0, Required),
712                    Float("y", 0.0, Required),
713                    Float("z", 0.0, Required),
714                    Boolean("Dismount from ship", "true".to_string(), Optional),
715                ],
716                Content::localized("command-goto-desc"),
717                Some(Admin),
718            ),
719            ServerChatCommand::GotoRand => cmd(
720                vec![Boolean("Dismount from ship", "true".to_string(), Optional)],
721                Content::localized("command-goto-rand"),
722                Some(Admin),
723            ),
724            ServerChatCommand::Group => cmd(
725                vec![Message(Optional)],
726                Content::localized("command-group-desc"),
727                None,
728            ),
729            ServerChatCommand::GroupInvite => cmd(
730                vec![PlayerName(Required)],
731                Content::localized("command-group_invite-desc"),
732                None,
733            ),
734            ServerChatCommand::GroupKick => cmd(
735                vec![PlayerName(Required)],
736                Content::localized("command-group_kick-desc"),
737                None,
738            ),
739            ServerChatCommand::GroupLeave => {
740                cmd(vec![], Content::localized("command-group_leave-desc"), None)
741            },
742            ServerChatCommand::GroupPromote => cmd(
743                vec![PlayerName(Required)],
744                Content::localized("command-group_promote-desc"),
745                None,
746            ),
747            ServerChatCommand::Health => cmd(
748                vec![Integer("hp", 100, Required)],
749                Content::localized("command-health-desc"),
750                Some(Admin),
751            ),
752            ServerChatCommand::Respawn => cmd(
753                vec![],
754                Content::localized("command-respawn-desc"),
755                Some(Moderator),
756            ),
757            ServerChatCommand::JoinFaction => cmd(
758                vec![Any("faction", Optional)],
759                Content::localized("command-join_faction-desc"),
760                None,
761            ),
762            ServerChatCommand::Jump => cmd(
763                vec![
764                    Float("x", 0.0, Required),
765                    Float("y", 0.0, Required),
766                    Float("z", 0.0, Required),
767                    Boolean("Dismount from ship", "true".to_string(), Optional),
768                ],
769                Content::localized("command-jump-desc"),
770                Some(Admin),
771            ),
772            ServerChatCommand::Kick => cmd(
773                vec![PlayerName(Required), Message(Optional)],
774                Content::localized("command-kick-desc"),
775                Some(Moderator),
776            ),
777            ServerChatCommand::Kill => cmd(vec![], Content::localized("command-kill-desc"), None),
778            ServerChatCommand::KillNpcs => cmd(
779                vec![Float("radius", 100.0, Optional), Flag("--also-pets")],
780                Content::localized("command-kill_npcs-desc"),
781                Some(Admin),
782            ),
783            ServerChatCommand::Kit => cmd(
784                vec![Enum("kit_name", KITS.to_vec(), Required)],
785                Content::localized("command-kit-desc"),
786                Some(Admin),
787            ),
788            ServerChatCommand::Lantern => cmd(
789                vec![
790                    Float("strength", 5.0, Required),
791                    Float("r", 1.0, Optional),
792                    Float("g", 1.0, Optional),
793                    Float("b", 1.0, Optional),
794                ],
795                Content::localized("command-lantern-desc"),
796                Some(Admin),
797            ),
798            ServerChatCommand::Light => cmd(
799                vec![
800                    Float("r", 1.0, Optional),
801                    Float("g", 1.0, Optional),
802                    Float("b", 1.0, Optional),
803                    Float("x", 0.0, Optional),
804                    Float("y", 0.0, Optional),
805                    Float("z", 0.0, Optional),
806                    Float("strength", 5.0, Optional),
807                ],
808                Content::localized("command-light-desc"),
809                Some(Admin),
810            ),
811            ServerChatCommand::MakeBlock => cmd(
812                vec![
813                    Enum("block", BLOCK_KINDS.clone(), Required),
814                    Integer("r", 255, Optional),
815                    Integer("g", 255, Optional),
816                    Integer("b", 255, Optional),
817                ],
818                Content::localized("command-make_block-desc"),
819                Some(Admin),
820            ),
821            ServerChatCommand::MakeNpc => cmd(
822                vec![
823                    AssetPath(
824                        "entity_config",
825                        "common.entity.",
826                        ENTITY_CONFIGS.clone(),
827                        Required,
828                    ),
829                    Integer("num", 1, Optional),
830                ],
831                Content::localized("command-make_npc-desc"),
832                Some(Admin),
833            ),
834            ServerChatCommand::MakeSprite => cmd(
835                vec![Enum("sprite", SPRITE_KINDS.clone(), Required)],
836                Content::localized("command-make_sprite-desc"),
837                Some(Admin),
838            ),
839            ServerChatCommand::Motd => cmd(vec![], Content::localized("command-motd-desc"), None),
840            ServerChatCommand::Object => cmd(
841                vec![Enum("object", OBJECTS.clone(), Required)],
842                Content::localized("command-object-desc"),
843                Some(Admin),
844            ),
845            ServerChatCommand::Outcome => cmd(
846                vec![Enum("outcome", OUTCOME_KINDS.clone(), Required)],
847                Content::localized("command-outcome-desc"),
848                Some(Admin),
849            ),
850            ServerChatCommand::PermitBuild => cmd(
851                vec![Any("area_name", Required)],
852                Content::localized("command-permit_build-desc"),
853                Some(Admin),
854            ),
855            ServerChatCommand::Players => {
856                cmd(vec![], Content::localized("command-players-desc"), None)
857            },
858            ServerChatCommand::Poise => cmd(
859                vec![Integer("poise", 100, Required)],
860                Content::localized("command-poise-desc"),
861                Some(Admin),
862            ),
863            ServerChatCommand::Portal => cmd(
864                vec![
865                    Float("x", 0., Required),
866                    Float("y", 0., Required),
867                    Float("z", 0., Required),
868                    Boolean("requires_no_aggro", "true".to_string(), Optional),
869                    Float("buildup_time", 5., Optional),
870                ],
871                Content::localized("command-portal-desc"),
872                Some(Admin),
873            ),
874            ServerChatCommand::ReloadChunks => cmd(
875                vec![Integer("chunk_radius", 6, Optional)],
876                Content::localized("command-reload_chunks-desc"),
877                Some(Admin),
878            ),
879            ServerChatCommand::ResetRecipes => cmd(
880                vec![],
881                Content::localized("command-reset_recipes-desc"),
882                Some(Admin),
883            ),
884            ServerChatCommand::RemoveLights => cmd(
885                vec![Float("radius", 20.0, Optional)],
886                Content::localized("command-remove_lights-desc"),
887                Some(Admin),
888            ),
889            ServerChatCommand::RevokeBuild => cmd(
890                vec![Any("area_name", Required)],
891                Content::localized("command-revoke_build-desc"),
892                Some(Admin),
893            ),
894            ServerChatCommand::RevokeBuildAll => cmd(
895                vec![],
896                Content::localized("command-revoke_build_all-desc"),
897                Some(Admin),
898            ),
899            ServerChatCommand::Region => cmd(
900                vec![Message(Optional)],
901                Content::localized("command-region-desc"),
902                None,
903            ),
904            ServerChatCommand::Safezone => cmd(
905                vec![Float("range", 100.0, Optional)],
906                Content::localized("command-safezone-desc"),
907                Some(Moderator),
908            ),
909            ServerChatCommand::Say => cmd(
910                vec![Message(Optional)],
911                Content::localized("command-say-desc"),
912                None,
913            ),
914            ServerChatCommand::ServerPhysics => cmd(
915                vec![
916                    PlayerName(Required),
917                    Boolean("enabled", "true".to_string(), Optional),
918                    Message(Optional),
919                ],
920                Content::localized("command-server_physics-desc"),
921                Some(Moderator),
922            ),
923            ServerChatCommand::SetMotd => cmd(
924                vec![Any("locale", Optional), Message(Optional)],
925                Content::localized("command-set_motd-desc"),
926                Some(Admin),
927            ),
928            ServerChatCommand::SetBodyType => cmd(
929                vec![
930                    Enum(
931                        "body type",
932                        vec!["Female".to_string(), "Male".to_string()],
933                        Required,
934                    ),
935                    Boolean("permanent", "false".to_string(), Requirement::Optional),
936                ],
937                Content::localized("command-set_body_type-desc"),
938                Some(Admin),
939            ),
940            ServerChatCommand::Ship => cmd(
941                vec![
942                    Enum(
943                        "kind",
944                        comp::ship::ALL_SHIPS
945                            .iter()
946                            .map(|b| format!("{b:?}"))
947                            .collect(),
948                        Optional,
949                    ),
950                    Boolean(
951                        "Whether the ship should be tethered to the target (or its mount)",
952                        "false".to_string(),
953                        Optional,
954                    ),
955                    Float("destination_degrees_ccw_of_east", 90.0, Optional),
956                ],
957                Content::localized("command-ship-desc"),
958                Some(Admin),
959            ),
960            // Uses Message because site names can contain spaces,
961            // which would be assumed to be separators otherwise
962            ServerChatCommand::Site => cmd(
963                vec![
964                    SiteName(Required),
965                    Boolean("Dismount from ship", "true".to_string(), Optional),
966                ],
967                Content::localized("command-site-desc"),
968                Some(Moderator),
969            ),
970            ServerChatCommand::SkillPoint => cmd(
971                vec![
972                    Enum("skill tree", SKILL_TREES.clone(), Required),
973                    Integer("amount", 1, Optional),
974                ],
975                Content::localized("command-skill_point-desc"),
976                Some(Admin),
977            ),
978            ServerChatCommand::SkillPreset => cmd(
979                vec![Enum("preset_name", PRESET_LIST.to_vec(), Required)],
980                Content::localized("command-skill_preset-desc"),
981                Some(Admin),
982            ),
983            ServerChatCommand::Spawn => cmd(
984                vec![
985                    Enum("alignment", ALIGNMENTS.clone(), Required),
986                    Enum("entity", ENTITIES.clone(), Required),
987                    Integer("amount", 1, Optional),
988                    Boolean("ai", "true".to_string(), Optional),
989                    Float("scale", 1.0, Optional),
990                    Boolean("tethered", "false".to_string(), Optional),
991                ],
992                Content::localized("command-spawn-desc"),
993                Some(Admin),
994            ),
995            ServerChatCommand::Spot => cmd(
996                vec![Enum("Spot kind to find", SPOTS.clone(), Required)],
997                Content::localized("command-spot-desc"),
998                Some(Admin),
999            ),
1000            ServerChatCommand::Sudo => cmd(
1001                vec![EntityTarget(Required), SubCommand],
1002                Content::localized("command-sudo-desc"),
1003                Some(Moderator),
1004            ),
1005            ServerChatCommand::Tell => cmd(
1006                vec![PlayerName(Required), Message(Optional)],
1007                Content::localized("command-tell-desc"),
1008                None,
1009            ),
1010            ServerChatCommand::Time => cmd(
1011                vec![Enum("time", TIMES.clone(), Optional)],
1012                Content::localized("command-time-desc"),
1013                Some(Admin),
1014            ),
1015            ServerChatCommand::TimeScale => cmd(
1016                vec![Float("time scale", 1.0, Optional)],
1017                Content::localized("command-time_scale-desc"),
1018                Some(Admin),
1019            ),
1020            ServerChatCommand::Tp => cmd(
1021                vec![
1022                    EntityTarget(Optional),
1023                    Boolean("Dismount from ship", "true".to_string(), Optional),
1024                ],
1025                Content::localized("command-tp-desc"),
1026                Some(Moderator),
1027            ),
1028            ServerChatCommand::RtsimTp => cmd(
1029                vec![
1030                    Integer("npc index", 0, Required),
1031                    Boolean("Dismount from ship", "true".to_string(), Optional),
1032                ],
1033                Content::localized("command-rtsim_tp-desc"),
1034                Some(Admin),
1035            ),
1036            ServerChatCommand::RtsimInfo => cmd(
1037                vec![Integer("npc index", 0, Required)],
1038                Content::localized("command-rtsim_info-desc"),
1039                Some(Admin),
1040            ),
1041            ServerChatCommand::RtsimNpc => cmd(
1042                vec![Any("query", Required), Integer("max number", 20, Optional)],
1043                Content::localized("command-rtsim_npc-desc"),
1044                Some(Admin),
1045            ),
1046            ServerChatCommand::RtsimPurge => cmd(
1047                vec![Boolean(
1048                    "whether purging of rtsim data should occur on next startup",
1049                    true.to_string(),
1050                    Required,
1051                )],
1052                Content::localized("command-rtsim_purge-desc"),
1053                Some(Admin),
1054            ),
1055            ServerChatCommand::RtsimChunk => cmd(
1056                vec![],
1057                Content::localized("command-rtsim_chunk-desc"),
1058                Some(Admin),
1059            ),
1060            ServerChatCommand::Unban => cmd(
1061                vec![PlayerName(Required)],
1062                Content::localized("command-unban-desc"),
1063                Some(Moderator),
1064            ),
1065            ServerChatCommand::UnbanIp => cmd(
1066                vec![PlayerName(Required)],
1067                Content::localized("command-unban-ip-desc"),
1068                Some(Moderator),
1069            ),
1070            ServerChatCommand::Version => {
1071                cmd(vec![], Content::localized("command-version-desc"), None)
1072            },
1073            ServerChatCommand::SetWaypoint => cmd(
1074                vec![],
1075                Content::localized("command-waypoint-desc"),
1076                Some(Admin),
1077            ),
1078            ServerChatCommand::Wiring => cmd(
1079                vec![],
1080                Content::localized("command-wiring-desc"),
1081                Some(Admin),
1082            ),
1083            ServerChatCommand::Whitelist => cmd(
1084                vec![Any("add/remove", Required), PlayerName(Required)],
1085                Content::localized("command-whitelist-desc"),
1086                Some(Moderator),
1087            ),
1088            ServerChatCommand::World => cmd(
1089                vec![Message(Optional)],
1090                Content::localized("command-world-desc"),
1091                None,
1092            ),
1093            ServerChatCommand::MakeVolume => cmd(
1094                vec![Integer("size", 15, Optional)],
1095                Content::localized("command-make_volume-desc"),
1096                Some(Admin),
1097            ),
1098            ServerChatCommand::Location => cmd(
1099                vec![Any("name", Required)],
1100                Content::localized("command-location-desc"),
1101                None,
1102            ),
1103            ServerChatCommand::CreateLocation => cmd(
1104                vec![Any("name", Required)],
1105                Content::localized("command-create_location-desc"),
1106                Some(Moderator),
1107            ),
1108            ServerChatCommand::DeleteLocation => cmd(
1109                vec![Any("name", Required)],
1110                Content::localized("command-delete_location-desc"),
1111                Some(Moderator),
1112            ),
1113            ServerChatCommand::WeatherZone => cmd(
1114                vec![
1115                    Enum("weather kind", WEATHERS.clone(), Required),
1116                    Float("radius", 500.0, Optional),
1117                    Float("time", 300.0, Optional),
1118                ],
1119                Content::localized("command-weather_zone-desc"),
1120                Some(Admin),
1121            ),
1122            ServerChatCommand::Lightning => cmd(
1123                vec![],
1124                Content::localized("command-lightning-desc"),
1125                Some(Admin),
1126            ),
1127            ServerChatCommand::Scale => cmd(
1128                vec![
1129                    Float("factor", 1.0, Required),
1130                    Boolean("reset_mass", true.to_string(), Optional),
1131                ],
1132                Content::localized("command-scale-desc"),
1133                Some(Admin),
1134            ),
1135            ServerChatCommand::RepairEquipment => cmd(
1136                vec![ArgumentSpec::Boolean(
1137                    "repair inventory",
1138                    true.to_string(),
1139                    Optional,
1140                )],
1141                Content::localized("command-repair_equipment-desc"),
1142                Some(Admin),
1143            ),
1144            ServerChatCommand::Tether => cmd(
1145                vec![
1146                    EntityTarget(Required),
1147                    Boolean("automatic length", "true".to_string(), Optional),
1148                ],
1149                Content::localized("command-tether-desc"),
1150                Some(Admin),
1151            ),
1152            ServerChatCommand::DestroyTethers => cmd(
1153                vec![],
1154                Content::localized("command-destroy_tethers-desc"),
1155                Some(Admin),
1156            ),
1157            ServerChatCommand::Mount => cmd(
1158                vec![EntityTarget(Required)],
1159                Content::localized("command-mount-desc"),
1160                Some(Admin),
1161            ),
1162            ServerChatCommand::Dismount => cmd(
1163                vec![EntityTarget(Required)],
1164                Content::localized("command-dismount-desc"),
1165                Some(Admin),
1166            ),
1167        }
1168    }
1169
1170    /// The keyword used to invoke the command, omitting the prefix.
1171    pub fn keyword(&self) -> &'static str {
1172        match self {
1173            ServerChatCommand::Adminify => "adminify",
1174            ServerChatCommand::Airship => "airship",
1175            ServerChatCommand::Alias => "alias",
1176            ServerChatCommand::AreaAdd => "area_add",
1177            ServerChatCommand::AreaList => "area_list",
1178            ServerChatCommand::AreaRemove => "area_remove",
1179            ServerChatCommand::Aura => "aura",
1180            ServerChatCommand::Ban => "ban",
1181            ServerChatCommand::BanIp => "ban_ip",
1182            ServerChatCommand::BanLog => "ban_log",
1183            ServerChatCommand::BattleMode => "battlemode",
1184            ServerChatCommand::BattleModeForce => "battlemode_force",
1185            ServerChatCommand::Body => "body",
1186            ServerChatCommand::Buff => "buff",
1187            ServerChatCommand::Build => "build",
1188            ServerChatCommand::Campfire => "campfire",
1189            ServerChatCommand::ClearPersistedTerrain => "clear_persisted_terrain",
1190            ServerChatCommand::DeathEffect => "death_effect",
1191            ServerChatCommand::DebugColumn => "debug_column",
1192            ServerChatCommand::DebugWays => "debug_ways",
1193            ServerChatCommand::DisconnectAllPlayers => "disconnect_all_players",
1194            ServerChatCommand::DropAll => "dropall",
1195            ServerChatCommand::Dummy => "dummy",
1196            ServerChatCommand::Explosion => "explosion",
1197            ServerChatCommand::Faction => "faction",
1198            ServerChatCommand::GiveItem => "give_item",
1199            ServerChatCommand::Gizmos => "gizmos",
1200            ServerChatCommand::GizmosRange => "gizmos_range",
1201            ServerChatCommand::Goto => "goto",
1202            ServerChatCommand::GotoRand => "goto_rand",
1203            ServerChatCommand::Group => "group",
1204            ServerChatCommand::GroupInvite => "group_invite",
1205            ServerChatCommand::GroupKick => "group_kick",
1206            ServerChatCommand::GroupLeave => "group_leave",
1207            ServerChatCommand::GroupPromote => "group_promote",
1208            ServerChatCommand::Health => "health",
1209            ServerChatCommand::IntoNpc => "into_npc",
1210            ServerChatCommand::JoinFaction => "join_faction",
1211            ServerChatCommand::Jump => "jump",
1212            ServerChatCommand::Kick => "kick",
1213            ServerChatCommand::Kill => "kill",
1214            ServerChatCommand::KillNpcs => "kill_npcs",
1215            ServerChatCommand::Kit => "kit",
1216            ServerChatCommand::Lantern => "lantern",
1217            ServerChatCommand::Respawn => "respawn",
1218            ServerChatCommand::Light => "light",
1219            ServerChatCommand::MakeBlock => "make_block",
1220            ServerChatCommand::MakeNpc => "make_npc",
1221            ServerChatCommand::MakeSprite => "make_sprite",
1222            ServerChatCommand::Motd => "motd",
1223            ServerChatCommand::Object => "object",
1224            ServerChatCommand::Outcome => "outcome",
1225            ServerChatCommand::PermitBuild => "permit_build",
1226            ServerChatCommand::Players => "players",
1227            ServerChatCommand::Poise => "poise",
1228            ServerChatCommand::Portal => "portal",
1229            ServerChatCommand::ResetRecipes => "reset_recipes",
1230            ServerChatCommand::Region => "region",
1231            ServerChatCommand::ReloadChunks => "reload_chunks",
1232            ServerChatCommand::RemoveLights => "remove_lights",
1233            ServerChatCommand::RevokeBuild => "revoke_build",
1234            ServerChatCommand::RevokeBuildAll => "revoke_build_all",
1235            ServerChatCommand::Safezone => "safezone",
1236            ServerChatCommand::Say => "say",
1237            ServerChatCommand::ServerPhysics => "server_physics",
1238            ServerChatCommand::SetMotd => "set_motd",
1239            ServerChatCommand::SetBodyType => "set_body_type",
1240            ServerChatCommand::Ship => "ship",
1241            ServerChatCommand::Site => "site",
1242            ServerChatCommand::SkillPoint => "skill_point",
1243            ServerChatCommand::SkillPreset => "skill_preset",
1244            ServerChatCommand::Spawn => "spawn",
1245            ServerChatCommand::Spot => "spot",
1246            ServerChatCommand::Sudo => "sudo",
1247            ServerChatCommand::Tell => "tell",
1248            ServerChatCommand::Time => "time",
1249            ServerChatCommand::TimeScale => "time_scale",
1250            ServerChatCommand::Tp => "tp",
1251            ServerChatCommand::RtsimTp => "rtsim_tp",
1252            ServerChatCommand::RtsimInfo => "rtsim_info",
1253            ServerChatCommand::RtsimNpc => "rtsim_npc",
1254            ServerChatCommand::RtsimPurge => "rtsim_purge",
1255            ServerChatCommand::RtsimChunk => "rtsim_chunk",
1256            ServerChatCommand::Unban => "unban",
1257            ServerChatCommand::UnbanIp => "unban_ip",
1258            ServerChatCommand::Version => "version",
1259            ServerChatCommand::SetWaypoint => "set_waypoint",
1260            ServerChatCommand::Wiring => "wiring",
1261            ServerChatCommand::Whitelist => "whitelist",
1262            ServerChatCommand::World => "world",
1263            ServerChatCommand::MakeVolume => "make_volume",
1264            ServerChatCommand::Location => "location",
1265            ServerChatCommand::CreateLocation => "create_location",
1266            ServerChatCommand::DeleteLocation => "delete_location",
1267            ServerChatCommand::WeatherZone => "weather_zone",
1268            ServerChatCommand::Lightning => "lightning",
1269            ServerChatCommand::Scale => "scale",
1270            ServerChatCommand::RepairEquipment => "repair_equipment",
1271            ServerChatCommand::Tether => "tether",
1272            ServerChatCommand::DestroyTethers => "destroy_tethers",
1273            ServerChatCommand::Mount => "mount",
1274            ServerChatCommand::Dismount => "dismount",
1275        }
1276    }
1277
1278    /// The short keyword used to invoke the command, omitting the leading '/'.
1279    /// Returns None if the command doesn't have a short keyword
1280    pub fn short_keyword(&self) -> Option<&'static str> {
1281        Some(match self {
1282            ServerChatCommand::Faction => "f",
1283            ServerChatCommand::Group => "g",
1284            ServerChatCommand::Region => "r",
1285            ServerChatCommand::Say => "s",
1286            ServerChatCommand::Tell => "t",
1287            ServerChatCommand::World => "w",
1288            _ => return None,
1289        })
1290    }
1291
1292    /// Produce an iterator over all the available commands
1293    pub fn iter() -> impl Iterator<Item = Self> + Clone { <Self as IntoEnumIterator>::iter() }
1294
1295    /// A message that explains what the command does
1296    pub fn help_content(&self) -> Content {
1297        let data = self.data();
1298
1299        let usage = std::iter::once(format!("/{}", self.keyword()))
1300            .chain(data.args.iter().map(|arg| arg.usage_string()))
1301            .collect::<Vec<_>>()
1302            .join(" ");
1303
1304        Content::localized_with_args("command-help-template", [
1305            ("usage", Content::Plain(usage)),
1306            ("description", data.description),
1307        ])
1308    }
1309
1310    /// Produce an iterator that first goes over all the short keywords
1311    /// and their associated commands and then iterates over all the normal
1312    /// keywords with their associated commands
1313    pub fn iter_with_keywords() -> impl Iterator<Item = (&'static str, Self)> {
1314        Self::iter()
1315        // Go through all the shortcuts first
1316        .filter_map(|c| c.short_keyword().map(|s| (s, c)))
1317        .chain(Self::iter().map(|c| (c.keyword(), c)))
1318    }
1319
1320    pub fn needs_role(&self) -> Option<comp::AdminRole> { self.data().needs_role }
1321
1322    /// Returns a format string for parsing arguments with scan_fmt
1323    pub fn arg_fmt(&self) -> String {
1324        self.data()
1325            .args
1326            .iter()
1327            .map(|arg| match arg {
1328                ArgumentSpec::PlayerName(_) => "{}",
1329                ArgumentSpec::EntityTarget(_) => "{}",
1330                ArgumentSpec::SiteName(_) => "{/.*/}",
1331                ArgumentSpec::Float(_, _, _) => "{}",
1332                ArgumentSpec::Integer(_, _, _) => "{d}",
1333                ArgumentSpec::Any(_, _) => "{}",
1334                ArgumentSpec::Command(_) => "{}",
1335                ArgumentSpec::Message(_) => "{/.*/}",
1336                ArgumentSpec::SubCommand => "{} {/.*/}",
1337                ArgumentSpec::Enum(_, _, _) => "{}",
1338                ArgumentSpec::AssetPath(_, _, _, _) => "{}",
1339                ArgumentSpec::Boolean(_, _, _) => "{}",
1340                ArgumentSpec::Flag(_) => "{}",
1341            })
1342            .collect::<Vec<_>>()
1343            .join(" ")
1344    }
1345}
1346
1347impl Display for ServerChatCommand {
1348    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
1349        write!(f, "{}", self.keyword())
1350    }
1351}
1352
1353impl FromStr for ServerChatCommand {
1354    type Err = ();
1355
1356    fn from_str(keyword: &str) -> Result<ServerChatCommand, ()> {
1357        Self::iter()
1358        // Go through all the shortcuts first
1359        .filter_map(|c| c.short_keyword().map(|s| (s, c)))
1360        .chain(Self::iter().map(|c| (c.keyword(), c)))
1361            // Find command with matching string as keyword
1362            .find_map(|(kwd, command)| (kwd == keyword).then_some(command))
1363            // Return error if not found
1364            .ok_or(())
1365    }
1366}
1367
1368#[derive(Eq, PartialEq, Debug, Clone, Copy)]
1369pub enum Requirement {
1370    Required,
1371    Optional,
1372}
1373
1374/// Representation for chat command arguments
1375pub enum ArgumentSpec {
1376    /// The argument refers to a player by alias
1377    PlayerName(Requirement),
1378    /// The arguments refers to an entity in some way.
1379    EntityTarget(Requirement),
1380    // The argument refers to a site, by name.
1381    SiteName(Requirement),
1382    /// The argument is a float. The associated values are
1383    /// * label
1384    /// * suggested tab-completion
1385    /// * whether it's optional
1386    Float(&'static str, f32, Requirement),
1387    /// The argument is an integer. The associated values are
1388    /// * label
1389    /// * suggested tab-completion
1390    /// * whether it's optional
1391    Integer(&'static str, i32, Requirement),
1392    /// The argument is any string that doesn't contain spaces
1393    Any(&'static str, Requirement),
1394    /// The argument is a command name (such as in /help)
1395    Command(Requirement),
1396    /// This is the final argument, consuming all characters until the end of
1397    /// input.
1398    Message(Requirement),
1399    /// This command is followed by another command (such as in /sudo)
1400    SubCommand,
1401    /// The argument is likely an enum. The associated values are
1402    /// * label
1403    /// * Predefined string completions
1404    /// * whether it's optional
1405    Enum(&'static str, Vec<String>, Requirement),
1406    /// The argument is an asset path. The associated values are
1407    /// * label
1408    /// * Path prefix shared by all assets
1409    /// * List of all asset paths as strings for completion
1410    /// * whether it's optional
1411    AssetPath(&'static str, &'static str, Vec<String>, Requirement),
1412    /// The argument is likely a boolean. The associated values are
1413    /// * label
1414    /// * suggested tab-completion
1415    /// * whether it's optional
1416    Boolean(&'static str, String, Requirement),
1417    /// The argument is a flag that enables or disables a feature.
1418    Flag(&'static str),
1419}
1420
1421impl ArgumentSpec {
1422    pub fn usage_string(&self) -> String {
1423        match self {
1424            ArgumentSpec::PlayerName(req) => {
1425                if &Requirement::Required == req {
1426                    "<player>".to_string()
1427                } else {
1428                    "[player]".to_string()
1429                }
1430            },
1431            ArgumentSpec::EntityTarget(req) => {
1432                if &Requirement::Required == req {
1433                    "<entity>".to_string()
1434                } else {
1435                    "[entity]".to_string()
1436                }
1437            },
1438            ArgumentSpec::SiteName(req) => {
1439                if &Requirement::Required == req {
1440                    "<site>".to_string()
1441                } else {
1442                    "[site]".to_string()
1443                }
1444            },
1445            ArgumentSpec::Float(label, _, req) => {
1446                if &Requirement::Required == req {
1447                    format!("<{}>", label)
1448                } else {
1449                    format!("[{}]", label)
1450                }
1451            },
1452            ArgumentSpec::Integer(label, _, req) => {
1453                if &Requirement::Required == req {
1454                    format!("<{}>", label)
1455                } else {
1456                    format!("[{}]", label)
1457                }
1458            },
1459            ArgumentSpec::Any(label, req) => {
1460                if &Requirement::Required == req {
1461                    format!("<{}>", label)
1462                } else {
1463                    format!("[{}]", label)
1464                }
1465            },
1466            ArgumentSpec::Command(req) => {
1467                if &Requirement::Required == req {
1468                    "<[/]command>".to_string()
1469                } else {
1470                    "[[/]command]".to_string()
1471                }
1472            },
1473            ArgumentSpec::Message(req) => {
1474                if &Requirement::Required == req {
1475                    "<message>".to_string()
1476                } else {
1477                    "[message]".to_string()
1478                }
1479            },
1480            ArgumentSpec::SubCommand => "<[/]command> [args...]".to_string(),
1481            ArgumentSpec::Enum(label, _, req) => {
1482                if &Requirement::Required == req {
1483                    format!("<{}>", label)
1484                } else {
1485                    format!("[{}]", label)
1486                }
1487            },
1488            ArgumentSpec::AssetPath(label, _, _, req) => {
1489                if &Requirement::Required == req {
1490                    format!("<{}>", label)
1491                } else {
1492                    format!("[{}]", label)
1493                }
1494            },
1495            ArgumentSpec::Boolean(label, _, req) => {
1496                if &Requirement::Required == req {
1497                    format!("<{}>", label)
1498                } else {
1499                    format!("[{}]", label)
1500                }
1501            },
1502            ArgumentSpec::Flag(label) => {
1503                format!("[{}]", label)
1504            },
1505        }
1506    }
1507
1508    pub fn requirement(&self) -> Requirement {
1509        match self {
1510            ArgumentSpec::PlayerName(r)
1511            | ArgumentSpec::EntityTarget(r)
1512            | ArgumentSpec::SiteName(r)
1513            | ArgumentSpec::Float(_, _, r)
1514            | ArgumentSpec::Integer(_, _, r)
1515            | ArgumentSpec::Any(_, r)
1516            | ArgumentSpec::Command(r)
1517            | ArgumentSpec::Message(r)
1518            | ArgumentSpec::Enum(_, _, r)
1519            | ArgumentSpec::AssetPath(_, _, _, r)
1520            | ArgumentSpec::Boolean(_, _, r) => *r,
1521            ArgumentSpec::Flag(_) => Requirement::Optional,
1522            ArgumentSpec::SubCommand => Requirement::Required,
1523        }
1524    }
1525}
1526
1527pub trait CommandEnumArg: FromStr {
1528    fn all_options() -> Vec<String>;
1529}
1530
1531macro_rules! impl_from_to_str_cmd {
1532    ($enum:ident, ($($attribute:ident => $str:expr),*)) => {
1533        impl std::str::FromStr for $enum {
1534            type Err = String;
1535
1536            fn from_str(s: &str) -> Result<Self, Self::Err> {
1537                match s {
1538                    $(
1539                        $str => Ok($enum::$attribute),
1540                    )*
1541                    s => Err(format!("Invalid variant: {s}")),
1542                }
1543            }
1544        }
1545
1546        impl $crate::cmd::CommandEnumArg for $enum {
1547            fn all_options() -> Vec<String> {
1548                vec![$($str.to_string()),*]
1549            }
1550        }
1551    }
1552}
1553
1554impl_from_to_str_cmd!(AuraKindVariant, (
1555    Buff => "buff",
1556    FriendlyFire => "friendly_fire",
1557    ForcePvP => "force_pvp"
1558));
1559
1560impl_from_to_str_cmd!(GroupTarget, (
1561    InGroup => "in_group",
1562    OutOfGroup => "out_of_group",
1563    All => "all"
1564));
1565
1566/// Parse a series of command arguments into values, including collecting all
1567/// trailing arguments.
1568#[macro_export]
1569macro_rules! parse_cmd_args {
1570    ($args:expr, $($t:ty),* $(, ..$tail:ty)? $(,)?) => {
1571        {
1572            let mut args = $args.into_iter().peekable();
1573            (
1574                // We only consume the input argument when parsing is successful. If this fails, we
1575                // will then attempt to parse it as the next argument type. This is done regardless
1576                // of whether the argument is optional because that information is not available
1577                // here. Nevertheless, if the caller only precedes to use the parsed arguments when
1578                // all required arguments parse successfully to `Some(val)` this should not create
1579                // any unexpected behavior.
1580                //
1581                // This does mean that optional arguments will be included in the trailing args or
1582                // that one optional arg could be interpreted as another, if the user makes a
1583                // mistake that causes an optional arg to fail to parse. But there is no way to
1584                // discern this in the current model with the optional args and trailing arg being
1585                // solely position based.
1586                $({
1587                    let parsed = args.peek().and_then(|s| s.parse::<$t>().ok());
1588                    // Consume successfully parsed arg.
1589                    if parsed.is_some() { args.next(); }
1590                    parsed
1591                }),*
1592                $(, args.map(|s| s.to_string()).collect::<$tail>())?
1593            )
1594        }
1595    };
1596}
1597
1598#[cfg(test)]
1599mod tests {
1600    use super::*;
1601    use crate::comp::Item;
1602
1603    #[test]
1604    fn verify_cmd_list_sorted() {
1605        let mut list = ServerChatCommand::iter()
1606            .map(|c| c.keyword())
1607            .collect::<Vec<_>>();
1608
1609        // Vec::is_sorted is unstable, so we do it the hard way
1610        let list2 = list.clone();
1611        list.sort_unstable();
1612        assert_eq!(list, list2);
1613    }
1614
1615    #[test]
1616    fn test_loading_skill_presets() {
1617        SkillPresetManifest::load_expect_combined_static(PRESET_MANIFEST_PATH);
1618    }
1619
1620    #[test]
1621    fn test_load_kits() {
1622        let kits = KitManifest::load_expect_combined_static(KIT_MANIFEST_PATH).read();
1623        let mut rng = rand::thread_rng();
1624        for kit in kits.0.values() {
1625            for (item_id, _) in kit.iter() {
1626                match item_id {
1627                    KitSpec::Item(item_id) => {
1628                        Item::new_from_asset_expect(item_id);
1629                    },
1630                    KitSpec::ModularWeaponSet {
1631                        tool,
1632                        material,
1633                        hands,
1634                    } => {
1635                        comp::item::modular::generate_weapons(*tool, *material, *hands)
1636                            .unwrap_or_else(|_| {
1637                                panic!(
1638                                    "Failed to synthesize a modular {tool:?} set made of \
1639                                     {material:?}."
1640                                )
1641                            });
1642                    },
1643                    KitSpec::ModularWeaponRandom {
1644                        tool,
1645                        material,
1646                        hands,
1647                    } => {
1648                        comp::item::modular::random_weapon(*tool, *material, *hands, &mut rng)
1649                            .unwrap_or_else(|_| {
1650                                panic!(
1651                                    "Failed to synthesize a random {hands:?}-handed modular \
1652                                     {tool:?} made of {material:?}."
1653                                )
1654                            });
1655                    },
1656                }
1657            }
1658        }
1659    }
1660}