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 uid::Uid,
14};
15use common_i18n::Content;
16use hashbrown::{HashMap, HashSet};
17use lazy_static::lazy_static;
18use serde::{Deserialize, Serialize};
19use std::{
20 fmt::{self, Display},
21 num::NonZeroU64,
22 str::FromStr,
23};
24use strum::{AsRefStr, EnumIter, EnumString, IntoEnumIterator, VariantNames};
25use tracing::warn;
26
27pub struct ChatCommandData {
29 pub args: Vec<ArgumentSpec>,
31 pub description: Content,
33 pub needs_role: Option<Role>,
35}
36
37impl ChatCommandData {
38 pub fn new(args: Vec<ArgumentSpec>, description: Content, needs_role: Option<Role>) -> Self {
39 Self {
40 args,
41 description,
42 needs_role,
43 }
44 }
45}
46
47#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
48pub enum KitSpec {
49 Item(String),
50 ModularWeaponSet {
51 tool: comp::tool::ToolKind,
52 material: comp::item::Material,
53 hands: Option<comp::item::tool::Hands>,
54 },
55 ModularWeaponRandom {
56 tool: comp::tool::ToolKind,
57 material: comp::item::Material,
58 hands: Option<comp::item::tool::Hands>,
59 },
60}
61
62pub type KitManifest = Ron<HashMap<String, Vec<(KitSpec, u32)>>>;
63
64pub type SkillPresetManifest = Ron<HashMap<String, Vec<(Skill, u8)>>>;
65
66pub const KIT_MANIFEST_PATH: &str = "server.manifests.kits";
67pub const PRESET_MANIFEST_PATH: &str = "server.manifests.presets";
68
69#[derive(Debug, Clone, EnumIter, EnumString, AsRefStr)]
71pub enum AreaKind {
72 #[strum(serialize = "build")]
73 Build,
74 #[strum(serialize = "no_durability")]
75 NoDurability,
76}
77
78lazy_static! {
79 static ref ALIGNMENTS: Vec<String> = ["wild", "enemy", "npc", "pet"]
80 .iter()
81 .map(|s| s.to_string())
82 .collect();
83 static ref SKILL_TREES: Vec<String> = ["general", "sword", "axe", "hammer", "bow", "staff", "sceptre", "mining"]
84 .iter()
85 .map(|s| s.to_string())
86 .collect();
87 pub static ref ENTITIES: Vec<String> = {
89 let npc_names = &npc::NPC_NAMES.read();
90
91 let mut categories = HashSet::new();
93 let mut species = HashSet::new();
94 for body in comp::Body::iter() {
95 if matches!(body, comp::Body::Plugin(_)) {
97 continue;
98 }
99
100 if let Some(meta) = npc_names.get_species_meta(&body) {
101 categories.insert(npc_names[&body].keyword.clone());
102 species.insert(meta.keyword.clone());
103 }
104 }
105
106 let mut strings = Vec::new();
107 strings.extend(categories);
108 strings.extend(species);
109
110 strings
111 };
112 static ref AREA_KINDS: Vec<String> = AreaKind::iter().map(|kind| kind.as_ref().to_string()).collect();
113 static ref OBJECTS: Vec<String> = comp::object::ALL_OBJECTS
114 .iter()
115 .map(|o| o.to_string().to_string())
116 .collect();
117 static ref RECIPES: Vec<String> = {
118 let rbm = RecipeBookManifest::load().cloned();
119 rbm.keys().cloned().collect::<Vec<String>>()
120 };
121 static ref TIMES: Vec<String> = [
122 "midnight", "night", "dawn", "morning", "day", "noon", "dusk"
123 ]
124 .iter()
125 .map(|s| s.to_string())
126 .collect();
127
128 static ref WEATHERS: Vec<String> = [
129 "clear", "cloudy", "rain", "wind", "storm"
130 ]
131 .iter()
132 .map(|s| s.to_string())
133 .collect();
134
135 pub static ref BUFF_PARSER: HashMap<String, BuffKind> = {
136 let string_from_buff = |kind| match kind {
137 BuffKind::Burning => "burning",
138 BuffKind::Regeneration => "regeneration",
139 BuffKind::Saturation => "saturation",
140 BuffKind::Bleeding => "bleeding",
141 BuffKind::Cursed => "cursed",
142 BuffKind::Potion => "potion",
143 BuffKind::Agility => "agility",
144 BuffKind::RestingHeal => "resting_heal",
145 BuffKind::EnergyRegen => "energy_regen",
146 BuffKind::ComboGeneration => "combo_generation",
147 BuffKind::IncreaseMaxEnergy => "increase_max_energy",
148 BuffKind::IncreaseMaxHealth => "increase_max_health",
149 BuffKind::Invulnerability => "invulnerability",
150 BuffKind::ProtectingWard => "protecting_ward",
151 BuffKind::Frenzied => "frenzied",
152 BuffKind::Crippled => "crippled",
153 BuffKind::Frozen => "frozen",
154 BuffKind::Wet => "wet",
155 BuffKind::Ensnared => "ensnared",
156 BuffKind::Poisoned => "poisoned",
157 BuffKind::Hastened => "hastened",
158 BuffKind::Fortitude => "fortitude",
159 BuffKind::Parried => "parried",
160 BuffKind::PotionSickness => "potion_sickness",
161 BuffKind::Reckless => "reckless",
162 BuffKind::Polymorphed => "polymorphed",
163 BuffKind::Flame => "flame",
164 BuffKind::Frigid => "frigid",
165 BuffKind::Lifesteal => "lifesteal",
166 BuffKind::ImminentCritical => "imminent_critical",
168 BuffKind::Fury => "fury",
169 BuffKind::Sunderer => "sunderer",
170 BuffKind::Defiance => "defiance",
171 BuffKind::Bloodfeast => "bloodfeast",
172 BuffKind::Berserk => "berserk",
173 BuffKind::Heatstroke => "heatstroke",
174 BuffKind::ScornfulTaunt => "scornful_taunt",
175 BuffKind::Rooted => "rooted",
176 BuffKind::Winded => "winded",
177 BuffKind::Amnesia => "amnesia",
178 BuffKind::OffBalance => "off_balance",
179 BuffKind::Tenacity => "tenacity",
180 BuffKind::Resilience => "resilience",
181 };
182 let mut buff_parser = HashMap::new();
183 for kind in BuffKind::iter() {
184 buff_parser.insert(string_from_buff(kind).to_string(), kind);
185 }
186 buff_parser
187 };
188
189 pub static ref BUFF_PACK: Vec<String> = {
190 let mut buff_pack: Vec<_> = BUFF_PARSER.keys().cloned().collect();
191 buff_pack.retain(|kind| kind != "invulnerability");
193 buff_pack
194 };
195
196 static ref BUFFS: Vec<String> = {
197 let mut buff_pack: Vec<String> = BUFF_PARSER.keys().cloned().collect();
198
199 buff_pack.push("all".to_owned());
201 buff_pack.push("clear".to_owned());
202 buff_pack
203 };
204
205 static ref BLOCK_KINDS: Vec<String> = terrain::block::BlockKind::iter()
206 .map(|bk| bk.to_string())
207 .collect();
208
209 static ref SPRITE_KINDS: Vec<String> = terrain::sprite::SPRITE_KINDS
210 .keys()
211 .cloned()
212 .collect();
213
214 static ref OUTCOME_KINDS: Vec<String> = outcome::Outcome::VARIANTS
215 .iter()
216 .map(|s| s.to_string())
217 .collect();
218
219 static ref ROLES: Vec<String> = ["admin", "moderator"].iter().copied().map(Into::into).collect();
220
221 pub static ref ITEM_SPECS: Vec<String> = {
225 let mut items = try_all_item_defs()
226 .unwrap_or_else(|e| {
227 warn!(?e, "Failed to load item specifiers");
228 Vec::new()
229 });
230 items.sort();
231 items
232 };
233
234 pub static ref ENTITY_CONFIGS: Vec<String> = {
236 try_all_entity_configs()
237 .unwrap_or_else(|e| {
238 warn!(?e, "Failed to load entity configs");
239 Vec::new()
240 })
241 };
242
243 pub static ref KITS: Vec<String> = {
244 let mut kits = if let Ok(kits) = KitManifest::load_and_combine_static(KIT_MANIFEST_PATH) {
245 let mut kits = kits.read().0.keys().cloned().collect::<Vec<String>>();
246 kits.sort();
247 kits
248 } else {
249 Vec::new()
250 };
251 kits.push("all".to_owned());
252
253 kits
254 };
255
256 static ref PRESETS: HashMap<String, Vec<(Skill, u8)>> = {
257 if let Ok(presets) = SkillPresetManifest::load_and_combine_static(PRESET_MANIFEST_PATH) {
258 presets.read().0.clone()
259 } else {
260 warn!("Error while loading presets");
261 HashMap::new()
262 }
263 };
264
265 static ref PRESET_LIST: Vec<String> = {
266 let mut preset_list: Vec<String> = PRESETS.keys().cloned().collect();
267 preset_list.push("clear".to_owned());
268
269 preset_list
270 };
271
272 pub static ref SPOT_PARSER: HashMap<String, Spot> = {
274 let spot_to_string = |kind| match kind {
275 Spot::DwarvenGrave => "dwarven_grave",
276 Spot::SaurokAltar => "saurok_altar",
277 Spot::MyrmidonTemple => "myrmidon_temple",
278 Spot::GnarlingTotem => "gnarling_totem",
279 Spot::WitchHouse => "witch_house",
280 Spot::GnomeSpring => "gnome_spring",
281 Spot::WolfBurrow => "wolf_burrow",
282 Spot::Igloo => "igloo",
283 Spot::LionRock => "lion_rock",
284 Spot::TreeStumpForest => "tree_stump_forest",
285 Spot::DesertBones => "desert_bones",
286 Spot::Arch => "arch",
287 Spot::AirshipCrash => "airship_crash",
288 Spot::FruitTree => "fruit_tree",
289 Spot::Shipwreck => "shipwreck",
290 Spot::Shipwreck2 => "shipwreck2",
291 Spot::FallenTree => "fallen_tree",
292 Spot::GraveSmall => "grave_small",
293 Spot::JungleTemple => "jungle_temple",
294 Spot::SaurokTotem => "saurok_totem",
295 Spot::JungleOutpost => "jungle_outpost",
296 Spot::RonFile(props) => &props.base_structures,
298 };
299
300 let mut map = HashMap::new();
301 for spot_kind in Spot::iter() {
302 map.insert(spot_to_string(spot_kind).to_owned(), spot_kind);
303 }
304
305 map
306 };
307
308 pub static ref SPOTS: Vec<String> = {
309 let mut config_spots = crate::spot::RON_SPOT_PROPERTIES
310 .0
311 .iter()
312 .map(|s| s.base_structures.clone())
313 .collect::<Vec<_>>();
314
315 config_spots.extend(SPOT_PARSER.keys().cloned());
316 config_spots
317 };
318}
319
320pub enum EntityTarget {
321 Player(String),
322 RtsimNpc(u64),
323 Uid(crate::uid::Uid),
324}
325
326impl FromStr for EntityTarget {
327 type Err = String;
328
329 fn from_str(s: &str) -> Result<Self, Self::Err> {
330 if let Some((spec, data)) = s.split_once('@') {
332 match spec {
333 "rtsim" => Ok(EntityTarget::RtsimNpc(u64::from_str(data).map_err(
334 |_| format!("Expected a valid number after 'rtsim@' but found {data}."),
335 )?)),
336 "uid" => {
337 let raw = u64::from_str(data).map_err(|_| {
338 format!("Expected a valid number after 'uid@' but found {data}.")
339 })?;
340 let nz =
341 NonZeroU64::new(raw).ok_or_else(|| "Uid cannot be zero".to_string())?;
342 Ok(EntityTarget::Uid(Uid(nz)))
343 },
344 _ => Err(format!(
345 "Expected either 'rtsim' or 'uid' before '@' but found '{spec}'"
346 )),
347 }
348 } else {
349 Ok(EntityTarget::Player(s.to_string()))
350 }
351 }
352}
353
354#[derive(Copy, Clone, strum::EnumIter)]
356pub enum ServerChatCommand {
357 Adminify,
358 Airship,
359 Alias,
360 AreaAdd,
361 AreaList,
362 AreaRemove,
363 Aura,
364 Ban,
365 BanIp,
366 BanLog,
367 BattleMode,
368 BattleModeForce,
369 Body,
370 Buff,
371 Build,
372 Campfire,
373 ClearPersistedTerrain,
374 CreateLocation,
375 DeathEffect,
376 DebugColumn,
377 DebugWays,
378 DeleteLocation,
379 DestroyTethers,
380 DisconnectAllPlayers,
381 Dismount,
382 DropAll,
383 Dummy,
384 Explosion,
385 Faction,
386 GiveItem,
387 Gizmos,
388 GizmosRange,
389 Goto,
390 GotoRand,
391 Group,
392 GroupInvite,
393 GroupKick,
394 GroupLeave,
395 GroupPromote,
396 Health,
397 IntoNpc,
398 JoinFaction,
399 Jump,
400 Kick,
401 Kill,
402 KillNpcs,
403 Kit,
404 Lantern,
405 Light,
406 Lightning,
407 Location,
408 MakeBlock,
409 MakeNpc,
410 MakeSprite,
411 MakeVolume,
412 Motd,
413 Mount,
414 Object,
415 Outcome,
416 PermitBuild,
417 Players,
418 Poise,
419 Portal,
420 Region,
421 ReloadChunks,
422 RemoveLights,
423 RepairEquipment,
424 ResetRecipes,
425 Respawn,
426 RevokeBuild,
427 RevokeBuildAll,
428 RtsimChunk,
429 RtsimInfo,
430 RtsimNpc,
431 RtsimPurge,
432 RtsimTp,
433 Safezone,
434 Say,
435 Scale,
436 ServerPhysics,
437 SetBodyType,
438 SetMotd,
439 SetWaypoint,
440 Ship,
441 Site,
442 SkillPoint,
443 SkillPreset,
444 Spawn,
445 Spot,
446 Sudo,
447 Tell,
448 Tether,
449 Time,
450 TimeScale,
451 Tp,
452 Unban,
453 UnbanIp,
454 Version,
455 WeatherZone,
456 Whitelist,
457 Wiring,
458 World,
459}
460
461impl ServerChatCommand {
462 pub fn data(&self) -> ChatCommandData {
463 use ArgumentSpec::*;
464 use Requirement::*;
465 use Role::*;
466 let cmd = ChatCommandData::new;
467 match self {
468 ServerChatCommand::Adminify => cmd(
469 vec![PlayerName(Required), Enum("role", ROLES.clone(), Optional)],
470 Content::localized("command-adminify-desc"),
471 Some(Admin),
472 ),
473 ServerChatCommand::Airship => cmd(
474 vec![
475 Enum(
476 "kind",
477 comp::ship::ALL_AIRSHIPS
478 .iter()
479 .map(|b| format!("{b:?}"))
480 .collect(),
481 Optional,
482 ),
483 Float("destination_degrees_ccw_of_east", 90.0, Optional),
484 ],
485 Content::localized("command-airship-desc"),
486 Some(Admin),
487 ),
488 ServerChatCommand::Alias => cmd(
489 vec![Any("name", Required)],
490 Content::localized("command-alias-desc"),
491 Some(Moderator),
492 ),
493 ServerChatCommand::Aura => cmd(
494 vec![
495 Float("aura_radius", 10.0, Required),
496 Float("aura_duration", 10.0, Optional),
497 Boolean("new_entity", "true".to_string(), Optional),
498 Enum("aura_target", GroupTarget::all_options(), Optional),
499 Enum("aura_kind", AuraKindVariant::all_options(), Required),
500 Any("aura spec", Optional),
501 ],
502 Content::localized("command-aura-desc"),
503 Some(Admin),
504 ),
505 ServerChatCommand::Buff => cmd(
506 vec![
507 Enum("buff", BUFFS.clone(), Required),
508 Float("strength", 0.01, Optional),
509 Float("duration", 10.0, Optional),
510 Any("buff data spec", Optional),
511 ],
512 Content::localized("command-buff-desc"),
513 Some(Admin),
514 ),
515 ServerChatCommand::Ban => cmd(
516 vec![
517 PlayerName(Required),
518 Boolean("overwrite", "true".to_string(), Optional),
519 Any("ban duration", Optional),
520 Message(Optional),
521 ],
522 Content::localized("command-ban-desc"),
523 Some(Moderator),
524 ),
525 ServerChatCommand::BanIp => cmd(
526 vec![
527 PlayerName(Required),
528 Boolean("overwrite", "true".to_string(), Optional),
529 Any("ban duration", Optional),
530 Message(Optional),
531 ],
532 Content::localized("command-ban-ip-desc"),
533 Some(Moderator),
534 ),
535 ServerChatCommand::BanLog => cmd(
536 vec![PlayerName(Required), Integer("max entries", 10, Optional)],
537 Content::localized("command-ban-ip-desc"),
538 Some(Moderator),
539 ),
540 #[rustfmt::skip]
541 ServerChatCommand::BattleMode => cmd(
542 vec![Enum(
543 "battle mode",
544 vec!["pvp".to_owned(), "pve".to_owned()],
545 Optional,
546 )],
547 Content::localized("command-battlemode-desc"),
548 None,
549
550 ),
551 ServerChatCommand::IntoNpc => cmd(
552 vec![AssetPath(
553 "entity_config",
554 "common.entity.",
555 ENTITY_CONFIGS.clone(),
556 Required,
557 )],
558 Content::localized("command-into_npc-desc"),
559 Some(Admin),
560 ),
561 ServerChatCommand::Body => cmd(
562 vec![Enum("body", ENTITIES.clone(), Required)],
563 Content::localized("command-body-desc"),
564 Some(Admin),
565 ),
566 ServerChatCommand::BattleModeForce => cmd(
567 vec![Enum(
568 "battle mode",
569 vec!["pvp".to_owned(), "pve".to_owned()],
570 Required,
571 )],
572 Content::localized("command-battlemode_force-desc"),
573 Some(Admin),
574 ),
575 ServerChatCommand::Build => cmd(vec![], Content::localized("command-build-desc"), None),
576 ServerChatCommand::AreaAdd => cmd(
577 vec![
578 Any("name", Required),
579 Enum("kind", AREA_KINDS.clone(), Required),
580 Integer("xlo", 0, Required),
581 Integer("xhi", 10, Required),
582 Integer("ylo", 0, Required),
583 Integer("yhi", 10, Required),
584 Integer("zlo", 0, Required),
585 Integer("zhi", 10, Required),
586 ],
587 Content::localized("command-area_add-desc"),
588 Some(Admin),
589 ),
590 ServerChatCommand::AreaList => cmd(
591 vec![],
592 Content::localized("command-area_list-desc"),
593 Some(Admin),
594 ),
595 ServerChatCommand::AreaRemove => cmd(
596 vec![
597 Any("name", Required),
598 Enum("kind", AREA_KINDS.clone(), Required),
599 ],
600 Content::localized("command-area_remove-desc"),
601 Some(Admin),
602 ),
603 ServerChatCommand::Campfire => cmd(
604 vec![],
605 Content::localized("command-campfire-desc"),
606 Some(Admin),
607 ),
608 ServerChatCommand::ClearPersistedTerrain => cmd(
609 vec![Integer("chunk_radius", 6, Required)],
610 Content::localized("command-clear_persisted_terrain-desc"),
611 Some(Admin),
612 ),
613 ServerChatCommand::DeathEffect => cmd(
614 vec![
615 Enum("death_effect", vec!["transform".to_string()], Required),
616 AssetPath(
620 "entity_config",
621 "common.entity.",
622 ENTITY_CONFIGS.clone(),
623 Required,
624 ),
625 ],
626 Content::localized("command-death_effect-dest"),
627 Some(Admin),
628 ),
629 ServerChatCommand::DebugColumn => cmd(
630 vec![Integer("x", 15000, Required), Integer("y", 15000, Required)],
631 Content::localized("command-debug_column-desc"),
632 Some(Admin),
633 ),
634 ServerChatCommand::DebugWays => cmd(
635 vec![Integer("x", 15000, Required), Integer("y", 15000, Required)],
636 Content::localized("command-debug_ways-desc"),
637 Some(Admin),
638 ),
639 ServerChatCommand::DisconnectAllPlayers => cmd(
640 vec![Any("confirm", Required)],
641 Content::localized("command-disconnect_all_players-desc"),
642 Some(Admin),
643 ),
644 ServerChatCommand::DropAll => cmd(
645 vec![],
646 Content::localized("command-dropall-desc"),
647 Some(Moderator),
648 ),
649 ServerChatCommand::Dummy => cmd(
650 vec![],
651 Content::localized("command-dummy-desc"),
652 Some(Admin),
653 ),
654 ServerChatCommand::Explosion => cmd(
655 vec![Float("radius", 5.0, Required)],
656 Content::localized("command-explosion-desc"),
657 Some(Admin),
658 ),
659 ServerChatCommand::Faction => cmd(
660 vec![Message(Optional)],
661 Content::localized("command-faction-desc"),
662 None,
663 ),
664 ServerChatCommand::GiveItem => cmd(
665 vec![
666 AssetPath("item", "common.items.", ITEM_SPECS.clone(), Required),
667 Integer("num", 1, Optional),
668 ],
669 Content::localized("command-give_item-desc"),
670 Some(Admin),
671 ),
672 ServerChatCommand::Gizmos => cmd(
673 vec![
674 Enum(
675 "kind",
676 ["All".to_string(), "None".to_string()]
677 .into_iter()
678 .chain(
679 comp::gizmos::GizmoSubscription::iter()
680 .map(|kind| kind.to_string()),
681 )
682 .collect(),
683 Required,
684 ),
685 EntityTarget(Optional),
686 ],
687 Content::localized("command-gizmos-desc"),
688 Some(Admin),
689 ),
690 ServerChatCommand::GizmosRange => cmd(
691 vec![Float("range", 32.0, Required)],
692 Content::localized("command-gizmos_range-desc"),
693 Some(Admin),
694 ),
695 ServerChatCommand::Goto => cmd(
696 vec![
697 Float("x", 0.0, Required),
698 Float("y", 0.0, Required),
699 Float("z", 0.0, Required),
700 Boolean("Dismount from ship", "true".to_string(), Optional),
701 ],
702 Content::localized("command-goto-desc"),
703 Some(Admin),
704 ),
705 ServerChatCommand::GotoRand => cmd(
706 vec![Boolean("Dismount from ship", "true".to_string(), Optional)],
707 Content::localized("command-goto-rand"),
708 Some(Admin),
709 ),
710 ServerChatCommand::Group => cmd(
711 vec![Message(Optional)],
712 Content::localized("command-group-desc"),
713 None,
714 ),
715 ServerChatCommand::GroupInvite => cmd(
716 vec![PlayerName(Required)],
717 Content::localized("command-group_invite-desc"),
718 None,
719 ),
720 ServerChatCommand::GroupKick => cmd(
721 vec![PlayerName(Required)],
722 Content::localized("command-group_kick-desc"),
723 None,
724 ),
725 ServerChatCommand::GroupLeave => {
726 cmd(vec![], Content::localized("command-group_leave-desc"), None)
727 },
728 ServerChatCommand::GroupPromote => cmd(
729 vec![PlayerName(Required)],
730 Content::localized("command-group_promote-desc"),
731 None,
732 ),
733 ServerChatCommand::Health => cmd(
734 vec![Integer("hp", 100, Required)],
735 Content::localized("command-health-desc"),
736 Some(Admin),
737 ),
738 ServerChatCommand::Respawn => cmd(
739 vec![],
740 Content::localized("command-respawn-desc"),
741 Some(Moderator),
742 ),
743 ServerChatCommand::JoinFaction => cmd(
744 vec![Any("faction", Optional)],
745 Content::localized("command-join_faction-desc"),
746 None,
747 ),
748 ServerChatCommand::Jump => cmd(
749 vec![
750 Float("x", 0.0, Required),
751 Float("y", 0.0, Required),
752 Float("z", 0.0, Required),
753 Boolean("Dismount from ship", "true".to_string(), Optional),
754 ],
755 Content::localized("command-jump-desc"),
756 Some(Admin),
757 ),
758 ServerChatCommand::Kick => cmd(
759 vec![PlayerName(Required), Message(Optional)],
760 Content::localized("command-kick-desc"),
761 Some(Moderator),
762 ),
763 ServerChatCommand::Kill => cmd(vec![], Content::localized("command-kill-desc"), None),
764 ServerChatCommand::KillNpcs => cmd(
765 vec![Float("radius", 100.0, Optional), Flag("--also-pets")],
766 Content::localized("command-kill_npcs-desc"),
767 Some(Admin),
768 ),
769 ServerChatCommand::Kit => cmd(
770 vec![Enum("kit_name", KITS.to_vec(), Required)],
771 Content::localized("command-kit-desc"),
772 Some(Admin),
773 ),
774 ServerChatCommand::Lantern => cmd(
775 vec![
776 Float("strength", 5.0, Required),
777 Float("r", 1.0, Optional),
778 Float("g", 1.0, Optional),
779 Float("b", 1.0, Optional),
780 ],
781 Content::localized("command-lantern-desc"),
782 Some(Admin),
783 ),
784 ServerChatCommand::Light => cmd(
785 vec![
786 Float("r", 1.0, Optional),
787 Float("g", 1.0, Optional),
788 Float("b", 1.0, Optional),
789 Float("x", 0.0, Optional),
790 Float("y", 0.0, Optional),
791 Float("z", 0.0, Optional),
792 Float("strength", 5.0, Optional),
793 ],
794 Content::localized("command-light-desc"),
795 Some(Admin),
796 ),
797 ServerChatCommand::MakeBlock => cmd(
798 vec![
799 Enum("block", BLOCK_KINDS.clone(), Required),
800 Integer("r", 255, Optional),
801 Integer("g", 255, Optional),
802 Integer("b", 255, Optional),
803 ],
804 Content::localized("command-make_block-desc"),
805 Some(Admin),
806 ),
807 ServerChatCommand::MakeNpc => cmd(
808 vec![
809 AssetPath(
810 "entity_config",
811 "common.entity.",
812 ENTITY_CONFIGS.clone(),
813 Required,
814 ),
815 Integer("num", 1, Optional),
816 ],
817 Content::localized("command-make_npc-desc"),
818 Some(Admin),
819 ),
820 ServerChatCommand::MakeSprite => cmd(
821 vec![Enum("sprite", SPRITE_KINDS.clone(), Required)],
822 Content::localized("command-make_sprite-desc"),
823 Some(Admin),
824 ),
825 ServerChatCommand::Motd => cmd(vec![], Content::localized("command-motd-desc"), None),
826 ServerChatCommand::Object => cmd(
827 vec![Enum("object", OBJECTS.clone(), Required)],
828 Content::localized("command-object-desc"),
829 Some(Admin),
830 ),
831 ServerChatCommand::Outcome => cmd(
832 vec![Enum("outcome", OUTCOME_KINDS.clone(), Required)],
833 Content::localized("command-outcome-desc"),
834 Some(Admin),
835 ),
836 ServerChatCommand::PermitBuild => cmd(
837 vec![Any("area_name", Required)],
838 Content::localized("command-permit_build-desc"),
839 Some(Admin),
840 ),
841 ServerChatCommand::Players => {
842 cmd(vec![], Content::localized("command-players-desc"), None)
843 },
844 ServerChatCommand::Poise => cmd(
845 vec![Integer("poise", 100, Required)],
846 Content::localized("command-poise-desc"),
847 Some(Admin),
848 ),
849 ServerChatCommand::Portal => cmd(
850 vec![
851 Float("x", 0., Required),
852 Float("y", 0., Required),
853 Float("z", 0., Required),
854 Boolean("requires_no_aggro", "true".to_string(), Optional),
855 Float("buildup_time", 5., Optional),
856 ],
857 Content::localized("command-portal-desc"),
858 Some(Admin),
859 ),
860 ServerChatCommand::ReloadChunks => cmd(
861 vec![Integer("chunk_radius", 6, Optional)],
862 Content::localized("command-reload_chunks-desc"),
863 Some(Admin),
864 ),
865 ServerChatCommand::ResetRecipes => cmd(
866 vec![],
867 Content::localized("command-reset_recipes-desc"),
868 Some(Admin),
869 ),
870 ServerChatCommand::RemoveLights => cmd(
871 vec![Float("radius", 20.0, Optional)],
872 Content::localized("command-remove_lights-desc"),
873 Some(Admin),
874 ),
875 ServerChatCommand::RevokeBuild => cmd(
876 vec![Any("area_name", Required)],
877 Content::localized("command-revoke_build-desc"),
878 Some(Admin),
879 ),
880 ServerChatCommand::RevokeBuildAll => cmd(
881 vec![],
882 Content::localized("command-revoke_build_all-desc"),
883 Some(Admin),
884 ),
885 ServerChatCommand::Region => cmd(
886 vec![Message(Optional)],
887 Content::localized("command-region-desc"),
888 None,
889 ),
890 ServerChatCommand::Safezone => cmd(
891 vec![Float("range", 100.0, Optional)],
892 Content::localized("command-safezone-desc"),
893 Some(Moderator),
894 ),
895 ServerChatCommand::Say => cmd(
896 vec![Message(Optional)],
897 Content::localized("command-say-desc"),
898 None,
899 ),
900 ServerChatCommand::ServerPhysics => cmd(
901 vec![
902 PlayerName(Required),
903 Boolean("enabled", "true".to_string(), Optional),
904 Message(Optional),
905 ],
906 Content::localized("command-server_physics-desc"),
907 Some(Moderator),
908 ),
909 ServerChatCommand::SetMotd => cmd(
910 vec![Any("locale", Optional), Message(Optional)],
911 Content::localized("command-set_motd-desc"),
912 Some(Admin),
913 ),
914 ServerChatCommand::SetBodyType => cmd(
915 vec![
916 Enum(
917 "body type",
918 vec!["Female".to_string(), "Male".to_string()],
919 Required,
920 ),
921 Boolean("permanent", "false".to_string(), Requirement::Optional),
922 ],
923 Content::localized("command-set_body_type-desc"),
924 Some(Admin),
925 ),
926 ServerChatCommand::Ship => cmd(
927 vec![
928 Enum(
929 "kind",
930 comp::ship::ALL_SHIPS
931 .iter()
932 .map(|b| format!("{b:?}"))
933 .collect(),
934 Optional,
935 ),
936 Boolean(
937 "Whether the ship should be tethered to the target (or its mount)",
938 "false".to_string(),
939 Optional,
940 ),
941 Float("destination_degrees_ccw_of_east", 90.0, Optional),
942 ],
943 Content::localized("command-ship-desc"),
944 Some(Admin),
945 ),
946 ServerChatCommand::Site => cmd(
949 vec![
950 SiteName(Required),
951 Boolean("Dismount from ship", "true".to_string(), Optional),
952 ],
953 Content::localized("command-site-desc"),
954 Some(Moderator),
955 ),
956 ServerChatCommand::SkillPoint => cmd(
957 vec![
958 Enum("skill tree", SKILL_TREES.clone(), Required),
959 Integer("amount", 1, Optional),
960 ],
961 Content::localized("command-skill_point-desc"),
962 Some(Admin),
963 ),
964 ServerChatCommand::SkillPreset => cmd(
965 vec![Enum("preset_name", PRESET_LIST.to_vec(), Required)],
966 Content::localized("command-skill_preset-desc"),
967 Some(Admin),
968 ),
969 ServerChatCommand::Spawn => cmd(
970 vec![
971 Enum("alignment", ALIGNMENTS.clone(), Required),
972 Enum("entity", ENTITIES.clone(), Required),
973 Integer("amount", 1, Optional),
974 Boolean("ai", "true".to_string(), Optional),
975 Float("scale", 1.0, Optional),
976 Boolean("tethered", "false".to_string(), Optional),
977 ],
978 Content::localized("command-spawn-desc"),
979 Some(Admin),
980 ),
981 ServerChatCommand::Spot => cmd(
982 vec![Enum("Spot kind to find", SPOTS.clone(), Required)],
983 Content::localized("command-spot-desc"),
984 Some(Admin),
985 ),
986 ServerChatCommand::Sudo => cmd(
987 vec![EntityTarget(Required), SubCommand],
988 Content::localized("command-sudo-desc"),
989 Some(Moderator),
990 ),
991 ServerChatCommand::Tell => cmd(
992 vec![PlayerName(Required), Message(Optional)],
993 Content::localized("command-tell-desc"),
994 None,
995 ),
996 ServerChatCommand::Time => cmd(
997 vec![Enum("time", TIMES.clone(), Optional)],
998 Content::localized("command-time-desc"),
999 Some(Admin),
1000 ),
1001 ServerChatCommand::TimeScale => cmd(
1002 vec![Float("time scale", 1.0, Optional)],
1003 Content::localized("command-time_scale-desc"),
1004 Some(Admin),
1005 ),
1006 ServerChatCommand::Tp => cmd(
1007 vec![
1008 EntityTarget(Optional),
1009 Boolean("Dismount from ship", "true".to_string(), Optional),
1010 ],
1011 Content::localized("command-tp-desc"),
1012 Some(Moderator),
1013 ),
1014 ServerChatCommand::RtsimTp => cmd(
1015 vec![
1016 Integer("npc index", 0, Required),
1017 Boolean("Dismount from ship", "true".to_string(), Optional),
1018 ],
1019 Content::localized("command-rtsim_tp-desc"),
1020 Some(Admin),
1021 ),
1022 ServerChatCommand::RtsimInfo => cmd(
1023 vec![Integer("npc index", 0, Required)],
1024 Content::localized("command-rtsim_info-desc"),
1025 Some(Admin),
1026 ),
1027 ServerChatCommand::RtsimNpc => cmd(
1028 vec![Any("query", Required), Integer("max number", 20, Optional)],
1029 Content::localized("command-rtsim_npc-desc"),
1030 Some(Admin),
1031 ),
1032 ServerChatCommand::RtsimPurge => cmd(
1033 vec![Boolean(
1034 "whether purging of rtsim data should occur on next startup",
1035 true.to_string(),
1036 Required,
1037 )],
1038 Content::localized("command-rtsim_purge-desc"),
1039 Some(Admin),
1040 ),
1041 ServerChatCommand::RtsimChunk => cmd(
1042 vec![],
1043 Content::localized("command-rtsim_chunk-desc"),
1044 Some(Admin),
1045 ),
1046 ServerChatCommand::Unban => cmd(
1047 vec![PlayerName(Required)],
1048 Content::localized("command-unban-desc"),
1049 Some(Moderator),
1050 ),
1051 ServerChatCommand::UnbanIp => cmd(
1052 vec![PlayerName(Required)],
1053 Content::localized("command-unban-ip-desc"),
1054 Some(Moderator),
1055 ),
1056 ServerChatCommand::Version => {
1057 cmd(vec![], Content::localized("command-version-desc"), None)
1058 },
1059 ServerChatCommand::SetWaypoint => cmd(
1060 vec![],
1061 Content::localized("command-waypoint-desc"),
1062 Some(Admin),
1063 ),
1064 ServerChatCommand::Wiring => cmd(
1065 vec![],
1066 Content::localized("command-wiring-desc"),
1067 Some(Admin),
1068 ),
1069 ServerChatCommand::Whitelist => cmd(
1070 vec![Any("add/remove", Required), PlayerName(Required)],
1071 Content::localized("command-whitelist-desc"),
1072 Some(Moderator),
1073 ),
1074 ServerChatCommand::World => cmd(
1075 vec![Message(Optional)],
1076 Content::localized("command-world-desc"),
1077 None,
1078 ),
1079 ServerChatCommand::MakeVolume => cmd(
1080 vec![Integer("size", 15, Optional)],
1081 Content::localized("command-make_volume-desc"),
1082 Some(Admin),
1083 ),
1084 ServerChatCommand::Location => cmd(
1085 vec![Any("name", Required)],
1086 Content::localized("command-location-desc"),
1087 None,
1088 ),
1089 ServerChatCommand::CreateLocation => cmd(
1090 vec![Any("name", Required)],
1091 Content::localized("command-create_location-desc"),
1092 Some(Moderator),
1093 ),
1094 ServerChatCommand::DeleteLocation => cmd(
1095 vec![Any("name", Required)],
1096 Content::localized("command-delete_location-desc"),
1097 Some(Moderator),
1098 ),
1099 ServerChatCommand::WeatherZone => cmd(
1100 vec![
1101 Enum("weather kind", WEATHERS.clone(), Required),
1102 Float("radius", 500.0, Optional),
1103 Float("time", 300.0, Optional),
1104 ],
1105 Content::localized("command-weather_zone-desc"),
1106 Some(Admin),
1107 ),
1108 ServerChatCommand::Lightning => cmd(
1109 vec![],
1110 Content::localized("command-lightning-desc"),
1111 Some(Admin),
1112 ),
1113 ServerChatCommand::Scale => cmd(
1114 vec![
1115 Float("factor", 1.0, Required),
1116 Boolean("reset_mass", true.to_string(), Optional),
1117 ],
1118 Content::localized("command-scale-desc"),
1119 Some(Admin),
1120 ),
1121 ServerChatCommand::RepairEquipment => cmd(
1122 vec![ArgumentSpec::Boolean(
1123 "repair inventory",
1124 true.to_string(),
1125 Optional,
1126 )],
1127 Content::localized("command-repair_equipment-desc"),
1128 Some(Admin),
1129 ),
1130 ServerChatCommand::Tether => cmd(
1131 vec![
1132 EntityTarget(Required),
1133 Boolean("automatic length", "true".to_string(), Optional),
1134 ],
1135 Content::localized("command-tether-desc"),
1136 Some(Admin),
1137 ),
1138 ServerChatCommand::DestroyTethers => cmd(
1139 vec![],
1140 Content::localized("command-destroy_tethers-desc"),
1141 Some(Admin),
1142 ),
1143 ServerChatCommand::Mount => cmd(
1144 vec![EntityTarget(Required)],
1145 Content::localized("command-mount-desc"),
1146 Some(Admin),
1147 ),
1148 ServerChatCommand::Dismount => cmd(
1149 vec![EntityTarget(Required)],
1150 Content::localized("command-dismount-desc"),
1151 Some(Admin),
1152 ),
1153 }
1154 }
1155
1156 pub fn keyword(&self) -> &'static str {
1158 match self {
1159 ServerChatCommand::Adminify => "adminify",
1160 ServerChatCommand::Airship => "airship",
1161 ServerChatCommand::Alias => "alias",
1162 ServerChatCommand::AreaAdd => "area_add",
1163 ServerChatCommand::AreaList => "area_list",
1164 ServerChatCommand::AreaRemove => "area_remove",
1165 ServerChatCommand::Aura => "aura",
1166 ServerChatCommand::Ban => "ban",
1167 ServerChatCommand::BanIp => "ban_ip",
1168 ServerChatCommand::BanLog => "ban_log",
1169 ServerChatCommand::BattleMode => "battlemode",
1170 ServerChatCommand::BattleModeForce => "battlemode_force",
1171 ServerChatCommand::Body => "body",
1172 ServerChatCommand::Buff => "buff",
1173 ServerChatCommand::Build => "build",
1174 ServerChatCommand::Campfire => "campfire",
1175 ServerChatCommand::ClearPersistedTerrain => "clear_persisted_terrain",
1176 ServerChatCommand::DeathEffect => "death_effect",
1177 ServerChatCommand::DebugColumn => "debug_column",
1178 ServerChatCommand::DebugWays => "debug_ways",
1179 ServerChatCommand::DisconnectAllPlayers => "disconnect_all_players",
1180 ServerChatCommand::DropAll => "dropall",
1181 ServerChatCommand::Dummy => "dummy",
1182 ServerChatCommand::Explosion => "explosion",
1183 ServerChatCommand::Faction => "faction",
1184 ServerChatCommand::GiveItem => "give_item",
1185 ServerChatCommand::Gizmos => "gizmos",
1186 ServerChatCommand::GizmosRange => "gizmos_range",
1187 ServerChatCommand::Goto => "goto",
1188 ServerChatCommand::GotoRand => "goto_rand",
1189 ServerChatCommand::Group => "group",
1190 ServerChatCommand::GroupInvite => "group_invite",
1191 ServerChatCommand::GroupKick => "group_kick",
1192 ServerChatCommand::GroupLeave => "group_leave",
1193 ServerChatCommand::GroupPromote => "group_promote",
1194 ServerChatCommand::Health => "health",
1195 ServerChatCommand::IntoNpc => "into_npc",
1196 ServerChatCommand::JoinFaction => "join_faction",
1197 ServerChatCommand::Jump => "jump",
1198 ServerChatCommand::Kick => "kick",
1199 ServerChatCommand::Kill => "kill",
1200 ServerChatCommand::KillNpcs => "kill_npcs",
1201 ServerChatCommand::Kit => "kit",
1202 ServerChatCommand::Lantern => "lantern",
1203 ServerChatCommand::Respawn => "respawn",
1204 ServerChatCommand::Light => "light",
1205 ServerChatCommand::MakeBlock => "make_block",
1206 ServerChatCommand::MakeNpc => "make_npc",
1207 ServerChatCommand::MakeSprite => "make_sprite",
1208 ServerChatCommand::Motd => "motd",
1209 ServerChatCommand::Object => "object",
1210 ServerChatCommand::Outcome => "outcome",
1211 ServerChatCommand::PermitBuild => "permit_build",
1212 ServerChatCommand::Players => "players",
1213 ServerChatCommand::Poise => "poise",
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::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}