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