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