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::Concussion => "concussion",
193 BuffKind::Staggered => "staggered",
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 Portal,
432 Region,
433 ReloadChunks,
434 RemoveLights,
435 RepairEquipment,
436 ResetRecipes,
437 Respawn,
438 RevokeBuild,
439 RevokeBuildAll,
440 RtsimChunk,
441 RtsimInfo,
442 RtsimNpc,
443 RtsimPurge,
444 RtsimTp,
445 Safezone,
446 Say,
447 Scale,
448 ServerPhysics,
449 SetBodyType,
450 SetMotd,
451 SetWaypoint,
452 Ship,
453 Site,
454 SkillPoint,
455 SkillPreset,
456 Spawn,
457 Spot,
458 Sudo,
459 Tell,
460 Tether,
461 Time,
462 TimeScale,
463 Tp,
464 Unban,
465 UnbanIp,
466 Version,
467 WeatherZone,
468 Whitelist,
469 Wiring,
470 World,
471}
472
473impl ServerChatCommand {
474 pub fn data(&self) -> ChatCommandData {
475 use ArgumentSpec::*;
476 use Requirement::*;
477 use Role::*;
478 let cmd = ChatCommandData::new;
479 match self {
480 ServerChatCommand::Adminify => cmd(
481 vec![PlayerName(Required), Enum("role", ROLES.clone(), Optional)],
482 Content::localized("command-adminify-desc"),
483 Some(Admin),
484 ),
485 ServerChatCommand::Airship => cmd(
486 vec![
487 Enum(
488 "kind",
489 comp::ship::ALL_AIRSHIPS
490 .iter()
491 .map(|b| format!("{b:?}"))
492 .collect(),
493 Optional,
494 ),
495 Float("destination_degrees_ccw_of_east", 90.0, Optional),
496 ],
497 Content::localized("command-airship-desc"),
498 Some(Admin),
499 ),
500 ServerChatCommand::Alias => cmd(
501 vec![Any("name", Required)],
502 Content::localized("command-alias-desc"),
503 Some(Moderator),
504 ),
505 ServerChatCommand::Aura => cmd(
506 vec![
507 Float("aura_radius", 10.0, Required),
508 Float("aura_duration", 10.0, Optional),
509 Boolean("new_entity", "true".to_string(), Optional),
510 Enum("aura_target", GroupTarget::all_options(), Optional),
511 Enum("aura_kind", AuraKindVariant::all_options(), Required),
512 Any("aura spec", Optional),
513 ],
514 Content::localized("command-aura-desc"),
515 Some(Admin),
516 ),
517 ServerChatCommand::Buff => cmd(
518 vec![
519 Enum("buff", BUFFS.clone(), Required),
520 Float("strength", 0.01, Optional),
521 Float("duration", 10.0, Optional),
522 Any("buff data spec", Optional),
523 ],
524 Content::localized("command-buff-desc"),
525 Some(Admin),
526 ),
527 ServerChatCommand::Ban => cmd(
528 vec![
529 PlayerName(Required),
530 Boolean("overwrite", "true".to_string(), Optional),
531 Any("ban duration", Optional),
532 Message(Optional),
533 ],
534 Content::localized("command-ban-desc"),
535 Some(Moderator),
536 ),
537 ServerChatCommand::BanIp => cmd(
538 vec![
539 PlayerName(Required),
540 Boolean("overwrite", "true".to_string(), Optional),
541 Any("ban duration", Optional),
542 Message(Optional),
543 ],
544 Content::localized("command-ban-ip-desc"),
545 Some(Moderator),
546 ),
547 #[rustfmt::skip]
548 ServerChatCommand::BattleMode => cmd(
549 vec![Enum(
550 "battle mode",
551 vec!["pvp".to_owned(), "pve".to_owned()],
552 Optional,
553 )],
554 Content::localized("command-battlemode-desc"),
555 None,
556
557 ),
558 ServerChatCommand::IntoNpc => cmd(
559 vec![AssetPath(
560 "entity_config",
561 "common.entity.",
562 ENTITY_CONFIGS.clone(),
563 Required,
564 )],
565 Content::localized("command-into_npc-desc"),
566 Some(Admin),
567 ),
568 ServerChatCommand::Body => cmd(
569 vec![Enum("body", ENTITIES.clone(), Required)],
570 Content::localized("command-body-desc"),
571 Some(Admin),
572 ),
573 ServerChatCommand::BattleModeForce => cmd(
574 vec![Enum(
575 "battle mode",
576 vec!["pvp".to_owned(), "pve".to_owned()],
577 Required,
578 )],
579 Content::localized("command-battlemode_force-desc"),
580 Some(Admin),
581 ),
582 ServerChatCommand::Build => cmd(vec![], Content::localized("command-build-desc"), None),
583 ServerChatCommand::AreaAdd => cmd(
584 vec![
585 Any("name", Required),
586 Enum("kind", AREA_KINDS.clone(), Required),
587 Integer("xlo", 0, Required),
588 Integer("xhi", 10, Required),
589 Integer("ylo", 0, Required),
590 Integer("yhi", 10, Required),
591 Integer("zlo", 0, Required),
592 Integer("zhi", 10, Required),
593 ],
594 Content::localized("command-area_add-desc"),
595 Some(Admin),
596 ),
597 ServerChatCommand::AreaList => cmd(
598 vec![],
599 Content::localized("command-area_list-desc"),
600 Some(Admin),
601 ),
602 ServerChatCommand::AreaRemove => cmd(
603 vec![
604 Any("name", Required),
605 Enum("kind", AREA_KINDS.clone(), Required),
606 ],
607 Content::localized("command-area_remove-desc"),
608 Some(Admin),
609 ),
610 ServerChatCommand::Campfire => cmd(
611 vec![],
612 Content::localized("command-campfire-desc"),
613 Some(Admin),
614 ),
615 ServerChatCommand::ClearPersistedTerrain => cmd(
616 vec![Integer("chunk_radius", 6, Required)],
617 Content::localized("command-clear_persisted_terrain-desc"),
618 Some(Admin),
619 ),
620 ServerChatCommand::DeathEffect => cmd(
621 vec![
622 Enum("death_effect", vec!["transform".to_string()], Required),
623 AssetPath(
627 "entity_config",
628 "common.entity.",
629 ENTITY_CONFIGS.clone(),
630 Required,
631 ),
632 ],
633 Content::localized("command-death_effect-dest"),
634 Some(Admin),
635 ),
636 ServerChatCommand::DebugColumn => cmd(
637 vec![Integer("x", 15000, Required), Integer("y", 15000, Required)],
638 Content::localized("command-debug_column-desc"),
639 Some(Admin),
640 ),
641 ServerChatCommand::DebugWays => cmd(
642 vec![Integer("x", 15000, Required), Integer("y", 15000, Required)],
643 Content::localized("command-debug_ways-desc"),
644 Some(Admin),
645 ),
646 ServerChatCommand::DisconnectAllPlayers => cmd(
647 vec![Any("confirm", Required)],
648 Content::localized("command-disconnect_all_players-desc"),
649 Some(Admin),
650 ),
651 ServerChatCommand::DropAll => cmd(
652 vec![],
653 Content::localized("command-dropall-desc"),
654 Some(Moderator),
655 ),
656 ServerChatCommand::Dummy => cmd(
657 vec![],
658 Content::localized("command-dummy-desc"),
659 Some(Admin),
660 ),
661 ServerChatCommand::Explosion => cmd(
662 vec![Float("radius", 5.0, Required)],
663 Content::localized("command-explosion-desc"),
664 Some(Admin),
665 ),
666 ServerChatCommand::Faction => cmd(
667 vec![Message(Optional)],
668 Content::localized("command-faction-desc"),
669 None,
670 ),
671 ServerChatCommand::GiveItem => cmd(
672 vec![
673 AssetPath("item", "common.items.", ITEM_SPECS.clone(), Required),
674 Integer("num", 1, Optional),
675 ],
676 Content::localized("command-give_item-desc"),
677 Some(Admin),
678 ),
679 ServerChatCommand::Gizmos => cmd(
680 vec![
681 Enum(
682 "kind",
683 ["All".to_string(), "None".to_string()]
684 .into_iter()
685 .chain(
686 comp::gizmos::GizmoSubscription::iter()
687 .map(|kind| kind.to_string()),
688 )
689 .collect(),
690 Required,
691 ),
692 EntityTarget(Optional),
693 ],
694 Content::localized("command-gizmos-desc"),
695 Some(Admin),
696 ),
697 ServerChatCommand::GizmosRange => cmd(
698 vec![Float("range", 32.0, Required)],
699 Content::localized("command-gizmos_range-desc"),
700 Some(Admin),
701 ),
702 ServerChatCommand::Goto => cmd(
703 vec![
704 Float("x", 0.0, Required),
705 Float("y", 0.0, Required),
706 Float("z", 0.0, Required),
707 Boolean("Dismount from ship", "true".to_string(), Optional),
708 ],
709 Content::localized("command-goto-desc"),
710 Some(Admin),
711 ),
712 ServerChatCommand::GotoRand => cmd(
713 vec![Boolean("Dismount from ship", "true".to_string(), Optional)],
714 Content::localized("command-goto-rand"),
715 Some(Admin),
716 ),
717 ServerChatCommand::Group => cmd(
718 vec![Message(Optional)],
719 Content::localized("command-group-desc"),
720 None,
721 ),
722 ServerChatCommand::GroupInvite => cmd(
723 vec![PlayerName(Required)],
724 Content::localized("command-group_invite-desc"),
725 None,
726 ),
727 ServerChatCommand::GroupKick => cmd(
728 vec![PlayerName(Required)],
729 Content::localized("command-group_kick-desc"),
730 None,
731 ),
732 ServerChatCommand::GroupLeave => {
733 cmd(vec![], Content::localized("command-group_leave-desc"), None)
734 },
735 ServerChatCommand::GroupPromote => cmd(
736 vec![PlayerName(Required)],
737 Content::localized("command-group_promote-desc"),
738 None,
739 ),
740 ServerChatCommand::Health => cmd(
741 vec![Integer("hp", 100, Required)],
742 Content::localized("command-health-desc"),
743 Some(Admin),
744 ),
745 ServerChatCommand::Respawn => cmd(
746 vec![],
747 Content::localized("command-respawn-desc"),
748 Some(Moderator),
749 ),
750 ServerChatCommand::JoinFaction => cmd(
751 vec![Any("faction", Optional)],
752 Content::localized("command-join_faction-desc"),
753 None,
754 ),
755 ServerChatCommand::Jump => cmd(
756 vec![
757 Float("x", 0.0, Required),
758 Float("y", 0.0, Required),
759 Float("z", 0.0, Required),
760 Boolean("Dismount from ship", "true".to_string(), Optional),
761 ],
762 Content::localized("command-jump-desc"),
763 Some(Admin),
764 ),
765 ServerChatCommand::Kick => cmd(
766 vec![PlayerName(Required), Message(Optional)],
767 Content::localized("command-kick-desc"),
768 Some(Moderator),
769 ),
770 ServerChatCommand::Kill => cmd(vec![], Content::localized("command-kill-desc"), None),
771 ServerChatCommand::KillNpcs => cmd(
772 vec![Float("radius", 100.0, Optional), Flag("--also-pets")],
773 Content::localized("command-kill_npcs-desc"),
774 Some(Admin),
775 ),
776 ServerChatCommand::Kit => cmd(
777 vec![Enum("kit_name", KITS.to_vec(), Required)],
778 Content::localized("command-kit-desc"),
779 Some(Admin),
780 ),
781 ServerChatCommand::Lantern => cmd(
782 vec![
783 Float("strength", 5.0, Required),
784 Float("r", 1.0, Optional),
785 Float("g", 1.0, Optional),
786 Float("b", 1.0, Optional),
787 ],
788 Content::localized("command-lantern-desc"),
789 Some(Admin),
790 ),
791 ServerChatCommand::Light => cmd(
792 vec![
793 Float("r", 1.0, Optional),
794 Float("g", 1.0, Optional),
795 Float("b", 1.0, Optional),
796 Float("x", 0.0, Optional),
797 Float("y", 0.0, Optional),
798 Float("z", 0.0, Optional),
799 Float("strength", 5.0, Optional),
800 ],
801 Content::localized("command-light-desc"),
802 Some(Admin),
803 ),
804 ServerChatCommand::MakeBlock => cmd(
805 vec![
806 Enum("block", BLOCK_KINDS.clone(), Required),
807 Integer("r", 255, Optional),
808 Integer("g", 255, Optional),
809 Integer("b", 255, Optional),
810 ],
811 Content::localized("command-make_block-desc"),
812 Some(Admin),
813 ),
814 ServerChatCommand::MakeNpc => cmd(
815 vec![
816 AssetPath(
817 "entity_config",
818 "common.entity.",
819 ENTITY_CONFIGS.clone(),
820 Required,
821 ),
822 Integer("num", 1, Optional),
823 ],
824 Content::localized("command-make_npc-desc"),
825 Some(Admin),
826 ),
827 ServerChatCommand::MakeSprite => cmd(
828 vec![Enum("sprite", SPRITE_KINDS.clone(), Required)],
829 Content::localized("command-make_sprite-desc"),
830 Some(Admin),
831 ),
832 ServerChatCommand::Motd => cmd(vec![], Content::localized("command-motd-desc"), None),
833 ServerChatCommand::Object => cmd(
834 vec![Enum("object", OBJECTS.clone(), Required)],
835 Content::localized("command-object-desc"),
836 Some(Admin),
837 ),
838 ServerChatCommand::Outcome => cmd(
839 vec![Enum("outcome", OUTCOME_KINDS.clone(), Required)],
840 Content::localized("command-outcome-desc"),
841 Some(Admin),
842 ),
843 ServerChatCommand::PermitBuild => cmd(
844 vec![Any("area_name", Required)],
845 Content::localized("command-permit_build-desc"),
846 Some(Admin),
847 ),
848 ServerChatCommand::Players => {
849 cmd(vec![], Content::localized("command-players-desc"), None)
850 },
851 ServerChatCommand::Portal => cmd(
852 vec![
853 Float("x", 0., Required),
854 Float("y", 0., Required),
855 Float("z", 0., Required),
856 Boolean("requires_no_aggro", "true".to_string(), Optional),
857 Float("buildup_time", 5., Optional),
858 ],
859 Content::localized("command-portal-desc"),
860 Some(Admin),
861 ),
862 ServerChatCommand::ReloadChunks => cmd(
863 vec![Integer("chunk_radius", 6, Optional)],
864 Content::localized("command-reload_chunks-desc"),
865 Some(Admin),
866 ),
867 ServerChatCommand::ResetRecipes => cmd(
868 vec![],
869 Content::localized("command-reset_recipes-desc"),
870 Some(Admin),
871 ),
872 ServerChatCommand::RemoveLights => cmd(
873 vec![Float("radius", 20.0, Optional)],
874 Content::localized("command-remove_lights-desc"),
875 Some(Admin),
876 ),
877 ServerChatCommand::RevokeBuild => cmd(
878 vec![Any("area_name", Required)],
879 Content::localized("command-revoke_build-desc"),
880 Some(Admin),
881 ),
882 ServerChatCommand::RevokeBuildAll => cmd(
883 vec![],
884 Content::localized("command-revoke_build_all-desc"),
885 Some(Admin),
886 ),
887 ServerChatCommand::Region => cmd(
888 vec![Message(Optional)],
889 Content::localized("command-region-desc"),
890 None,
891 ),
892 ServerChatCommand::Safezone => cmd(
893 vec![Float("range", 100.0, Optional)],
894 Content::localized("command-safezone-desc"),
895 Some(Moderator),
896 ),
897 ServerChatCommand::Say => cmd(
898 vec![Message(Optional)],
899 Content::localized("command-say-desc"),
900 None,
901 ),
902 ServerChatCommand::ServerPhysics => cmd(
903 vec![
904 PlayerName(Required),
905 Boolean("enabled", "true".to_string(), Optional),
906 Message(Optional),
907 ],
908 Content::localized("command-server_physics-desc"),
909 Some(Moderator),
910 ),
911 ServerChatCommand::SetMotd => cmd(
912 vec![Any("locale", Optional), Message(Optional)],
913 Content::localized("command-set_motd-desc"),
914 Some(Admin),
915 ),
916 ServerChatCommand::SetBodyType => cmd(
917 vec![
918 Enum(
919 "body type",
920 vec!["Female".to_string(), "Male".to_string()],
921 Required,
922 ),
923 Boolean("permanent", "false".to_string(), Requirement::Optional),
924 ],
925 Content::localized("command-set_body_type-desc"),
926 Some(Admin),
927 ),
928 ServerChatCommand::Ship => cmd(
929 vec![
930 Enum(
931 "kind",
932 comp::ship::ALL_SHIPS
933 .iter()
934 .map(|b| format!("{b:?}"))
935 .collect(),
936 Optional,
937 ),
938 Boolean(
939 "Whether the ship should be tethered to the target (or its mount)",
940 "false".to_string(),
941 Optional,
942 ),
943 Float("destination_degrees_ccw_of_east", 90.0, Optional),
944 ],
945 Content::localized("command-ship-desc"),
946 Some(Admin),
947 ),
948 ServerChatCommand::Site => cmd(
951 vec![
952 SiteName(Required),
953 Boolean("Dismount from ship", "true".to_string(), Optional),
954 ],
955 Content::localized("command-site-desc"),
956 Some(Moderator),
957 ),
958 ServerChatCommand::SkillPoint => cmd(
959 vec![
960 Enum("skill tree", SKILL_TREES.clone(), Required),
961 Integer("amount", 1, Optional),
962 ],
963 Content::localized("command-skill_point-desc"),
964 Some(Admin),
965 ),
966 ServerChatCommand::SkillPreset => cmd(
967 vec![Enum("preset_name", PRESET_LIST.to_vec(), Required)],
968 Content::localized("command-skill_preset-desc"),
969 Some(Admin),
970 ),
971 ServerChatCommand::Spawn => cmd(
972 vec![
973 Enum("alignment", ALIGNMENTS.clone(), Required),
974 Enum("entity", ENTITIES.clone(), Required),
975 Integer("amount", 1, Optional),
976 Boolean("ai", "true".to_string(), Optional),
977 Float("scale", 1.0, Optional),
978 Boolean("tethered", "false".to_string(), Optional),
979 ],
980 Content::localized("command-spawn-desc"),
981 Some(Admin),
982 ),
983 ServerChatCommand::Spot => cmd(
984 vec![Enum("Spot kind to find", SPOTS.clone(), Required)],
985 Content::localized("command-spot-desc"),
986 Some(Admin),
987 ),
988 ServerChatCommand::Sudo => cmd(
989 vec![EntityTarget(Required), SubCommand],
990 Content::localized("command-sudo-desc"),
991 Some(Moderator),
992 ),
993 ServerChatCommand::Tell => cmd(
994 vec![PlayerName(Required), Message(Optional)],
995 Content::localized("command-tell-desc"),
996 None,
997 ),
998 ServerChatCommand::Time => cmd(
999 vec![Enum("time", TIMES.clone(), Optional)],
1000 Content::localized("command-time-desc"),
1001 Some(Admin),
1002 ),
1003 ServerChatCommand::TimeScale => cmd(
1004 vec![Float("time scale", 1.0, Optional)],
1005 Content::localized("command-time_scale-desc"),
1006 Some(Admin),
1007 ),
1008 ServerChatCommand::Tp => cmd(
1009 vec![
1010 EntityTarget(Optional),
1011 Boolean("Dismount from ship", "true".to_string(), Optional),
1012 ],
1013 Content::localized("command-tp-desc"),
1014 Some(Moderator),
1015 ),
1016 ServerChatCommand::RtsimTp => cmd(
1017 vec![
1018 Integer("npc index", 0, Required),
1019 Boolean("Dismount from ship", "true".to_string(), Optional),
1020 ],
1021 Content::localized("command-rtsim_tp-desc"),
1022 Some(Admin),
1023 ),
1024 ServerChatCommand::RtsimInfo => cmd(
1025 vec![Integer("npc index", 0, Required)],
1026 Content::localized("command-rtsim_info-desc"),
1027 Some(Admin),
1028 ),
1029 ServerChatCommand::RtsimNpc => cmd(
1030 vec![Any("query", Required), Integer("max number", 20, Optional)],
1031 Content::localized("command-rtsim_npc-desc"),
1032 Some(Admin),
1033 ),
1034 ServerChatCommand::RtsimPurge => cmd(
1035 vec![Boolean(
1036 "whether purging of rtsim data should occur on next startup",
1037 true.to_string(),
1038 Required,
1039 )],
1040 Content::localized("command-rtsim_purge-desc"),
1041 Some(Admin),
1042 ),
1043 ServerChatCommand::RtsimChunk => cmd(
1044 vec![],
1045 Content::localized("command-rtsim_chunk-desc"),
1046 Some(Admin),
1047 ),
1048 ServerChatCommand::Unban => cmd(
1049 vec![PlayerName(Required)],
1050 Content::localized("command-unban-desc"),
1051 Some(Moderator),
1052 ),
1053 ServerChatCommand::UnbanIp => cmd(
1054 vec![PlayerName(Required)],
1055 Content::localized("command-unban-ip-desc"),
1056 Some(Moderator),
1057 ),
1058 ServerChatCommand::Version => {
1059 cmd(vec![], Content::localized("command-version-desc"), None)
1060 },
1061 ServerChatCommand::SetWaypoint => cmd(
1062 vec![],
1063 Content::localized("command-waypoint-desc"),
1064 Some(Admin),
1065 ),
1066 ServerChatCommand::Wiring => cmd(
1067 vec![],
1068 Content::localized("command-wiring-desc"),
1069 Some(Admin),
1070 ),
1071 ServerChatCommand::Whitelist => cmd(
1072 vec![Any("add/remove", Required), PlayerName(Required)],
1073 Content::localized("command-whitelist-desc"),
1074 Some(Moderator),
1075 ),
1076 ServerChatCommand::World => cmd(
1077 vec![Message(Optional)],
1078 Content::localized("command-world-desc"),
1079 None,
1080 ),
1081 ServerChatCommand::MakeVolume => cmd(
1082 vec![Integer("size", 15, Optional)],
1083 Content::localized("command-make_volume-desc"),
1084 Some(Admin),
1085 ),
1086 ServerChatCommand::Location => cmd(
1087 vec![Any("name", Required)],
1088 Content::localized("command-location-desc"),
1089 None,
1090 ),
1091 ServerChatCommand::CreateLocation => cmd(
1092 vec![Any("name", Required)],
1093 Content::localized("command-create_location-desc"),
1094 Some(Moderator),
1095 ),
1096 ServerChatCommand::DeleteLocation => cmd(
1097 vec![Any("name", Required)],
1098 Content::localized("command-delete_location-desc"),
1099 Some(Moderator),
1100 ),
1101 ServerChatCommand::WeatherZone => cmd(
1102 vec![
1103 Enum("weather kind", WEATHERS.clone(), Required),
1104 Float("radius", 500.0, Optional),
1105 Float("time", 300.0, Optional),
1106 ],
1107 Content::localized("command-weather_zone-desc"),
1108 Some(Admin),
1109 ),
1110 ServerChatCommand::Lightning => cmd(
1111 vec![],
1112 Content::localized("command-lightning-desc"),
1113 Some(Admin),
1114 ),
1115 ServerChatCommand::Scale => cmd(
1116 vec![
1117 Float("factor", 1.0, Required),
1118 Boolean("reset_mass", true.to_string(), Optional),
1119 ],
1120 Content::localized("command-scale-desc"),
1121 Some(Admin),
1122 ),
1123 ServerChatCommand::RepairEquipment => cmd(
1124 vec![ArgumentSpec::Boolean(
1125 "repair inventory",
1126 true.to_string(),
1127 Optional,
1128 )],
1129 Content::localized("command-repair_equipment-desc"),
1130 Some(Admin),
1131 ),
1132 ServerChatCommand::Tether => cmd(
1133 vec![
1134 EntityTarget(Required),
1135 Boolean("automatic length", "true".to_string(), Optional),
1136 ],
1137 Content::localized("command-tether-desc"),
1138 Some(Admin),
1139 ),
1140 ServerChatCommand::DestroyTethers => cmd(
1141 vec![],
1142 Content::localized("command-destroy_tethers-desc"),
1143 Some(Admin),
1144 ),
1145 ServerChatCommand::Mount => cmd(
1146 vec![EntityTarget(Required)],
1147 Content::localized("command-mount-desc"),
1148 Some(Admin),
1149 ),
1150 ServerChatCommand::Dismount => cmd(
1151 vec![EntityTarget(Required)],
1152 Content::localized("command-dismount-desc"),
1153 Some(Admin),
1154 ),
1155 }
1156 }
1157
1158 pub fn keyword(&self) -> &'static str {
1160 match self {
1161 ServerChatCommand::Adminify => "adminify",
1162 ServerChatCommand::Airship => "airship",
1163 ServerChatCommand::Alias => "alias",
1164 ServerChatCommand::AreaAdd => "area_add",
1165 ServerChatCommand::AreaList => "area_list",
1166 ServerChatCommand::AreaRemove => "area_remove",
1167 ServerChatCommand::Aura => "aura",
1168 ServerChatCommand::Ban => "ban",
1169 ServerChatCommand::BanIp => "ban_ip",
1170 ServerChatCommand::BattleMode => "battlemode",
1171 ServerChatCommand::BattleModeForce => "battlemode_force",
1172 ServerChatCommand::Body => "body",
1173 ServerChatCommand::Buff => "buff",
1174 ServerChatCommand::Build => "build",
1175 ServerChatCommand::Campfire => "campfire",
1176 ServerChatCommand::ClearPersistedTerrain => "clear_persisted_terrain",
1177 ServerChatCommand::DeathEffect => "death_effect",
1178 ServerChatCommand::DebugColumn => "debug_column",
1179 ServerChatCommand::DebugWays => "debug_ways",
1180 ServerChatCommand::DisconnectAllPlayers => "disconnect_all_players",
1181 ServerChatCommand::DropAll => "dropall",
1182 ServerChatCommand::Dummy => "dummy",
1183 ServerChatCommand::Explosion => "explosion",
1184 ServerChatCommand::Faction => "faction",
1185 ServerChatCommand::GiveItem => "give_item",
1186 ServerChatCommand::Gizmos => "gizmos",
1187 ServerChatCommand::GizmosRange => "gizmos_range",
1188 ServerChatCommand::Goto => "goto",
1189 ServerChatCommand::GotoRand => "goto_rand",
1190 ServerChatCommand::Group => "group",
1191 ServerChatCommand::GroupInvite => "group_invite",
1192 ServerChatCommand::GroupKick => "group_kick",
1193 ServerChatCommand::GroupLeave => "group_leave",
1194 ServerChatCommand::GroupPromote => "group_promote",
1195 ServerChatCommand::Health => "health",
1196 ServerChatCommand::IntoNpc => "into_npc",
1197 ServerChatCommand::JoinFaction => "join_faction",
1198 ServerChatCommand::Jump => "jump",
1199 ServerChatCommand::Kick => "kick",
1200 ServerChatCommand::Kill => "kill",
1201 ServerChatCommand::KillNpcs => "kill_npcs",
1202 ServerChatCommand::Kit => "kit",
1203 ServerChatCommand::Lantern => "lantern",
1204 ServerChatCommand::Respawn => "respawn",
1205 ServerChatCommand::Light => "light",
1206 ServerChatCommand::MakeBlock => "make_block",
1207 ServerChatCommand::MakeNpc => "make_npc",
1208 ServerChatCommand::MakeSprite => "make_sprite",
1209 ServerChatCommand::Motd => "motd",
1210 ServerChatCommand::Object => "object",
1211 ServerChatCommand::Outcome => "outcome",
1212 ServerChatCommand::PermitBuild => "permit_build",
1213 ServerChatCommand::Players => "players",
1214 ServerChatCommand::Portal => "portal",
1215 ServerChatCommand::ResetRecipes => "reset_recipes",
1216 ServerChatCommand::Region => "region",
1217 ServerChatCommand::ReloadChunks => "reload_chunks",
1218 ServerChatCommand::RemoveLights => "remove_lights",
1219 ServerChatCommand::RevokeBuild => "revoke_build",
1220 ServerChatCommand::RevokeBuildAll => "revoke_build_all",
1221 ServerChatCommand::Safezone => "safezone",
1222 ServerChatCommand::Say => "say",
1223 ServerChatCommand::ServerPhysics => "server_physics",
1224 ServerChatCommand::SetMotd => "set_motd",
1225 ServerChatCommand::SetBodyType => "set_body_type",
1226 ServerChatCommand::Ship => "ship",
1227 ServerChatCommand::Site => "site",
1228 ServerChatCommand::SkillPoint => "skill_point",
1229 ServerChatCommand::SkillPreset => "skill_preset",
1230 ServerChatCommand::Spawn => "spawn",
1231 ServerChatCommand::Spot => "spot",
1232 ServerChatCommand::Sudo => "sudo",
1233 ServerChatCommand::Tell => "tell",
1234 ServerChatCommand::Time => "time",
1235 ServerChatCommand::TimeScale => "time_scale",
1236 ServerChatCommand::Tp => "tp",
1237 ServerChatCommand::RtsimTp => "rtsim_tp",
1238 ServerChatCommand::RtsimInfo => "rtsim_info",
1239 ServerChatCommand::RtsimNpc => "rtsim_npc",
1240 ServerChatCommand::RtsimPurge => "rtsim_purge",
1241 ServerChatCommand::RtsimChunk => "rtsim_chunk",
1242 ServerChatCommand::Unban => "unban",
1243 ServerChatCommand::UnbanIp => "unban_ip",
1244 ServerChatCommand::Version => "version",
1245 ServerChatCommand::SetWaypoint => "set_waypoint",
1246 ServerChatCommand::Wiring => "wiring",
1247 ServerChatCommand::Whitelist => "whitelist",
1248 ServerChatCommand::World => "world",
1249 ServerChatCommand::MakeVolume => "make_volume",
1250 ServerChatCommand::Location => "location",
1251 ServerChatCommand::CreateLocation => "create_location",
1252 ServerChatCommand::DeleteLocation => "delete_location",
1253 ServerChatCommand::WeatherZone => "weather_zone",
1254 ServerChatCommand::Lightning => "lightning",
1255 ServerChatCommand::Scale => "scale",
1256 ServerChatCommand::RepairEquipment => "repair_equipment",
1257 ServerChatCommand::Tether => "tether",
1258 ServerChatCommand::DestroyTethers => "destroy_tethers",
1259 ServerChatCommand::Mount => "mount",
1260 ServerChatCommand::Dismount => "dismount",
1261 }
1262 }
1263
1264 pub fn short_keyword(&self) -> Option<&'static str> {
1267 Some(match self {
1268 ServerChatCommand::Faction => "f",
1269 ServerChatCommand::Group => "g",
1270 ServerChatCommand::Region => "r",
1271 ServerChatCommand::Say => "s",
1272 ServerChatCommand::Tell => "t",
1273 ServerChatCommand::World => "w",
1274 _ => return None,
1275 })
1276 }
1277
1278 pub fn iter() -> impl Iterator<Item = Self> + Clone { <Self as IntoEnumIterator>::iter() }
1280
1281 pub fn help_content(&self) -> Content {
1283 let data = self.data();
1284
1285 let usage = std::iter::once(format!("/{}", self.keyword()))
1286 .chain(data.args.iter().map(|arg| arg.usage_string()))
1287 .collect::<Vec<_>>()
1288 .join(" ");
1289
1290 Content::localized_with_args("command-help-template", [
1291 ("usage", Content::Plain(usage)),
1292 ("description", data.description),
1293 ])
1294 }
1295
1296 pub fn iter_with_keywords() -> impl Iterator<Item = (&'static str, Self)> {
1300 Self::iter()
1301 .filter_map(|c| c.short_keyword().map(|s| (s, c)))
1303 .chain(Self::iter().map(|c| (c.keyword(), c)))
1304 }
1305
1306 pub fn needs_role(&self) -> Option<comp::AdminRole> { self.data().needs_role }
1307
1308 pub fn arg_fmt(&self) -> String {
1310 self.data()
1311 .args
1312 .iter()
1313 .map(|arg| match arg {
1314 ArgumentSpec::PlayerName(_) => "{}",
1315 ArgumentSpec::EntityTarget(_) => "{}",
1316 ArgumentSpec::SiteName(_) => "{/.*/}",
1317 ArgumentSpec::Float(_, _, _) => "{}",
1318 ArgumentSpec::Integer(_, _, _) => "{d}",
1319 ArgumentSpec::Any(_, _) => "{}",
1320 ArgumentSpec::Command(_) => "{}",
1321 ArgumentSpec::Message(_) => "{/.*/}",
1322 ArgumentSpec::SubCommand => "{} {/.*/}",
1323 ArgumentSpec::Enum(_, _, _) => "{}",
1324 ArgumentSpec::AssetPath(_, _, _, _) => "{}",
1325 ArgumentSpec::Boolean(_, _, _) => "{}",
1326 ArgumentSpec::Flag(_) => "{}",
1327 })
1328 .collect::<Vec<_>>()
1329 .join(" ")
1330 }
1331}
1332
1333impl Display for ServerChatCommand {
1334 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
1335 write!(f, "{}", self.keyword())
1336 }
1337}
1338
1339impl FromStr for ServerChatCommand {
1340 type Err = ();
1341
1342 fn from_str(keyword: &str) -> Result<ServerChatCommand, ()> {
1343 Self::iter()
1344 .filter_map(|c| c.short_keyword().map(|s| (s, c)))
1346 .chain(Self::iter().map(|c| (c.keyword(), c)))
1347 .find_map(|(kwd, command)| (kwd == keyword).then_some(command))
1349 .ok_or(())
1351 }
1352}
1353
1354#[derive(Eq, PartialEq, Debug, Clone, Copy)]
1355pub enum Requirement {
1356 Required,
1357 Optional,
1358}
1359
1360pub enum ArgumentSpec {
1362 PlayerName(Requirement),
1364 EntityTarget(Requirement),
1366 SiteName(Requirement),
1368 Float(&'static str, f32, Requirement),
1373 Integer(&'static str, i32, Requirement),
1378 Any(&'static str, Requirement),
1380 Command(Requirement),
1382 Message(Requirement),
1385 SubCommand,
1387 Enum(&'static str, Vec<String>, Requirement),
1392 AssetPath(&'static str, &'static str, Vec<String>, Requirement),
1398 Boolean(&'static str, String, Requirement),
1403 Flag(&'static str),
1405}
1406
1407impl ArgumentSpec {
1408 pub fn usage_string(&self) -> String {
1409 match self {
1410 ArgumentSpec::PlayerName(req) => {
1411 if &Requirement::Required == req {
1412 "<player>".to_string()
1413 } else {
1414 "[player]".to_string()
1415 }
1416 },
1417 ArgumentSpec::EntityTarget(req) => {
1418 if &Requirement::Required == req {
1419 "<entity>".to_string()
1420 } else {
1421 "[entity]".to_string()
1422 }
1423 },
1424 ArgumentSpec::SiteName(req) => {
1425 if &Requirement::Required == req {
1426 "<site>".to_string()
1427 } else {
1428 "[site]".to_string()
1429 }
1430 },
1431 ArgumentSpec::Float(label, _, req) => {
1432 if &Requirement::Required == req {
1433 format!("<{}>", label)
1434 } else {
1435 format!("[{}]", label)
1436 }
1437 },
1438 ArgumentSpec::Integer(label, _, req) => {
1439 if &Requirement::Required == req {
1440 format!("<{}>", label)
1441 } else {
1442 format!("[{}]", label)
1443 }
1444 },
1445 ArgumentSpec::Any(label, req) => {
1446 if &Requirement::Required == req {
1447 format!("<{}>", label)
1448 } else {
1449 format!("[{}]", label)
1450 }
1451 },
1452 ArgumentSpec::Command(req) => {
1453 if &Requirement::Required == req {
1454 "<[/]command>".to_string()
1455 } else {
1456 "[[/]command]".to_string()
1457 }
1458 },
1459 ArgumentSpec::Message(req) => {
1460 if &Requirement::Required == req {
1461 "<message>".to_string()
1462 } else {
1463 "[message]".to_string()
1464 }
1465 },
1466 ArgumentSpec::SubCommand => "<[/]command> [args...]".to_string(),
1467 ArgumentSpec::Enum(label, _, req) => {
1468 if &Requirement::Required == req {
1469 format!("<{}>", label)
1470 } else {
1471 format!("[{}]", label)
1472 }
1473 },
1474 ArgumentSpec::AssetPath(label, _, _, req) => {
1475 if &Requirement::Required == req {
1476 format!("<{}>", label)
1477 } else {
1478 format!("[{}]", label)
1479 }
1480 },
1481 ArgumentSpec::Boolean(label, _, req) => {
1482 if &Requirement::Required == req {
1483 format!("<{}>", label)
1484 } else {
1485 format!("[{}]", label)
1486 }
1487 },
1488 ArgumentSpec::Flag(label) => {
1489 format!("[{}]", label)
1490 },
1491 }
1492 }
1493
1494 pub fn requirement(&self) -> Requirement {
1495 match self {
1496 ArgumentSpec::PlayerName(r)
1497 | ArgumentSpec::EntityTarget(r)
1498 | ArgumentSpec::SiteName(r)
1499 | ArgumentSpec::Float(_, _, r)
1500 | ArgumentSpec::Integer(_, _, r)
1501 | ArgumentSpec::Any(_, r)
1502 | ArgumentSpec::Command(r)
1503 | ArgumentSpec::Message(r)
1504 | ArgumentSpec::Enum(_, _, r)
1505 | ArgumentSpec::AssetPath(_, _, _, r)
1506 | ArgumentSpec::Boolean(_, _, r) => *r,
1507 ArgumentSpec::Flag(_) => Requirement::Optional,
1508 ArgumentSpec::SubCommand => Requirement::Required,
1509 }
1510 }
1511}
1512
1513pub trait CommandEnumArg: FromStr {
1514 fn all_options() -> Vec<String>;
1515}
1516
1517macro_rules! impl_from_to_str_cmd {
1518 ($enum:ident, ($($attribute:ident => $str:expr),*)) => {
1519 impl std::str::FromStr for $enum {
1520 type Err = String;
1521
1522 fn from_str(s: &str) -> Result<Self, Self::Err> {
1523 match s {
1524 $(
1525 $str => Ok($enum::$attribute),
1526 )*
1527 s => Err(format!("Invalid variant: {s}")),
1528 }
1529 }
1530 }
1531
1532 impl $crate::cmd::CommandEnumArg for $enum {
1533 fn all_options() -> Vec<String> {
1534 vec![$($str.to_string()),*]
1535 }
1536 }
1537 }
1538}
1539
1540impl_from_to_str_cmd!(AuraKindVariant, (
1541 Buff => "buff",
1542 FriendlyFire => "friendly_fire",
1543 ForcePvP => "force_pvp"
1544));
1545
1546impl_from_to_str_cmd!(GroupTarget, (
1547 InGroup => "in_group",
1548 OutOfGroup => "out_of_group",
1549 All => "all"
1550));
1551
1552#[macro_export]
1555macro_rules! parse_cmd_args {
1556 ($args:expr, $($t:ty),* $(, ..$tail:ty)? $(,)?) => {
1557 {
1558 let mut args = $args.into_iter().peekable();
1559 (
1560 $({
1573 let parsed = args.peek().and_then(|s| s.parse::<$t>().ok());
1574 if parsed.is_some() { args.next(); }
1576 parsed
1577 }),*
1578 $(, args.map(|s| s.to_string()).collect::<$tail>())?
1579 )
1580 }
1581 };
1582}
1583
1584#[cfg(test)]
1585mod tests {
1586 use super::*;
1587 use crate::comp::Item;
1588
1589 #[test]
1590 fn verify_cmd_list_sorted() {
1591 let mut list = ServerChatCommand::iter()
1592 .map(|c| c.keyword())
1593 .collect::<Vec<_>>();
1594
1595 let list2 = list.clone();
1597 list.sort_unstable();
1598 assert_eq!(list, list2);
1599 }
1600
1601 #[test]
1602 fn test_loading_skill_presets() {
1603 SkillPresetManifest::load_expect_combined_static(PRESET_MANIFEST_PATH);
1604 }
1605
1606 #[test]
1607 fn test_load_kits() {
1608 let kits = KitManifest::load_expect_combined_static(KIT_MANIFEST_PATH).read();
1609 let mut rng = rand::thread_rng();
1610 for kit in kits.0.values() {
1611 for (item_id, _) in kit.iter() {
1612 match item_id {
1613 KitSpec::Item(item_id) => {
1614 Item::new_from_asset_expect(item_id);
1615 },
1616 KitSpec::ModularWeaponSet {
1617 tool,
1618 material,
1619 hands,
1620 } => {
1621 comp::item::modular::generate_weapons(*tool, *material, *hands)
1622 .unwrap_or_else(|_| {
1623 panic!(
1624 "Failed to synthesize a modular {tool:?} set made of \
1625 {material:?}."
1626 )
1627 });
1628 },
1629 KitSpec::ModularWeaponRandom {
1630 tool,
1631 material,
1632 hands,
1633 } => {
1634 comp::item::modular::random_weapon(*tool, *material, *hands, &mut rng)
1635 .unwrap_or_else(|_| {
1636 panic!(
1637 "Failed to synthesize a random {hands:?}-handed modular \
1638 {tool:?} made of {material:?}."
1639 )
1640 });
1641 },
1642 }
1643 }
1644 }
1645 }
1646}