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