From 7fbd9a0dc8847450c4689b25bc10b28f449893e3 Mon Sep 17 00:00:00 2001 From: Jupeyy Date: Wed, 1 Jan 2025 19:21:07 +0100 Subject: [PATCH] Add whisper functionalty (client side), some server fixes --- examples/wasm-modules/chat/src/chat/page.rs | 23 +- game/client-render-game/src/render_game.rs | 28 +- game/client-render/src/chat/render.rs | 23 +- game/client-ui/src/chat/chat_entry.rs | 163 +++++----- game/client-ui/src/chat/chat_list.rs | 2 +- game/client-ui/src/chat/input.rs | 320 ++++++++++++++++++-- game/client-ui/src/chat/user_data.rs | 16 +- game/server/src/server.rs | 80 ++--- src/tests/chat.rs | 1 + 9 files changed, 514 insertions(+), 142 deletions(-) diff --git a/examples/wasm-modules/chat/src/chat/page.rs b/examples/wasm-modules/chat/src/chat/page.rs index 5b6b820..d73ec9f 100644 --- a/examples/wasm-modules/chat/src/chat/page.rs +++ b/examples/wasm-modules/chat/src/chat/page.rs @@ -5,8 +5,8 @@ use client_containers::skins::SkinContainer; use client_render_base::render::tee::RenderTee; use client_types::chat::{ChatMsg, MsgSystem, ServerMsg, SystemMsgPlayerSkin}; use client_ui::chat::user_data::{ChatMode, MsgInChat}; -use game_base::network::types::chat::NetChatMsgPlayerChannel; -use game_interface::types::character_info::NetworkSkinInfo; +use game_base::network::types::chat::{ChatPlayerInfo, NetChatMsgPlayerChannel}; +use game_interface::types::{character_info::NetworkSkinInfo, id_gen::IdGenerator}; use graphics::{ graphics::graphics::Graphics, handles::{canvas::canvas::GraphicsCanvasHandle, stream::stream::GraphicsStreamHandle}, @@ -38,6 +38,7 @@ impl ChatPage { pipe: &mut UiRenderPipe<()>, ui_state: &mut UiState, ) { + let id_gen = IdGenerator::default(); let mut entries: VecDeque = vec![ MsgInChat { msg: ServerMsg::Chat(ChatMsg { @@ -49,7 +50,7 @@ impl ChatPage { feet_color: ubvec4::new(255, 255, 255, 255), }, msg: "test".into(), - channel: NetChatMsgPlayerChannel::Global, + channel: NetChatMsgPlayerChannel::GameTeam, }), add_time: Duration::MAX, }, @@ -66,7 +67,15 @@ impl ChatPage { smth like that bla bla bla bla bla bla bla bla bla bla \ bla bla bla bla bla bla" .into(), - channel: NetChatMsgPlayerChannel::Global, + channel: NetChatMsgPlayerChannel::Whisper(ChatPlayerInfo { + id: id_gen.next_id(), + name: "other".try_into().unwrap(), + skin: "skin".try_into().unwrap(), + skin_info: NetworkSkinInfo::Custom { + body_color: ubvec4::new(0, 255, 255, 255), + feet_color: ubvec4::new(255, 255, 255, 255), + }, + }), }), add_time: Duration::MAX, }, @@ -150,7 +159,13 @@ impl ChatPage { skin_container: &mut self.skin_container, render_tee: &self.render_tee, mode: ChatMode::Global, + character_infos: &Default::default(), + local_character_ids: &Default::default(), + + find_player_prompt: &mut Default::default(), + find_player_id: &mut Default::default(), + cur_whisper_player_id: &mut Default::default(), }, ), ui_state, diff --git a/game/client-render-game/src/render_game.rs b/game/client-render-game/src/render_game.rs index e3b14c5..8811e18 100644 --- a/game/client-render-game/src/render_game.rs +++ b/game/client-render-game/src/render_game.rs @@ -1,4 +1,10 @@ -use std::{borrow::Borrow, collections::HashMap, num::NonZeroU32, sync::Arc, time::Duration}; +use std::{ + borrow::Borrow, + collections::{HashMap, HashSet}, + num::NonZeroU32, + sync::Arc, + time::Duration, +}; use crate::components::{ cursor::{RenderCursor, RenderCursorPipe}, @@ -781,6 +787,7 @@ impl RenderGame { render_info: &RenderGameInput, mut player_info: Option<(&PlayerId, &mut RenderGameForPlayer)>, + local_player_ids: &Option>, player_vote_rect: &mut Option, expects_player_vote_miniscreen: bool, ) -> Vec { @@ -806,6 +813,9 @@ impl RenderGame { false }; + let dummy_local_player_ids = HashSet::default(); + let local_player_ids = local_player_ids.as_ref().unwrap_or(&dummy_local_player_ids); + res.extend( self.chat .render(&mut ChatRenderPipe { @@ -820,6 +830,7 @@ impl RenderGame { skin_container: &mut self.containers.skin_container, tee_render: &mut self.players.tee_renderer, character_infos: &render_info.character_infos, + local_character_ids: local_player_ids, }) .into_iter() .map(PlayerFeedbackEvent::Chat), @@ -2526,6 +2537,7 @@ impl RenderGameInterface for RenderGame { self.handle_events(cur_time, &mut input); let mut has_scoreboard = false; + let mut has_chat_input = false; let mut next_sound_listeners = self.world_sound_listeners_pool.new(); std::mem::swap(&mut *next_sound_listeners, &mut self.world_sound_listeners); @@ -2576,6 +2588,7 @@ impl RenderGameInterface for RenderGame { } has_scoreboard |= player.render_for_player.scoreboard_active; + has_chat_input |= player.render_for_player.chat_info.is_some(); } // always clear motd if scoreboard is open @@ -2584,11 +2597,21 @@ impl RenderGameInterface for RenderGame { self.motd.started_at = None; } + // only call this when chat input is active, since it will allocate memory on heap + let local_player_ids = has_chat_input.then(|| { + input + .players + .keys() + .copied() + .chain(input.dummies.iter().copied()) + .collect() + }); + let player_count = input.players.len(); if player_count == 0 { self.render_ingame(config_map, cur_time, &input, None); self.backend_handle.consumble_multi_samples(); - let _ = self.render_uis(cur_time, &input, None, &mut None, false); + let _ = self.render_uis(cur_time, &input, None, &local_player_ids, &mut None, false); } else { let players_per_row = Self::calc_players_per_row(player_count); let window_props = self.canvas_handle.window_props(); @@ -2660,6 +2683,7 @@ impl RenderGameInterface for RenderGame { cur_time, &input, Some((player_id, render_for_player_game)), + &local_player_ids, &mut player_vote_rect, expected_vote_miniscreen, ); diff --git a/game/client-render/src/chat/render.rs b/game/client-render/src/chat/render.rs index e8cecb3..6bfb010 100644 --- a/game/client-render/src/chat/render.rs +++ b/game/client-render/src/chat/render.rs @@ -1,4 +1,7 @@ -use std::{collections::VecDeque, time::Duration}; +use std::{ + collections::{HashSet, VecDeque}, + time::Duration, +}; use base::linked_hash_map_view::FxLinkedHashMap; use client_containers::skins::SkinContainer; @@ -8,7 +11,10 @@ use client_ui::chat::{ user_data::{ChatEvent, ChatMode, MsgInChat, UserData}, }; use egui::Color32; -use game_interface::types::{id_types::CharacterId, render::character::CharacterInfo}; +use game_interface::types::{ + id_types::{CharacterId, PlayerId}, + render::character::CharacterInfo, +}; use graphics::{ graphics::graphics::Graphics, handles::{ @@ -40,6 +46,7 @@ pub struct ChatRenderPipe<'a> { pub skin_container: &'a mut SkinContainer, pub tee_render: &'a RenderTee, pub character_infos: &'a FxLinkedHashMap, + pub local_character_ids: &'a HashSet, } pub struct ChatRender { @@ -49,6 +56,10 @@ pub struct ChatRender { pub msgs: RememberMut>, pub last_render_options: Option, + find_player_prompt: String, + find_player_id: Option, + cur_whisper_player_id: Option, + backend_handle: GraphicsBackendHandle, canvas_handle: GraphicsCanvasHandle, stream_handle: GraphicsStreamHandle, @@ -66,6 +77,10 @@ impl ChatRender { msgs: Default::default(), last_render_options: None, + find_player_prompt: Default::default(), + find_player_id: Default::default(), + cur_whisper_player_id: Default::default(), + backend_handle: graphics.backend_handle.clone(), canvas_handle: graphics.canvas_handle.clone(), stream_handle: graphics.stream_handle.clone(), @@ -103,6 +118,10 @@ impl ChatRender { render_tee: pipe.tee_render, mode: pipe.mode, character_infos: pipe.character_infos, + local_character_ids: pipe.local_character_ids, + find_player_prompt: &mut self.find_player_prompt, + find_player_id: &mut self.find_player_id, + cur_whisper_player_id: &mut self.cur_whisper_player_id, }; let mut dummy_pipe = UiRenderPipe::new(*pipe.cur_time, &mut user_data); let (screen_rect, full_output, zoom_level) = self.ui.render_cached( diff --git a/game/client-ui/src/chat/chat_entry.rs b/game/client-ui/src/chat/chat_entry.rs index d384ad2..3a53809 100644 --- a/game/client-ui/src/chat/chat_entry.rs +++ b/game/client-ui/src/chat/chat_entry.rs @@ -1,8 +1,10 @@ +use std::borrow::Borrow; + use client_types::chat::ChatMsg; use egui::{text::LayoutJob, Align, Color32, FontId, Layout, Stroke, Vec2}; +use game_base::network::types::chat::NetChatMsgPlayerChannel; use game_interface::types::render::character::TeeEye; use math::math::vector::vec2; -use game_base::network::types::chat::NetChatMsgPlayerChannel; use ui_base::types::{UiRenderPipe, UiState}; use crate::utils::render_tee_for_ui; @@ -19,78 +21,99 @@ pub fn render( ui_state: &mut UiState, msg: &ChatMsg, ) { - entry_frame( - ui, - match &msg.channel { - NetChatMsgPlayerChannel::Global => Stroke::NONE, - NetChatMsgPlayerChannel::GameTeam => Stroke::new(2.0, Color32::LIGHT_GREEN), - NetChatMsgPlayerChannel::Whisper(_) => Stroke::new(2.0, Color32::RED), - }, - |ui| { + let (stroke, to) = match &msg.channel { + NetChatMsgPlayerChannel::Global => (Stroke::NONE, None), + NetChatMsgPlayerChannel::GameTeam => (Stroke::new(2.0, Color32::LIGHT_GREEN), None), + NetChatMsgPlayerChannel::Whisper(to) => (Stroke::new(2.0, Color32::RED), Some(to)), + }; + entry_frame(ui, stroke, |ui| { + ui.add_space(MARGIN); + let response = ui.horizontal(|ui| { ui.add_space(MARGIN); - let response = ui.horizontal(|ui| { - ui.add_space(MARGIN); - ui.add_space(TEE_SIZE + MARGIN_FROM_TEE); - ui.style_mut().spacing.item_spacing.x = 4.0; - ui.style_mut().spacing.item_spacing.y = 0.0; - ui.with_layout(Layout::bottom_up(egui::Align::Min), |ui| { - ui.add_space(2.0); - let text_format = egui::TextFormat { - color: Color32::WHITE, - ..Default::default() - }; - let job = LayoutJob::single_section(msg.msg.clone(), text_format); - ui.label(job); - ui.allocate_ui_with_layout( - Vec2::new(ui.available_width(), 14.0), - Layout::left_to_right(Align::Max), - |ui| { - let text_format = egui::TextFormat { - line_height: Some(14.0), - font_id: FontId::proportional(12.0), - valign: Align::BOTTOM, - color: Color32::WHITE, - ..Default::default() - }; - let mut job = - LayoutJob::single_section(msg.player.clone(), text_format); - let text_format_clan = egui::TextFormat { - line_height: Some(12.0), - font_id: FontId::proportional(10.0), - valign: Align::BOTTOM, - color: Color32::LIGHT_GRAY, - ..Default::default() - }; - job.append(&msg.clan, 4.0, text_format_clan); - ui.label(job); - }, - ); - ui.add_space(2.0); - }); - ui.add_space(ui.available_width().min(4.0)); - ui.add_space(MARGIN); + ui.add_space(TEE_SIZE + MARGIN_FROM_TEE); + ui.style_mut().spacing.item_spacing.x = 4.0; + ui.style_mut().spacing.item_spacing.y = 0.0; + ui.with_layout(Layout::bottom_up(egui::Align::Min), |ui| { + ui.add_space(2.0); + let text_format = egui::TextFormat { + color: Color32::WHITE, + ..Default::default() + }; + let job = LayoutJob::single_section(msg.msg.clone(), text_format); + ui.label(job); + ui.allocate_ui_with_layout( + Vec2::new(ui.available_width(), 14.0), + Layout::left_to_right(Align::Max), + |ui| { + let text_format = egui::TextFormat { + line_height: Some(14.0), + font_id: FontId::proportional(12.0), + valign: Align::BOTTOM, + color: Color32::WHITE, + ..Default::default() + }; + let mut job = LayoutJob::single_section(msg.player.clone(), text_format); + let text_format_clan = egui::TextFormat { + line_height: Some(12.0), + font_id: FontId::proportional(10.0), + valign: Align::BOTTOM, + color: Color32::LIGHT_GRAY, + ..Default::default() + }; + job.append(&msg.clan, 4.0, text_format_clan); + ui.label(job); + + if let Some(to) = to { + ui.colored_label(Color32::WHITE, "to"); + ui.colored_label(Color32::WHITE, to.name.as_str()); + + let rect = ui.available_rect_before_wrap(); + + const TEE_SIZE_MINI: f32 = 12.0; + ui.add_space(TEE_SIZE_MINI); + + render_tee_for_ui( + pipe.user_data.canvas_handle, + pipe.user_data.skin_container, + pipe.user_data.render_tee, + ui, + ui_state, + ui.ctx().screen_rect(), + Some(ui.clip_rect()), + to.skin.borrow(), + Some(&to.skin_info), + vec2::new(rect.min.x + TEE_SIZE_MINI / 2.0, rect.left_center().y), + TEE_SIZE_MINI, + TeeEye::Normal, + ); + } + }, + ); + ui.add_space(2.0); }); + ui.add_space(ui.available_width().min(4.0)); ui.add_space(MARGIN); + }); + ui.add_space(MARGIN); - let rect = response.response.rect; + let rect = response.response.rect; - render_tee_for_ui( - pipe.user_data.canvas_handle, - pipe.user_data.skin_container, - pipe.user_data.render_tee, - ui, - ui_state, - ui.ctx().screen_rect(), - Some(ui.clip_rect()), - &msg.skin_name, - Some(&msg.skin_info), - vec2::new( - rect.min.x + MARGIN + TEE_SIZE / 2.0, - rect.min.y + TEE_SIZE / 2.0 + 5.0, - ), - TEE_SIZE, - TeeEye::Normal, - ); - }, - ); + render_tee_for_ui( + pipe.user_data.canvas_handle, + pipe.user_data.skin_container, + pipe.user_data.render_tee, + ui, + ui_state, + ui.ctx().screen_rect(), + Some(ui.clip_rect()), + &msg.skin_name, + Some(&msg.skin_info), + vec2::new( + rect.min.x + MARGIN + TEE_SIZE / 2.0, + rect.min.y + TEE_SIZE / 2.0 + 5.0, + ), + TEE_SIZE, + TeeEye::Normal, + ); + }); } diff --git a/game/client-ui/src/chat/chat_list.rs b/game/client-ui/src/chat/chat_list.rs index 3219df3..47f57aa 100644 --- a/game/client-ui/src/chat/chat_list.rs +++ b/game/client-ui/src/chat/chat_list.rs @@ -11,7 +11,7 @@ use super::user_data::UserData; pub fn render(ui: &mut egui::Ui, pipe: &mut UiRenderPipe, ui_state: &mut UiState) { ui.with_layout(Layout::bottom_up(egui::Align::Min), |ui| { // active input comes first (most bottom) - super::input::render(ui, pipe); + super::input::render(ui, ui_state, pipe); for msg in pipe.user_data.entries.iter() { let time_diff = if pipe.user_data.show_chat_history { diff --git a/game/client-ui/src/chat/input.rs b/game/client-ui/src/chat/input.rs index 472eca0..27e0798 100644 --- a/game/client-ui/src/chat/input.rs +++ b/game/client-ui/src/chat/input.rs @@ -1,35 +1,307 @@ -use ui_base::types::UiRenderPipe; +use std::borrow::Borrow; + +use egui::{ + scroll_area::ScrollBarVisibility, text::LayoutJob, Color32, Frame, Margin, ScrollArea, Shadow, + TextFormat, +}; +use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; +use game_interface::types::render::character::TeeEye; +use math::math::vector::vec2; +use ui_base::{ + style::bg_frame_color, + types::{UiRenderPipe, UiState}, + utils::add_margins, +}; + +use crate::utils::render_tee_for_ui; use super::user_data::{ChatEvent, ChatMode, UserData}; +const SKIN_SIZE: f32 = 20.0; + /// chat input -pub fn render(ui: &mut egui::Ui, pipe: &mut UiRenderPipe) { - if pipe.user_data.is_input_active { - ui.horizontal(|ui| { - ui.label(match pipe.user_data.mode { - ChatMode::Global => "All:".to_string(), - ChatMode::Team => "Team:".to_string(), - ChatMode::Whisper(player_id) => format!("To {:?}:", player_id), - }); - let label = ui.text_edit_singleline(pipe.user_data.msg); - if label.lost_focus() { - pipe.user_data.chat_events.push(ChatEvent::ChatClosed); - if matches!(pipe.user_data.mode, ChatMode::Whisper(Some(_))) - || !matches!(pipe.user_data.mode, ChatMode::Whisper(_)) - { - pipe.user_data.chat_events.push(ChatEvent::MsgSend { - msg: pipe.user_data.msg.clone(), - mode: pipe.user_data.mode, - }); +fn render_inner(ui: &mut egui::Ui, ui_state: &mut UiState, pipe: &mut UiRenderPipe) { + let (is_escape, is_tab, is_enter, is_backspace) = ui.input(|i| { + ( + i.key_pressed(egui::Key::Escape), + i.key_pressed(egui::Key::Tab), + i.key_pressed(egui::Key::Enter), + i.key_pressed(egui::Key::Backspace), + ) + }); + + // Some whisper related stuff + if matches!(pipe.user_data.mode, ChatMode::Whisper(_)) { + // in case backspace is pressed and the current msg is empty we allow + // the user to reenter a new name + if is_backspace && pipe.user_data.msg.is_empty() { + *pipe.user_data.cur_whisper_player_id = None; + pipe.user_data.mode = ChatMode::Whisper(None); + } + } + if let ChatMode::Whisper(None) = &mut pipe.user_data.mode { + // if whisper is empty, try to set from previous state + pipe.user_data.mode = ChatMode::Whisper(*pipe.user_data.cur_whisper_player_id); + } + + let to = ui + .allocate_ui(egui::vec2(ui.available_width(), 30.0), |ui| { + ui.horizontal_centered(|ui| { + let (mode_name, to) = match pipe.user_data.mode { + ChatMode::Global => ("All", None), + ChatMode::Team => ("Team", None), + ChatMode::Whisper(player_id) => ("To", { + player_id + .and_then(|player_id| { + (!pipe.user_data.local_character_ids.contains(&player_id)) + .then_some(player_id) + }) + .and_then(|player_id| pipe.user_data.character_infos.get(&player_id)) + }), + }; + let rect = ui.label(mode_name).rect; + if let Some(to) = to { + let x = ui.style().spacing.item_spacing.x; + ui.style_mut().spacing.item_spacing.x = 0.0; + ui.add_space(SKIN_SIZE); + ui.style_mut().spacing.item_spacing.x = x; + + render_tee_for_ui( + pipe.user_data.canvas_handle, + pipe.user_data.skin_container, + pipe.user_data.render_tee, + ui, + ui_state, + ui.ctx().screen_rect(), + None, + to.info.skin.borrow(), + Some(&to.info.skin_info), + vec2::new( + rect.max.x + ui.style().spacing.item_spacing.x + SKIN_SIZE / 2.0, + rect.right_center().y, + ), + SKIN_SIZE, + TeeEye::Happy, + ); + ui.label(to.info.name.as_str()); } - } else { - pipe.user_data.chat_events.push(ChatEvent::CurMsg { - msg: pipe.user_data.msg.clone(), - mode: pipe.user_data.mode, - }); + ui.label(":"); + // If no use was selected for a whisper, then make a prompt to find one + let unfinished_whisper = + to.is_none() && matches!(pipe.user_data.mode, ChatMode::Whisper(_)); + let label = if unfinished_whisper { + ui.text_edit_singleline(pipe.user_data.find_player_prompt) + } else { + ui.text_edit_singleline(pipe.user_data.msg) + }; + // handled later + if !unfinished_whisper { + if label.lost_focus() { + if is_escape || (!is_tab && is_enter) { + pipe.user_data.chat_events.push(ChatEvent::ChatClosed); + } + if (matches!(pipe.user_data.mode, ChatMode::Whisper(Some(_))) + || !matches!(pipe.user_data.mode, ChatMode::Whisper(_))) + && !pipe.user_data.msg.is_empty() + { + pipe.user_data.chat_events.push(ChatEvent::MsgSend { + msg: pipe.user_data.msg.clone(), + mode: pipe.user_data.mode, + }); + } + } else { + pipe.user_data.chat_events.push(ChatEvent::CurMsg { + msg: pipe.user_data.msg.clone(), + mode: pipe.user_data.mode, + }); + } + } label.request_focus(); + + to + }) + .inner + }) + .inner; + + let unfinished_whisper = to.is_none() && matches!(pipe.user_data.mode, ChatMode::Whisper(_)); + if let Some(whispered_id) = unfinished_whisper.then_some(&mut *pipe.user_data.find_player_id) { + let matcher = SkimMatcherV2::default(); + let matches: Vec<_> = pipe + .user_data + .character_infos + .iter() + .filter(|(id, _)| !pipe.user_data.local_character_ids.contains(id)) + .map(|(id, c)| { + ( + id, + c, + c.info.name.len() as i64, + matcher.fuzzy_indices(&c.info.name, pipe.user_data.find_player_prompt), + ) + }) + .filter(|(_, _, _, m)| m.is_some()) + .map(|(id, c, len, m)| (id, c, len, m.unwrap())) + .collect(); + let shadow_color = ui.style().visuals.window_shadow.color; + ui.add_space(5.0); + ui.allocate_ui(egui::vec2(ui.available_width(), 45.0), |ui| { + ui.style_mut().visuals.clip_rect_margin = 6.0; + ScrollArea::horizontal() + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .show(ui, |ui| { + ui.horizontal_centered(|ui| { + for (&char_id, msg_char, _, (_, matching_char_indices)) in &matches { + let (bg_color_text, match_color, default_color, margin, shadow) = + if *whispered_id == Some(char_id) { + ( + Color32::from_rgba_unmultiplied(140, 140, 140, 15), + Color32::from_rgb(180, 180, 255), + Color32::from_rgb(255, 255, 255), + Margin::symmetric(5.0, 5.0), + Shadow { + blur: 10.0, + spread: 1.0, + color: shadow_color, + ..Default::default() + }, + ) + } else { + ( + Color32::TRANSPARENT, + Color32::from_rgb(180, 180, 255), + if ui.visuals().dark_mode { + Color32::WHITE + } else { + Color32::DARK_GRAY + }, + Margin::symmetric(5.0, 5.0), + Shadow::NONE, + ) + }; + + let msg_chars = msg_char.info.name.as_str().chars().enumerate(); + let mut text_label = LayoutJob::default(); + for (i, msg_char) in msg_chars { + if matching_char_indices.contains(&i) { + text_label.append( + &msg_char.to_string(), + 0.0, + TextFormat { + color: match_color, + ..Default::default() + }, + ); + } else { + text_label.append( + &msg_char.to_string(), + 0.0, + TextFormat { + color: default_color, + ..Default::default() + }, + ); + } + } + let label = Frame::default() + .fill(bg_color_text) + .rounding(5.0) + .inner_margin(margin) + .shadow(shadow) + .show(ui, |ui| { + let rect = ui.available_rect_before_wrap(); + ui.horizontal(|ui| { + ui.add_space(SKIN_SIZE); + render_tee_for_ui( + pipe.user_data.canvas_handle, + pipe.user_data.skin_container, + pipe.user_data.render_tee, + ui, + ui_state, + ui.ctx().screen_rect(), + None, + msg_char.info.skin.borrow(), + Some(&msg_char.info.skin_info), + vec2::new( + rect.left() + SKIN_SIZE / 2.0, + rect.left_center().y, + ), + SKIN_SIZE, + TeeEye::Happy, + ); + + ui.label(text_label); + }); + }); + if *whispered_id == Some(char_id) { + label.response.scroll_to_me(Some(egui::Align::Max)); + } + } + }); + }); + + ui.colored_label( + Color32::YELLOW, + "Press tab to switch the player. Press enter or space to select the player.", + ) + }); + if is_tab { + // chain here so we can simply call it.next() + let mut it = matches + .iter() + .map(|(&id, _, _, _)| id) + .chain(matches.iter().map(|(&id, _, _, _)| id).take(1)) + .skip_while(|id| Some(*id) != *whispered_id); + // this would be current selection + it.next(); + if let Some(next_id) = it.next() { + *whispered_id = Some(next_id); + } + if whispered_id.is_none() { + *whispered_id = matches.iter().map(|(&id, _, _, _)| id).next(); } + } else if let Some(whisper_id) = is_enter.then_some(*whispered_id).and_then(|find_id| { + matches + .iter() + .any(|(&id, _, _, _)| Some(id) == find_id) + .then_some(find_id) + }) { + pipe.user_data.mode = ChatMode::Whisper(whisper_id); + *pipe.user_data.cur_whisper_player_id = whisper_id; + } + pipe.user_data.chat_events.push(ChatEvent::CurMsg { + msg: pipe.user_data.msg.clone(), + mode: pipe.user_data.mode, }); + + if is_escape { + pipe.user_data.chat_events.push(ChatEvent::ChatClosed); + } + } +} + +pub fn render(ui: &mut egui::Ui, ui_state: &mut UiState, pipe: &mut UiRenderPipe) { + if pipe.user_data.is_input_active { + ui.allocate_ui( + egui::vec2( + ui.available_width(), + if matches!(pipe.user_data.mode, ChatMode::Whisper(_)) { + 80.0 + } else { + 30.0 + }, + ), + |ui| { + Frame::none() + .rounding(5.0) + .fill(bg_frame_color()) + .show(ui, |ui| { + add_margins(ui, |ui| { + render_inner(ui, ui_state, pipe); + }); + }); + }, + ); } } diff --git a/game/client-ui/src/chat/user_data.rs b/game/client-ui/src/chat/user_data.rs index f4d7606..0fc9b5a 100644 --- a/game/client-ui/src/chat/user_data.rs +++ b/game/client-ui/src/chat/user_data.rs @@ -1,4 +1,7 @@ -use std::{collections::VecDeque, time::Duration}; +use std::{ + collections::{HashSet, VecDeque}, + time::Duration, +}; use base::linked_hash_map_view::FxLinkedHashMap; use client_containers::skins::SkinContainer; @@ -42,8 +45,15 @@ pub struct UserData<'a> { pub chat_events: &'a mut Vec, pub stream_handle: &'a GraphicsStreamHandle, pub canvas_handle: &'a GraphicsCanvasHandle, - pub skin_container: &'a mut SkinContainer, - pub render_tee: &'a RenderTee, pub mode: ChatMode, + pub character_infos: &'a FxLinkedHashMap, + pub local_character_ids: &'a HashSet, + + pub skin_container: &'a mut SkinContainer, + pub render_tee: &'a RenderTee, + + pub find_player_prompt: &'a mut String, + pub find_player_id: &'a mut Option, + pub cur_whisper_player_id: &'a mut Option, } diff --git a/game/server/src/server.rs b/game/server/src/server.rs index 4f38bf6..8acdaf8 100644 --- a/game/server/src/server.rs +++ b/game/server/src/server.rs @@ -38,6 +38,7 @@ use game_database::{ traits::{DbInterface, DbKind, DbKindExtra}, }; use game_database_backend::GameDbBackend; +use game_state_wasm::game::state_wasm_manager::GameStateWasmManager; use http_accounts::http::AccountHttp; use master_server_types::response::RegisterResponse; use network::network::{ @@ -59,12 +60,11 @@ use network::network::{ }; use pool::{datatypes::PoolFxLinkedHashMap, mt_datatypes::PoolCow, pool::Pool}; use rand::RngCore; +use sql::database::{Database, DatabaseDetails}; use vanilla::{ command_chain::{Command, CommandChain}, sql::account_info::AccountInfo, }; -use game_state_wasm::game::state_wasm_manager::GameStateWasmManager; -use sql::database::{Database, DatabaseDetails}; use x509_cert::der::Encode; use crate::{ @@ -1066,8 +1066,12 @@ impl Server { } } ClientToServerPlayerMessage::Chat(msg) => { + fn prepare_msg(msg: &str) -> String { + msg.trim_matches(char::is_whitespace) + .replace(|c: char| c.is_control(), "") + } let mut handle_msg = |msg: &str, channel: NetChatMsgPlayerChannel| { - if !msg.is_empty() { + if !prepare_msg(msg).is_empty() { if self .game_server .game @@ -1158,41 +1162,45 @@ impl Server { handle_msg(&msg, NetChatMsgPlayerChannel::GameTeam); } MsgClChatMsg::Whisper { receiver_id, msg } => { - if let ( - Some(own_char_info), - Some(recv_char_info), - Some(recv_client), - ) = ( - self.game_server.cached_character_infos.get(player_id), - self.game_server.cached_character_infos.get(&receiver_id), - self.game_server.players.get(&receiver_id), - ) { - let net_channel = NetworkInOrderChannel::Custom(3841); // This number reads as "chat". - let pkt = ServerToClientMessage::Chat(MsgSvChatMsg { - msg: NetChatMsg { - sender: ChatPlayerInfo { - id: *player_id, - name: own_char_info.info.name.clone(), - skin: own_char_info.info.skin.clone(), - skin_info: own_char_info.info.skin_info, - }, - msg: msg.to_string(), - channel: NetChatMsgPlayerChannel::Whisper( - ChatPlayerInfo { - id: receiver_id, - name: recv_char_info.info.name.clone(), - skin: recv_char_info.info.skin.clone(), - skin_info: recv_char_info.info.skin_info, + if !prepare_msg(&msg).is_empty() { + if let ( + Some(own_char_info), + Some(recv_char_info), + Some(recv_client), + ) = ( + self.game_server.cached_character_infos.get(player_id), + self.game_server.cached_character_infos.get(&receiver_id), + self.game_server.players.get(&receiver_id), + ) { + let net_channel = NetworkInOrderChannel::Custom(3841); // This number reads as "chat". + let pkt = ServerToClientMessage::Chat(MsgSvChatMsg { + msg: NetChatMsg { + sender: ChatPlayerInfo { + id: *player_id, + name: own_char_info.info.name.clone(), + skin: own_char_info.info.skin.clone(), + skin_info: own_char_info.info.skin_info, }, - ), - }, - }); + msg: msg.to_string(), + channel: NetChatMsgPlayerChannel::Whisper( + ChatPlayerInfo { + id: receiver_id, + name: recv_char_info.info.name.clone(), + skin: recv_char_info.info.skin.clone(), + skin_info: recv_char_info.info.skin_info, + }, + ), + }, + }); - self.network.send_in_order_to( - &pkt, - &recv_client.network_id, - net_channel, - ); + self.network.send_in_order_to( + &pkt, + &recv_client.network_id, + net_channel, + ); + // and also send it back to the sender + self.network.send_in_order_to(&pkt, con_id, net_channel); + } } } } diff --git a/src/tests/chat.rs b/src/tests/chat.rs index a5ba632..864b9c8 100644 --- a/src/tests/chat.rs +++ b/src/tests/chat.rs @@ -39,6 +39,7 @@ pub fn test_chat( tee_render: render_tee, mode: ChatMode::Global, character_infos: &Default::default(), + local_character_ids: &Default::default(), }); }; render_helper(