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