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 if !shader.is_supported() {
527 Err(Content::localized_with_args(
528 "command-experimental-shaders-not-supported",
529 [("shader", LocalizationArg::from(item))],
530 ))
531 } else {
532 new_render_mode.experimental_shaders.insert(shader);
533 Ok(Some(Content::localized_with_args(
534 "command-experimental-shaders-enabled",
535 [("shader", LocalizationArg::from(item))],
536 )))
537 };
538
539 change_render_mode(
540 new_render_mode,
541 &mut global_state.window,
542 &mut global_state.settings,
543 );
544
545 res
546 } else {
547 Err(Content::localized_with_args(
548 "command-experimental-shaders-not-a-shader",
549 [("shader", LocalizationArg::from(item))],
550 ))
551 }
552 } else {
553 Err(Content::localized("command-experimental-shaders-not-valid"))
554 }
555}
556
557fn handle_help(
563 session_state: &mut SessionState,
564 global_state: &mut GlobalState,
565 args: Vec<String>,
566) -> CommandResult {
567 let i18n = global_state.i18n.read();
568
569 if let Some(cmd) = parse_cmd_args!(&args, ServerChatCommand) {
570 Ok(Some(cmd.help_content()))
571 } else if let Some(cmd) = parse_cmd_args!(&args, ClientChatCommand) {
572 Ok(Some(cmd.help_content()))
573 } else {
574 let client = &mut session_state.client.borrow_mut();
575
576 let entity_role = client
577 .state()
578 .read_storage::<Admin>()
579 .get(client.entity())
580 .map(|admin| admin.0);
581
582 let client_commands = ClientChatCommand::iter()
583 .map(|cmd| i18n.get_content(&cmd.help_content()))
584 .join("\n");
585
586 let server_commands = ServerChatCommand::iter()
588 .filter(|cmd| cmd.needs_role() <= entity_role)
589 .map(|cmd| i18n.get_content(&cmd.help_content()))
590 .join("\n");
591
592 let additional_shortcuts = ServerChatCommand::iter()
593 .filter(|cmd| cmd.needs_role() <= entity_role)
594 .filter_map(|cmd| cmd.short_keyword().map(|k| (k, cmd)))
595 .map(|(k, cmd)| format!("/{} => /{}", k, cmd.keyword()))
596 .join("\n");
597
598 Ok(Some(Content::localized_with_args("command-help-list", [
599 ("client-commands", LocalizationArg::from(client_commands)),
600 ("server-commands", LocalizationArg::from(server_commands)),
601 (
602 "additional-shortcuts",
603 LocalizationArg::from(additional_shortcuts),
604 ),
605 ])))
606 }
607}
608
609fn handle_naga(
613 _session_state: &mut SessionState,
614 global_state: &mut GlobalState,
615 _args: Vec<String>,
616) -> CommandResult {
617 let mut new_render_mode = global_state.settings.graphics.render_mode.clone();
618 new_render_mode.enable_naga ^= true;
619 let naga_enabled = new_render_mode.enable_naga;
620 change_render_mode(
621 new_render_mode,
622 &mut global_state.window,
623 &mut global_state.settings,
624 );
625
626 Ok(Some(Content::localized_with_args(
627 "command-shader-backend",
628 [(
629 "shader-backend",
630 if naga_enabled {
631 LocalizationArg::from("naga")
632 } else {
633 LocalizationArg::from("shaderc")
634 },
635 )],
636 )))
637}
638
639fn handle_mute(
641 session_state: &mut SessionState,
642 global_state: &mut GlobalState,
643 args: Vec<String>,
644) -> CommandResult {
645 if let Some(alias) = parse_cmd_args!(args, String) {
646 let client = &mut session_state.client.borrow_mut();
647
648 let target = client
649 .player_list()
650 .values()
651 .find(|p| p.player_alias == alias)
652 .ok_or_else(|| {
653 Content::localized_with_args("command-mute-no-player-found", [(
654 "player",
655 LocalizationArg::from(alias.clone()),
656 )])
657 })?;
658
659 if let Some(me) = client.uid().and_then(|uid| client.player_list().get(&uid))
660 && target.uuid == me.uuid
661 {
662 return Err(Content::localized("command-mute-cannot-mute-self"));
663 }
664
665 if global_state
666 .profile
667 .mutelist
668 .insert(target.uuid, alias.clone())
669 .is_none()
670 {
671 Ok(Some(Content::localized_with_args(
672 "command-mute-success",
673 [("player", LocalizationArg::from(alias))],
674 )))
675 } else {
676 Err(Content::localized_with_args(
677 "command-mute-already-muted",
678 [("player", LocalizationArg::from(alias))],
679 ))
680 }
681 } else {
682 Err(Content::localized("command-mute-no-player-specified"))
683 }
684}
685
686fn handle_unmute(
688 session_state: &mut SessionState,
689 global_state: &mut GlobalState,
690 args: Vec<String>,
691) -> CommandResult {
692 if let Some(alias) = parse_cmd_args!(args, String) {
695 if let Some(uuid) = global_state
696 .profile
697 .mutelist
698 .iter()
699 .find(|(_, v)| **v == alias)
700 .map(|(k, _)| *k)
701 {
702 let client = &mut session_state.client.borrow_mut();
703
704 if let Some(me) = client.uid().and_then(|uid| client.player_list().get(&uid))
705 && uuid == me.uuid
706 {
707 return Err(Content::localized("command-unmute-cannot-unmute-self"));
708 }
709
710 global_state.profile.mutelist.remove(&uuid);
711
712 Ok(Some(Content::localized_with_args(
713 "command-unmute-success",
714 [("player", LocalizationArg::from(alias))],
715 )))
716 } else {
717 Err(Content::localized_with_args(
718 "command-unmute-no-muted-player-found",
719 [("player", LocalizationArg::from(alias))],
720 ))
721 }
722 } else {
723 Err(Content::localized("command-unmute-no-player-specified"))
724 }
725}
726
727fn handle_waypoint(
729 session_state: &mut SessionState,
730 _global_state: &mut GlobalState,
731 _args: Vec<String>,
732) -> CommandResult {
733 let client = &mut session_state.client.borrow();
734
735 if let Some(waypoint) = client.waypoint() {
736 Ok(Some(Content::localized_with_args(
737 "command-waypoint-result",
738 [("waypoint", LocalizationArg::from(waypoint.clone()))],
739 )))
740 } else {
741 Err(Content::localized("command-waypoint-error"))
742 }
743}
744
745fn handle_wiki(
751 _session_state: &mut SessionState,
752 _global_state: &mut GlobalState,
753 args: Vec<String>,
754) -> CommandResult {
755 let url = if args.is_empty() {
756 "https://wiki.veloren.net/".to_string()
757 } else {
758 let query_string = args.join("+");
759
760 format!("https://wiki.veloren.net/w/index.php?search={query_string}")
761 };
762
763 open::that_detached(url)
764 .map(|_| Some(Content::localized("command-wiki-success")))
765 .map_err(|e| {
766 Content::localized_with_args("command-wiki-fail", [(
767 "error",
768 LocalizationArg::from(e.to_string()),
769 )])
770 })
771}
772
773fn handle_reset_tutorial(
777 _session_state: &mut SessionState,
778 global_state: &mut GlobalState,
779 _args: Vec<String>,
780) -> CommandResult {
781 global_state.profile.tutorial = Default::default();
782 Ok(Some(Content::localized("command-reset_tutorial-success")))
783}
784
785trait TabComplete {
790 fn complete(&self, part: &str, client: &Client, i18n: &Localization) -> Vec<String>;
791}
792
793impl TabComplete for ArgumentSpec {
794 fn complete(&self, part: &str, client: &Client, i18n: &Localization) -> Vec<String> {
795 match self {
796 ArgumentSpec::PlayerName(_) => complete_player(part, client),
797 ArgumentSpec::EntityTarget(_) => {
798 if let Some((spec, end)) = part.split_once(ClientEntityTarget::PREFIX) {
800 match spec {
801 "" => ClientEntityTarget::iter()
803 .filter_map(|target| {
804 let ident = target.keyword();
805 if ident.starts_with(end) {
806 Some(format!("@{ident}"))
807 } else {
808 None
809 }
810 })
811 .collect(),
812 "uid" => {
814 let end_res = end.trim().parse().map_err(|_| Vec::<String>::new());
815 client
816 .state()
817 .ecs()
818 .read_storage::<Uid>()
819 .join()
820 .filter_map(|uid: &Uid| {
821 let u = uid.0;
822 match end_res {
823 Ok(e) if u > e => Some(format!("uid@{}", u.get())),
824 Ok(_) => None,
825 Err(_) => None,
826 }
827 })
828 .collect()
829 },
830 _ => vec![],
831 }
832 } else {
833 complete_player(part, client)
834 }
835 },
836 ArgumentSpec::SiteName(_) => complete_site(part, client, i18n),
837 ArgumentSpec::Float(_, x, _) => {
838 if part.is_empty() {
839 vec![format!("{:.1}", x)] } else {
841 vec![] }
843 },
844 ArgumentSpec::Integer(_, x, _) => {
845 if part.is_empty() {
846 vec![format!("{}", x)]
847 } else {
848 vec![]
849 }
850 },
851 ArgumentSpec::Any(_, _) => vec![],
853 ArgumentSpec::Command(_) => complete_command(part, ""),
854 ArgumentSpec::Message(_) => complete_player(part, client),
855 ArgumentSpec::SubCommand => complete_command(part, ""),
856 ArgumentSpec::Enum(_, strings, _) => strings
857 .iter()
858 .filter(|string| string.starts_with(part)) .map(|c| c.to_string())
860 .collect(),
861 ArgumentSpec::AssetPath(_, prefix, paths, _) => {
863 if let Some(part_stripped) = part.strip_prefix('#') {
865 paths
866 .iter()
867 .filter(|string| string.contains(part_stripped))
868 .filter_map(|c| Some(c.strip_prefix(prefix)?.to_string()))
869 .collect()
870 } else {
871 let part_with_prefix = prefix.to_string() + part;
873 let depth = part_with_prefix.split('.').count();
874 paths
875 .iter()
876 .map(|path| path.as_str().split('.').take(depth).join("."))
877 .dedup()
878 .filter(|string| string.starts_with(&part_with_prefix))
879 .filter_map(|c| Some(c.strip_prefix(prefix)?.to_string()))
880 .collect()
881 }
882 },
883 ArgumentSpec::Boolean(_, part, _) => ["true", "false"]
884 .iter()
885 .filter(|string| string.starts_with(part))
886 .map(|c| c.to_string())
887 .collect(),
888 ArgumentSpec::Flag(part) => vec![part.to_string()],
889 }
890 }
891}
892
893fn complete_player(part: &str, client: &Client) -> Vec<String> {
895 client
896 .player_list()
897 .values()
898 .map(|player_info| &player_info.player_alias)
899 .filter(|alias| alias.starts_with(part))
900 .cloned()
901 .collect()
902}
903
904fn complete_site(mut part: &str, client: &Client, i18n: &Localization) -> Vec<String> {
906 if let Some(p) = part.strip_prefix('"') {
907 part = p;
908 }
909 client
910 .sites()
911 .values()
912 .filter_map(|site| match site.marker.kind {
913 common::map::MarkerKind::Cave => None,
914 _ => Some(i18n.get_content(site.marker.label.as_ref()?)),
916 })
917 .filter(|name| name.starts_with(part))
918 .map(|name| {
919 if name.contains(' ') {
920 format!("\"{}\"", name)
921 } else {
922 name.clone()
923 }
924 })
925 .collect()
926}
927
928fn nth_word(line: &str, n: usize) -> Option<usize> {
930 let mut is_space = false;
931 let mut word_counter = 0;
932
933 for (i, c) in line.char_indices() {
934 match (is_space, c.is_whitespace()) {
935 (true, true) => {},
936 (true, false) => {
938 is_space = false;
939 word_counter += 1;
940 },
941 (false, true) => {
943 is_space = true;
944 },
945 (false, false) => {},
946 }
947
948 if word_counter == n {
949 return Some(i);
950 }
951 }
952
953 None
954}
955
956fn complete_command(part: &str, prefix: &str) -> Vec<String> {
959 ServerChatCommand::iter_with_keywords()
960 .map(|(kwd, _)| kwd)
961 .chain(ClientChatCommand::iter_with_keywords().map(|(kwd, _)| kwd))
962 .filter(|kwd| kwd.starts_with(part))
963 .map(|kwd| format!("{}{}", prefix, kwd))
964 .collect()
965}
966
967pub fn complete(line: &str, client: &Client, i18n: &Localization, cmd_prefix: &str) -> Vec<String> {
973 let word = if line.chars().last().is_none_or(char::is_whitespace) {
976 ""
977 } else {
978 line.split_whitespace().last().unwrap_or("")
979 };
980
981 if line.starts_with(cmd_prefix) {
983 let line = line.strip_prefix(cmd_prefix).unwrap_or(line);
985 let mut iter = line.split_whitespace();
986
987 let cmd = iter.next().unwrap_or("");
989
990 let argument_position = iter.count() + usize::from(word.is_empty());
992
993 if argument_position == 0 {
995 let word = word.strip_prefix(cmd_prefix).unwrap_or(word);
998 return complete_command(word, cmd_prefix);
999 }
1000
1001 let args = {
1003 if let Ok(cmd) = cmd.parse::<ServerChatCommand>() {
1004 Some(cmd.data().args)
1005 } else if let Ok(cmd) = cmd.parse::<ClientChatCommand>() {
1006 Some(cmd.data().args)
1007 } else {
1008 None
1009 }
1010 };
1011
1012 if let Some(args) = args {
1013 if let Some(arg) = args.get(argument_position - 1) {
1015 arg.complete(word, client, i18n)
1017 } else {
1018 match args.last() {
1020 Some(ArgumentSpec::SubCommand) => {
1022 if let Some(index) = nth_word(line, args.len()) {
1024 complete(&line[index..], client, i18n, "")
1026 } else {
1027 vec![]
1028 }
1029 },
1030 Some(ArgumentSpec::Message(_)) => complete_player(word, client),
1032 _ => vec![],
1033 }
1034 }
1035 } else {
1036 complete_player(word, client)
1037 }
1038 } else {
1039 complete_player(word, client)
1040 }
1041}
1042
1043#[test]
1044fn verify_cmd_list_sorted() {
1045 let mut list = ClientChatCommand::iter()
1046 .map(|c| c.keyword())
1047 .collect::<Vec<_>>();
1048
1049 let list2 = list.clone();
1051 list.sort_unstable();
1052 assert_eq!(list, list2);
1053}
1054
1055#[test]
1056fn test_complete_command() {
1057 assert_eq!(complete_command("mu", "/"), vec!["/mute".to_string()]);
1058 assert_eq!(complete_command("unba", "/"), vec![
1059 "/unban".to_string(),
1060 "/unban_ip".to_string()
1061 ]);
1062 assert_eq!(complete_command("make_", "/"), vec![
1063 "/make_block".to_string(),
1064 "/make_npc".to_string(),
1065 "/make_sprite".to_string(),
1066 "/make_volume".to_string()
1067 ]);
1068}