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