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 #[strum(serialize = "battlemode_change")]
77 BattleModeChange,
78}
79
80lazy_static! {
81 static ref ALIGNMENTS: Vec<String> = ["wild", "enemy", "npc", "pet"]
82 .iter()
83 .map(|s| s.to_string())
84 .collect();
85 static ref SKILL_TREES: Vec<String> = ["general", "sword", "axe", "hammer", "bow", "staff", "sceptre", "mining"]
86 .iter()
87 .map(|s| s.to_string())
88 .collect();
89 pub static ref ENTITIES: Vec<String> = {
91 let npc_names = &npc::NPC_NAMES.read();
92
93 let mut categories = HashSet::new();
95 let mut species = HashSet::new();
96 for body in comp::Body::iter() {
97 if matches!(body, comp::Body::Plugin(_)) {
99 continue;
100 }
101
102 if let Some(meta) = npc_names.get_species_meta(&body) {
103 categories.insert(npc_names[&body].keyword.clone());
104 species.insert(meta.keyword.clone());
105 }
106 }
107
108 let mut strings = Vec::new();
109 strings.extend(categories);
110 strings.extend(species);
111
112 strings
113 };
114 static ref AREA_KINDS: Vec<String> = AreaKind::iter().map(|kind| kind.as_ref().to_string()).collect();
115 static ref OBJECTS: Vec<String> = comp::object::ALL_OBJECTS
116 .iter()
117 .map(|o| o.to_string().to_string())
118 .collect();
119 static ref RECIPES: Vec<String> = {
120 let rbm = RecipeBookManifest::load().cloned();
121 rbm.keys().cloned().collect::<Vec<String>>()
122 };
123 static ref TIMES: Vec<String> = [
124 "midnight", "night", "dawn", "morning", "day", "noon", "dusk"
125 ]
126 .iter()
127 .map(|s| s.to_string())
128 .collect();
129
130 static ref WEATHERS: Vec<String> = [
131 "clear", "cloudy", "rain", "wind", "storm"
132 ]
133 .iter()
134 .map(|s| s.to_string())
135 .collect();
136
137 pub static ref BUFF_PARSER: HashMap<String, BuffKind> = {
138 let string_from_buff = |kind| match kind {
139 BuffKind::Burning => "burning",
140 BuffKind::Regeneration => "regeneration",
141 BuffKind::Saturation => "saturation",
142 BuffKind::Bleeding => "bleeding",
143 BuffKind::Cursed => "cursed",
144 BuffKind::Potion => "potion",
145 BuffKind::Agility => "agility",
146 BuffKind::RestingHeal => "resting_heal",
147 BuffKind::EnergyRegen => "energy_regen",
148 BuffKind::ComboGeneration => "combo_generation",
149 BuffKind::IncreaseMaxEnergy => "increase_max_energy",
150 BuffKind::IncreaseMaxHealth => "increase_max_health",
151 BuffKind::Invulnerability => "invulnerability",
152 BuffKind::ProtectingWard => "protecting_ward",
153 BuffKind::Frenzied => "frenzied",
154 BuffKind::Crippled => "crippled",
155 BuffKind::Frozen => "frozen",
156 BuffKind::Wet => "wet",
157 BuffKind::Ensnared => "ensnared",
158 BuffKind::Poisoned => "poisoned",
159 BuffKind::Hastened => "hastened",
160 BuffKind::Fortitude => "fortitude",
161 BuffKind::Parried => "parried",
162 BuffKind::PotionSickness => "potion_sickness",
163 BuffKind::Reckless => "reckless",
164 BuffKind::Polymorphed => "polymorphed",
165 BuffKind::Flame => "flame",
166 BuffKind::Frigid => "frigid",
167 BuffKind::Lifesteal => "lifesteal",
168 BuffKind::ImminentCritical => "imminent_critical",
170 BuffKind::Fury => "fury",
171 BuffKind::Sunderer => "sunderer",
172 BuffKind::Defiance => "defiance",
173 BuffKind::Bloodfeast => "bloodfeast",
174 BuffKind::Berserk => "berserk",
175 BuffKind::Heatstroke => "heatstroke",
176 BuffKind::ScornfulTaunt => "scornful_taunt",
177 BuffKind::Rooted => "rooted",
178 BuffKind::Winded => "winded",
179 BuffKind::Amnesia => "amnesia",
180 BuffKind::OffBalance => "off_balance",
181 BuffKind::Tenacity => "tenacity",
182 BuffKind::Resilience => "resilience",
183 BuffKind::StormChaser => "storm_chaser",
184 BuffKind::EagleEye => "eagle_eye",
185 BuffKind::Chilled => "chilled",
186 BuffKind::ArdentHunt => "ardent_hunt",
187 BuffKind::IgniteArrow => "ignite_arrow",
188 BuffKind::FreezeArrow => "freeze_arrow",
189 BuffKind::DrenchArrow => "drench_arrow",
190 BuffKind::JoltArrow => "jolt_arrow",
191 };
192 let mut buff_parser = HashMap::new();
193 for kind in BuffKind::iter() {
194 buff_parser.insert(string_from_buff(kind).to_string(), kind);
195 }
196 buff_parser
197 };
198
199 pub static ref BUFF_PACK: Vec<String> = {
200 let mut buff_pack: Vec<_> = BUFF_PARSER.keys().cloned().collect();
201 buff_pack.retain(|kind| kind != "invulnerability");
203 buff_pack
204 };
205
206 static ref BUFFS: Vec<String> = {
207 let mut buff_pack: Vec<String> = BUFF_PARSER.keys().cloned().collect();
208
209 buff_pack.push("all".to_owned());
211 buff_pack.push("clear".to_owned());
212 buff_pack
213 };
214
215 static ref BLOCK_KINDS: Vec<String> = terrain::block::BlockKind::iter()
216 .map(|bk| bk.to_string())
217 .collect();
218
219 static ref SPRITE_KINDS: Vec<String> = terrain::sprite::SPRITE_KINDS
220 .keys()
221 .cloned()
222 .collect();
223
224 static ref OUTCOME_KINDS: Vec<String> = outcome::Outcome::VARIANTS
225 .iter()
226 .map(|s| s.to_string())
227 .collect();
228
229 static ref ROLES: Vec<String> = ["admin", "moderator"].iter().copied().map(Into::into).collect();
230
231 pub static ref ITEM_SPECS: Vec<String> = {
235 let mut items = try_all_item_defs()
236 .unwrap_or_else(|e| {
237 warn!(?e, "Failed to load item specifiers");
238 Vec::new()
239 });
240 items.sort();
241 items
242 };
243
244 pub static ref ENTITY_CONFIGS: Vec<String> = {
246 try_all_entity_configs()
247 .unwrap_or_else(|e| {
248 warn!(?e, "Failed to load entity configs");
249 Vec::new()
250 })
251 };
252
253 pub static ref KITS: Vec<String> = {
254 let mut kits = if let Ok(kits) = KitManifest::load_and_combine_static(KIT_MANIFEST_PATH) {
255 let mut kits = kits.read().0.keys().cloned().collect::<Vec<String>>();
256 kits.sort();
257 kits
258 } else {
259 Vec::new()
260 };
261 kits.push("all".to_owned());
262
263 kits
264 };
265
266 static ref PRESETS: HashMap<String, Vec<(Skill, u8)>> = {
267 if let Ok(presets) = SkillPresetManifest::load_and_combine_static(PRESET_MANIFEST_PATH) {
268 presets.read().0.clone()
269 } else {
270 warn!("Error while loading presets");
271 HashMap::new()
272 }
273 };
274
275 static ref PRESET_LIST: Vec<String> = {
276 let mut preset_list: Vec<String> = PRESETS.keys().cloned().collect();
277 preset_list.push("clear".to_owned());
278
279 preset_list
280 };
281
282 pub static ref SPOT_PARSER: HashMap<String, Spot> = {
284 let spot_to_string = |kind| match kind {
285 Spot::DwarvenGrave => "dwarven_grave",
286 Spot::SaurokAltar => "saurok_altar",
287 Spot::MyrmidonTemple => "myrmidon_temple",
288 Spot::GnarlingTotem => "gnarling_totem",
289 Spot::WitchHouse => "witch_house",
290 Spot::GnomeSpring => "gnome_spring",
291 Spot::WolfBurrow => "wolf_burrow",
292 Spot::Igloo => "igloo",
293 Spot::LionRock => "lion_rock",
294 Spot::TreeStumpForest => "tree_stump_forest",
295 Spot::DesertBones => "desert_bones",
296 Spot::Arch => "arch",
297 Spot::AirshipCrash => "airship_crash",
298 Spot::FruitTree => "fruit_tree",
299 Spot::Shipwreck => "shipwreck",
300 Spot::Shipwreck2 => "shipwreck2",
301 Spot::FallenTree => "fallen_tree",
302 Spot::GraveSmall => "grave_small",
303 Spot::JungleTemple => "jungle_temple",
304 Spot::SaurokTotem => "saurok_totem",
305 Spot::JungleOutpost => "jungle_outpost",
306 Spot::RonFile(props) => &props.base_structures,
308 };
309
310 let mut map = HashMap::new();
311 for spot_kind in Spot::iter() {
312 map.insert(spot_to_string(spot_kind).to_owned(), spot_kind);
313 }
314
315 map
316 };
317
318 pub static ref SPOTS: Vec<String> = {
319 let mut config_spots = crate::spot::RON_SPOT_PROPERTIES
320 .0
321 .iter()
322 .map(|s| s.base_structures.clone())
323 .collect::<Vec<_>>();
324
325 config_spots.extend(SPOT_PARSER.keys().cloned());
326 config_spots
327 };
328}
329
330pub enum EntityTarget {
331 Player(String),
332 RtsimNpc(String),
333 Uid(crate::uid::Uid),
334}
335
336impl FromStr for EntityTarget {
337 type Err = String;
338
339 fn from_str(s: &str) -> Result<Self, Self::Err> {
340 if let Some((spec, data)) = s.split_once('@') {
342 match spec {
343 "rtsim" => Ok(EntityTarget::RtsimNpc(data.to_string())),
344 "uid" => {
345 let raw = u64::from_str(data).map_err(|_| {
346 format!("Expected a valid number after 'uid@' but found {data}.")
347 })?;
348 let nz =
349 NonZeroU64::new(raw).ok_or_else(|| "Uid cannot be zero".to_string())?;
350 Ok(EntityTarget::Uid(Uid(nz)))
351 },
352 _ => Err(format!(
353 "Expected either 'rtsim' or 'uid' before '@' but found '{spec}'"
354 )),
355 }
356 } else {
357 Ok(EntityTarget::Player(s.to_string()))
358 }
359 }
360}
361
362#[derive(Copy, Clone, strum::EnumIter)]
364pub enum ServerChatCommand {
365 Adminify,
366 Airship,
367 Alias,
368 AreaAdd,
369 AreaList,
370 AreaRemove,
371 Aura,
372 Ban,
373 BanIp,
374 BanLog,
375 BattleMode,
376 BattleModeForce,
377 Body,
378 Buff,
379 Build,
380 Campfire,
381 ClearPersistedTerrain,
382 CreateLocation,
383 DeathEffect,
384 DebugColumn,
385 DebugWays,
386 DeleteLocation,
387 DestroyTethers,
388 DisconnectAllPlayers,
389 Dismount,
390 DropAll,
391 Dummy,
392 Explosion,
393 Faction,
394 GiveItem,
395 Gizmos,
396 GizmosRange,
397 Goto,
398 GotoRand,
399 Group,
400 GroupInvite,
401 GroupKick,
402 GroupLeave,
403 GroupPromote,
404 Health,
405 IntoNpc,
406 JoinFaction,
407 Jump,
408 Kick,
409 Kill,
410 KillNpcs,
411 Kit,
412 Lantern,
413 Light,
414 Lightning,
415 Location,
416 MakeBlock,
417 MakeNpc,
418 MakeSprite,
419 MakeVolume,
420 Motd,
421 Mount,
422 Object,
423 Outcome,
424 PermitBuild,
425 Players,
426 Poise,
427 Portal,
428 Region,
429 ReloadChunks,
430 RemoveLights,
431 RepairEquipment,
432 ResetRecipes,
433 Respawn,
434 RevokeBuild,
435 RevokeBuildAll,
436 RtsimChunk,
437 RtsimInfo,
438 RtsimNpc,
439 RtsimPurge,
440 RtsimTp,
441 Safezone,
442 Say,
443 Scale,
444 ServerPhysics,
445 SetBodyType,
446 SetMotd,
447 SetWaypoint,
448 Ship,
449 Site,
450 SkillPoint,
451 SkillPreset,
452 Spawn,
453 Spot,
454 Sudo,
455 Tell,
456 Tether,
457 Time,
458 TimeScale,
459 Tp,
460 Unban,
461 UnbanIp,
462 Version,
463 WeatherZone,
464 Whitelist,
465 Wiring,
466 World,
467}
468
469impl ServerChatCommand {
470 pub fn data(&self) -> ChatCommandData {
471 use ArgumentSpec::*;
472 use Requirement::*;
473 use Role::*;
474 let cmd = ChatCommandData::new;
475 match self {
476 ServerChatCommand::Adminify => cmd(
477 vec![PlayerName(Required), Enum("role", ROLES.clone(), Optional)],
478 Content::localized("command-adminify-desc"),
479 Some(Admin),
480 ),
481 ServerChatCommand::Airship => cmd(
482 vec![
483 Enum(
484 "kind",
485 comp::ship::ALL_AIRSHIPS
486 .iter()
487 .map(|b| format!("{b:?}"))
488 .collect(),
489 Optional,
490 ),
491 Float("destination_degrees_ccw_of_east", 90.0, Optional),
492 ],
493 Content::localized("command-airship-desc"),
494 Some(Admin),
495 ),
496 ServerChatCommand::Alias => cmd(
497 vec![Any("name", Required)],
498 Content::localized("command-alias-desc"),
499 Some(Moderator),
500 ),
501 ServerChatCommand::Aura => cmd(
502 vec![
503 Float("aura_radius", 10.0, Required),
504 Float("aura_duration", 10.0, Optional),
505 Boolean("new_entity", "true".to_string(), Optional),
506 Enum("aura_target", GroupTarget::all_options(), Optional),
507 Enum("aura_kind", AuraKindVariant::all_options(), Required),
508 Any("aura spec", Optional),
509 ],
510 Content::localized("command-aura-desc"),
511 Some(Admin),
512 ),
513 ServerChatCommand::Buff => cmd(
514 vec![
515 Enum("buff", BUFFS.clone(), Required),
516 Float("strength", 0.01, Optional),
517 Float("duration", 10.0, Optional),
518 Any("buff data spec", Optional),
519 ],
520 Content::localized("command-buff-desc"),
521 Some(Admin),
522 ),
523 ServerChatCommand::Ban => cmd(
524 vec![
525 PlayerName(Required),
526 Boolean("overwrite", "true".to_string(), Optional),
527 Any("ban duration", Optional),
528 Message(Optional),
529 ],
530 Content::localized("command-ban-desc"),
531 Some(Moderator),
532 ),
533 ServerChatCommand::BanIp => cmd(
534 vec![
535 PlayerName(Required),
536 Boolean("overwrite", "true".to_string(), Optional),
537 Any("ban duration", Optional),
538 Message(Optional),
539 ],
540 Content::localized("command-ban-ip-desc"),
541 Some(Moderator),
542 ),
543 ServerChatCommand::BanLog => cmd(
544 vec![PlayerName(Required), Integer("max entries", 10, Optional)],
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![
870 Integer("chunk_radius", 6, Optional),
871 Boolean("only_sites", "true".to_string(), Optional),
872 ],
873 Content::localized("command-reload_chunks-desc"),
874 Some(Admin),
875 ),
876 ServerChatCommand::ResetRecipes => cmd(
877 vec![],
878 Content::localized("command-reset_recipes-desc"),
879 Some(Admin),
880 ),
881 ServerChatCommand::RemoveLights => cmd(
882 vec![Float("radius", 20.0, Optional)],
883 Content::localized("command-remove_lights-desc"),
884 Some(Admin),
885 ),
886 ServerChatCommand::RevokeBuild => cmd(
887 vec![Any("area_name", Required)],
888 Content::localized("command-revoke_build-desc"),
889 Some(Admin),
890 ),
891 ServerChatCommand::RevokeBuildAll => cmd(
892 vec![],
893 Content::localized("command-revoke_build_all-desc"),
894 Some(Admin),
895 ),
896 ServerChatCommand::Region => cmd(
897 vec![Message(Optional)],
898 Content::localized("command-region-desc"),
899 None,
900 ),
901 ServerChatCommand::Safezone => cmd(
902 vec![Float("range", 100.0, Optional)],
903 Content::localized("command-safezone-desc"),
904 Some(Moderator),
905 ),
906 ServerChatCommand::Say => cmd(
907 vec![Message(Optional)],
908 Content::localized("command-say-desc"),
909 None,
910 ),
911 ServerChatCommand::ServerPhysics => cmd(
912 vec![
913 PlayerName(Required),
914 Boolean("enabled", "true".to_string(), Optional),
915 Message(Optional),
916 ],
917 Content::localized("command-server_physics-desc"),
918 Some(Moderator),
919 ),
920 ServerChatCommand::SetMotd => cmd(
921 vec![Any("locale", Optional), Message(Optional)],
922 Content::localized("command-set_motd-desc"),
923 Some(Admin),
924 ),
925 ServerChatCommand::SetBodyType => cmd(
926 vec![
927 Enum(
928 "body type",
929 vec!["Female".to_string(), "Male".to_string()],
930 Required,
931 ),
932 Boolean("permanent", "false".to_string(), Requirement::Optional),
933 ],
934 Content::localized("command-set_body_type-desc"),
935 Some(Admin),
936 ),
937 ServerChatCommand::Ship => cmd(
938 vec![
939 Enum(
940 "kind",
941 comp::ship::ALL_SHIPS
942 .iter()
943 .map(|b| format!("{b:?}"))
944 .collect(),
945 Optional,
946 ),
947 Boolean(
948 "Whether the ship should be tethered to the target (or its mount)",
949 "false".to_string(),
950 Optional,
951 ),
952 Float("destination_degrees_ccw_of_east", 90.0, Optional),
953 ],
954 Content::localized("command-ship-desc"),
955 Some(Admin),
956 ),
957 ServerChatCommand::Site => cmd(
960 vec![
961 SiteName(Required),
962 Boolean("Dismount from ship", "true".to_string(), Optional),
963 ],
964 Content::localized("command-site-desc"),
965 Some(Moderator),
966 ),
967 ServerChatCommand::SkillPoint => cmd(
968 vec![
969 Enum("skill tree", SKILL_TREES.clone(), Required),
970 Integer("amount", 1, Optional),
971 ],
972 Content::localized("command-skill_point-desc"),
973 Some(Admin),
974 ),
975 ServerChatCommand::SkillPreset => cmd(
976 vec![Enum("preset_name", PRESET_LIST.to_vec(), Required)],
977 Content::localized("command-skill_preset-desc"),
978 Some(Admin),
979 ),
980 ServerChatCommand::Spawn => cmd(
981 vec![
982 Enum("alignment", ALIGNMENTS.clone(), Required),
983 Enum("entity", ENTITIES.clone(), Required),
984 Integer("amount", 1, Optional),
985 Boolean("ai", "true".to_string(), Optional),
986 Float("scale", 1.0, Optional),
987 Boolean("tethered", "false".to_string(), Optional),
988 ],
989 Content::localized("command-spawn-desc"),
990 Some(Admin),
991 ),
992 ServerChatCommand::Spot => cmd(
993 vec![Enum("Spot kind to find", SPOTS.clone(), Required)],
994 Content::localized("command-spot-desc"),
995 Some(Admin),
996 ),
997 ServerChatCommand::Sudo => cmd(
998 vec![EntityTarget(Required), SubCommand],
999 Content::localized("command-sudo-desc"),
1000 Some(Moderator),
1001 ),
1002 ServerChatCommand::Tell => cmd(
1003 vec![PlayerName(Required), Message(Optional)],
1004 Content::localized("command-tell-desc"),
1005 None,
1006 ),
1007 ServerChatCommand::Time => cmd(
1008 vec![Enum("time", TIMES.clone(), Optional)],
1009 Content::localized("command-time-desc"),
1010 Some(Admin),
1011 ),
1012 ServerChatCommand::TimeScale => cmd(
1013 vec![Float("time scale", 1.0, Optional)],
1014 Content::localized("command-time_scale-desc"),
1015 Some(Admin),
1016 ),
1017 ServerChatCommand::Tp => cmd(
1018 vec![
1019 EntityTarget(Optional),
1020 Boolean("Dismount from ship", "true".to_string(), Optional),
1021 ],
1022 Content::localized("command-tp-desc"),
1023 Some(Moderator),
1024 ),
1025 ServerChatCommand::RtsimTp => cmd(
1026 vec![
1027 Integer("npc index", 0, Required),
1028 Boolean("Dismount from ship", "true".to_string(), Optional),
1029 ],
1030 Content::localized("command-rtsim_tp-desc"),
1031 Some(Admin),
1032 ),
1033 ServerChatCommand::RtsimInfo => cmd(
1034 vec![Integer("npc index", 0, Required)],
1035 Content::localized("command-rtsim_info-desc"),
1036 Some(Admin),
1037 ),
1038 ServerChatCommand::RtsimNpc => cmd(
1039 vec![Any("query", Required), Integer("max number", 20, Optional)],
1040 Content::localized("command-rtsim_npc-desc"),
1041 Some(Admin),
1042 ),
1043 ServerChatCommand::RtsimPurge => cmd(
1044 vec![Boolean(
1045 "whether purging of rtsim data should occur on next startup",
1046 true.to_string(),
1047 Required,
1048 )],
1049 Content::localized("command-rtsim_purge-desc"),
1050 Some(Admin),
1051 ),
1052 ServerChatCommand::RtsimChunk => cmd(
1053 vec![],
1054 Content::localized("command-rtsim_chunk-desc"),
1055 Some(Admin),
1056 ),
1057 ServerChatCommand::Unban => cmd(
1058 vec![PlayerName(Required)],
1059 Content::localized("command-unban-desc"),
1060 Some(Moderator),
1061 ),
1062 ServerChatCommand::UnbanIp => cmd(
1063 vec![PlayerName(Required)],
1064 Content::localized("command-unban-ip-desc"),
1065 Some(Moderator),
1066 ),
1067 ServerChatCommand::Version => {
1068 cmd(vec![], Content::localized("command-version-desc"), None)
1069 },
1070 ServerChatCommand::SetWaypoint => cmd(
1071 vec![],
1072 Content::localized("command-set-waypoint-desc"),
1073 Some(Admin),
1074 ),
1075 ServerChatCommand::Wiring => cmd(
1076 vec![],
1077 Content::localized("command-wiring-desc"),
1078 Some(Admin),
1079 ),
1080 ServerChatCommand::Whitelist => cmd(
1081 vec![Any("add/remove", Required), PlayerName(Required)],
1082 Content::localized("command-whitelist-desc"),
1083 Some(Moderator),
1084 ),
1085 ServerChatCommand::World => cmd(
1086 vec![Message(Optional)],
1087 Content::localized("command-world-desc"),
1088 None,
1089 ),
1090 ServerChatCommand::MakeVolume => cmd(
1091 vec![Integer("size", 15, Optional)],
1092 Content::localized("command-make_volume-desc"),
1093 Some(Admin),
1094 ),
1095 ServerChatCommand::Location => cmd(
1096 vec![Any("name", Required)],
1097 Content::localized("command-location-desc"),
1098 None,
1099 ),
1100 ServerChatCommand::CreateLocation => cmd(
1101 vec![Any("name", Required)],
1102 Content::localized("command-create_location-desc"),
1103 Some(Moderator),
1104 ),
1105 ServerChatCommand::DeleteLocation => cmd(
1106 vec![Any("name", Required)],
1107 Content::localized("command-delete_location-desc"),
1108 Some(Moderator),
1109 ),
1110 ServerChatCommand::WeatherZone => cmd(
1111 vec![
1112 Enum("weather kind", WEATHERS.clone(), Required),
1113 Float("radius", 500.0, Optional),
1114 Float("time", 300.0, Optional),
1115 ],
1116 Content::localized("command-weather_zone-desc"),
1117 Some(Admin),
1118 ),
1119 ServerChatCommand::Lightning => cmd(
1120 vec![],
1121 Content::localized("command-lightning-desc"),
1122 Some(Admin),
1123 ),
1124 ServerChatCommand::Scale => cmd(
1125 vec![
1126 Float("factor", 1.0, Required),
1127 Boolean("reset_mass", true.to_string(), Optional),
1128 ],
1129 Content::localized("command-scale-desc"),
1130 Some(Admin),
1131 ),
1132 ServerChatCommand::RepairEquipment => cmd(
1133 vec![ArgumentSpec::Boolean(
1134 "repair inventory",
1135 true.to_string(),
1136 Optional,
1137 )],
1138 Content::localized("command-repair_equipment-desc"),
1139 Some(Admin),
1140 ),
1141 ServerChatCommand::Tether => cmd(
1142 vec![
1143 EntityTarget(Required),
1144 Boolean("automatic length", "true".to_string(), Optional),
1145 ],
1146 Content::localized("command-tether-desc"),
1147 Some(Admin),
1148 ),
1149 ServerChatCommand::DestroyTethers => cmd(
1150 vec![],
1151 Content::localized("command-destroy_tethers-desc"),
1152 Some(Admin),
1153 ),
1154 ServerChatCommand::Mount => cmd(
1155 vec![EntityTarget(Required)],
1156 Content::localized("command-mount-desc"),
1157 Some(Admin),
1158 ),
1159 ServerChatCommand::Dismount => cmd(
1160 vec![EntityTarget(Required)],
1161 Content::localized("command-dismount-desc"),
1162 Some(Admin),
1163 ),
1164 }
1165 }
1166
1167 pub fn keyword(&self) -> &'static str {
1169 match self {
1170 ServerChatCommand::Adminify => "adminify",
1171 ServerChatCommand::Airship => "airship",
1172 ServerChatCommand::Alias => "alias",
1173 ServerChatCommand::AreaAdd => "area_add",
1174 ServerChatCommand::AreaList => "area_list",
1175 ServerChatCommand::AreaRemove => "area_remove",
1176 ServerChatCommand::Aura => "aura",
1177 ServerChatCommand::Ban => "ban",
1178 ServerChatCommand::BanIp => "ban_ip",
1179 ServerChatCommand::BanLog => "ban_log",
1180 ServerChatCommand::BattleMode => "battlemode",
1181 ServerChatCommand::BattleModeForce => "battlemode_force",
1182 ServerChatCommand::Body => "body",
1183 ServerChatCommand::Buff => "buff",
1184 ServerChatCommand::Build => "build",
1185 ServerChatCommand::Campfire => "campfire",
1186 ServerChatCommand::ClearPersistedTerrain => "clear_persisted_terrain",
1187 ServerChatCommand::DeathEffect => "death_effect",
1188 ServerChatCommand::DebugColumn => "debug_column",
1189 ServerChatCommand::DebugWays => "debug_ways",
1190 ServerChatCommand::DisconnectAllPlayers => "disconnect_all_players",
1191 ServerChatCommand::DropAll => "dropall",
1192 ServerChatCommand::Dummy => "dummy",
1193 ServerChatCommand::Explosion => "explosion",
1194 ServerChatCommand::Faction => "faction",
1195 ServerChatCommand::GiveItem => "give_item",
1196 ServerChatCommand::Gizmos => "gizmos",
1197 ServerChatCommand::GizmosRange => "gizmos_range",
1198 ServerChatCommand::Goto => "goto",
1199 ServerChatCommand::GotoRand => "goto_rand",
1200 ServerChatCommand::Group => "group",
1201 ServerChatCommand::GroupInvite => "group_invite",
1202 ServerChatCommand::GroupKick => "group_kick",
1203 ServerChatCommand::GroupLeave => "group_leave",
1204 ServerChatCommand::GroupPromote => "group_promote",
1205 ServerChatCommand::Health => "health",
1206 ServerChatCommand::IntoNpc => "into_npc",
1207 ServerChatCommand::JoinFaction => "join_faction",
1208 ServerChatCommand::Jump => "jump",
1209 ServerChatCommand::Kick => "kick",
1210 ServerChatCommand::Kill => "kill",
1211 ServerChatCommand::KillNpcs => "kill_npcs",
1212 ServerChatCommand::Kit => "kit",
1213 ServerChatCommand::Lantern => "lantern",
1214 ServerChatCommand::Respawn => "respawn",
1215 ServerChatCommand::Light => "light",
1216 ServerChatCommand::MakeBlock => "make_block",
1217 ServerChatCommand::MakeNpc => "make_npc",
1218 ServerChatCommand::MakeSprite => "make_sprite",
1219 ServerChatCommand::Motd => "motd",
1220 ServerChatCommand::Object => "object",
1221 ServerChatCommand::Outcome => "outcome",
1222 ServerChatCommand::PermitBuild => "permit_build",
1223 ServerChatCommand::Players => "players",
1224 ServerChatCommand::Poise => "poise",
1225 ServerChatCommand::Portal => "portal",
1226 ServerChatCommand::ResetRecipes => "reset_recipes",
1227 ServerChatCommand::Region => "region",
1228 ServerChatCommand::ReloadChunks => "reload_chunks",
1229 ServerChatCommand::RemoveLights => "remove_lights",
1230 ServerChatCommand::RevokeBuild => "revoke_build",
1231 ServerChatCommand::RevokeBuildAll => "revoke_build_all",
1232 ServerChatCommand::Safezone => "safezone",
1233 ServerChatCommand::Say => "say",
1234 ServerChatCommand::ServerPhysics => "server_physics",
1235 ServerChatCommand::SetMotd => "set_motd",
1236 ServerChatCommand::SetBodyType => "set_body_type",
1237 ServerChatCommand::Ship => "ship",
1238 ServerChatCommand::Site => "site",
1239 ServerChatCommand::SkillPoint => "skill_point",
1240 ServerChatCommand::SkillPreset => "skill_preset",
1241 ServerChatCommand::Spawn => "spawn",
1242 ServerChatCommand::Spot => "spot",
1243 ServerChatCommand::Sudo => "sudo",
1244 ServerChatCommand::Tell => "tell",
1245 ServerChatCommand::Time => "time",
1246 ServerChatCommand::TimeScale => "time_scale",
1247 ServerChatCommand::Tp => "tp",
1248 ServerChatCommand::RtsimTp => "rtsim_tp",
1249 ServerChatCommand::RtsimInfo => "rtsim_info",
1250 ServerChatCommand::RtsimNpc => "rtsim_npc",
1251 ServerChatCommand::RtsimPurge => "rtsim_purge",
1252 ServerChatCommand::RtsimChunk => "rtsim_chunk",
1253 ServerChatCommand::Unban => "unban",
1254 ServerChatCommand::UnbanIp => "unban_ip",
1255 ServerChatCommand::Version => "version",
1256 ServerChatCommand::SetWaypoint => "set_waypoint",
1257 ServerChatCommand::Wiring => "wiring",
1258 ServerChatCommand::Whitelist => "whitelist",
1259 ServerChatCommand::World => "world",
1260 ServerChatCommand::MakeVolume => "make_volume",
1261 ServerChatCommand::Location => "location",
1262 ServerChatCommand::CreateLocation => "create_location",
1263 ServerChatCommand::DeleteLocation => "delete_location",
1264 ServerChatCommand::WeatherZone => "weather_zone",
1265 ServerChatCommand::Lightning => "lightning",
1266 ServerChatCommand::Scale => "scale",
1267 ServerChatCommand::RepairEquipment => "repair_equipment",
1268 ServerChatCommand::Tether => "tether",
1269 ServerChatCommand::DestroyTethers => "destroy_tethers",
1270 ServerChatCommand::Mount => "mount",
1271 ServerChatCommand::Dismount => "dismount",
1272 }
1273 }
1274
1275 pub fn short_keyword(&self) -> Option<&'static str> {
1278 Some(match self {
1279 ServerChatCommand::Faction => "f",
1280 ServerChatCommand::Group => "g",
1281 ServerChatCommand::Region => "r",
1282 ServerChatCommand::Say => "s",
1283 ServerChatCommand::Tell => "t",
1284 ServerChatCommand::World => "w",
1285 _ => return None,
1286 })
1287 }
1288
1289 pub fn iter() -> impl Iterator<Item = Self> + Clone { <Self as IntoEnumIterator>::iter() }
1291
1292 pub fn help_content(&self) -> Content {
1294 let data = self.data();
1295
1296 let usage = std::iter::once(format!("/{}", self.keyword()))
1297 .chain(data.args.iter().map(|arg| arg.usage_string()))
1298 .collect::<Vec<_>>()
1299 .join(" ");
1300
1301 Content::localized_with_args("command-help-template", [
1302 ("usage", Content::Plain(usage)),
1303 ("description", data.description),
1304 ])
1305 }
1306
1307 pub fn iter_with_keywords() -> impl Iterator<Item = (&'static str, Self)> {
1311 Self::iter()
1312 .filter_map(|c| c.short_keyword().map(|s| (s, c)))
1314 .chain(Self::iter().map(|c| (c.keyword(), c)))
1315 }
1316
1317 pub fn needs_role(&self) -> Option<comp::AdminRole> { self.data().needs_role }
1318
1319 pub fn arg_fmt(&self) -> String {
1321 self.data()
1322 .args
1323 .iter()
1324 .map(|arg| match arg {
1325 ArgumentSpec::PlayerName(_) => "{}",
1326 ArgumentSpec::EntityTarget(_) => "{}",
1327 ArgumentSpec::SiteName(_) => "{/.*/}",
1328 ArgumentSpec::Float(_, _, _) => "{}",
1329 ArgumentSpec::Integer(_, _, _) => "{d}",
1330 ArgumentSpec::Any(_, _) => "{}",
1331 ArgumentSpec::Command(_) => "{}",
1332 ArgumentSpec::Message(_) => "{/.*/}",
1333 ArgumentSpec::SubCommand => "{} {/.*/}",
1334 ArgumentSpec::Enum(_, _, _) => "{}",
1335 ArgumentSpec::AssetPath(_, _, _, _) => "{}",
1336 ArgumentSpec::Boolean(_, _, _) => "{}",
1337 ArgumentSpec::Flag(_) => "{}",
1338 })
1339 .collect::<Vec<_>>()
1340 .join(" ")
1341 }
1342}
1343
1344impl Display for ServerChatCommand {
1345 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
1346 write!(f, "{}", self.keyword())
1347 }
1348}
1349
1350impl FromStr for ServerChatCommand {
1351 type Err = ();
1352
1353 fn from_str(keyword: &str) -> Result<ServerChatCommand, ()> {
1354 Self::iter()
1355 .filter_map(|c| c.short_keyword().map(|s| (s, c)))
1357 .chain(Self::iter().map(|c| (c.keyword(), c)))
1358 .find_map(|(kwd, command)| (kwd == keyword).then_some(command))
1360 .ok_or(())
1362 }
1363}
1364
1365#[derive(Eq, PartialEq, Debug, Clone, Copy)]
1366pub enum Requirement {
1367 Required,
1368 Optional,
1369}
1370
1371pub enum ArgumentSpec {
1373 PlayerName(Requirement),
1375 EntityTarget(Requirement),
1377 SiteName(Requirement),
1379 Float(&'static str, f32, Requirement),
1384 Integer(&'static str, i32, Requirement),
1389 Any(&'static str, Requirement),
1391 Command(Requirement),
1393 Message(Requirement),
1396 SubCommand,
1398 Enum(&'static str, Vec<String>, Requirement),
1403 AssetPath(&'static str, &'static str, Vec<String>, Requirement),
1409 Boolean(&'static str, String, Requirement),
1414 Flag(&'static str),
1416}
1417
1418impl ArgumentSpec {
1419 pub fn usage_string(&self) -> String {
1420 match self {
1421 ArgumentSpec::PlayerName(req) => {
1422 if &Requirement::Required == req {
1423 "<player>".to_string()
1424 } else {
1425 "[player]".to_string()
1426 }
1427 },
1428 ArgumentSpec::EntityTarget(req) => {
1429 if &Requirement::Required == req {
1430 "<entity>".to_string()
1431 } else {
1432 "[entity]".to_string()
1433 }
1434 },
1435 ArgumentSpec::SiteName(req) => {
1436 if &Requirement::Required == req {
1437 "<site>".to_string()
1438 } else {
1439 "[site]".to_string()
1440 }
1441 },
1442 ArgumentSpec::Float(label, _, req) => {
1443 if &Requirement::Required == req {
1444 format!("<{}>", label)
1445 } else {
1446 format!("[{}]", label)
1447 }
1448 },
1449 ArgumentSpec::Integer(label, _, req) => {
1450 if &Requirement::Required == req {
1451 format!("<{}>", label)
1452 } else {
1453 format!("[{}]", label)
1454 }
1455 },
1456 ArgumentSpec::Any(label, req) => {
1457 if &Requirement::Required == req {
1458 format!("<{}>", label)
1459 } else {
1460 format!("[{}]", label)
1461 }
1462 },
1463 ArgumentSpec::Command(req) => {
1464 if &Requirement::Required == req {
1465 "<[/]command>".to_string()
1466 } else {
1467 "[[/]command]".to_string()
1468 }
1469 },
1470 ArgumentSpec::Message(req) => {
1471 if &Requirement::Required == req {
1472 "<message>".to_string()
1473 } else {
1474 "[message]".to_string()
1475 }
1476 },
1477 ArgumentSpec::SubCommand => "<[/]command> [args...]".to_string(),
1478 ArgumentSpec::Enum(label, _, req) => {
1479 if &Requirement::Required == req {
1480 format!("<{}>", label)
1481 } else {
1482 format!("[{}]", label)
1483 }
1484 },
1485 ArgumentSpec::AssetPath(label, _, _, req) => {
1486 if &Requirement::Required == req {
1487 format!("<{}>", label)
1488 } else {
1489 format!("[{}]", label)
1490 }
1491 },
1492 ArgumentSpec::Boolean(label, _, req) => {
1493 if &Requirement::Required == req {
1494 format!("<{}>", label)
1495 } else {
1496 format!("[{}]", label)
1497 }
1498 },
1499 ArgumentSpec::Flag(label) => {
1500 format!("[{}]", label)
1501 },
1502 }
1503 }
1504
1505 pub fn requirement(&self) -> Requirement {
1506 match self {
1507 ArgumentSpec::PlayerName(r)
1508 | ArgumentSpec::EntityTarget(r)
1509 | ArgumentSpec::SiteName(r)
1510 | ArgumentSpec::Float(_, _, r)
1511 | ArgumentSpec::Integer(_, _, r)
1512 | ArgumentSpec::Any(_, r)
1513 | ArgumentSpec::Command(r)
1514 | ArgumentSpec::Message(r)
1515 | ArgumentSpec::Enum(_, _, r)
1516 | ArgumentSpec::AssetPath(_, _, _, r)
1517 | ArgumentSpec::Boolean(_, _, r) => *r,
1518 ArgumentSpec::Flag(_) => Requirement::Optional,
1519 ArgumentSpec::SubCommand => Requirement::Required,
1520 }
1521 }
1522}
1523
1524pub trait CommandEnumArg: FromStr {
1525 fn all_options() -> Vec<String>;
1526}
1527
1528macro_rules! impl_from_to_str_cmd {
1529 ($enum:ident, ($($attribute:ident => $str:expr),*)) => {
1530 impl std::str::FromStr for $enum {
1531 type Err = String;
1532
1533 fn from_str(s: &str) -> Result<Self, Self::Err> {
1534 match s {
1535 $(
1536 $str => Ok($enum::$attribute),
1537 )*
1538 s => Err(format!("Invalid variant: {s}")),
1539 }
1540 }
1541 }
1542
1543 impl $crate::cmd::CommandEnumArg for $enum {
1544 fn all_options() -> Vec<String> {
1545 vec![$($str.to_string()),*]
1546 }
1547 }
1548 }
1549}
1550
1551impl_from_to_str_cmd!(AuraKindVariant, (
1552 Buff => "buff",
1553 FriendlyFire => "friendly_fire",
1554 ForcePvP => "force_pvp"
1555));
1556
1557impl_from_to_str_cmd!(GroupTarget, (
1558 InGroup => "in_group",
1559 OutOfGroup => "out_of_group",
1560 All => "all"
1561));
1562
1563#[macro_export]
1566macro_rules! parse_cmd_args {
1567 ($args:expr, $($t:ty),* $(, ..$tail:ty)? $(,)?) => {
1568 {
1569 let mut args = $args.into_iter().peekable();
1570 (
1571 $({
1584 let parsed = args.peek().and_then(|s| s.parse::<$t>().ok());
1585 if parsed.is_some() { args.next(); }
1587 parsed
1588 }),*
1589 $(, args.map(|s| s.to_string()).collect::<$tail>())?
1590 )
1591 }
1592 };
1593}
1594
1595#[cfg(test)]
1596mod tests {
1597 use super::*;
1598 use crate::comp::Item;
1599
1600 #[test]
1601 fn verify_cmd_list_sorted() {
1602 let mut list = ServerChatCommand::iter()
1603 .map(|c| c.keyword())
1604 .collect::<Vec<_>>();
1605
1606 let list2 = list.clone();
1608 list.sort_unstable();
1609 assert_eq!(list, list2);
1610 }
1611
1612 #[test]
1613 fn test_loading_skill_presets() {
1614 SkillPresetManifest::load_expect_combined_static(PRESET_MANIFEST_PATH);
1615 }
1616
1617 #[test]
1618 fn test_load_kits() {
1619 let kits = KitManifest::load_expect_combined_static(KIT_MANIFEST_PATH).read();
1620 let mut rng = rand::rng();
1621 for kit in kits.0.values() {
1622 for (item_id, _) in kit.iter() {
1623 match item_id {
1624 KitSpec::Item(item_id) => {
1625 Item::new_from_asset_expect(item_id);
1626 },
1627 KitSpec::ModularWeaponSet {
1628 tool,
1629 material,
1630 hands,
1631 } => {
1632 comp::item::modular::generate_weapons(*tool, *material, *hands)
1633 .unwrap_or_else(|_| {
1634 panic!(
1635 "Failed to synthesize a modular {tool:?} set made of \
1636 {material:?}."
1637 )
1638 });
1639 },
1640 KitSpec::ModularWeaponRandom {
1641 tool,
1642 material,
1643 hands,
1644 } => {
1645 comp::item::modular::random_weapon(*tool, *material, *hands, &mut rng)
1646 .unwrap_or_else(|_| {
1647 panic!(
1648 "Failed to synthesize a random {hands:?}-handed modular \
1649 {tool:?} made of {material:?}."
1650 )
1651 });
1652 },
1653 }
1654 }
1655 }
1656 }
1657}