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