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::OwlTalon => "owl_talon",
182 BuffKind::HeavyNock => "heavy_nock",
183 BuffKind::Heartseeker => "heartseeker",
184 BuffKind::EagleEye => "eagle_eye",
185 BuffKind::Chilled => "chilled",
186 BuffKind::ArdentHunter => "ardent_hunter",
187 BuffKind::ArdentHunted => "ardent_hunted",
188 BuffKind::SepticShot => "septic_shot",
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(u64),
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(u64::from_str(data).map_err(
342 |_| format!("Expected a valid number after 'rtsim@' but found {data}."),
343 )?)),
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![Integer("chunk_radius", 6, Optional)],
870 Content::localized("command-reload_chunks-desc"),
871 Some(Admin),
872 ),
873 ServerChatCommand::ResetRecipes => cmd(
874 vec![],
875 Content::localized("command-reset_recipes-desc"),
876 Some(Admin),
877 ),
878 ServerChatCommand::RemoveLights => cmd(
879 vec![Float("radius", 20.0, Optional)],
880 Content::localized("command-remove_lights-desc"),
881 Some(Admin),
882 ),
883 ServerChatCommand::RevokeBuild => cmd(
884 vec![Any("area_name", Required)],
885 Content::localized("command-revoke_build-desc"),
886 Some(Admin),
887 ),
888 ServerChatCommand::RevokeBuildAll => cmd(
889 vec![],
890 Content::localized("command-revoke_build_all-desc"),
891 Some(Admin),
892 ),
893 ServerChatCommand::Region => cmd(
894 vec![Message(Optional)],
895 Content::localized("command-region-desc"),
896 None,
897 ),
898 ServerChatCommand::Safezone => cmd(
899 vec![Float("range", 100.0, Optional)],
900 Content::localized("command-safezone-desc"),
901 Some(Moderator),
902 ),
903 ServerChatCommand::Say => cmd(
904 vec![Message(Optional)],
905 Content::localized("command-say-desc"),
906 None,
907 ),
908 ServerChatCommand::ServerPhysics => cmd(
909 vec![
910 PlayerName(Required),
911 Boolean("enabled", "true".to_string(), Optional),
912 Message(Optional),
913 ],
914 Content::localized("command-server_physics-desc"),
915 Some(Moderator),
916 ),
917 ServerChatCommand::SetMotd => cmd(
918 vec![Any("locale", Optional), Message(Optional)],
919 Content::localized("command-set_motd-desc"),
920 Some(Admin),
921 ),
922 ServerChatCommand::SetBodyType => cmd(
923 vec![
924 Enum(
925 "body type",
926 vec!["Female".to_string(), "Male".to_string()],
927 Required,
928 ),
929 Boolean("permanent", "false".to_string(), Requirement::Optional),
930 ],
931 Content::localized("command-set_body_type-desc"),
932 Some(Admin),
933 ),
934 ServerChatCommand::Ship => cmd(
935 vec![
936 Enum(
937 "kind",
938 comp::ship::ALL_SHIPS
939 .iter()
940 .map(|b| format!("{b:?}"))
941 .collect(),
942 Optional,
943 ),
944 Boolean(
945 "Whether the ship should be tethered to the target (or its mount)",
946 "false".to_string(),
947 Optional,
948 ),
949 Float("destination_degrees_ccw_of_east", 90.0, Optional),
950 ],
951 Content::localized("command-ship-desc"),
952 Some(Admin),
953 ),
954 ServerChatCommand::Site => cmd(
957 vec![
958 SiteName(Required),
959 Boolean("Dismount from ship", "true".to_string(), Optional),
960 ],
961 Content::localized("command-site-desc"),
962 Some(Moderator),
963 ),
964 ServerChatCommand::SkillPoint => cmd(
965 vec![
966 Enum("skill tree", SKILL_TREES.clone(), Required),
967 Integer("amount", 1, Optional),
968 ],
969 Content::localized("command-skill_point-desc"),
970 Some(Admin),
971 ),
972 ServerChatCommand::SkillPreset => cmd(
973 vec![Enum("preset_name", PRESET_LIST.to_vec(), Required)],
974 Content::localized("command-skill_preset-desc"),
975 Some(Admin),
976 ),
977 ServerChatCommand::Spawn => cmd(
978 vec![
979 Enum("alignment", ALIGNMENTS.clone(), Required),
980 Enum("entity", ENTITIES.clone(), Required),
981 Integer("amount", 1, Optional),
982 Boolean("ai", "true".to_string(), Optional),
983 Float("scale", 1.0, Optional),
984 Boolean("tethered", "false".to_string(), Optional),
985 ],
986 Content::localized("command-spawn-desc"),
987 Some(Admin),
988 ),
989 ServerChatCommand::Spot => cmd(
990 vec![Enum("Spot kind to find", SPOTS.clone(), Required)],
991 Content::localized("command-spot-desc"),
992 Some(Admin),
993 ),
994 ServerChatCommand::Sudo => cmd(
995 vec![EntityTarget(Required), SubCommand],
996 Content::localized("command-sudo-desc"),
997 Some(Moderator),
998 ),
999 ServerChatCommand::Tell => cmd(
1000 vec![PlayerName(Required), Message(Optional)],
1001 Content::localized("command-tell-desc"),
1002 None,
1003 ),
1004 ServerChatCommand::Time => cmd(
1005 vec![Enum("time", TIMES.clone(), Optional)],
1006 Content::localized("command-time-desc"),
1007 Some(Admin),
1008 ),
1009 ServerChatCommand::TimeScale => cmd(
1010 vec![Float("time scale", 1.0, Optional)],
1011 Content::localized("command-time_scale-desc"),
1012 Some(Admin),
1013 ),
1014 ServerChatCommand::Tp => cmd(
1015 vec![
1016 EntityTarget(Optional),
1017 Boolean("Dismount from ship", "true".to_string(), Optional),
1018 ],
1019 Content::localized("command-tp-desc"),
1020 Some(Moderator),
1021 ),
1022 ServerChatCommand::RtsimTp => cmd(
1023 vec![
1024 Integer("npc index", 0, Required),
1025 Boolean("Dismount from ship", "true".to_string(), Optional),
1026 ],
1027 Content::localized("command-rtsim_tp-desc"),
1028 Some(Admin),
1029 ),
1030 ServerChatCommand::RtsimInfo => cmd(
1031 vec![Integer("npc index", 0, Required)],
1032 Content::localized("command-rtsim_info-desc"),
1033 Some(Admin),
1034 ),
1035 ServerChatCommand::RtsimNpc => cmd(
1036 vec![Any("query", Required), Integer("max number", 20, Optional)],
1037 Content::localized("command-rtsim_npc-desc"),
1038 Some(Admin),
1039 ),
1040 ServerChatCommand::RtsimPurge => cmd(
1041 vec![Boolean(
1042 "whether purging of rtsim data should occur on next startup",
1043 true.to_string(),
1044 Required,
1045 )],
1046 Content::localized("command-rtsim_purge-desc"),
1047 Some(Admin),
1048 ),
1049 ServerChatCommand::RtsimChunk => cmd(
1050 vec![],
1051 Content::localized("command-rtsim_chunk-desc"),
1052 Some(Admin),
1053 ),
1054 ServerChatCommand::Unban => cmd(
1055 vec![PlayerName(Required)],
1056 Content::localized("command-unban-desc"),
1057 Some(Moderator),
1058 ),
1059 ServerChatCommand::UnbanIp => cmd(
1060 vec![PlayerName(Required)],
1061 Content::localized("command-unban-ip-desc"),
1062 Some(Moderator),
1063 ),
1064 ServerChatCommand::Version => {
1065 cmd(vec![], Content::localized("command-version-desc"), None)
1066 },
1067 ServerChatCommand::SetWaypoint => cmd(
1068 vec![],
1069 Content::localized("command-waypoint-desc"),
1070 Some(Admin),
1071 ),
1072 ServerChatCommand::Wiring => cmd(
1073 vec![],
1074 Content::localized("command-wiring-desc"),
1075 Some(Admin),
1076 ),
1077 ServerChatCommand::Whitelist => cmd(
1078 vec![Any("add/remove", Required), PlayerName(Required)],
1079 Content::localized("command-whitelist-desc"),
1080 Some(Moderator),
1081 ),
1082 ServerChatCommand::World => cmd(
1083 vec![Message(Optional)],
1084 Content::localized("command-world-desc"),
1085 None,
1086 ),
1087 ServerChatCommand::MakeVolume => cmd(
1088 vec![Integer("size", 15, Optional)],
1089 Content::localized("command-make_volume-desc"),
1090 Some(Admin),
1091 ),
1092 ServerChatCommand::Location => cmd(
1093 vec![Any("name", Required)],
1094 Content::localized("command-location-desc"),
1095 None,
1096 ),
1097 ServerChatCommand::CreateLocation => cmd(
1098 vec![Any("name", Required)],
1099 Content::localized("command-create_location-desc"),
1100 Some(Moderator),
1101 ),
1102 ServerChatCommand::DeleteLocation => cmd(
1103 vec![Any("name", Required)],
1104 Content::localized("command-delete_location-desc"),
1105 Some(Moderator),
1106 ),
1107 ServerChatCommand::WeatherZone => cmd(
1108 vec![
1109 Enum("weather kind", WEATHERS.clone(), Required),
1110 Float("radius", 500.0, Optional),
1111 Float("time", 300.0, Optional),
1112 ],
1113 Content::localized("command-weather_zone-desc"),
1114 Some(Admin),
1115 ),
1116 ServerChatCommand::Lightning => cmd(
1117 vec![],
1118 Content::localized("command-lightning-desc"),
1119 Some(Admin),
1120 ),
1121 ServerChatCommand::Scale => cmd(
1122 vec![
1123 Float("factor", 1.0, Required),
1124 Boolean("reset_mass", true.to_string(), Optional),
1125 ],
1126 Content::localized("command-scale-desc"),
1127 Some(Admin),
1128 ),
1129 ServerChatCommand::RepairEquipment => cmd(
1130 vec![ArgumentSpec::Boolean(
1131 "repair inventory",
1132 true.to_string(),
1133 Optional,
1134 )],
1135 Content::localized("command-repair_equipment-desc"),
1136 Some(Admin),
1137 ),
1138 ServerChatCommand::Tether => cmd(
1139 vec![
1140 EntityTarget(Required),
1141 Boolean("automatic length", "true".to_string(), Optional),
1142 ],
1143 Content::localized("command-tether-desc"),
1144 Some(Admin),
1145 ),
1146 ServerChatCommand::DestroyTethers => cmd(
1147 vec![],
1148 Content::localized("command-destroy_tethers-desc"),
1149 Some(Admin),
1150 ),
1151 ServerChatCommand::Mount => cmd(
1152 vec![EntityTarget(Required)],
1153 Content::localized("command-mount-desc"),
1154 Some(Admin),
1155 ),
1156 ServerChatCommand::Dismount => cmd(
1157 vec![EntityTarget(Required)],
1158 Content::localized("command-dismount-desc"),
1159 Some(Admin),
1160 ),
1161 }
1162 }
1163
1164 pub fn keyword(&self) -> &'static str {
1166 match self {
1167 ServerChatCommand::Adminify => "adminify",
1168 ServerChatCommand::Airship => "airship",
1169 ServerChatCommand::Alias => "alias",
1170 ServerChatCommand::AreaAdd => "area_add",
1171 ServerChatCommand::AreaList => "area_list",
1172 ServerChatCommand::AreaRemove => "area_remove",
1173 ServerChatCommand::Aura => "aura",
1174 ServerChatCommand::Ban => "ban",
1175 ServerChatCommand::BanIp => "ban_ip",
1176 ServerChatCommand::BanLog => "ban_log",
1177 ServerChatCommand::BattleMode => "battlemode",
1178 ServerChatCommand::BattleModeForce => "battlemode_force",
1179 ServerChatCommand::Body => "body",
1180 ServerChatCommand::Buff => "buff",
1181 ServerChatCommand::Build => "build",
1182 ServerChatCommand::Campfire => "campfire",
1183 ServerChatCommand::ClearPersistedTerrain => "clear_persisted_terrain",
1184 ServerChatCommand::DeathEffect => "death_effect",
1185 ServerChatCommand::DebugColumn => "debug_column",
1186 ServerChatCommand::DebugWays => "debug_ways",
1187 ServerChatCommand::DisconnectAllPlayers => "disconnect_all_players",
1188 ServerChatCommand::DropAll => "dropall",
1189 ServerChatCommand::Dummy => "dummy",
1190 ServerChatCommand::Explosion => "explosion",
1191 ServerChatCommand::Faction => "faction",
1192 ServerChatCommand::GiveItem => "give_item",
1193 ServerChatCommand::Gizmos => "gizmos",
1194 ServerChatCommand::GizmosRange => "gizmos_range",
1195 ServerChatCommand::Goto => "goto",
1196 ServerChatCommand::GotoRand => "goto_rand",
1197 ServerChatCommand::Group => "group",
1198 ServerChatCommand::GroupInvite => "group_invite",
1199 ServerChatCommand::GroupKick => "group_kick",
1200 ServerChatCommand::GroupLeave => "group_leave",
1201 ServerChatCommand::GroupPromote => "group_promote",
1202 ServerChatCommand::Health => "health",
1203 ServerChatCommand::IntoNpc => "into_npc",
1204 ServerChatCommand::JoinFaction => "join_faction",
1205 ServerChatCommand::Jump => "jump",
1206 ServerChatCommand::Kick => "kick",
1207 ServerChatCommand::Kill => "kill",
1208 ServerChatCommand::KillNpcs => "kill_npcs",
1209 ServerChatCommand::Kit => "kit",
1210 ServerChatCommand::Lantern => "lantern",
1211 ServerChatCommand::Respawn => "respawn",
1212 ServerChatCommand::Light => "light",
1213 ServerChatCommand::MakeBlock => "make_block",
1214 ServerChatCommand::MakeNpc => "make_npc",
1215 ServerChatCommand::MakeSprite => "make_sprite",
1216 ServerChatCommand::Motd => "motd",
1217 ServerChatCommand::Object => "object",
1218 ServerChatCommand::Outcome => "outcome",
1219 ServerChatCommand::PermitBuild => "permit_build",
1220 ServerChatCommand::Players => "players",
1221 ServerChatCommand::Poise => "poise",
1222 ServerChatCommand::Portal => "portal",
1223 ServerChatCommand::ResetRecipes => "reset_recipes",
1224 ServerChatCommand::Region => "region",
1225 ServerChatCommand::ReloadChunks => "reload_chunks",
1226 ServerChatCommand::RemoveLights => "remove_lights",
1227 ServerChatCommand::RevokeBuild => "revoke_build",
1228 ServerChatCommand::RevokeBuildAll => "revoke_build_all",
1229 ServerChatCommand::Safezone => "safezone",
1230 ServerChatCommand::Say => "say",
1231 ServerChatCommand::ServerPhysics => "server_physics",
1232 ServerChatCommand::SetMotd => "set_motd",
1233 ServerChatCommand::SetBodyType => "set_body_type",
1234 ServerChatCommand::Ship => "ship",
1235 ServerChatCommand::Site => "site",
1236 ServerChatCommand::SkillPoint => "skill_point",
1237 ServerChatCommand::SkillPreset => "skill_preset",
1238 ServerChatCommand::Spawn => "spawn",
1239 ServerChatCommand::Spot => "spot",
1240 ServerChatCommand::Sudo => "sudo",
1241 ServerChatCommand::Tell => "tell",
1242 ServerChatCommand::Time => "time",
1243 ServerChatCommand::TimeScale => "time_scale",
1244 ServerChatCommand::Tp => "tp",
1245 ServerChatCommand::RtsimTp => "rtsim_tp",
1246 ServerChatCommand::RtsimInfo => "rtsim_info",
1247 ServerChatCommand::RtsimNpc => "rtsim_npc",
1248 ServerChatCommand::RtsimPurge => "rtsim_purge",
1249 ServerChatCommand::RtsimChunk => "rtsim_chunk",
1250 ServerChatCommand::Unban => "unban",
1251 ServerChatCommand::UnbanIp => "unban_ip",
1252 ServerChatCommand::Version => "version",
1253 ServerChatCommand::SetWaypoint => "set_waypoint",
1254 ServerChatCommand::Wiring => "wiring",
1255 ServerChatCommand::Whitelist => "whitelist",
1256 ServerChatCommand::World => "world",
1257 ServerChatCommand::MakeVolume => "make_volume",
1258 ServerChatCommand::Location => "location",
1259 ServerChatCommand::CreateLocation => "create_location",
1260 ServerChatCommand::DeleteLocation => "delete_location",
1261 ServerChatCommand::WeatherZone => "weather_zone",
1262 ServerChatCommand::Lightning => "lightning",
1263 ServerChatCommand::Scale => "scale",
1264 ServerChatCommand::RepairEquipment => "repair_equipment",
1265 ServerChatCommand::Tether => "tether",
1266 ServerChatCommand::DestroyTethers => "destroy_tethers",
1267 ServerChatCommand::Mount => "mount",
1268 ServerChatCommand::Dismount => "dismount",
1269 }
1270 }
1271
1272 pub fn short_keyword(&self) -> Option<&'static str> {
1275 Some(match self {
1276 ServerChatCommand::Faction => "f",
1277 ServerChatCommand::Group => "g",
1278 ServerChatCommand::Region => "r",
1279 ServerChatCommand::Say => "s",
1280 ServerChatCommand::Tell => "t",
1281 ServerChatCommand::World => "w",
1282 _ => return None,
1283 })
1284 }
1285
1286 pub fn iter() -> impl Iterator<Item = Self> + Clone { <Self as IntoEnumIterator>::iter() }
1288
1289 pub fn help_content(&self) -> Content {
1291 let data = self.data();
1292
1293 let usage = std::iter::once(format!("/{}", self.keyword()))
1294 .chain(data.args.iter().map(|arg| arg.usage_string()))
1295 .collect::<Vec<_>>()
1296 .join(" ");
1297
1298 Content::localized_with_args("command-help-template", [
1299 ("usage", Content::Plain(usage)),
1300 ("description", data.description),
1301 ])
1302 }
1303
1304 pub fn iter_with_keywords() -> impl Iterator<Item = (&'static str, Self)> {
1308 Self::iter()
1309 .filter_map(|c| c.short_keyword().map(|s| (s, c)))
1311 .chain(Self::iter().map(|c| (c.keyword(), c)))
1312 }
1313
1314 pub fn needs_role(&self) -> Option<comp::AdminRole> { self.data().needs_role }
1315
1316 pub fn arg_fmt(&self) -> String {
1318 self.data()
1319 .args
1320 .iter()
1321 .map(|arg| match arg {
1322 ArgumentSpec::PlayerName(_) => "{}",
1323 ArgumentSpec::EntityTarget(_) => "{}",
1324 ArgumentSpec::SiteName(_) => "{/.*/}",
1325 ArgumentSpec::Float(_, _, _) => "{}",
1326 ArgumentSpec::Integer(_, _, _) => "{d}",
1327 ArgumentSpec::Any(_, _) => "{}",
1328 ArgumentSpec::Command(_) => "{}",
1329 ArgumentSpec::Message(_) => "{/.*/}",
1330 ArgumentSpec::SubCommand => "{} {/.*/}",
1331 ArgumentSpec::Enum(_, _, _) => "{}",
1332 ArgumentSpec::AssetPath(_, _, _, _) => "{}",
1333 ArgumentSpec::Boolean(_, _, _) => "{}",
1334 ArgumentSpec::Flag(_) => "{}",
1335 })
1336 .collect::<Vec<_>>()
1337 .join(" ")
1338 }
1339}
1340
1341impl Display for ServerChatCommand {
1342 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
1343 write!(f, "{}", self.keyword())
1344 }
1345}
1346
1347impl FromStr for ServerChatCommand {
1348 type Err = ();
1349
1350 fn from_str(keyword: &str) -> Result<ServerChatCommand, ()> {
1351 Self::iter()
1352 .filter_map(|c| c.short_keyword().map(|s| (s, c)))
1354 .chain(Self::iter().map(|c| (c.keyword(), c)))
1355 .find_map(|(kwd, command)| (kwd == keyword).then_some(command))
1357 .ok_or(())
1359 }
1360}
1361
1362#[derive(Eq, PartialEq, Debug, Clone, Copy)]
1363pub enum Requirement {
1364 Required,
1365 Optional,
1366}
1367
1368pub enum ArgumentSpec {
1370 PlayerName(Requirement),
1372 EntityTarget(Requirement),
1374 SiteName(Requirement),
1376 Float(&'static str, f32, Requirement),
1381 Integer(&'static str, i32, Requirement),
1386 Any(&'static str, Requirement),
1388 Command(Requirement),
1390 Message(Requirement),
1393 SubCommand,
1395 Enum(&'static str, Vec<String>, Requirement),
1400 AssetPath(&'static str, &'static str, Vec<String>, Requirement),
1406 Boolean(&'static str, String, Requirement),
1411 Flag(&'static str),
1413}
1414
1415impl ArgumentSpec {
1416 pub fn usage_string(&self) -> String {
1417 match self {
1418 ArgumentSpec::PlayerName(req) => {
1419 if &Requirement::Required == req {
1420 "<player>".to_string()
1421 } else {
1422 "[player]".to_string()
1423 }
1424 },
1425 ArgumentSpec::EntityTarget(req) => {
1426 if &Requirement::Required == req {
1427 "<entity>".to_string()
1428 } else {
1429 "[entity]".to_string()
1430 }
1431 },
1432 ArgumentSpec::SiteName(req) => {
1433 if &Requirement::Required == req {
1434 "<site>".to_string()
1435 } else {
1436 "[site]".to_string()
1437 }
1438 },
1439 ArgumentSpec::Float(label, _, req) => {
1440 if &Requirement::Required == req {
1441 format!("<{}>", label)
1442 } else {
1443 format!("[{}]", label)
1444 }
1445 },
1446 ArgumentSpec::Integer(label, _, req) => {
1447 if &Requirement::Required == req {
1448 format!("<{}>", label)
1449 } else {
1450 format!("[{}]", label)
1451 }
1452 },
1453 ArgumentSpec::Any(label, req) => {
1454 if &Requirement::Required == req {
1455 format!("<{}>", label)
1456 } else {
1457 format!("[{}]", label)
1458 }
1459 },
1460 ArgumentSpec::Command(req) => {
1461 if &Requirement::Required == req {
1462 "<[/]command>".to_string()
1463 } else {
1464 "[[/]command]".to_string()
1465 }
1466 },
1467 ArgumentSpec::Message(req) => {
1468 if &Requirement::Required == req {
1469 "<message>".to_string()
1470 } else {
1471 "[message]".to_string()
1472 }
1473 },
1474 ArgumentSpec::SubCommand => "<[/]command> [args...]".to_string(),
1475 ArgumentSpec::Enum(label, _, req) => {
1476 if &Requirement::Required == req {
1477 format!("<{}>", label)
1478 } else {
1479 format!("[{}]", label)
1480 }
1481 },
1482 ArgumentSpec::AssetPath(label, _, _, req) => {
1483 if &Requirement::Required == req {
1484 format!("<{}>", label)
1485 } else {
1486 format!("[{}]", label)
1487 }
1488 },
1489 ArgumentSpec::Boolean(label, _, req) => {
1490 if &Requirement::Required == req {
1491 format!("<{}>", label)
1492 } else {
1493 format!("[{}]", label)
1494 }
1495 },
1496 ArgumentSpec::Flag(label) => {
1497 format!("[{}]", label)
1498 },
1499 }
1500 }
1501
1502 pub fn requirement(&self) -> Requirement {
1503 match self {
1504 ArgumentSpec::PlayerName(r)
1505 | ArgumentSpec::EntityTarget(r)
1506 | ArgumentSpec::SiteName(r)
1507 | ArgumentSpec::Float(_, _, r)
1508 | ArgumentSpec::Integer(_, _, r)
1509 | ArgumentSpec::Any(_, r)
1510 | ArgumentSpec::Command(r)
1511 | ArgumentSpec::Message(r)
1512 | ArgumentSpec::Enum(_, _, r)
1513 | ArgumentSpec::AssetPath(_, _, _, r)
1514 | ArgumentSpec::Boolean(_, _, r) => *r,
1515 ArgumentSpec::Flag(_) => Requirement::Optional,
1516 ArgumentSpec::SubCommand => Requirement::Required,
1517 }
1518 }
1519}
1520
1521pub trait CommandEnumArg: FromStr {
1522 fn all_options() -> Vec<String>;
1523}
1524
1525macro_rules! impl_from_to_str_cmd {
1526 ($enum:ident, ($($attribute:ident => $str:expr),*)) => {
1527 impl std::str::FromStr for $enum {
1528 type Err = String;
1529
1530 fn from_str(s: &str) -> Result<Self, Self::Err> {
1531 match s {
1532 $(
1533 $str => Ok($enum::$attribute),
1534 )*
1535 s => Err(format!("Invalid variant: {s}")),
1536 }
1537 }
1538 }
1539
1540 impl $crate::cmd::CommandEnumArg for $enum {
1541 fn all_options() -> Vec<String> {
1542 vec![$($str.to_string()),*]
1543 }
1544 }
1545 }
1546}
1547
1548impl_from_to_str_cmd!(AuraKindVariant, (
1549 Buff => "buff",
1550 FriendlyFire => "friendly_fire",
1551 ForcePvP => "force_pvp"
1552));
1553
1554impl_from_to_str_cmd!(GroupTarget, (
1555 InGroup => "in_group",
1556 OutOfGroup => "out_of_group",
1557 All => "all"
1558));
1559
1560#[macro_export]
1563macro_rules! parse_cmd_args {
1564 ($args:expr, $($t:ty),* $(, ..$tail:ty)? $(,)?) => {
1565 {
1566 let mut args = $args.into_iter().peekable();
1567 (
1568 $({
1581 let parsed = args.peek().and_then(|s| s.parse::<$t>().ok());
1582 if parsed.is_some() { args.next(); }
1584 parsed
1585 }),*
1586 $(, args.map(|s| s.to_string()).collect::<$tail>())?
1587 )
1588 }
1589 };
1590}
1591
1592#[cfg(test)]
1593mod tests {
1594 use super::*;
1595 use crate::comp::Item;
1596
1597 #[test]
1598 fn verify_cmd_list_sorted() {
1599 let mut list = ServerChatCommand::iter()
1600 .map(|c| c.keyword())
1601 .collect::<Vec<_>>();
1602
1603 let list2 = list.clone();
1605 list.sort_unstable();
1606 assert_eq!(list, list2);
1607 }
1608
1609 #[test]
1610 fn test_loading_skill_presets() {
1611 SkillPresetManifest::load_expect_combined_static(PRESET_MANIFEST_PATH);
1612 }
1613
1614 #[test]
1615 fn test_load_kits() {
1616 let kits = KitManifest::load_expect_combined_static(KIT_MANIFEST_PATH).read();
1617 let mut rng = rand::rng();
1618 for kit in kits.0.values() {
1619 for (item_id, _) in kit.iter() {
1620 match item_id {
1621 KitSpec::Item(item_id) => {
1622 Item::new_from_asset_expect(item_id);
1623 },
1624 KitSpec::ModularWeaponSet {
1625 tool,
1626 material,
1627 hands,
1628 } => {
1629 comp::item::modular::generate_weapons(*tool, *material, *hands)
1630 .unwrap_or_else(|_| {
1631 panic!(
1632 "Failed to synthesize a modular {tool:?} set made of \
1633 {material:?}."
1634 )
1635 });
1636 },
1637 KitSpec::ModularWeaponRandom {
1638 tool,
1639 material,
1640 hands,
1641 } => {
1642 comp::item::modular::random_weapon(*tool, *material, *hands, &mut rng)
1643 .unwrap_or_else(|_| {
1644 panic!(
1645 "Failed to synthesize a random {hands:?}-handed modular \
1646 {tool:?} made of {material:?}."
1647 )
1648 });
1649 },
1650 }
1651 }
1652 }
1653 }
1654}