veloren_common/
cmd.rs

1use crate::{
2    assets::{AssetCombined, Ron},
3    combat::GroupTarget,
4    comp::{
5        self, AdminRole as Role, Skill, aura::AuraKindVariant, buff::BuffKind,
6        inventory::item::try_all_item_defs,
7    },
8    generation::try_all_entity_configs,
9    npc, outcome,
10    recipe::RecipeBookManifest,
11    spot::Spot,
12    terrain,
13    uid::Uid,
14};
15use common_i18n::Content;
16use hashbrown::{HashMap, HashSet};
17use lazy_static::lazy_static;
18use serde::{Deserialize, Serialize};
19use std::{
20    fmt::{self, Display},
21    num::NonZeroU64,
22    str::FromStr,
23};
24use strum::{AsRefStr, EnumIter, EnumString, IntoEnumIterator, VariantNames};
25use tracing::warn;
26
27/// Struct representing a command that a user can run from server chat.
28pub struct ChatCommandData {
29    /// A list of arguments useful for both tab completion and parsing
30    pub args: Vec<ArgumentSpec>,
31    /// The i18n content for the description of the command
32    pub description: Content,
33    /// Whether the command requires administrator permissions.
34    pub needs_role: Option<Role>,
35}
36
37impl ChatCommandData {
38    pub fn new(args: Vec<ArgumentSpec>, description: Content, needs_role: Option<Role>) -> Self {
39        Self {
40            args,
41            description,
42            needs_role,
43        }
44    }
45}
46
47#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
48pub enum KitSpec {
49    Item(String),
50    ModularWeaponSet {
51        tool: comp::tool::ToolKind,
52        material: comp::item::Material,
53        hands: Option<comp::item::tool::Hands>,
54    },
55    ModularWeaponRandom {
56        tool: comp::tool::ToolKind,
57        material: comp::item::Material,
58        hands: Option<comp::item::tool::Hands>,
59    },
60}
61
62pub type KitManifest = Ron<HashMap<String, Vec<(KitSpec, u32)>>>;
63
64pub type SkillPresetManifest = Ron<HashMap<String, Vec<(Skill, u8)>>>;
65
66pub const KIT_MANIFEST_PATH: &str = "server.manifests.kits";
67pub const PRESET_MANIFEST_PATH: &str = "server.manifests.presets";
68
69/// Enum for all possible area types
70#[derive(Debug, Clone, EnumIter, EnumString, AsRefStr)]
71pub enum AreaKind {
72    #[strum(serialize = "build")]
73    Build,
74    #[strum(serialize = "no_durability")]
75    NoDurability,
76}
77
78lazy_static! {
79    static ref ALIGNMENTS: Vec<String> = ["wild", "enemy", "npc", "pet"]
80        .iter()
81        .map(|s| s.to_string())
82        .collect();
83    static ref SKILL_TREES: Vec<String> = ["general", "sword", "axe", "hammer", "bow", "staff", "sceptre", "mining"]
84        .iter()
85        .map(|s| s.to_string())
86        .collect();
87    /// TODO: Make this use hot-reloading
88    pub static ref ENTITIES: Vec<String> = {
89        let npc_names = &npc::NPC_NAMES.read();
90
91        // HashSets for deduplication of male, female, etc
92        let mut categories = HashSet::new();
93        let mut species = HashSet::new();
94        for body in comp::Body::iter() {
95            // plugin doesn't seem to be spawnable, yet
96            if matches!(body, comp::Body::Plugin(_)) {
97                continue;
98            }
99
100            if let Some(meta) = npc_names.get_species_meta(&body) {
101                categories.insert(npc_names[&body].keyword.clone());
102                species.insert(meta.keyword.clone());
103            }
104        }
105
106        let mut strings = Vec::new();
107        strings.extend(categories);
108        strings.extend(species);
109
110        strings
111    };
112    static ref AREA_KINDS: Vec<String> = AreaKind::iter().map(|kind| kind.as_ref().to_string()).collect();
113    static ref OBJECTS: Vec<String> = comp::object::ALL_OBJECTS
114        .iter()
115        .map(|o| o.to_string().to_string())
116        .collect();
117    static ref RECIPES: Vec<String> = {
118        let rbm = RecipeBookManifest::load().cloned();
119        rbm.keys().cloned().collect::<Vec<String>>()
120    };
121    static ref TIMES: Vec<String> = [
122        "midnight", "night", "dawn", "morning", "day", "noon", "dusk"
123    ]
124    .iter()
125    .map(|s| s.to_string())
126    .collect();
127
128    static ref WEATHERS: Vec<String> = [
129        "clear", "cloudy", "rain", "wind", "storm"
130    ]
131    .iter()
132    .map(|s| s.to_string())
133    .collect();
134
135    pub static ref BUFF_PARSER: HashMap<String, BuffKind> = {
136        let string_from_buff = |kind| match kind {
137            BuffKind::Burning => "burning",
138            BuffKind::Regeneration => "regeneration",
139            BuffKind::Saturation => "saturation",
140            BuffKind::Bleeding => "bleeding",
141            BuffKind::Cursed => "cursed",
142            BuffKind::Potion => "potion",
143            BuffKind::Agility => "agility",
144            BuffKind::RestingHeal => "resting_heal",
145            BuffKind::EnergyRegen => "energy_regen",
146            BuffKind::ComboGeneration => "combo_generation",
147            BuffKind::IncreaseMaxEnergy => "increase_max_energy",
148            BuffKind::IncreaseMaxHealth => "increase_max_health",
149            BuffKind::Invulnerability => "invulnerability",
150            BuffKind::ProtectingWard => "protecting_ward",
151            BuffKind::Frenzied => "frenzied",
152            BuffKind::Crippled => "crippled",
153            BuffKind::Frozen => "frozen",
154            BuffKind::Wet => "wet",
155            BuffKind::Ensnared => "ensnared",
156            BuffKind::Poisoned => "poisoned",
157            BuffKind::Hastened => "hastened",
158            BuffKind::Fortitude => "fortitude",
159            BuffKind::Parried => "parried",
160            BuffKind::PotionSickness => "potion_sickness",
161            BuffKind::Reckless => "reckless",
162            BuffKind::Polymorphed => "polymorphed",
163            BuffKind::Flame => "flame",
164            BuffKind::Frigid => "frigid",
165            BuffKind::Lifesteal => "lifesteal",
166            // BuffKind::SalamanderAspect => "salamander_aspect",
167            BuffKind::ImminentCritical => "imminent_critical",
168            BuffKind::Fury => "fury",
169            BuffKind::Sunderer => "sunderer",
170            BuffKind::Defiance => "defiance",
171            BuffKind::Bloodfeast => "bloodfeast",
172            BuffKind::Berserk => "berserk",
173            BuffKind::Heatstroke => "heatstroke",
174            BuffKind::ScornfulTaunt => "scornful_taunt",
175            BuffKind::Rooted => "rooted",
176            BuffKind::Winded => "winded",
177            BuffKind::Amnesia => "amnesia",
178            BuffKind::OffBalance => "off_balance",
179            BuffKind::Tenacity => "tenacity",
180            BuffKind::Resilience => "resilience",
181        };
182        let mut buff_parser = HashMap::new();
183        for kind in BuffKind::iter() {
184            buff_parser.insert(string_from_buff(kind).to_string(), kind);
185        }
186        buff_parser
187    };
188
189    pub static ref BUFF_PACK: Vec<String> = {
190        let mut buff_pack: Vec<_> = BUFF_PARSER.keys().cloned().collect();
191        // Remove invulnerability as it removes debuffs
192        buff_pack.retain(|kind| kind != "invulnerability");
193        buff_pack
194    };
195
196    static ref BUFFS: Vec<String> = {
197        let mut buff_pack: Vec<String> = BUFF_PARSER.keys().cloned().collect();
198
199        // Add `all` and `clear` as valid command
200        buff_pack.push("all".to_owned());
201        buff_pack.push("clear".to_owned());
202        buff_pack
203    };
204
205    static ref BLOCK_KINDS: Vec<String> = terrain::block::BlockKind::iter()
206        .map(|bk| bk.to_string())
207        .collect();
208
209    static ref SPRITE_KINDS: Vec<String> = terrain::sprite::SPRITE_KINDS
210        .keys()
211        .cloned()
212        .collect();
213
214    static ref OUTCOME_KINDS: Vec<String> = outcome::Outcome::VARIANTS
215        .iter()
216        .map(|s| s.to_string())
217        .collect();
218
219    static ref ROLES: Vec<String> = ["admin", "moderator"].iter().copied().map(Into::into).collect();
220
221    /// List of item's asset specifiers. Useful for tab completing.
222    /// Doesn't cover all items (like modulars), includes "fake" items like
223    /// TagExamples.
224    pub static ref ITEM_SPECS: Vec<String> = {
225        let mut items = try_all_item_defs()
226            .unwrap_or_else(|e| {
227                warn!(?e, "Failed to load item specifiers");
228                Vec::new()
229            });
230        items.sort();
231        items
232    };
233
234    /// List of all entity configs. Useful for tab completing
235    pub static ref ENTITY_CONFIGS: Vec<String> = {
236        try_all_entity_configs()
237            .unwrap_or_else(|e| {
238                warn!(?e, "Failed to load entity configs");
239                Vec::new()
240            })
241    };
242
243    pub static ref KITS: Vec<String> = {
244        let mut kits = if let Ok(kits) = KitManifest::load_and_combine_static(KIT_MANIFEST_PATH) {
245            let mut kits = kits.read().0.keys().cloned().collect::<Vec<String>>();
246            kits.sort();
247            kits
248        } else {
249            Vec::new()
250        };
251        kits.push("all".to_owned());
252
253        kits
254    };
255
256    static ref PRESETS: HashMap<String, Vec<(Skill, u8)>> = {
257        if let Ok(presets) = SkillPresetManifest::load_and_combine_static(PRESET_MANIFEST_PATH) {
258            presets.read().0.clone()
259        } else {
260            warn!("Error while loading presets");
261            HashMap::new()
262        }
263    };
264
265    static ref PRESET_LIST: Vec<String> = {
266        let mut preset_list: Vec<String> = PRESETS.keys().cloned().collect();
267        preset_list.push("clear".to_owned());
268
269        preset_list
270    };
271
272    /// Map from string to a Spot's kind (except RonFile)
273    pub static ref SPOT_PARSER: HashMap<String, Spot> = {
274        let spot_to_string = |kind| match kind {
275            Spot::DwarvenGrave => "dwarven_grave",
276            Spot::SaurokAltar => "saurok_altar",
277            Spot::MyrmidonTemple => "myrmidon_temple",
278            Spot::GnarlingTotem => "gnarling_totem",
279            Spot::WitchHouse => "witch_house",
280            Spot::GnomeSpring => "gnome_spring",
281            Spot::WolfBurrow => "wolf_burrow",
282            Spot::Igloo => "igloo",
283            Spot::LionRock => "lion_rock",
284            Spot::TreeStumpForest => "tree_stump_forest",
285            Spot::DesertBones => "desert_bones",
286            Spot::Arch => "arch",
287            Spot::AirshipCrash => "airship_crash",
288            Spot::FruitTree => "fruit_tree",
289            Spot::Shipwreck => "shipwreck",
290            Spot::Shipwreck2 => "shipwreck2",
291            Spot::FallenTree => "fallen_tree",
292            Spot::GraveSmall => "grave_small",
293            Spot::JungleTemple => "jungle_temple",
294            Spot::SaurokTotem => "saurok_totem",
295            Spot::JungleOutpost => "jungle_outpost",
296            // unused here, but left for completeness
297            Spot::RonFile(props) => &props.base_structures,
298        };
299
300        let mut map = HashMap::new();
301        for spot_kind in Spot::iter() {
302            map.insert(spot_to_string(spot_kind).to_owned(), spot_kind);
303        }
304
305        map
306    };
307
308    pub static ref SPOTS: Vec<String> = {
309        let mut config_spots = crate::spot::RON_SPOT_PROPERTIES
310            .0
311            .iter()
312            .map(|s| s.base_structures.clone())
313            .collect::<Vec<_>>();
314
315        config_spots.extend(SPOT_PARSER.keys().cloned());
316        config_spots
317    };
318}
319
320pub enum EntityTarget {
321    Player(String),
322    RtsimNpc(u64),
323    Uid(crate::uid::Uid),
324}
325
326impl FromStr for EntityTarget {
327    type Err = String;
328
329    fn from_str(s: &str) -> Result<Self, Self::Err> {
330        // NOTE: `@` is an invalid character in usernames, so we can use it here.
331        if let Some((spec, data)) = s.split_once('@') {
332            match spec {
333                "rtsim" => Ok(EntityTarget::RtsimNpc(u64::from_str(data).map_err(
334                    |_| format!("Expected a valid number after 'rtsim@' but found {data}."),
335                )?)),
336                "uid" => {
337                    let raw = u64::from_str(data).map_err(|_| {
338                        format!("Expected a valid number after 'uid@' but found {data}.")
339                    })?;
340                    let nz =
341                        NonZeroU64::new(raw).ok_or_else(|| "Uid cannot be zero".to_string())?;
342                    Ok(EntityTarget::Uid(Uid(nz)))
343                },
344                _ => Err(format!(
345                    "Expected either 'rtsim' or 'uid' before '@' but found '{spec}'"
346                )),
347            }
348        } else {
349            Ok(EntityTarget::Player(s.to_string()))
350        }
351    }
352}
353
354// Please keep this sorted alphabetically :-)
355#[derive(Copy, Clone, strum::EnumIter)]
356pub enum ServerChatCommand {
357    Adminify,
358    Airship,
359    Alias,
360    AreaAdd,
361    AreaList,
362    AreaRemove,
363    Aura,
364    Ban,
365    BanIp,
366    BanLog,
367    BattleMode,
368    BattleModeForce,
369    Body,
370    Buff,
371    Build,
372    Campfire,
373    ClearPersistedTerrain,
374    CreateLocation,
375    DeathEffect,
376    DebugColumn,
377    DebugWays,
378    DeleteLocation,
379    DestroyTethers,
380    DisconnectAllPlayers,
381    Dismount,
382    DropAll,
383    Dummy,
384    Explosion,
385    Faction,
386    GiveItem,
387    Gizmos,
388    GizmosRange,
389    Goto,
390    GotoRand,
391    Group,
392    GroupInvite,
393    GroupKick,
394    GroupLeave,
395    GroupPromote,
396    Health,
397    IntoNpc,
398    JoinFaction,
399    Jump,
400    Kick,
401    Kill,
402    KillNpcs,
403    Kit,
404    Lantern,
405    Light,
406    Lightning,
407    Location,
408    MakeBlock,
409    MakeNpc,
410    MakeSprite,
411    MakeVolume,
412    Motd,
413    Mount,
414    Object,
415    Outcome,
416    PermitBuild,
417    Players,
418    Poise,
419    Portal,
420    Region,
421    ReloadChunks,
422    RemoveLights,
423    RepairEquipment,
424    ResetRecipes,
425    Respawn,
426    RevokeBuild,
427    RevokeBuildAll,
428    RtsimChunk,
429    RtsimInfo,
430    RtsimNpc,
431    RtsimPurge,
432    RtsimTp,
433    Safezone,
434    Say,
435    Scale,
436    ServerPhysics,
437    SetBodyType,
438    SetMotd,
439    SetWaypoint,
440    Ship,
441    Site,
442    SkillPoint,
443    SkillPreset,
444    Spawn,
445    Spot,
446    Sudo,
447    Tell,
448    Tether,
449    Time,
450    TimeScale,
451    Tp,
452    Unban,
453    UnbanIp,
454    Version,
455    WeatherZone,
456    Whitelist,
457    Wiring,
458    World,
459}
460
461impl ServerChatCommand {
462    pub fn data(&self) -> ChatCommandData {
463        use ArgumentSpec::*;
464        use Requirement::*;
465        use Role::*;
466        let cmd = ChatCommandData::new;
467        match self {
468            ServerChatCommand::Adminify => cmd(
469                vec![PlayerName(Required), Enum("role", ROLES.clone(), Optional)],
470                Content::localized("command-adminify-desc"),
471                Some(Admin),
472            ),
473            ServerChatCommand::Airship => cmd(
474                vec![
475                    Enum(
476                        "kind",
477                        comp::ship::ALL_AIRSHIPS
478                            .iter()
479                            .map(|b| format!("{b:?}"))
480                            .collect(),
481                        Optional,
482                    ),
483                    Float("destination_degrees_ccw_of_east", 90.0, Optional),
484                ],
485                Content::localized("command-airship-desc"),
486                Some(Admin),
487            ),
488            ServerChatCommand::Alias => cmd(
489                vec![Any("name", Required)],
490                Content::localized("command-alias-desc"),
491                Some(Moderator),
492            ),
493            ServerChatCommand::Aura => cmd(
494                vec![
495                    Float("aura_radius", 10.0, Required),
496                    Float("aura_duration", 10.0, Optional),
497                    Boolean("new_entity", "true".to_string(), Optional),
498                    Enum("aura_target", GroupTarget::all_options(), Optional),
499                    Enum("aura_kind", AuraKindVariant::all_options(), Required),
500                    Any("aura spec", Optional),
501                ],
502                Content::localized("command-aura-desc"),
503                Some(Admin),
504            ),
505            ServerChatCommand::Buff => cmd(
506                vec![
507                    Enum("buff", BUFFS.clone(), Required),
508                    Float("strength", 0.01, Optional),
509                    Float("duration", 10.0, Optional),
510                    Any("buff data spec", Optional),
511                ],
512                Content::localized("command-buff-desc"),
513                Some(Admin),
514            ),
515            ServerChatCommand::Ban => cmd(
516                vec![
517                    PlayerName(Required),
518                    Boolean("overwrite", "true".to_string(), Optional),
519                    Any("ban duration", Optional),
520                    Message(Optional),
521                ],
522                Content::localized("command-ban-desc"),
523                Some(Moderator),
524            ),
525            ServerChatCommand::BanIp => cmd(
526                vec![
527                    PlayerName(Required),
528                    Boolean("overwrite", "true".to_string(), Optional),
529                    Any("ban duration", Optional),
530                    Message(Optional),
531                ],
532                Content::localized("command-ban-ip-desc"),
533                Some(Moderator),
534            ),
535            ServerChatCommand::BanLog => cmd(
536                vec![PlayerName(Required), Integer("max entries", 10, Optional)],
537                Content::localized("command-ban-ip-desc"),
538                Some(Moderator),
539            ),
540            #[rustfmt::skip]
541            ServerChatCommand::BattleMode => cmd(
542                vec![Enum(
543                    "battle mode",
544                    vec!["pvp".to_owned(), "pve".to_owned()],
545                    Optional,
546                )],
547                Content::localized("command-battlemode-desc"),
548                None,
549
550            ),
551            ServerChatCommand::IntoNpc => cmd(
552                vec![AssetPath(
553                    "entity_config",
554                    "common.entity.",
555                    ENTITY_CONFIGS.clone(),
556                    Required,
557                )],
558                Content::localized("command-into_npc-desc"),
559                Some(Admin),
560            ),
561            ServerChatCommand::Body => cmd(
562                vec![Enum("body", ENTITIES.clone(), Required)],
563                Content::localized("command-body-desc"),
564                Some(Admin),
565            ),
566            ServerChatCommand::BattleModeForce => cmd(
567                vec![Enum(
568                    "battle mode",
569                    vec!["pvp".to_owned(), "pve".to_owned()],
570                    Required,
571                )],
572                Content::localized("command-battlemode_force-desc"),
573                Some(Admin),
574            ),
575            ServerChatCommand::Build => cmd(vec![], Content::localized("command-build-desc"), None),
576            ServerChatCommand::AreaAdd => cmd(
577                vec![
578                    Any("name", Required),
579                    Enum("kind", AREA_KINDS.clone(), Required),
580                    Integer("xlo", 0, Required),
581                    Integer("xhi", 10, Required),
582                    Integer("ylo", 0, Required),
583                    Integer("yhi", 10, Required),
584                    Integer("zlo", 0, Required),
585                    Integer("zhi", 10, Required),
586                ],
587                Content::localized("command-area_add-desc"),
588                Some(Admin),
589            ),
590            ServerChatCommand::AreaList => cmd(
591                vec![],
592                Content::localized("command-area_list-desc"),
593                Some(Admin),
594            ),
595            ServerChatCommand::AreaRemove => cmd(
596                vec![
597                    Any("name", Required),
598                    Enum("kind", AREA_KINDS.clone(), Required),
599                ],
600                Content::localized("command-area_remove-desc"),
601                Some(Admin),
602            ),
603            ServerChatCommand::Campfire => cmd(
604                vec![],
605                Content::localized("command-campfire-desc"),
606                Some(Admin),
607            ),
608            ServerChatCommand::ClearPersistedTerrain => cmd(
609                vec![Integer("chunk_radius", 6, Required)],
610                Content::localized("command-clear_persisted_terrain-desc"),
611                Some(Admin),
612            ),
613            ServerChatCommand::DeathEffect => cmd(
614                vec![
615                    Enum("death_effect", vec!["transform".to_string()], Required),
616                    // NOTE: I added this for QoL as transform is currently the only death effect
617                    // and takes an asset path, when more on-death effects are added to the command
618                    // remove this.
619                    AssetPath(
620                        "entity_config",
621                        "common.entity.",
622                        ENTITY_CONFIGS.clone(),
623                        Required,
624                    ),
625                ],
626                Content::localized("command-death_effect-dest"),
627                Some(Admin),
628            ),
629            ServerChatCommand::DebugColumn => cmd(
630                vec![Integer("x", 15000, Required), Integer("y", 15000, Required)],
631                Content::localized("command-debug_column-desc"),
632                Some(Admin),
633            ),
634            ServerChatCommand::DebugWays => cmd(
635                vec![Integer("x", 15000, Required), Integer("y", 15000, Required)],
636                Content::localized("command-debug_ways-desc"),
637                Some(Admin),
638            ),
639            ServerChatCommand::DisconnectAllPlayers => cmd(
640                vec![Any("confirm", Required)],
641                Content::localized("command-disconnect_all_players-desc"),
642                Some(Admin),
643            ),
644            ServerChatCommand::DropAll => cmd(
645                vec![],
646                Content::localized("command-dropall-desc"),
647                Some(Moderator),
648            ),
649            ServerChatCommand::Dummy => cmd(
650                vec![],
651                Content::localized("command-dummy-desc"),
652                Some(Admin),
653            ),
654            ServerChatCommand::Explosion => cmd(
655                vec![Float("radius", 5.0, Required)],
656                Content::localized("command-explosion-desc"),
657                Some(Admin),
658            ),
659            ServerChatCommand::Faction => cmd(
660                vec![Message(Optional)],
661                Content::localized("command-faction-desc"),
662                None,
663            ),
664            ServerChatCommand::GiveItem => cmd(
665                vec![
666                    AssetPath("item", "common.items.", ITEM_SPECS.clone(), Required),
667                    Integer("num", 1, Optional),
668                ],
669                Content::localized("command-give_item-desc"),
670                Some(Admin),
671            ),
672            ServerChatCommand::Gizmos => cmd(
673                vec![
674                    Enum(
675                        "kind",
676                        ["All".to_string(), "None".to_string()]
677                            .into_iter()
678                            .chain(
679                                comp::gizmos::GizmoSubscription::iter()
680                                    .map(|kind| kind.to_string()),
681                            )
682                            .collect(),
683                        Required,
684                    ),
685                    EntityTarget(Optional),
686                ],
687                Content::localized("command-gizmos-desc"),
688                Some(Admin),
689            ),
690            ServerChatCommand::GizmosRange => cmd(
691                vec![Float("range", 32.0, Required)],
692                Content::localized("command-gizmos_range-desc"),
693                Some(Admin),
694            ),
695            ServerChatCommand::Goto => cmd(
696                vec![
697                    Float("x", 0.0, Required),
698                    Float("y", 0.0, Required),
699                    Float("z", 0.0, Required),
700                    Boolean("Dismount from ship", "true".to_string(), Optional),
701                ],
702                Content::localized("command-goto-desc"),
703                Some(Admin),
704            ),
705            ServerChatCommand::GotoRand => cmd(
706                vec![Boolean("Dismount from ship", "true".to_string(), Optional)],
707                Content::localized("command-goto-rand"),
708                Some(Admin),
709            ),
710            ServerChatCommand::Group => cmd(
711                vec![Message(Optional)],
712                Content::localized("command-group-desc"),
713                None,
714            ),
715            ServerChatCommand::GroupInvite => cmd(
716                vec![PlayerName(Required)],
717                Content::localized("command-group_invite-desc"),
718                None,
719            ),
720            ServerChatCommand::GroupKick => cmd(
721                vec![PlayerName(Required)],
722                Content::localized("command-group_kick-desc"),
723                None,
724            ),
725            ServerChatCommand::GroupLeave => {
726                cmd(vec![], Content::localized("command-group_leave-desc"), None)
727            },
728            ServerChatCommand::GroupPromote => cmd(
729                vec![PlayerName(Required)],
730                Content::localized("command-group_promote-desc"),
731                None,
732            ),
733            ServerChatCommand::Health => cmd(
734                vec![Integer("hp", 100, Required)],
735                Content::localized("command-health-desc"),
736                Some(Admin),
737            ),
738            ServerChatCommand::Respawn => cmd(
739                vec![],
740                Content::localized("command-respawn-desc"),
741                Some(Moderator),
742            ),
743            ServerChatCommand::JoinFaction => cmd(
744                vec![Any("faction", Optional)],
745                Content::localized("command-join_faction-desc"),
746                None,
747            ),
748            ServerChatCommand::Jump => cmd(
749                vec![
750                    Float("x", 0.0, Required),
751                    Float("y", 0.0, Required),
752                    Float("z", 0.0, Required),
753                    Boolean("Dismount from ship", "true".to_string(), Optional),
754                ],
755                Content::localized("command-jump-desc"),
756                Some(Admin),
757            ),
758            ServerChatCommand::Kick => cmd(
759                vec![PlayerName(Required), Message(Optional)],
760                Content::localized("command-kick-desc"),
761                Some(Moderator),
762            ),
763            ServerChatCommand::Kill => cmd(vec![], Content::localized("command-kill-desc"), None),
764            ServerChatCommand::KillNpcs => cmd(
765                vec![Float("radius", 100.0, Optional), Flag("--also-pets")],
766                Content::localized("command-kill_npcs-desc"),
767                Some(Admin),
768            ),
769            ServerChatCommand::Kit => cmd(
770                vec![Enum("kit_name", KITS.to_vec(), Required)],
771                Content::localized("command-kit-desc"),
772                Some(Admin),
773            ),
774            ServerChatCommand::Lantern => cmd(
775                vec![
776                    Float("strength", 5.0, Required),
777                    Float("r", 1.0, Optional),
778                    Float("g", 1.0, Optional),
779                    Float("b", 1.0, Optional),
780                ],
781                Content::localized("command-lantern-desc"),
782                Some(Admin),
783            ),
784            ServerChatCommand::Light => cmd(
785                vec![
786                    Float("r", 1.0, Optional),
787                    Float("g", 1.0, Optional),
788                    Float("b", 1.0, Optional),
789                    Float("x", 0.0, Optional),
790                    Float("y", 0.0, Optional),
791                    Float("z", 0.0, Optional),
792                    Float("strength", 5.0, Optional),
793                ],
794                Content::localized("command-light-desc"),
795                Some(Admin),
796            ),
797            ServerChatCommand::MakeBlock => cmd(
798                vec![
799                    Enum("block", BLOCK_KINDS.clone(), Required),
800                    Integer("r", 255, Optional),
801                    Integer("g", 255, Optional),
802                    Integer("b", 255, Optional),
803                ],
804                Content::localized("command-make_block-desc"),
805                Some(Admin),
806            ),
807            ServerChatCommand::MakeNpc => cmd(
808                vec![
809                    AssetPath(
810                        "entity_config",
811                        "common.entity.",
812                        ENTITY_CONFIGS.clone(),
813                        Required,
814                    ),
815                    Integer("num", 1, Optional),
816                ],
817                Content::localized("command-make_npc-desc"),
818                Some(Admin),
819            ),
820            ServerChatCommand::MakeSprite => cmd(
821                vec![Enum("sprite", SPRITE_KINDS.clone(), Required)],
822                Content::localized("command-make_sprite-desc"),
823                Some(Admin),
824            ),
825            ServerChatCommand::Motd => cmd(vec![], Content::localized("command-motd-desc"), None),
826            ServerChatCommand::Object => cmd(
827                vec![Enum("object", OBJECTS.clone(), Required)],
828                Content::localized("command-object-desc"),
829                Some(Admin),
830            ),
831            ServerChatCommand::Outcome => cmd(
832                vec![Enum("outcome", OUTCOME_KINDS.clone(), Required)],
833                Content::localized("command-outcome-desc"),
834                Some(Admin),
835            ),
836            ServerChatCommand::PermitBuild => cmd(
837                vec![Any("area_name", Required)],
838                Content::localized("command-permit_build-desc"),
839                Some(Admin),
840            ),
841            ServerChatCommand::Players => {
842                cmd(vec![], Content::localized("command-players-desc"), None)
843            },
844            ServerChatCommand::Poise => cmd(
845                vec![Integer("poise", 100, Required)],
846                Content::localized("command-poise-desc"),
847                Some(Admin),
848            ),
849            ServerChatCommand::Portal => cmd(
850                vec![
851                    Float("x", 0., Required),
852                    Float("y", 0., Required),
853                    Float("z", 0., Required),
854                    Boolean("requires_no_aggro", "true".to_string(), Optional),
855                    Float("buildup_time", 5., Optional),
856                ],
857                Content::localized("command-portal-desc"),
858                Some(Admin),
859            ),
860            ServerChatCommand::ReloadChunks => cmd(
861                vec![Integer("chunk_radius", 6, Optional)],
862                Content::localized("command-reload_chunks-desc"),
863                Some(Admin),
864            ),
865            ServerChatCommand::ResetRecipes => cmd(
866                vec![],
867                Content::localized("command-reset_recipes-desc"),
868                Some(Admin),
869            ),
870            ServerChatCommand::RemoveLights => cmd(
871                vec![Float("radius", 20.0, Optional)],
872                Content::localized("command-remove_lights-desc"),
873                Some(Admin),
874            ),
875            ServerChatCommand::RevokeBuild => cmd(
876                vec![Any("area_name", Required)],
877                Content::localized("command-revoke_build-desc"),
878                Some(Admin),
879            ),
880            ServerChatCommand::RevokeBuildAll => cmd(
881                vec![],
882                Content::localized("command-revoke_build_all-desc"),
883                Some(Admin),
884            ),
885            ServerChatCommand::Region => cmd(
886                vec![Message(Optional)],
887                Content::localized("command-region-desc"),
888                None,
889            ),
890            ServerChatCommand::Safezone => cmd(
891                vec![Float("range", 100.0, Optional)],
892                Content::localized("command-safezone-desc"),
893                Some(Moderator),
894            ),
895            ServerChatCommand::Say => cmd(
896                vec![Message(Optional)],
897                Content::localized("command-say-desc"),
898                None,
899            ),
900            ServerChatCommand::ServerPhysics => cmd(
901                vec![
902                    PlayerName(Required),
903                    Boolean("enabled", "true".to_string(), Optional),
904                    Message(Optional),
905                ],
906                Content::localized("command-server_physics-desc"),
907                Some(Moderator),
908            ),
909            ServerChatCommand::SetMotd => cmd(
910                vec![Any("locale", Optional), Message(Optional)],
911                Content::localized("command-set_motd-desc"),
912                Some(Admin),
913            ),
914            ServerChatCommand::SetBodyType => cmd(
915                vec![
916                    Enum(
917                        "body type",
918                        vec!["Female".to_string(), "Male".to_string()],
919                        Required,
920                    ),
921                    Boolean("permanent", "false".to_string(), Requirement::Optional),
922                ],
923                Content::localized("command-set_body_type-desc"),
924                Some(Admin),
925            ),
926            ServerChatCommand::Ship => cmd(
927                vec![
928                    Enum(
929                        "kind",
930                        comp::ship::ALL_SHIPS
931                            .iter()
932                            .map(|b| format!("{b:?}"))
933                            .collect(),
934                        Optional,
935                    ),
936                    Boolean(
937                        "Whether the ship should be tethered to the target (or its mount)",
938                        "false".to_string(),
939                        Optional,
940                    ),
941                    Float("destination_degrees_ccw_of_east", 90.0, Optional),
942                ],
943                Content::localized("command-ship-desc"),
944                Some(Admin),
945            ),
946            // Uses Message because site names can contain spaces,
947            // which would be assumed to be separators otherwise
948            ServerChatCommand::Site => cmd(
949                vec![
950                    SiteName(Required),
951                    Boolean("Dismount from ship", "true".to_string(), Optional),
952                ],
953                Content::localized("command-site-desc"),
954                Some(Moderator),
955            ),
956            ServerChatCommand::SkillPoint => cmd(
957                vec![
958                    Enum("skill tree", SKILL_TREES.clone(), Required),
959                    Integer("amount", 1, Optional),
960                ],
961                Content::localized("command-skill_point-desc"),
962                Some(Admin),
963            ),
964            ServerChatCommand::SkillPreset => cmd(
965                vec![Enum("preset_name", PRESET_LIST.to_vec(), Required)],
966                Content::localized("command-skill_preset-desc"),
967                Some(Admin),
968            ),
969            ServerChatCommand::Spawn => cmd(
970                vec![
971                    Enum("alignment", ALIGNMENTS.clone(), Required),
972                    Enum("entity", ENTITIES.clone(), Required),
973                    Integer("amount", 1, Optional),
974                    Boolean("ai", "true".to_string(), Optional),
975                    Float("scale", 1.0, Optional),
976                    Boolean("tethered", "false".to_string(), Optional),
977                ],
978                Content::localized("command-spawn-desc"),
979                Some(Admin),
980            ),
981            ServerChatCommand::Spot => cmd(
982                vec![Enum("Spot kind to find", SPOTS.clone(), Required)],
983                Content::localized("command-spot-desc"),
984                Some(Admin),
985            ),
986            ServerChatCommand::Sudo => cmd(
987                vec![EntityTarget(Required), SubCommand],
988                Content::localized("command-sudo-desc"),
989                Some(Moderator),
990            ),
991            ServerChatCommand::Tell => cmd(
992                vec![PlayerName(Required), Message(Optional)],
993                Content::localized("command-tell-desc"),
994                None,
995            ),
996            ServerChatCommand::Time => cmd(
997                vec![Enum("time", TIMES.clone(), Optional)],
998                Content::localized("command-time-desc"),
999                Some(Admin),
1000            ),
1001            ServerChatCommand::TimeScale => cmd(
1002                vec![Float("time scale", 1.0, Optional)],
1003                Content::localized("command-time_scale-desc"),
1004                Some(Admin),
1005            ),
1006            ServerChatCommand::Tp => cmd(
1007                vec![
1008                    EntityTarget(Optional),
1009                    Boolean("Dismount from ship", "true".to_string(), Optional),
1010                ],
1011                Content::localized("command-tp-desc"),
1012                Some(Moderator),
1013            ),
1014            ServerChatCommand::RtsimTp => cmd(
1015                vec![
1016                    Integer("npc index", 0, Required),
1017                    Boolean("Dismount from ship", "true".to_string(), Optional),
1018                ],
1019                Content::localized("command-rtsim_tp-desc"),
1020                Some(Admin),
1021            ),
1022            ServerChatCommand::RtsimInfo => cmd(
1023                vec![Integer("npc index", 0, Required)],
1024                Content::localized("command-rtsim_info-desc"),
1025                Some(Admin),
1026            ),
1027            ServerChatCommand::RtsimNpc => cmd(
1028                vec![Any("query", Required), Integer("max number", 20, Optional)],
1029                Content::localized("command-rtsim_npc-desc"),
1030                Some(Admin),
1031            ),
1032            ServerChatCommand::RtsimPurge => cmd(
1033                vec![Boolean(
1034                    "whether purging of rtsim data should occur on next startup",
1035                    true.to_string(),
1036                    Required,
1037                )],
1038                Content::localized("command-rtsim_purge-desc"),
1039                Some(Admin),
1040            ),
1041            ServerChatCommand::RtsimChunk => cmd(
1042                vec![],
1043                Content::localized("command-rtsim_chunk-desc"),
1044                Some(Admin),
1045            ),
1046            ServerChatCommand::Unban => cmd(
1047                vec![PlayerName(Required)],
1048                Content::localized("command-unban-desc"),
1049                Some(Moderator),
1050            ),
1051            ServerChatCommand::UnbanIp => cmd(
1052                vec![PlayerName(Required)],
1053                Content::localized("command-unban-ip-desc"),
1054                Some(Moderator),
1055            ),
1056            ServerChatCommand::Version => {
1057                cmd(vec![], Content::localized("command-version-desc"), None)
1058            },
1059            ServerChatCommand::SetWaypoint => cmd(
1060                vec![],
1061                Content::localized("command-waypoint-desc"),
1062                Some(Admin),
1063            ),
1064            ServerChatCommand::Wiring => cmd(
1065                vec![],
1066                Content::localized("command-wiring-desc"),
1067                Some(Admin),
1068            ),
1069            ServerChatCommand::Whitelist => cmd(
1070                vec![Any("add/remove", Required), PlayerName(Required)],
1071                Content::localized("command-whitelist-desc"),
1072                Some(Moderator),
1073            ),
1074            ServerChatCommand::World => cmd(
1075                vec![Message(Optional)],
1076                Content::localized("command-world-desc"),
1077                None,
1078            ),
1079            ServerChatCommand::MakeVolume => cmd(
1080                vec![Integer("size", 15, Optional)],
1081                Content::localized("command-make_volume-desc"),
1082                Some(Admin),
1083            ),
1084            ServerChatCommand::Location => cmd(
1085                vec![Any("name", Required)],
1086                Content::localized("command-location-desc"),
1087                None,
1088            ),
1089            ServerChatCommand::CreateLocation => cmd(
1090                vec![Any("name", Required)],
1091                Content::localized("command-create_location-desc"),
1092                Some(Moderator),
1093            ),
1094            ServerChatCommand::DeleteLocation => cmd(
1095                vec![Any("name", Required)],
1096                Content::localized("command-delete_location-desc"),
1097                Some(Moderator),
1098            ),
1099            ServerChatCommand::WeatherZone => cmd(
1100                vec![
1101                    Enum("weather kind", WEATHERS.clone(), Required),
1102                    Float("radius", 500.0, Optional),
1103                    Float("time", 300.0, Optional),
1104                ],
1105                Content::localized("command-weather_zone-desc"),
1106                Some(Admin),
1107            ),
1108            ServerChatCommand::Lightning => cmd(
1109                vec![],
1110                Content::localized("command-lightning-desc"),
1111                Some(Admin),
1112            ),
1113            ServerChatCommand::Scale => cmd(
1114                vec![
1115                    Float("factor", 1.0, Required),
1116                    Boolean("reset_mass", true.to_string(), Optional),
1117                ],
1118                Content::localized("command-scale-desc"),
1119                Some(Admin),
1120            ),
1121            ServerChatCommand::RepairEquipment => cmd(
1122                vec![ArgumentSpec::Boolean(
1123                    "repair inventory",
1124                    true.to_string(),
1125                    Optional,
1126                )],
1127                Content::localized("command-repair_equipment-desc"),
1128                Some(Admin),
1129            ),
1130            ServerChatCommand::Tether => cmd(
1131                vec![
1132                    EntityTarget(Required),
1133                    Boolean("automatic length", "true".to_string(), Optional),
1134                ],
1135                Content::localized("command-tether-desc"),
1136                Some(Admin),
1137            ),
1138            ServerChatCommand::DestroyTethers => cmd(
1139                vec![],
1140                Content::localized("command-destroy_tethers-desc"),
1141                Some(Admin),
1142            ),
1143            ServerChatCommand::Mount => cmd(
1144                vec![EntityTarget(Required)],
1145                Content::localized("command-mount-desc"),
1146                Some(Admin),
1147            ),
1148            ServerChatCommand::Dismount => cmd(
1149                vec![EntityTarget(Required)],
1150                Content::localized("command-dismount-desc"),
1151                Some(Admin),
1152            ),
1153        }
1154    }
1155
1156    /// The keyword used to invoke the command, omitting the prefix.
1157    pub fn keyword(&self) -> &'static str {
1158        match self {
1159            ServerChatCommand::Adminify => "adminify",
1160            ServerChatCommand::Airship => "airship",
1161            ServerChatCommand::Alias => "alias",
1162            ServerChatCommand::AreaAdd => "area_add",
1163            ServerChatCommand::AreaList => "area_list",
1164            ServerChatCommand::AreaRemove => "area_remove",
1165            ServerChatCommand::Aura => "aura",
1166            ServerChatCommand::Ban => "ban",
1167            ServerChatCommand::BanIp => "ban_ip",
1168            ServerChatCommand::BanLog => "ban_log",
1169            ServerChatCommand::BattleMode => "battlemode",
1170            ServerChatCommand::BattleModeForce => "battlemode_force",
1171            ServerChatCommand::Body => "body",
1172            ServerChatCommand::Buff => "buff",
1173            ServerChatCommand::Build => "build",
1174            ServerChatCommand::Campfire => "campfire",
1175            ServerChatCommand::ClearPersistedTerrain => "clear_persisted_terrain",
1176            ServerChatCommand::DeathEffect => "death_effect",
1177            ServerChatCommand::DebugColumn => "debug_column",
1178            ServerChatCommand::DebugWays => "debug_ways",
1179            ServerChatCommand::DisconnectAllPlayers => "disconnect_all_players",
1180            ServerChatCommand::DropAll => "dropall",
1181            ServerChatCommand::Dummy => "dummy",
1182            ServerChatCommand::Explosion => "explosion",
1183            ServerChatCommand::Faction => "faction",
1184            ServerChatCommand::GiveItem => "give_item",
1185            ServerChatCommand::Gizmos => "gizmos",
1186            ServerChatCommand::GizmosRange => "gizmos_range",
1187            ServerChatCommand::Goto => "goto",
1188            ServerChatCommand::GotoRand => "goto_rand",
1189            ServerChatCommand::Group => "group",
1190            ServerChatCommand::GroupInvite => "group_invite",
1191            ServerChatCommand::GroupKick => "group_kick",
1192            ServerChatCommand::GroupLeave => "group_leave",
1193            ServerChatCommand::GroupPromote => "group_promote",
1194            ServerChatCommand::Health => "health",
1195            ServerChatCommand::IntoNpc => "into_npc",
1196            ServerChatCommand::JoinFaction => "join_faction",
1197            ServerChatCommand::Jump => "jump",
1198            ServerChatCommand::Kick => "kick",
1199            ServerChatCommand::Kill => "kill",
1200            ServerChatCommand::KillNpcs => "kill_npcs",
1201            ServerChatCommand::Kit => "kit",
1202            ServerChatCommand::Lantern => "lantern",
1203            ServerChatCommand::Respawn => "respawn",
1204            ServerChatCommand::Light => "light",
1205            ServerChatCommand::MakeBlock => "make_block",
1206            ServerChatCommand::MakeNpc => "make_npc",
1207            ServerChatCommand::MakeSprite => "make_sprite",
1208            ServerChatCommand::Motd => "motd",
1209            ServerChatCommand::Object => "object",
1210            ServerChatCommand::Outcome => "outcome",
1211            ServerChatCommand::PermitBuild => "permit_build",
1212            ServerChatCommand::Players => "players",
1213            ServerChatCommand::Poise => "poise",
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::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}