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