1use std::{
4 sync::Arc,
5 time::{SystemTime, UNIX_EPOCH},
6};
7
8use azalea_chat::FormattedText;
9use azalea_protocol::packets::{
10 game::{
11 c_disguised_chat::ClientboundDisguisedChat,
12 c_player_chat::ClientboundPlayerChat,
13 c_system_chat::ClientboundSystemChat,
14 s_chat::{LastSeenMessagesUpdate, ServerboundChat},
15 s_chat_command::ServerboundChatCommand,
16 },
17 Packet,
18};
19use bevy_app::{App, Plugin, Update};
20use bevy_ecs::{
21 entity::Entity,
22 event::{EventReader, EventWriter},
23 prelude::Event,
24 schedule::IntoSystemConfigs,
25};
26use uuid::Uuid;
27
28use crate::{
29 client::Client,
30 packet_handling::game::{handle_send_packet_event, SendPacketEvent},
31};
32
33#[derive(Debug, Clone, PartialEq)]
35pub enum ChatPacket {
36 System(Arc<ClientboundSystemChat>),
37 Player(Arc<ClientboundPlayerChat>),
38 Disguised(Arc<ClientboundDisguisedChat>),
39}
40
41macro_rules! regex {
42 ($re:literal $(,)?) => {{
43 static RE: std::sync::LazyLock<regex::Regex> =
44 std::sync::LazyLock::new(|| regex::Regex::new($re).unwrap());
45 &RE
46 }};
47}
48
49impl ChatPacket {
50 pub fn message(&self) -> FormattedText {
52 match self {
53 ChatPacket::System(p) => p.content.clone(),
54 ChatPacket::Player(p) => p.message(),
55 ChatPacket::Disguised(p) => p.message(),
56 }
57 }
58
59 pub fn split_sender_and_content(&self) -> (Option<String>, String) {
64 match self {
65 ChatPacket::System(p) => {
66 let message = p.content.to_string();
67 if p.overlay {
69 return (None, message);
70 }
71 if let Some(m) = regex!("^<([a-zA-Z_0-9]{1,16})> (.+)$").captures(&message) {
74 return (Some(m[1].to_string()), m[2].to_string());
75 }
76
77 (None, message)
78 }
79 ChatPacket::Player(p) => (
80 Some(p.chat_type.name.to_string()),
83 p.body.content.clone(),
84 ),
85 ChatPacket::Disguised(p) => (
86 Some(p.chat_type.name.to_string()),
89 p.message.to_string(),
90 ),
91 }
92 }
93
94 pub fn username(&self) -> Option<String> {
98 self.split_sender_and_content().0
99 }
100
101 pub fn uuid(&self) -> Option<Uuid> {
105 match self {
106 ChatPacket::System(_) => None,
107 ChatPacket::Player(m) => Some(m.sender),
108 ChatPacket::Disguised(_) => None,
109 }
110 }
111
112 pub fn content(&self) -> String {
116 self.split_sender_and_content().1
117 }
118
119 pub fn new(message: &str) -> Self {
122 ChatPacket::System(Arc::new(ClientboundSystemChat {
123 content: FormattedText::from(message),
124 overlay: false,
125 }))
126 }
127
128 pub fn is_whisper(&self) -> bool {
132 match self.message() {
133 FormattedText::Text(_) => false,
134 FormattedText::Translatable(t) => t.key == "commands.message.display.incoming",
135 }
136 }
137}
138
139impl Client {
140 pub fn send_chat_packet(&self, message: &str) {
146 self.ecs.lock().send_event(SendChatKindEvent {
147 entity: self.entity,
148 content: message.to_string(),
149 kind: ChatKind::Message,
150 });
151 self.run_schedule_sender.send(()).unwrap();
152 }
153
154 pub fn send_command_packet(&self, command: &str) {
157 self.ecs.lock().send_event(SendChatKindEvent {
158 entity: self.entity,
159 content: command.to_string(),
160 kind: ChatKind::Command,
161 });
162 self.run_schedule_sender.send(()).unwrap();
163 }
164
165 pub fn chat(&self, content: &str) {
175 self.ecs.lock().send_event(SendChatEvent {
176 entity: self.entity,
177 content: content.to_string(),
178 });
179 self.run_schedule_sender.send(()).unwrap();
180 }
181}
182
183pub struct ChatPlugin;
184impl Plugin for ChatPlugin {
185 fn build(&self, app: &mut App) {
186 app.add_event::<SendChatEvent>()
187 .add_event::<SendChatKindEvent>()
188 .add_event::<ChatReceivedEvent>()
189 .add_systems(
190 Update,
191 (
192 handle_send_chat_event,
193 handle_send_chat_kind_event.after(handle_send_packet_event),
194 )
195 .chain(),
196 );
197 }
198}
199
200#[derive(Event, Debug, Clone)]
202pub struct ChatReceivedEvent {
203 pub entity: Entity,
204 pub packet: ChatPacket,
205}
206
207#[derive(Event)]
209pub struct SendChatEvent {
210 pub entity: Entity,
211 pub content: String,
212}
213
214pub fn handle_send_chat_event(
215 mut events: EventReader<SendChatEvent>,
216 mut send_chat_kind_events: EventWriter<SendChatKindEvent>,
217) {
218 for event in events.read() {
219 if event.content.starts_with('/') {
220 send_chat_kind_events.send(SendChatKindEvent {
221 entity: event.entity,
222 content: event.content[1..].to_string(),
223 kind: ChatKind::Command,
224 });
225 } else {
226 send_chat_kind_events.send(SendChatKindEvent {
227 entity: event.entity,
228 content: event.content.clone(),
229 kind: ChatKind::Message,
230 });
231 }
232 }
233}
234
235#[derive(Event)]
245pub struct SendChatKindEvent {
246 pub entity: Entity,
247 pub content: String,
248 pub kind: ChatKind,
249}
250
251pub enum ChatKind {
253 Message,
254 Command,
255}
256
257pub fn handle_send_chat_kind_event(
258 mut events: EventReader<SendChatKindEvent>,
259 mut send_packet_events: EventWriter<SendPacketEvent>,
260) {
261 for event in events.read() {
262 let content = event
263 .content
264 .chars()
265 .filter(|c| !matches!(c, '\x00'..='\x1F' | '\x7F' | 'ยง'))
266 .take(256)
267 .collect::<String>();
268 let packet = match event.kind {
269 ChatKind::Message => ServerboundChat {
270 message: content,
271 timestamp: SystemTime::now()
272 .duration_since(UNIX_EPOCH)
273 .expect("Time shouldn't be before epoch")
274 .as_millis()
275 .try_into()
276 .expect("Instant should fit into a u64"),
277 salt: azalea_crypto::make_salt(),
278 signature: None,
279 last_seen_messages: LastSeenMessagesUpdate::default(),
280 }
281 .into_variant(),
282 ChatKind::Command => {
283 ServerboundChatCommand { command: content }.into_variant()
285 }
286 };
287
288 send_packet_events.send(SendPacketEvent::new(event.entity, packet));
289 }
290}
291
292