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 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 Group,
361 GroupInvite,
362 GroupKick,
363 GroupLeave,
364 GroupPromote,
365 Health,
366 IntoNpc,
367 JoinFaction,
368 Jump,
369 Kick,
370 Kill,
371 KillNpcs,
372 Kit,
373 Lantern,
374 Light,
375 Lightning,
376 Location,
377 MakeBlock,
378 MakeNpc,
379 MakeSprite,
380 MakeVolume,
381 Motd,
382 Mount,
383 Object,
384 Outcome,
385 PermitBuild,
386 Players,
387 Portal,
388 Region,
389 ReloadChunks,
390 RemoveLights,
391 RepairEquipment,
392 ResetRecipes,
393 Respawn,
394 RevokeBuild,
395 RevokeBuildAll,
396 RtsimChunk,
397 RtsimInfo,
398 RtsimNpc,
399 RtsimPurge,
400 RtsimTp,
401 Safezone,
402 Say,
403 Scale,
404 ServerPhysics,
405 SetMotd,
406 Ship,
407 Site,
408 SkillPoint,
409 SkillPreset,
410 Spawn,
411 Spot,
412 Sudo,
413 Tell,
414 Tether,
415 Time,
416 TimeScale,
417 Tp,
418 Unban,
419 UnbanIp,
420 Version,
421 Waypoint,
422 WeatherZone,
423 Whitelist,
424 Wiring,
425 World,
426}
427
428impl ServerChatCommand {
429 pub fn data(&self) -> ChatCommandData {
430 use ArgumentSpec::*;
431 use Requirement::*;
432 use Role::*;
433 let cmd = ChatCommandData::new;
434 match self {
435 ServerChatCommand::Adminify => cmd(
436 vec![PlayerName(Required), Enum("role", ROLES.clone(), Optional)],
437 Content::localized("command-adminify-desc"),
438 Some(Admin),
439 ),
440 ServerChatCommand::Airship => cmd(
441 vec![
442 Enum(
443 "kind",
444 comp::ship::ALL_AIRSHIPS
445 .iter()
446 .map(|b| format!("{b:?}"))
447 .collect(),
448 Optional,
449 ),
450 Float("destination_degrees_ccw_of_east", 90.0, Optional),
451 ],
452 Content::localized("command-airship-desc"),
453 Some(Admin),
454 ),
455 ServerChatCommand::Alias => cmd(
456 vec![Any("name", Required)],
457 Content::localized("command-alias-desc"),
458 Some(Moderator),
459 ),
460 ServerChatCommand::Aura => cmd(
461 vec![
462 Float("aura_radius", 10.0, Required),
463 Float("aura_duration", 10.0, Optional),
464 Boolean("new_entity", "true".to_string(), Optional),
465 Enum("aura_target", GroupTarget::all_options(), Optional),
466 Enum("aura_kind", AuraKindVariant::all_options(), Required),
467 Any("aura spec", Optional),
468 ],
469 Content::localized("command-aura-desc"),
470 Some(Admin),
471 ),
472 ServerChatCommand::Buff => cmd(
473 vec![
474 Enum("buff", BUFFS.clone(), Required),
475 Float("strength", 0.01, Optional),
476 Float("duration", 10.0, Optional),
477 Any("buff data spec", Optional),
478 ],
479 Content::localized("command-buff-desc"),
480 Some(Admin),
481 ),
482 ServerChatCommand::Ban => cmd(
483 vec![
484 PlayerName(Required),
485 Boolean("overwrite", "true".to_string(), Optional),
486 Any("ban duration", Optional),
487 Message(Optional),
488 ],
489 Content::localized("command-ban-desc"),
490 Some(Moderator),
491 ),
492 ServerChatCommand::BanIp => cmd(
493 vec![
494 PlayerName(Required),
495 Boolean("overwrite", "true".to_string(), Optional),
496 Any("ban duration", Optional),
497 Message(Optional),
498 ],
499 Content::localized("command-ban-ip-desc"),
500 Some(Moderator),
501 ),
502 #[rustfmt::skip]
503 ServerChatCommand::BattleMode => cmd(
504 vec![Enum(
505 "battle mode",
506 vec!["pvp".to_owned(), "pve".to_owned()],
507 Optional,
508 )],
509 Content::localized("command-battlemode-desc"),
510 None,
511
512 ),
513 ServerChatCommand::IntoNpc => cmd(
514 vec![AssetPath(
515 "entity_config",
516 "common.entity.",
517 ENTITY_CONFIGS.clone(),
518 Required,
519 )],
520 Content::localized("command-into_npc-desc"),
521 Some(Admin),
522 ),
523 ServerChatCommand::Body => cmd(
524 vec![Enum("body", ENTITIES.clone(), Required)],
525 Content::localized("command-body-desc"),
526 Some(Admin),
527 ),
528 ServerChatCommand::BattleModeForce => cmd(
529 vec![Enum(
530 "battle mode",
531 vec!["pvp".to_owned(), "pve".to_owned()],
532 Required,
533 )],
534 Content::localized("command-battlemode_force-desc"),
535 Some(Admin),
536 ),
537 ServerChatCommand::Build => cmd(vec![], Content::localized("command-build-desc"), None),
538 ServerChatCommand::AreaAdd => cmd(
539 vec![
540 Any("name", Required),
541 Enum("kind", AREA_KINDS.clone(), Required),
542 Integer("xlo", 0, Required),
543 Integer("xhi", 10, Required),
544 Integer("ylo", 0, Required),
545 Integer("yhi", 10, Required),
546 Integer("zlo", 0, Required),
547 Integer("zhi", 10, Required),
548 ],
549 Content::localized("command-area_add-desc"),
550 Some(Admin),
551 ),
552 ServerChatCommand::AreaList => cmd(
553 vec![],
554 Content::localized("command-area_list-desc"),
555 Some(Admin),
556 ),
557 ServerChatCommand::AreaRemove => cmd(
558 vec![
559 Any("name", Required),
560 Enum("kind", AREA_KINDS.clone(), Required),
561 ],
562 Content::localized("command-area_remove-desc"),
563 Some(Admin),
564 ),
565 ServerChatCommand::Campfire => cmd(
566 vec![],
567 Content::localized("command-campfire-desc"),
568 Some(Admin),
569 ),
570 ServerChatCommand::ClearPersistedTerrain => cmd(
571 vec![Integer("chunk_radius", 6, Required)],
572 Content::localized("command-clear_persisted_terrain-desc"),
573 Some(Admin),
574 ),
575 ServerChatCommand::DeathEffect => cmd(
576 vec![
577 Enum("death_effect", vec!["transform".to_string()], Required),
578 AssetPath(
582 "entity_config",
583 "common.entity.",
584 ENTITY_CONFIGS.clone(),
585 Required,
586 ),
587 ],
588 Content::localized("command-death_effect-dest"),
589 Some(Admin),
590 ),
591 ServerChatCommand::DebugColumn => cmd(
592 vec![Integer("x", 15000, Required), Integer("y", 15000, Required)],
593 Content::localized("command-debug_column-desc"),
594 Some(Admin),
595 ),
596 ServerChatCommand::DebugWays => cmd(
597 vec![Integer("x", 15000, Required), Integer("y", 15000, Required)],
598 Content::localized("command-debug_ways-desc"),
599 Some(Admin),
600 ),
601 ServerChatCommand::DisconnectAllPlayers => cmd(
602 vec![Any("confirm", Required)],
603 Content::localized("command-disconnect_all_players-desc"),
604 Some(Admin),
605 ),
606 ServerChatCommand::DropAll => cmd(
607 vec![],
608 Content::localized("command-dropall-desc"),
609 Some(Moderator),
610 ),
611 ServerChatCommand::Dummy => cmd(
612 vec![],
613 Content::localized("command-dummy-desc"),
614 Some(Admin),
615 ),
616 ServerChatCommand::Explosion => cmd(
617 vec![Float("radius", 5.0, Required)],
618 Content::localized("command-explosion-desc"),
619 Some(Admin),
620 ),
621 ServerChatCommand::Faction => cmd(
622 vec![Message(Optional)],
623 Content::localized("command-faction-desc"),
624 None,
625 ),
626 ServerChatCommand::GiveItem => cmd(
627 vec![
628 AssetPath("item", "common.items.", ITEM_SPECS.clone(), Required),
629 Integer("num", 1, Optional),
630 ],
631 Content::localized("command-give_item-desc"),
632 Some(Admin),
633 ),
634 ServerChatCommand::Goto => cmd(
635 vec![
636 Float("x", 0.0, Required),
637 Float("y", 0.0, Required),
638 Float("z", 0.0, Required),
639 Boolean("Dismount from ship", "true".to_string(), Optional),
640 ],
641 Content::localized("command-goto-desc"),
642 Some(Admin),
643 ),
644 ServerChatCommand::Group => cmd(
645 vec![Message(Optional)],
646 Content::localized("command-group-desc"),
647 None,
648 ),
649 ServerChatCommand::GroupInvite => cmd(
650 vec![PlayerName(Required)],
651 Content::localized("command-group_invite-desc"),
652 None,
653 ),
654 ServerChatCommand::GroupKick => cmd(
655 vec![PlayerName(Required)],
656 Content::localized("command-group_kick-desc"),
657 None,
658 ),
659 ServerChatCommand::GroupLeave => {
660 cmd(vec![], Content::localized("command-group_leave-desc"), None)
661 },
662 ServerChatCommand::GroupPromote => cmd(
663 vec![PlayerName(Required)],
664 Content::localized("command-group_promote-desc"),
665 None,
666 ),
667 ServerChatCommand::Health => cmd(
668 vec![Integer("hp", 100, Required)],
669 Content::localized("command-health-desc"),
670 Some(Admin),
671 ),
672 ServerChatCommand::Respawn => cmd(
673 vec![],
674 Content::localized("command-respawn-desc"),
675 Some(Moderator),
676 ),
677 ServerChatCommand::JoinFaction => cmd(
678 vec![Any("faction", Optional)],
679 Content::localized("command-join_faction-desc"),
680 None,
681 ),
682 ServerChatCommand::Jump => cmd(
683 vec![
684 Float("x", 0.0, Required),
685 Float("y", 0.0, Required),
686 Float("z", 0.0, Required),
687 Boolean("Dismount from ship", "true".to_string(), Optional),
688 ],
689 Content::localized("command-jump-desc"),
690 Some(Admin),
691 ),
692 ServerChatCommand::Kick => cmd(
693 vec![PlayerName(Required), Message(Optional)],
694 Content::localized("command-kick-desc"),
695 Some(Moderator),
696 ),
697 ServerChatCommand::Kill => cmd(vec![], Content::localized("command-kill-desc"), None),
698 ServerChatCommand::KillNpcs => cmd(
699 vec![Float("radius", 100.0, Optional), Flag("--also-pets")],
700 Content::localized("command-kill_npcs-desc"),
701 Some(Admin),
702 ),
703 ServerChatCommand::Kit => cmd(
704 vec![Enum("kit_name", KITS.to_vec(), Required)],
705 Content::localized("command-kit-desc"),
706 Some(Admin),
707 ),
708 ServerChatCommand::Lantern => cmd(
709 vec![
710 Float("strength", 5.0, Required),
711 Float("r", 1.0, Optional),
712 Float("g", 1.0, Optional),
713 Float("b", 1.0, Optional),
714 ],
715 Content::localized("command-lantern-desc"),
716 Some(Admin),
717 ),
718 ServerChatCommand::Light => cmd(
719 vec![
720 Float("r", 1.0, Optional),
721 Float("g", 1.0, Optional),
722 Float("b", 1.0, Optional),
723 Float("x", 0.0, Optional),
724 Float("y", 0.0, Optional),
725 Float("z", 0.0, Optional),
726 Float("strength", 5.0, Optional),
727 ],
728 Content::localized("command-light-desc"),
729 Some(Admin),
730 ),
731 ServerChatCommand::MakeBlock => cmd(
732 vec![
733 Enum("block", BLOCK_KINDS.clone(), Required),
734 Integer("r", 255, Optional),
735 Integer("g", 255, Optional),
736 Integer("b", 255, Optional),
737 ],
738 Content::localized("command-make_block-desc"),
739 Some(Admin),
740 ),
741 ServerChatCommand::MakeNpc => cmd(
742 vec![
743 AssetPath(
744 "entity_config",
745 "common.entity.",
746 ENTITY_CONFIGS.clone(),
747 Required,
748 ),
749 Integer("num", 1, Optional),
750 ],
751 Content::localized("command-make_npc-desc"),
752 Some(Admin),
753 ),
754 ServerChatCommand::MakeSprite => cmd(
755 vec![Enum("sprite", SPRITE_KINDS.clone(), Required)],
756 Content::localized("command-make_sprite-desc"),
757 Some(Admin),
758 ),
759 ServerChatCommand::Motd => cmd(vec![], Content::localized("command-motd-desc"), None),
760 ServerChatCommand::Object => cmd(
761 vec![Enum("object", OBJECTS.clone(), Required)],
762 Content::localized("command-object-desc"),
763 Some(Admin),
764 ),
765 ServerChatCommand::Outcome => cmd(
766 vec![Enum("outcome", OUTCOME_KINDS.clone(), Required)],
767 Content::localized("command-outcome-desc"),
768 Some(Admin),
769 ),
770 ServerChatCommand::PermitBuild => cmd(
771 vec![Any("area_name", Required)],
772 Content::localized("command-permit_build-desc"),
773 Some(Admin),
774 ),
775 ServerChatCommand::Players => {
776 cmd(vec![], Content::localized("command-players-desc"), None)
777 },
778 ServerChatCommand::Portal => cmd(
779 vec![
780 Float("x", 0., Required),
781 Float("y", 0., Required),
782 Float("z", 0., Required),
783 Boolean("requires_no_aggro", "true".to_string(), Optional),
784 Float("buildup_time", 5., Optional),
785 ],
786 Content::localized("command-portal-desc"),
787 Some(Admin),
788 ),
789 ServerChatCommand::ReloadChunks => cmd(
790 vec![Integer("chunk_radius", 6, Optional)],
791 Content::localized("command-reload_chunks-desc"),
792 Some(Admin),
793 ),
794 ServerChatCommand::ResetRecipes => cmd(
795 vec![],
796 Content::localized("command-reset_recipes-desc"),
797 Some(Admin),
798 ),
799 ServerChatCommand::RemoveLights => cmd(
800 vec![Float("radius", 20.0, Optional)],
801 Content::localized("command-remove_lights-desc"),
802 Some(Admin),
803 ),
804 ServerChatCommand::RevokeBuild => cmd(
805 vec![Any("area_name", Required)],
806 Content::localized("command-revoke_build-desc"),
807 Some(Admin),
808 ),
809 ServerChatCommand::RevokeBuildAll => cmd(
810 vec![],
811 Content::localized("command-revoke_build_all-desc"),
812 Some(Admin),
813 ),
814 ServerChatCommand::Region => cmd(
815 vec![Message(Optional)],
816 Content::localized("command-region-desc"),
817 None,
818 ),
819 ServerChatCommand::Safezone => cmd(
820 vec![Float("range", 100.0, Optional)],
821 Content::localized("command-safezone-desc"),
822 Some(Moderator),
823 ),
824 ServerChatCommand::Say => cmd(
825 vec![Message(Optional)],
826 Content::localized("command-say-desc"),
827 None,
828 ),
829 ServerChatCommand::ServerPhysics => cmd(
830 vec![
831 PlayerName(Required),
832 Boolean("enabled", "true".to_string(), Optional),
833 Message(Optional),
834 ],
835 Content::localized("command-server_physics-desc"),
836 Some(Moderator),
837 ),
838 ServerChatCommand::SetMotd => cmd(
839 vec![Any("locale", Optional), Message(Optional)],
840 Content::localized("command-set_motd-desc"),
841 Some(Admin),
842 ),
843 ServerChatCommand::Ship => cmd(
844 vec![
845 Enum(
846 "kind",
847 comp::ship::ALL_SHIPS
848 .iter()
849 .map(|b| format!("{b:?}"))
850 .collect(),
851 Optional,
852 ),
853 Boolean(
854 "Whether the ship should be tethered to the target (or its mount)",
855 "false".to_string(),
856 Optional,
857 ),
858 Float("destination_degrees_ccw_of_east", 90.0, Optional),
859 ],
860 Content::localized("command-ship-desc"),
861 Some(Admin),
862 ),
863 ServerChatCommand::Site => cmd(
866 vec![
867 SiteName(Required),
868 Boolean("Dismount from ship", "true".to_string(), Optional),
869 ],
870 Content::localized("command-site-desc"),
871 Some(Moderator),
872 ),
873 ServerChatCommand::SkillPoint => cmd(
874 vec![
875 Enum("skill tree", SKILL_TREES.clone(), Required),
876 Integer("amount", 1, Optional),
877 ],
878 Content::localized("command-skill_point-desc"),
879 Some(Admin),
880 ),
881 ServerChatCommand::SkillPreset => cmd(
882 vec![Enum("preset_name", PRESET_LIST.to_vec(), Required)],
883 Content::localized("command-skill_preset-desc"),
884 Some(Admin),
885 ),
886 ServerChatCommand::Spawn => cmd(
887 vec![
888 Enum("alignment", ALIGNMENTS.clone(), Required),
889 Enum("entity", ENTITIES.clone(), Required),
890 Integer("amount", 1, Optional),
891 Boolean("ai", "true".to_string(), Optional),
892 Float("scale", 1.0, Optional),
893 Boolean("tethered", "false".to_string(), Optional),
894 ],
895 Content::localized("command-spawn-desc"),
896 Some(Admin),
897 ),
898 ServerChatCommand::Spot => cmd(
899 vec![Enum(
900 "Spot kind to find",
901 crate::spot::RON_SPOT_PROPERTIES
902 .0
903 .iter()
904 .map(|s| s.base_structures.clone())
905 .collect(),
906 Required,
907 )],
908 Content::localized("command-spot-desc"),
909 Some(Admin),
910 ),
911 ServerChatCommand::Sudo => cmd(
912 vec![EntityTarget(Required), SubCommand],
913 Content::localized("command-sudo-desc"),
914 Some(Moderator),
915 ),
916 ServerChatCommand::Tell => cmd(
917 vec![PlayerName(Required), Message(Optional)],
918 Content::localized("command-tell-desc"),
919 None,
920 ),
921 ServerChatCommand::Time => cmd(
922 vec![Enum("time", TIMES.clone(), Optional)],
923 Content::localized("command-time-desc"),
924 Some(Admin),
925 ),
926 ServerChatCommand::TimeScale => cmd(
927 vec![Float("time scale", 1.0, Optional)],
928 Content::localized("command-time_scale-desc"),
929 Some(Admin),
930 ),
931 ServerChatCommand::Tp => cmd(
932 vec![
933 EntityTarget(Optional),
934 Boolean("Dismount from ship", "true".to_string(), Optional),
935 ],
936 Content::localized("command-tp-desc"),
937 Some(Moderator),
938 ),
939 ServerChatCommand::RtsimTp => cmd(
940 vec![
941 Integer("npc index", 0, Required),
942 Boolean("Dismount from ship", "true".to_string(), Optional),
943 ],
944 Content::localized("command-rtsim_tp-desc"),
945 Some(Admin),
946 ),
947 ServerChatCommand::RtsimInfo => cmd(
948 vec![Integer("npc index", 0, Required)],
949 Content::localized("command-rtsim_info-desc"),
950 Some(Admin),
951 ),
952 ServerChatCommand::RtsimNpc => cmd(
953 vec![Any("query", Required), Integer("max number", 20, Optional)],
954 Content::localized("command-rtsim_npc-desc"),
955 Some(Admin),
956 ),
957 ServerChatCommand::RtsimPurge => cmd(
958 vec![Boolean(
959 "whether purging of rtsim data should occur on next startup",
960 true.to_string(),
961 Required,
962 )],
963 Content::localized("command-rtsim_purge-desc"),
964 Some(Admin),
965 ),
966 ServerChatCommand::RtsimChunk => cmd(
967 vec![],
968 Content::localized("command-rtsim_chunk-desc"),
969 Some(Admin),
970 ),
971 ServerChatCommand::Unban => cmd(
972 vec![PlayerName(Required)],
973 Content::localized("command-unban-desc"),
974 Some(Moderator),
975 ),
976 ServerChatCommand::UnbanIp => cmd(
977 vec![PlayerName(Required)],
978 Content::localized("command-unban-ip-desc"),
979 Some(Moderator),
980 ),
981 ServerChatCommand::Version => {
982 cmd(vec![], Content::localized("command-version-desc"), None)
983 },
984 ServerChatCommand::Waypoint => cmd(
985 vec![],
986 Content::localized("command-waypoint-desc"),
987 Some(Admin),
988 ),
989 ServerChatCommand::Wiring => cmd(
990 vec![],
991 Content::localized("command-wiring-desc"),
992 Some(Admin),
993 ),
994 ServerChatCommand::Whitelist => cmd(
995 vec![Any("add/remove", Required), PlayerName(Required)],
996 Content::localized("command-whitelist-desc"),
997 Some(Moderator),
998 ),
999 ServerChatCommand::World => cmd(
1000 vec![Message(Optional)],
1001 Content::localized("command-world-desc"),
1002 None,
1003 ),
1004 ServerChatCommand::MakeVolume => cmd(
1005 vec![Integer("size", 15, Optional)],
1006 Content::localized("command-make_volume-desc"),
1007 Some(Admin),
1008 ),
1009 ServerChatCommand::Location => cmd(
1010 vec![Any("name", Required)],
1011 Content::localized("command-location-desc"),
1012 None,
1013 ),
1014 ServerChatCommand::CreateLocation => cmd(
1015 vec![Any("name", Required)],
1016 Content::localized("command-create_location-desc"),
1017 Some(Moderator),
1018 ),
1019 ServerChatCommand::DeleteLocation => cmd(
1020 vec![Any("name", Required)],
1021 Content::localized("command-delete_location-desc"),
1022 Some(Moderator),
1023 ),
1024 ServerChatCommand::WeatherZone => cmd(
1025 vec![
1026 Enum("weather kind", WEATHERS.clone(), Required),
1027 Float("radius", 500.0, Optional),
1028 Float("time", 300.0, Optional),
1029 ],
1030 Content::localized("command-weather_zone-desc"),
1031 Some(Admin),
1032 ),
1033 ServerChatCommand::Lightning => cmd(
1034 vec![],
1035 Content::localized("command-lightning-desc"),
1036 Some(Admin),
1037 ),
1038 ServerChatCommand::Scale => cmd(
1039 vec![
1040 Float("factor", 1.0, Required),
1041 Boolean("reset_mass", true.to_string(), Optional),
1042 ],
1043 Content::localized("command-scale-desc"),
1044 Some(Admin),
1045 ),
1046 ServerChatCommand::RepairEquipment => cmd(
1047 vec![],
1048 Content::localized("command-repair_equipment-desc"),
1049 Some(Admin),
1050 ),
1051 ServerChatCommand::Tether => cmd(
1052 vec![
1053 EntityTarget(Required),
1054 Boolean("automatic length", "true".to_string(), Optional),
1055 ],
1056 Content::localized("command-tether-desc"),
1057 Some(Admin),
1058 ),
1059 ServerChatCommand::DestroyTethers => cmd(
1060 vec![],
1061 Content::localized("command-destroy_tethers-desc"),
1062 Some(Admin),
1063 ),
1064 ServerChatCommand::Mount => cmd(
1065 vec![EntityTarget(Required)],
1066 Content::localized("command-mount-desc"),
1067 Some(Admin),
1068 ),
1069 ServerChatCommand::Dismount => cmd(
1070 vec![EntityTarget(Required)],
1071 Content::localized("command-dismount-desc"),
1072 Some(Admin),
1073 ),
1074 }
1075 }
1076
1077 pub fn keyword(&self) -> &'static str {
1079 match self {
1080 ServerChatCommand::Adminify => "adminify",
1081 ServerChatCommand::Airship => "airship",
1082 ServerChatCommand::Alias => "alias",
1083 ServerChatCommand::AreaAdd => "area_add",
1084 ServerChatCommand::AreaList => "area_list",
1085 ServerChatCommand::AreaRemove => "area_remove",
1086 ServerChatCommand::Aura => "aura",
1087 ServerChatCommand::Ban => "ban",
1088 ServerChatCommand::BanIp => "ban_ip",
1089 ServerChatCommand::BattleMode => "battlemode",
1090 ServerChatCommand::BattleModeForce => "battlemode_force",
1091 ServerChatCommand::Body => "body",
1092 ServerChatCommand::Buff => "buff",
1093 ServerChatCommand::Build => "build",
1094 ServerChatCommand::Campfire => "campfire",
1095 ServerChatCommand::ClearPersistedTerrain => "clear_persisted_terrain",
1096 ServerChatCommand::DeathEffect => "death_effect",
1097 ServerChatCommand::DebugColumn => "debug_column",
1098 ServerChatCommand::DebugWays => "debug_ways",
1099 ServerChatCommand::DisconnectAllPlayers => "disconnect_all_players",
1100 ServerChatCommand::DropAll => "dropall",
1101 ServerChatCommand::Dummy => "dummy",
1102 ServerChatCommand::Explosion => "explosion",
1103 ServerChatCommand::Faction => "faction",
1104 ServerChatCommand::GiveItem => "give_item",
1105 ServerChatCommand::Goto => "goto",
1106 ServerChatCommand::Group => "group",
1107 ServerChatCommand::GroupInvite => "group_invite",
1108 ServerChatCommand::GroupKick => "group_kick",
1109 ServerChatCommand::GroupLeave => "group_leave",
1110 ServerChatCommand::GroupPromote => "group_promote",
1111 ServerChatCommand::Health => "health",
1112 ServerChatCommand::IntoNpc => "into_npc",
1113 ServerChatCommand::JoinFaction => "join_faction",
1114 ServerChatCommand::Jump => "jump",
1115 ServerChatCommand::Kick => "kick",
1116 ServerChatCommand::Kill => "kill",
1117 ServerChatCommand::KillNpcs => "kill_npcs",
1118 ServerChatCommand::Kit => "kit",
1119 ServerChatCommand::Lantern => "lantern",
1120 ServerChatCommand::Respawn => "respawn",
1121 ServerChatCommand::Light => "light",
1122 ServerChatCommand::MakeBlock => "make_block",
1123 ServerChatCommand::MakeNpc => "make_npc",
1124 ServerChatCommand::MakeSprite => "make_sprite",
1125 ServerChatCommand::Motd => "motd",
1126 ServerChatCommand::Object => "object",
1127 ServerChatCommand::Outcome => "outcome",
1128 ServerChatCommand::PermitBuild => "permit_build",
1129 ServerChatCommand::Players => "players",
1130 ServerChatCommand::Portal => "portal",
1131 ServerChatCommand::ResetRecipes => "reset_recipes",
1132 ServerChatCommand::Region => "region",
1133 ServerChatCommand::ReloadChunks => "reload_chunks",
1134 ServerChatCommand::RemoveLights => "remove_lights",
1135 ServerChatCommand::RevokeBuild => "revoke_build",
1136 ServerChatCommand::RevokeBuildAll => "revoke_build_all",
1137 ServerChatCommand::Safezone => "safezone",
1138 ServerChatCommand::Say => "say",
1139 ServerChatCommand::ServerPhysics => "server_physics",
1140 ServerChatCommand::SetMotd => "set_motd",
1141 ServerChatCommand::Ship => "ship",
1142 ServerChatCommand::Site => "site",
1143 ServerChatCommand::SkillPoint => "skill_point",
1144 ServerChatCommand::SkillPreset => "skill_preset",
1145 ServerChatCommand::Spawn => "spawn",
1146 ServerChatCommand::Spot => "spot",
1147 ServerChatCommand::Sudo => "sudo",
1148 ServerChatCommand::Tell => "tell",
1149 ServerChatCommand::Time => "time",
1150 ServerChatCommand::TimeScale => "time_scale",
1151 ServerChatCommand::Tp => "tp",
1152 ServerChatCommand::RtsimTp => "rtsim_tp",
1153 ServerChatCommand::RtsimInfo => "rtsim_info",
1154 ServerChatCommand::RtsimNpc => "rtsim_npc",
1155 ServerChatCommand::RtsimPurge => "rtsim_purge",
1156 ServerChatCommand::RtsimChunk => "rtsim_chunk",
1157 ServerChatCommand::Unban => "unban",
1158 ServerChatCommand::UnbanIp => "unban_ip",
1159 ServerChatCommand::Version => "version",
1160 ServerChatCommand::Waypoint => "waypoint",
1161 ServerChatCommand::Wiring => "wiring",
1162 ServerChatCommand::Whitelist => "whitelist",
1163 ServerChatCommand::World => "world",
1164 ServerChatCommand::MakeVolume => "make_volume",
1165 ServerChatCommand::Location => "location",
1166 ServerChatCommand::CreateLocation => "create_location",
1167 ServerChatCommand::DeleteLocation => "delete_location",
1168 ServerChatCommand::WeatherZone => "weather_zone",
1169 ServerChatCommand::Lightning => "lightning",
1170 ServerChatCommand::Scale => "scale",
1171 ServerChatCommand::RepairEquipment => "repair_equipment",
1172 ServerChatCommand::Tether => "tether",
1173 ServerChatCommand::DestroyTethers => "destroy_tethers",
1174 ServerChatCommand::Mount => "mount",
1175 ServerChatCommand::Dismount => "dismount",
1176 }
1177 }
1178
1179 pub fn short_keyword(&self) -> Option<&'static str> {
1182 Some(match self {
1183 ServerChatCommand::Faction => "f",
1184 ServerChatCommand::Group => "g",
1185 ServerChatCommand::Region => "r",
1186 ServerChatCommand::Say => "s",
1187 ServerChatCommand::Tell => "t",
1188 ServerChatCommand::World => "w",
1189 _ => return None,
1190 })
1191 }
1192
1193 pub fn iter() -> impl Iterator<Item = Self> + Clone { <Self as IntoEnumIterator>::iter() }
1195
1196 pub fn help_content(&self) -> Content {
1198 let data = self.data();
1199
1200 let usage = std::iter::once(format!("/{}", self.keyword()))
1201 .chain(data.args.iter().map(|arg| arg.usage_string()))
1202 .collect::<Vec<_>>()
1203 .join(" ");
1204
1205 Content::localized_with_args("command-help-template", [
1206 ("usage", Content::Plain(usage)),
1207 ("description", data.description),
1208 ])
1209 }
1210
1211 pub fn iter_with_keywords() -> impl Iterator<Item = (&'static str, Self)> {
1215 Self::iter()
1216 .filter_map(|c| c.short_keyword().map(|s| (s, c)))
1218 .chain(Self::iter().map(|c| (c.keyword(), c)))
1219 }
1220
1221 pub fn needs_role(&self) -> Option<comp::AdminRole> { self.data().needs_role }
1222
1223 pub fn arg_fmt(&self) -> String {
1225 self.data()
1226 .args
1227 .iter()
1228 .map(|arg| match arg {
1229 ArgumentSpec::PlayerName(_) => "{}",
1230 ArgumentSpec::EntityTarget(_) => "{}",
1231 ArgumentSpec::SiteName(_) => "{/.*/}",
1232 ArgumentSpec::Float(_, _, _) => "{}",
1233 ArgumentSpec::Integer(_, _, _) => "{d}",
1234 ArgumentSpec::Any(_, _) => "{}",
1235 ArgumentSpec::Command(_) => "{}",
1236 ArgumentSpec::Message(_) => "{/.*/}",
1237 ArgumentSpec::SubCommand => "{} {/.*/}",
1238 ArgumentSpec::Enum(_, _, _) => "{}",
1239 ArgumentSpec::AssetPath(_, _, _, _) => "{}",
1240 ArgumentSpec::Boolean(_, _, _) => "{}",
1241 ArgumentSpec::Flag(_) => "{}",
1242 })
1243 .collect::<Vec<_>>()
1244 .join(" ")
1245 }
1246}
1247
1248impl Display for ServerChatCommand {
1249 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
1250 write!(f, "{}", self.keyword())
1251 }
1252}
1253
1254impl FromStr for ServerChatCommand {
1255 type Err = ();
1256
1257 fn from_str(keyword: &str) -> Result<ServerChatCommand, ()> {
1258 Self::iter()
1259 .filter_map(|c| c.short_keyword().map(|s| (s, c)))
1261 .chain(Self::iter().map(|c| (c.keyword(), c)))
1262 .find_map(|(kwd, command)| (kwd == keyword).then_some(command))
1264 .ok_or(())
1266 }
1267}
1268
1269#[derive(Eq, PartialEq, Debug, Clone, Copy)]
1270pub enum Requirement {
1271 Required,
1272 Optional,
1273}
1274
1275pub enum ArgumentSpec {
1277 PlayerName(Requirement),
1279 EntityTarget(Requirement),
1281 SiteName(Requirement),
1283 Float(&'static str, f32, Requirement),
1288 Integer(&'static str, i32, Requirement),
1293 Any(&'static str, Requirement),
1295 Command(Requirement),
1297 Message(Requirement),
1300 SubCommand,
1302 Enum(&'static str, Vec<String>, Requirement),
1307 AssetPath(&'static str, &'static str, Vec<String>, Requirement),
1313 Boolean(&'static str, String, Requirement),
1318 Flag(&'static str),
1320}
1321
1322impl ArgumentSpec {
1323 pub fn usage_string(&self) -> String {
1324 match self {
1325 ArgumentSpec::PlayerName(req) => {
1326 if &Requirement::Required == req {
1327 "<player>".to_string()
1328 } else {
1329 "[player]".to_string()
1330 }
1331 },
1332 ArgumentSpec::EntityTarget(req) => {
1333 if &Requirement::Required == req {
1334 "<entity>".to_string()
1335 } else {
1336 "[entity]".to_string()
1337 }
1338 },
1339 ArgumentSpec::SiteName(req) => {
1340 if &Requirement::Required == req {
1341 "<site>".to_string()
1342 } else {
1343 "[site]".to_string()
1344 }
1345 },
1346 ArgumentSpec::Float(label, _, req) => {
1347 if &Requirement::Required == req {
1348 format!("<{}>", label)
1349 } else {
1350 format!("[{}]", label)
1351 }
1352 },
1353 ArgumentSpec::Integer(label, _, req) => {
1354 if &Requirement::Required == req {
1355 format!("<{}>", label)
1356 } else {
1357 format!("[{}]", label)
1358 }
1359 },
1360 ArgumentSpec::Any(label, req) => {
1361 if &Requirement::Required == req {
1362 format!("<{}>", label)
1363 } else {
1364 format!("[{}]", label)
1365 }
1366 },
1367 ArgumentSpec::Command(req) => {
1368 if &Requirement::Required == req {
1369 "<[/]command>".to_string()
1370 } else {
1371 "[[/]command]".to_string()
1372 }
1373 },
1374 ArgumentSpec::Message(req) => {
1375 if &Requirement::Required == req {
1376 "<message>".to_string()
1377 } else {
1378 "[message]".to_string()
1379 }
1380 },
1381 ArgumentSpec::SubCommand => "<[/]command> [args...]".to_string(),
1382 ArgumentSpec::Enum(label, _, req) => {
1383 if &Requirement::Required == req {
1384 format!("<{}>", label)
1385 } else {
1386 format!("[{}]", label)
1387 }
1388 },
1389 ArgumentSpec::AssetPath(label, _, _, req) => {
1390 if &Requirement::Required == req {
1391 format!("<{}>", label)
1392 } else {
1393 format!("[{}]", label)
1394 }
1395 },
1396 ArgumentSpec::Boolean(label, _, req) => {
1397 if &Requirement::Required == req {
1398 format!("<{}>", label)
1399 } else {
1400 format!("[{}]", label)
1401 }
1402 },
1403 ArgumentSpec::Flag(label) => {
1404 format!("[{}]", label)
1405 },
1406 }
1407 }
1408
1409 pub fn requirement(&self) -> Requirement {
1410 match self {
1411 ArgumentSpec::PlayerName(r)
1412 | ArgumentSpec::EntityTarget(r)
1413 | ArgumentSpec::SiteName(r)
1414 | ArgumentSpec::Float(_, _, r)
1415 | ArgumentSpec::Integer(_, _, r)
1416 | ArgumentSpec::Any(_, r)
1417 | ArgumentSpec::Command(r)
1418 | ArgumentSpec::Message(r)
1419 | ArgumentSpec::Enum(_, _, r)
1420 | ArgumentSpec::AssetPath(_, _, _, r)
1421 | ArgumentSpec::Boolean(_, _, r) => *r,
1422 ArgumentSpec::Flag(_) => Requirement::Optional,
1423 ArgumentSpec::SubCommand => Requirement::Required,
1424 }
1425 }
1426}
1427
1428pub trait CommandEnumArg: FromStr {
1429 fn all_options() -> Vec<String>;
1430}
1431
1432macro_rules! impl_from_to_str_cmd {
1433 ($enum:ident, ($($attribute:ident => $str:expr),*)) => {
1434 impl std::str::FromStr for $enum {
1435 type Err = String;
1436
1437 fn from_str(s: &str) -> Result<Self, Self::Err> {
1438 match s {
1439 $(
1440 $str => Ok($enum::$attribute),
1441 )*
1442 s => Err(format!("Invalid variant: {s}")),
1443 }
1444 }
1445 }
1446
1447 impl $crate::cmd::CommandEnumArg for $enum {
1448 fn all_options() -> Vec<String> {
1449 vec![$($str.to_string()),*]
1450 }
1451 }
1452 }
1453}
1454
1455impl_from_to_str_cmd!(AuraKindVariant, (
1456 Buff => "buff",
1457 FriendlyFire => "friendly_fire",
1458 ForcePvP => "force_pvp"
1459));
1460
1461impl_from_to_str_cmd!(GroupTarget, (
1462 InGroup => "in_group",
1463 OutOfGroup => "out_of_group"
1464));
1465
1466#[macro_export]
1469macro_rules! parse_cmd_args {
1470 ($args:expr, $($t:ty),* $(, ..$tail:ty)? $(,)?) => {
1471 {
1472 let mut args = $args.into_iter().peekable();
1473 (
1474 $({
1487 let parsed = args.peek().and_then(|s| s.parse::<$t>().ok());
1488 if parsed.is_some() { args.next(); }
1490 parsed
1491 }),*
1492 $(, args.map(|s| s.to_string()).collect::<$tail>())?
1493 )
1494 }
1495 };
1496}
1497
1498#[cfg(test)]
1499mod tests {
1500 use super::*;
1501 use crate::comp::Item;
1502
1503 #[test]
1504 fn verify_cmd_list_sorted() {
1505 let mut list = ServerChatCommand::iter()
1506 .map(|c| c.keyword())
1507 .collect::<Vec<_>>();
1508
1509 let list2 = list.clone();
1511 list.sort_unstable();
1512 assert_eq!(list, list2);
1513 }
1514
1515 #[test]
1516 fn test_loading_skill_presets() {
1517 SkillPresetManifest::load_expect_combined_static(PRESET_MANIFEST_PATH);
1518 }
1519
1520 #[test]
1521 fn test_load_kits() {
1522 let kits = KitManifest::load_expect_combined_static(KIT_MANIFEST_PATH).read();
1523 let mut rng = rand::thread_rng();
1524 for kit in kits.0.values() {
1525 for (item_id, _) in kit.iter() {
1526 match item_id {
1527 KitSpec::Item(item_id) => {
1528 Item::new_from_asset_expect(item_id);
1529 },
1530 KitSpec::ModularWeapon { tool, material } => {
1531 comp::item::modular::random_weapon(*tool, *material, None, &mut rng)
1532 .unwrap_or_else(|_| {
1533 panic!(
1534 "Failed to synthesize a modular {tool:?} made of {material:?}."
1535 )
1536 });
1537 },
1538 }
1539 }
1540 }
1541 }
1542}