Перехват чата: отмена и перезапись
Этот туториал показывает, как плагин отменяет строку чата или заменяет ее текст. Чат - первое и пока единственное публичное событие игрока в mcrs. Это первая влитая часть запланированных публичных отменяемых событий игрока, а событий входа, выхода, движения и блоков пока нет.12 Примеры взяты из тестовых файлов репозитория, и вне этих тестов мы их не запускали.
У события три поля
Заголовок раздела «У события три поля»Сервер вызывает событие PlayerChatEvent для строки чата до того, как разошлет ее.
#[derive(Event, Debug)]pub struct PlayerChatEvent { pub player: Entity, pub text: Text, pub cancelled: bool,}crates/mcrs_minecraft_chat_server/src/chat_plugin.rs:19-24
| Поле | Значение |
|---|---|
player |
сущность отправителя в мире его измерения |
text |
строка в виде Text, а не String; чтобы переписать строку, измените это поле |
cancelled |
если записать true, строка не разойдется |
Text - тип стилизованного текста из протокола, поэтому в строке могут быть цвета и компоненты. Получить Text из обычной строки можно через into_text(), как это делают тесты.
Какие строки вызывают событие
Заголовок раздела «Какие строки вызывают событие»Сервер сначала проверяет строку и вызывает событие, только если она прошла проверки. Поэтому событие приходит после проверок и до рассылки.
| Строка | Что делает сервер |
|---|---|
| Содержит недопустимый символ | Событие не вызывает и пишет в лог, что отбросил строку |
| Пришла от игрока, который скрыл чат | Событие не вызывает и отправляет игроку сообщение “чат отключен” |
| Любая другая | Вызывает событие в мире измерения отправителя. Когда наблюдатели (observer) отработают, читает cancelled и text и рассылает строку |
let text = msg.to_string().into_text();
let mut chat_event = PlayerChatEvent { player: event.entity, text, cancelled: false,};
let username = profile.username.clone();commands.queue(move |world: &mut World| { world.trigger_ref(&mut chat_event); if chat_event.cancelled { return; } info!("<{}> {:?}", username, chat_event.text); let text: Text = Text::translate( "chat.type.text", vec![username.clone().into_text(), chat_event.text], );
let text = match RawText::from_text(&text) { Ok(text) => text, Err(err) => { warn!(player = %username, "dropping a chat line that does not encode: {err:?}"); return; } };
world.write_message(to_target(PacketTarget::AllPlayers, system_chat(text)));})crates/mcrs_minecraft_chat_server/src/chat_plugin.rs:64-93
Триггер выполняется в отложенной команде. Из-за этого рассылка идет сразу после выхода из handle_chat, а не внутри нее.
Отмена строки
Заголовок раздела «Отмена строки»В Paper для этого вызывают setCancelled(true) у AsyncChatEvent. Здесь вы пишете наблюдателя, то есть функцию с параметром On<PlayerChatEvent>, и выставляете поле в ней.
#[test]fn an_observer_that_cancels_a_chat_line_stops_its_broadcast() { fn cancel(mut event: On<PlayerChatEvent>) { event.cancelled = true; } let mut app = host_app::make_host_app(); app.add_dimension_plugin(ChatPlugin); let mut server = PlayServer::start(app, &[], 0); let (sender, _) = server.join(Uuid::from_u128(1), "Sender"); let dim = server.dim; server.world_in(dim).add_observer(cancel); server.queue(sender, &line("hello"));
let sent = server.ticks_all(2);
assert!(!sent.iter().any(|packet| { matches!(packet.target, PacketTarget::AllPlayers) && matches!(packet.data, PacketPayload::SystemChat(_)) }));}crates/mcrs_minecraft_server/tests/it/chat.rs:123-142
Замена текста
Заголовок раздела «Замена текста»Запишите в event.text новый Text, и рассылка возьмет его. Тест ниже еще читает event.player, чтобы показать, кто говорил.
#[test]fn an_observer_that_replaces_a_chat_line_changes_its_broadcast() { #[derive(Resource)] struct Speaker(Entity); fn censor(mut event: On<PlayerChatEvent>, mut commands: Commands) { commands.insert_resource(Speaker(event.player)); event.text = "***".to_owned().into_text(); } let mut app = host_app::make_host_app(); app.add_dimension_plugin(ChatPlugin); let mut server = PlayServer::start(app, &[], 0); let (sender, _) = server.join(Uuid::from_u128(1), "Sender"); server.world().add_observer(censor); server.queue(sender, &line("hello"));
let heard: Vec<Text> = server .ticks_all(2) .iter() .filter(|packet| matches!(packet.target, PacketTarget::AllPlayers)) .filter_map(|packet| match &packet.data { PacketPayload::SystemChat(chat) => { Some(chat.content.to_text().unwrap().map(&ItemHoverMap).unwrap()) } _ => None, }) .collect();
let expected = Text::translate( "chat.type.text", vec![ "Sender".to_owned().into_text(), "***".to_owned().into_text(), ], ); assert_eq!(heard, [expected]); let player = server.player(sender); assert_eq!(server.world().resource::<Speaker>().0, player);}crates/mcrs_minecraft_server/tests/it/chat.rs:144-181
Наблюдателя добавляет плагин измерения
Заголовок раздела «Наблюдателя добавляет плагин измерения»Чат работает внутри каждого мира измерения, поэтому и наблюдатель должен находиться в мире измерения. Тесты добавляют его прямо в мир вызовом server.world().add_observer(...). На настоящем сервере поместите app.add_observer(...) в метод build плагина, который реализует Clone, и зарегистрируйте плагин через add_dimension_plugin. ChatPlugin точно так же добавляет собственного наблюдателя:
impl Plugin for ChatPlugin { fn build(&self, app: &mut App) { if !app.is_plugin_added::<ClientInfoPlugin>() { app.add_plugins(ClientInfoPlugin); } app.add_observer(handle_chat); }}crates/mcrs_minecraft_chat_server/src/chat_plugin.rs:26-33
Почему здесь не подходит add_plugins, объясняет страница Первый плагин.
Запуск тестов
Заголовок раздела «Запуск тестов»Тесты репозитория запускает команда cargo test -p mcrs_minecraft_server --test it chat::.2 О том, как написать свой тест, рассказывает страница Тест плагина без клиента.
Дальше: Добавить команду рассказывает про CommandEvent, событие команд с таким же полем cancelled. Таблица других событий Paper и того, что для них есть в mcrs, находится на странице Paper в mcrs.
Footnotes
Заголовок раздела «Footnotes»Источники и проверка
Проверено: 26.4-snapshot-7, 51d45c26a, 2026-11-09
Переведено с версии 51d45c26a