Перейти к содержимому

Перехват чата: отмена и перезапись

Меняется. Следите: #283, #636

Этот туториал показывает, как плагин отменяет строку чата или заменяет ее текст. Чат - первое и пока единственное публичное событие игрока в mcrs. Это первая влитая часть запланированных публичных отменяемых событий игрока, а событий входа, выхода, движения и блоков пока нет.12 Примеры взяты из тестовых файлов репозитория, и вне этих тестов мы их не запускали.

Сервер вызывает событие PlayerChatEvent для строки чата до того, как разошлет ее.

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 точно так же добавляет собственного наблюдателя:

ChatPlugin добавляет своего наблюдателя в build
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.

  1. Частично#283 Отслеживает публичные события игрока, которые плагин может отменить. ↩ ↩2

  2. Работает#636 Поднимает отменяемое событие для строки чата перед ее рассылкой. ↩ ↩2 ↩3

Источники и проверка

Проверено: 26.4-snapshot-7, 51d45c26a, 2026-11-09

Переведено с версии 51d45c26a

Следить: #283, #636