1use std::{num::NonZeroU64, str::FromStr};
14
15use crate::{
16 GlobalState,
17 render::ExperimentalShader,
18 session::{SessionState, settings_change::change_render_mode},
19};
20use client::Client;
21use common::{
22 cmd::*,
23 comp::Admin,
24 link::Is,
25 mounting::{Mount, Rider, VolumeRider},
26 parse_cmd_args,
27 resources::PlayerEntity,
28 uid::Uid,
29};
30use common_i18n::{Content, LocalizationArg};
31use common_net::sync::WorldSyncExt;
32use i18n::Localization;
33use itertools::Itertools;
34use levenshtein::levenshtein;
35use specs::{Join, WorldExt};
36use strum::{EnumIter, IntoEnumIterator};
37
38#[derive(Clone, Copy, strum::EnumIter)]
46pub enum ClientChatCommand {
47 Clear,
49 ExperimentalShader,
51 Help,
53 Mute,
55 Naga,
57 ResetTutorial,
59 Unmute,
61 Waypoint,
64 Wiki,
66}
67
68impl ClientChatCommand {
69 pub fn data(&self) -> ChatCommandData {
75 use ArgumentSpec::*;
76 use Requirement::*;
77 let cmd = ChatCommandData::new;
78 match self {
79 ClientChatCommand::Clear => {
80 cmd(Vec::new(), Content::localized("command-clear-desc"), None)
81 },
82 ClientChatCommand::ExperimentalShader => cmd(
83 vec![Enum(
84 "Shader",
85 ExperimentalShader::iter()
86 .map(|item| item.to_string())
87 .collect(),
88 Optional,
89 )],
90 Content::localized("command-experimental_shader-desc"),
91 None,
92 ),
93 ClientChatCommand::Help => cmd(
94 vec![Command(Optional)],
95 Content::localized("command-help-desc"),
96 None,
97 ),
98 ClientChatCommand::Naga => cmd(vec![], Content::localized("command-naga-desc"), None),
99 ClientChatCommand::Mute => cmd(
100 vec![PlayerName(Required)],
101 Content::localized("command-mute-desc"),
102 None,
103 ),
104 ClientChatCommand::Unmute => cmd(
105 vec![PlayerName(Required)],
106 Content::localized("command-unmute-desc"),
107 None,
108 ),
109 ClientChatCommand::Waypoint => {
110 cmd(vec![], Content::localized("command-waypoint-desc"), None)
111 },
112 ClientChatCommand::Wiki => cmd(
113 vec![Any("topic", Optional)],
114 Content::localized("command-wiki-desc"),
115 None,
116 ),
117 ClientChatCommand::ResetTutorial => cmd(
118 vec![],
119 Content::localized("command-reset_tutorial-desc"),
120 None,
121 ),
122 }
123 }
124
125 pub fn keyword(&self) -> &'static str {
129 match self {
130 ClientChatCommand::Clear => "clear",
131 ClientChatCommand::ExperimentalShader => "experimental_shader",
132 ClientChatCommand::Help => "help",
133 ClientChatCommand::Naga => "naga",
134 ClientChatCommand::Mute => "mute",
135 ClientChatCommand::Unmute => "unmute",
136 ClientChatCommand::Waypoint => "waypoint",
137 ClientChatCommand::Wiki => "wiki",
138 ClientChatCommand::ResetTutorial => "reset_tutorial",
139 }
140 }
141
142 pub fn help_content(&self) -> Content {
144 let data = self.data();
145
146 let usage = std::iter::once(format!("/{}", self.keyword()))
147 .chain(data.args.iter().map(|arg| arg.usage_string()))
148 .collect::<Vec<_>>()
149 .join(" ");
150
151 Content::localized_with_args("command-help-template", [
152 ("usage", Content::Plain(usage)),
153 ("description", data.description),
154 ])
155 }
156
157 pub fn iter() -> impl Iterator<Item = Self> + Clone {
159 <Self as strum::IntoEnumIterator>::iter()
160 }
161
162 pub fn iter_with_keywords() -> impl Iterator<Item = (&'static str, Self)> {
166 Self::iter().map(|c| (c.keyword(), c))
167 }
168}
169
170impl FromStr for ClientChatCommand {
171 type Err = ();
172
173 fn from_str(keyword: &str) -> Result<ClientChatCommand, ()> {
174 Self::iter()
175 .map(|c| (c.keyword(), c))
176 .find_map(|(kwd, command)| (kwd == keyword).then_some(command))
177 .ok_or(())
178 }
179}
180
181#[derive(Clone, Copy)]
187pub enum ChatCommandKind {
188 Client(ClientChatCommand),
189 Server(ServerChatCommand),
190}
191
192impl FromStr for ChatCommandKind {
193 type Err = ();
194
195 fn from_str(s: &str) -> Result<Self, ()> {
196 if let Ok(cmd) = s.parse::<ClientChatCommand>() {
197 Ok(ChatCommandKind::Client(cmd))
198 } else if let Ok(cmd) = s.parse::<ServerChatCommand>() {
199 Ok(ChatCommandKind::Server(cmd))
200 } else {
201 Err(())
202 }
203 }
204}
205
206type CommandResult = Result<Option<Content>, Content>;
215
216#[derive(EnumIter)]
221enum ClientEntityTarget {
222 Target,
224 Selected,
226 Viewpoint,
228 Mount,
230 Rider,
232 TargetSelf,
234}
235
236impl ClientEntityTarget {
237 const PREFIX: char = '@';
238
239 fn keyword(&self) -> &'static str {
240 match self {
241 ClientEntityTarget::Target => "target",
242 ClientEntityTarget::Selected => "selected",
243 ClientEntityTarget::Viewpoint => "viewpoint",
244 ClientEntityTarget::Mount => "mount",
245 ClientEntityTarget::Rider => "rider",
246 ClientEntityTarget::TargetSelf => "self",
247 }
248 }
249}
250
251fn preproccess_command(
257 session_state: &mut SessionState,
258 command: &ChatCommandKind,
259 args: &mut [String],
260) -> CommandResult {
261 let mut cmd_args = match command {
263 ChatCommandKind::Client(cmd) => cmd.data().args,
264 ChatCommandKind::Server(cmd) => cmd.data().args,
265 };
266 let client = &mut session_state.client.borrow_mut();
267 let ecs = client.state().ecs();
268 let player = ecs.read_resource::<PlayerEntity>().0;
269
270 let mut command_start = 0;
271
272 for (i, arg) in args.iter_mut().enumerate() {
273 let mut could_be_entity_target = false;
274
275 if let Some(post_cmd_args) = cmd_args.get(i - command_start..) {
276 for (j, arg_spec) in post_cmd_args.iter().enumerate() {
277 match arg_spec {
278 ArgumentSpec::EntityTarget(_) => could_be_entity_target = true,
279
280 ArgumentSpec::SubCommand => {
281 if let Some(sub_command) =
282 ServerChatCommand::iter().find(|cmd| cmd.keyword() == arg)
283 {
284 cmd_args = sub_command.data().args;
285 command_start = i + j + 1;
286 break;
287 }
288 },
289
290 ArgumentSpec::AssetPath(_, prefix, _, _) => {
291 *arg = prefix.to_string() + arg;
292 },
293 _ => {},
294 }
295
296 if matches!(arg_spec.requirement(), Requirement::Required) {
297 break;
298 }
299 }
300 } else if matches!(cmd_args.last(), Some(ArgumentSpec::SubCommand)) {
301 could_be_entity_target = true;
304 }
305 if could_be_entity_target && arg.starts_with(ClientEntityTarget::PREFIX) {
307 let target_str = arg.trim_start_matches(ClientEntityTarget::PREFIX);
309
310 let target = ClientEntityTarget::iter()
312 .find(|t| t.keyword() == target_str)
313 .ok_or_else(|| {
314 let expected_list = ClientEntityTarget::iter()
316 .map(|t| t.keyword().to_string())
317 .collect::<Vec<String>>()
318 .join("/");
319 Content::localized_with_args("command-preprocess-target-error", [
320 ("expected_list", LocalizationArg::from(expected_list)),
321 ("target", LocalizationArg::from(target_str)),
322 ])
323 })?;
324 let uid = match target {
325 ClientEntityTarget::Target => session_state
326 .target_entity
327 .and_then(|e| ecs.uid_from_entity(e))
328 .ok_or(Content::localized(
329 "command-preprocess-not-looking-at-valid-target",
330 ))?,
331 ClientEntityTarget::Selected => session_state
332 .selected_entity
333 .and_then(|(e, _)| ecs.uid_from_entity(e))
334 .ok_or(Content::localized(
335 "command-preprocess-not-selected-valid-target",
336 ))?,
337 ClientEntityTarget::Viewpoint => session_state
338 .viewpoint_entity
339 .and_then(|e| ecs.uid_from_entity(e))
340 .ok_or(Content::localized(
341 "command-preprocess-not-valid-viewpoint-entity",
342 ))?,
343 ClientEntityTarget::Mount => {
344 if let Some(player) = player {
345 ecs.read_storage::<Is<Rider>>()
346 .get(player)
347 .map(|is_rider| is_rider.mount)
348 .or(ecs.read_storage::<Is<VolumeRider>>().get(player).and_then(
349 |is_rider| match is_rider.pos.kind {
350 common::mounting::Volume::Terrain => None,
351 common::mounting::Volume::Entity(uid) => Some(uid),
352 },
353 ))
354 .ok_or(Content::localized(
355 "command-preprocess-not-riding-valid-entity",
356 ))?
357 } else {
358 return Err(Content::localized("command-preprocess-no-player-entity"));
359 }
360 },
361 ClientEntityTarget::Rider => {
362 if let Some(player) = player {
363 ecs.read_storage::<Is<Mount>>()
364 .get(player)
365 .map(|is_mount| is_mount.rider)
366 .ok_or(Content::localized("command-preprocess-not-valid-rider"))?
367 } else {
368 return Err(Content::localized("command-preprocess-no-player-entity"));
369 }
370 },
371 ClientEntityTarget::TargetSelf => player
372 .and_then(|e| ecs.uid_from_entity(e))
373 .ok_or(Content::localized("command-preprocess-no-player-entity"))?,
374 };
375
376 let uid = NonZeroU64::from(uid);
378 *arg = format!("uid@{uid}");
379 }
380 }
381
382 Ok(None)
383}
384
385pub fn run_command(
392 session_state: &mut SessionState,
393 global_state: &mut GlobalState,
394 cmd: &str,
395 mut args: Vec<String>,
396) -> CommandResult {
397 let command = ChatCommandKind::from_str(cmd)
398 .map_err(|_| invalid_command_message(&session_state.client.borrow(), cmd.to_string()))?;
399
400 preproccess_command(session_state, &command, &mut args)?;
401
402 match command {
403 ChatCommandKind::Server(cmd) => {
404 session_state
405 .client
406 .borrow_mut()
407 .send_command(cmd.keyword().into(), args);
408 Ok(None) },
411 ChatCommandKind::Client(cmd) => run_client_command(session_state, global_state, cmd, args),
412 }
413}
414
415fn invalid_command_message(client: &Client, user_entered_invalid_command: String) -> Content {
417 let entity_role = client
418 .state()
419 .read_storage::<Admin>()
420 .get(client.entity())
421 .map(|admin| admin.0);
422
423 let usable_commands = ServerChatCommand::iter()
424 .filter(|cmd| cmd.needs_role() <= entity_role)
425 .map(|cmd| cmd.keyword())
426 .chain(ClientChatCommand::iter().map(|cmd| cmd.keyword()));
427
428 let most_similar_cmd = usable_commands
429 .clone()
430 .min_by_key(|cmd| levenshtein(&user_entered_invalid_command, cmd))
431 .expect("At least one command exists.");
432
433 let commands_with_same_prefix = usable_commands
434 .filter(|cmd| cmd.starts_with(&user_entered_invalid_command) && cmd != &most_similar_cmd);
435
436 Content::localized_with_args("command-invalid-command-message", [
437 (
438 "invalid-command",
439 LocalizationArg::from(user_entered_invalid_command.clone()),
440 ),
441 (
442 "most-similar-command",
443 LocalizationArg::from(String::from("/") + most_similar_cmd),
444 ),
445 (
446 "commands-with-same-prefix",
447 LocalizationArg::from(
448 commands_with_same_prefix
449 .map(|cmd| format!("/{cmd}"))
450 .collect::<String>(),
451 ),
452 ),
453 ])
454}
455
456fn run_client_command(
461 session_state: &mut SessionState,
462 global_state: &mut GlobalState,
463 command: ClientChatCommand,
464 args: Vec<String>,
465) -> CommandResult {
466 let command = match command {
467 ClientChatCommand::Clear => handle_clear,
468 ClientChatCommand::ExperimentalShader => handle_experimental_shader,
469 ClientChatCommand::Help => handle_help,
470 ClientChatCommand::Naga => handle_naga,
471 ClientChatCommand::Mute => handle_mute,
472 ClientChatCommand::Unmute => handle_unmute,
473 ClientChatCommand::Waypoint => handle_waypoint,
474 ClientChatCommand::Wiki => handle_wiki,
475 ClientChatCommand::ResetTutorial => handle_reset_tutorial,
476 };
477
478 command(session_state, global_state, args)
479}
480
481fn handle_clear(
483 session_state: &mut SessionState,
484 _global_state: &mut GlobalState,
485 _args: Vec<String>,
486) -> CommandResult {
487 session_state.hud.clear_chat();
488 Ok(None)
489}
490
491fn handle_experimental_shader(
493 _session_state: &mut SessionState,
494 global_state: &mut GlobalState,
495 args: Vec<String>,
496) -> CommandResult {
497 if args.is_empty() {
498 Ok(Some(Content::localized_with_args(
499 "command-experimental-shaders-list",
500 [(
501 "shader-list",
502 LocalizationArg::from(
503 ExperimentalShader::iter()
504 .map(|s| {
505 let is_active = global_state
506 .settings
507 .graphics
508 .render_mode
509 .experimental_shaders
510 .contains(&s);
511 format!("[{}] {}", if is_active { "x" } else { " " }, s)
512 })
513 .collect::<Vec<String>>()
514 .join("/"),
515 ),
516 )],
517 )))
518 } else if let Some(item) = parse_cmd_args!(args, String) {
519 if let Ok(shader) = ExperimentalShader::from_str(&item) {
520 let mut new_render_mode = global_state.settings.graphics.render_mode.clone();
521 let res = if new_render_mode.experimental_shaders.remove(&shader) {
522 Ok(Some(Content::localized_with_args(
523 "command-experimental-shaders-disabled",
524 [("shader", LocalizationArg::from(item))],
525 )))
526 } else {
527 new_render_mode.experimental_shaders.insert(shader);
528 Ok(Some(Content::localized_with_args(
529 "command-experimental-shaders-enabled",
530 [("shader", LocalizationArg::from(item))],
531 )))
532 };
533
534 change_render_mode(
535 new_render_mode,
536 &mut global_state.window,
537 &mut global_state.settings,
538 );
539
540 res
541 } else {
542 Err(Content::localized_with_args(
543 "command-experimental-shaders-not-a-shader",
544 [("shader", LocalizationArg::from(item))],
545 ))
546 }
547 } else {
548 Err(Content::localized("command-experimental-shaders-not-valid"))
549 }
550}
551
552fn handle_help(
558 session_state: &mut SessionState,
559 global_state: &mut GlobalState,
560 args: Vec<String>,
561) -> CommandResult {
562 let i18n = global_state.i18n.read();
563
564 if let Some(cmd) = parse_cmd_args!(&args, ServerChatCommand) {
565 Ok(Some(cmd.help_content()))
566 } else if let Some(cmd) = parse_cmd_args!(&args, ClientChatCommand) {
567 Ok(Some(cmd.help_content()))
568 } else {
569 let client = &mut session_state.client.borrow_mut();
570
571 let entity_role = client
572 .state()
573 .read_storage::<Admin>()
574 .get(client.entity())
575 .map(|admin| admin.0);
576
577 let client_commands = ClientChatCommand::iter()
578 .map(|cmd| i18n.get_content(&cmd.help_content()))
579 .join("\n");
580
581 let server_commands = ServerChatCommand::iter()
583 .filter(|cmd| cmd.needs_role() <= entity_role)
584 .map(|cmd| i18n.get_content(&cmd.help_content()))
585 .join("\n");
586
587 let additional_shortcuts = ServerChatCommand::iter()
588 .filter(|cmd| cmd.needs_role() <= entity_role)
589 .filter_map(|cmd| cmd.short_keyword().map(|k| (k, cmd)))
590 .map(|(k, cmd)| format!("/{} => /{}", k, cmd.keyword()))
591 .join("\n");
592
593 Ok(Some(Content::localized_with_args("command-help-list", [
594 ("client-commands", LocalizationArg::from(client_commands)),
595 ("server-commands", LocalizationArg::from(server_commands)),
596 (
597 "additional-shortcuts",
598 LocalizationArg::from(additional_shortcuts),
599 ),
600 ])))
601 }
602}
603
604fn handle_naga(
608 _session_state: &mut SessionState,
609 global_state: &mut GlobalState,
610 _args: Vec<String>,
611) -> CommandResult {
612 let mut new_render_mode = global_state.settings.graphics.render_mode.clone();
613 new_render_mode.enable_naga ^= true;
614 let naga_enabled = new_render_mode.enable_naga;
615 change_render_mode(
616 new_render_mode,
617 &mut global_state.window,
618 &mut global_state.settings,
619 );
620
621 Ok(Some(Content::localized_with_args(
622 "command-shader-backend",
623 [(
624 "shader-backend",
625 if naga_enabled {
626 LocalizationArg::from("naga")
627 } else {
628 LocalizationArg::from("shaderc")
629 },
630 )],
631 )))
632}
633
634fn handle_mute(
636 session_state: &mut SessionState,
637 global_state: &mut GlobalState,
638 args: Vec<String>,
639) -> CommandResult {
640 if let Some(alias) = parse_cmd_args!(args, String) {
641 let client = &mut session_state.client.borrow_mut();
642
643 let target = client
644 .player_list()
645 .values()
646 .find(|p| p.player_alias == alias)
647 .ok_or_else(|| {
648 Content::localized_with_args("command-mute-no-player-found", [(
649 "player",
650 LocalizationArg::from(alias.clone()),
651 )])
652 })?;
653
654 if let Some(me) = client.uid().and_then(|uid| client.player_list().get(&uid))
655 && target.uuid == me.uuid
656 {
657 return Err(Content::localized("command-mute-cannot-mute-self"));
658 }
659
660 if global_state
661 .profile
662 .mutelist
663 .insert(target.uuid, alias.clone())
664 .is_none()
665 {
666 Ok(Some(Content::localized_with_args(
667 "command-mute-success",
668 [("player", LocalizationArg::from(alias))],
669 )))
670 } else {
671 Err(Content::localized_with_args(
672 "command-mute-already-muted",
673 [("player", LocalizationArg::from(alias))],
674 ))
675 }
676 } else {
677 Err(Content::localized("command-mute-no-player-specified"))
678 }
679}
680
681fn handle_unmute(
683 session_state: &mut SessionState,
684 global_state: &mut GlobalState,
685 args: Vec<String>,
686) -> CommandResult {
687 if let Some(alias) = parse_cmd_args!(args, String) {
690 if let Some(uuid) = global_state
691 .profile
692 .mutelist
693 .iter()
694 .find(|(_, v)| **v == alias)
695 .map(|(k, _)| *k)
696 {
697 let client = &mut session_state.client.borrow_mut();
698
699 if let Some(me) = client.uid().and_then(|uid| client.player_list().get(&uid))
700 && uuid == me.uuid
701 {
702 return Err(Content::localized("command-unmute-cannot-unmute-self"));
703 }
704
705 global_state.profile.mutelist.remove(&uuid);
706
707 Ok(Some(Content::localized_with_args(
708 "command-unmute-success",
709 [("player", LocalizationArg::from(alias))],
710 )))
711 } else {
712 Err(Content::localized_with_args(
713 "command-unmute-no-muted-player-found",
714 [("player", LocalizationArg::from(alias))],
715 ))
716 }
717 } else {
718 Err(Content::localized("command-unmute-no-player-specified"))
719 }
720}
721
722fn handle_waypoint(
724 session_state: &mut SessionState,
725 _global_state: &mut GlobalState,
726 _args: Vec<String>,
727) -> CommandResult {
728 let client = &mut session_state.client.borrow();
729
730 if let Some(waypoint) = client.waypoint() {
731 Ok(Some(Content::localized_with_args(
732 "command-waypoint-result",
733 [("waypoint", LocalizationArg::from(waypoint.clone()))],
734 )))
735 } else {
736 Err(Content::localized("command-waypoint-error"))
737 }
738}
739
740fn handle_wiki(
746 _session_state: &mut SessionState,
747 _global_state: &mut GlobalState,
748 args: Vec<String>,
749) -> CommandResult {
750 let url = if args.is_empty() {
751 "https://wiki.veloren.net/".to_string()
752 } else {
753 let query_string = args.join("+");
754
755 format!("https://wiki.veloren.net/w/index.php?search={query_string}")
756 };
757
758 open::that_detached(url)
759 .map(|_| Some(Content::localized("command-wiki-success")))
760 .map_err(|e| {
761 Content::localized_with_args("command-wiki-fail", [(
762 "error",
763 LocalizationArg::from(e.to_string()),
764 )])
765 })
766}
767
768fn handle_reset_tutorial(
772 _session_state: &mut SessionState,
773 global_state: &mut GlobalState,
774 _args: Vec<String>,
775) -> CommandResult {
776 global_state.profile.tutorial = Default::default();
777 Ok(Some(Content::localized("command-reset_tutorial-success")))
778}
779
780trait TabComplete {
785 fn complete(&self, part: &str, client: &Client, i18n: &Localization) -> Vec<String>;
786}
787
788impl TabComplete for ArgumentSpec {
789 fn complete(&self, part: &str, client: &Client, i18n: &Localization) -> Vec<String> {
790 match self {
791 ArgumentSpec::PlayerName(_) => complete_player(part, client),
792 ArgumentSpec::EntityTarget(_) => {
793 if let Some((spec, end)) = part.split_once(ClientEntityTarget::PREFIX) {
795 match spec {
796 "" => ClientEntityTarget::iter()
798 .filter_map(|target| {
799 let ident = target.keyword();
800 if ident.starts_with(end) {
801 Some(format!("@{ident}"))
802 } else {
803 None
804 }
805 })
806 .collect(),
807 "uid" => {
809 let end_res = end.trim().parse().map_err(|_| Vec::<String>::new());
810 client
811 .state()
812 .ecs()
813 .read_storage::<Uid>()
814 .join()
815 .filter_map(|uid: &Uid| {
816 let u = uid.0;
817 match end_res {
818 Ok(e) if u > e => Some(format!("uid@{}", u.get())),
819 Ok(_) => None,
820 Err(_) => None,
821 }
822 })
823 .collect()
824 },
825 _ => vec![],
826 }
827 } else {
828 complete_player(part, client)
829 }
830 },
831 ArgumentSpec::SiteName(_) => complete_site(part, client, i18n),
832 ArgumentSpec::Float(_, x, _) => {
833 if part.is_empty() {
834 vec![format!("{:.1}", x)] } else {
836 vec![] }
838 },
839 ArgumentSpec::Integer(_, x, _) => {
840 if part.is_empty() {
841 vec![format!("{}", x)]
842 } else {
843 vec![]
844 }
845 },
846 ArgumentSpec::Any(_, _) => vec![],
848 ArgumentSpec::Command(_) => complete_command(part, ""),
849 ArgumentSpec::Message(_) => complete_player(part, client),
850 ArgumentSpec::SubCommand => complete_command(part, ""),
851 ArgumentSpec::Enum(_, strings, _) => strings
852 .iter()
853 .filter(|string| string.starts_with(part)) .map(|c| c.to_string())
855 .collect(),
856 ArgumentSpec::AssetPath(_, prefix, paths, _) => {
858 if let Some(part_stripped) = part.strip_prefix('#') {
860 paths
861 .iter()
862 .filter(|string| string.contains(part_stripped))
863 .filter_map(|c| Some(c.strip_prefix(prefix)?.to_string()))
864 .collect()
865 } else {
866 let part_with_prefix = prefix.to_string() + part;
868 let depth = part_with_prefix.split('.').count();
869 paths
870 .iter()
871 .map(|path| path.as_str().split('.').take(depth).join("."))
872 .dedup()
873 .filter(|string| string.starts_with(&part_with_prefix))
874 .filter_map(|c| Some(c.strip_prefix(prefix)?.to_string()))
875 .collect()
876 }
877 },
878 ArgumentSpec::Boolean(_, part, _) => ["true", "false"]
879 .iter()
880 .filter(|string| string.starts_with(part))
881 .map(|c| c.to_string())
882 .collect(),
883 ArgumentSpec::Flag(part) => vec![part.to_string()],
884 }
885 }
886}
887
888fn complete_player(part: &str, client: &Client) -> Vec<String> {
890 client
891 .player_list()
892 .values()
893 .map(|player_info| &player_info.player_alias)
894 .filter(|alias| alias.starts_with(part))
895 .cloned()
896 .collect()
897}
898
899fn complete_site(mut part: &str, client: &Client, i18n: &Localization) -> Vec<String> {
901 if let Some(p) = part.strip_prefix('"') {
902 part = p;
903 }
904 client
905 .sites()
906 .values()
907 .filter_map(|site| match site.marker.kind {
908 common::map::MarkerKind::Cave => None,
909 _ => Some(i18n.get_content(site.marker.label.as_ref()?)),
911 })
912 .filter(|name| name.starts_with(part))
913 .map(|name| {
914 if name.contains(' ') {
915 format!("\"{}\"", name)
916 } else {
917 name.clone()
918 }
919 })
920 .collect()
921}
922
923fn nth_word(line: &str, n: usize) -> Option<usize> {
925 let mut is_space = false;
926 let mut word_counter = 0;
927
928 for (i, c) in line.char_indices() {
929 match (is_space, c.is_whitespace()) {
930 (true, true) => {},
931 (true, false) => {
933 is_space = false;
934 word_counter += 1;
935 },
936 (false, true) => {
938 is_space = true;
939 },
940 (false, false) => {},
941 }
942
943 if word_counter == n {
944 return Some(i);
945 }
946 }
947
948 None
949}
950
951fn complete_command(part: &str, prefix: &str) -> Vec<String> {
954 ServerChatCommand::iter_with_keywords()
955 .map(|(kwd, _)| kwd)
956 .chain(ClientChatCommand::iter_with_keywords().map(|(kwd, _)| kwd))
957 .filter(|kwd| kwd.starts_with(part))
958 .map(|kwd| format!("{}{}", prefix, kwd))
959 .collect()
960}
961
962pub fn complete(line: &str, client: &Client, i18n: &Localization, cmd_prefix: &str) -> Vec<String> {
968 let word = if line.chars().last().is_none_or(char::is_whitespace) {
971 ""
972 } else {
973 line.split_whitespace().last().unwrap_or("")
974 };
975
976 if line.starts_with(cmd_prefix) {
978 let line = line.strip_prefix(cmd_prefix).unwrap_or(line);
980 let mut iter = line.split_whitespace();
981
982 let cmd = iter.next().unwrap_or("");
984
985 let argument_position = iter.count() + usize::from(word.is_empty());
987
988 if argument_position == 0 {
990 let word = word.strip_prefix(cmd_prefix).unwrap_or(word);
993 return complete_command(word, cmd_prefix);
994 }
995
996 let args = {
998 if let Ok(cmd) = cmd.parse::<ServerChatCommand>() {
999 Some(cmd.data().args)
1000 } else if let Ok(cmd) = cmd.parse::<ClientChatCommand>() {
1001 Some(cmd.data().args)
1002 } else {
1003 None
1004 }
1005 };
1006
1007 if let Some(args) = args {
1008 if let Some(arg) = args.get(argument_position - 1) {
1010 arg.complete(word, client, i18n)
1012 } else {
1013 match args.last() {
1015 Some(ArgumentSpec::SubCommand) => {
1017 if let Some(index) = nth_word(line, args.len()) {
1019 complete(&line[index..], client, i18n, "")
1021 } else {
1022 vec![]
1023 }
1024 },
1025 Some(ArgumentSpec::Message(_)) => complete_player(word, client),
1027 _ => vec![],
1028 }
1029 }
1030 } else {
1031 complete_player(word, client)
1032 }
1033 } else {
1034 complete_player(word, client)
1035 }
1036}
1037
1038#[test]
1039fn verify_cmd_list_sorted() {
1040 let mut list = ClientChatCommand::iter()
1041 .map(|c| c.keyword())
1042 .collect::<Vec<_>>();
1043
1044 let list2 = list.clone();
1046 list.sort_unstable();
1047 assert_eq!(list, list2);
1048}
1049
1050#[test]
1051fn test_complete_command() {
1052 assert_eq!(complete_command("mu", "/"), vec!["/mute".to_string()]);
1053 assert_eq!(complete_command("unba", "/"), vec![
1054 "/unban".to_string(),
1055 "/unban_ip".to_string()
1056 ]);
1057 assert_eq!(complete_command("make_", "/"), vec![
1058 "/make_block".to_string(),
1059 "/make_npc".to_string(),
1060 "/make_sprite".to_string(),
1061 "/make_volume".to_string()
1062 ]);
1063}