Примеры#
Переключение каналов поставки: local и global#
Понять, какие слеш-команды вообще появятся: их нет в реестре мода, ChannelCommands.java читает chat.channels при старте.
/l
простое сообщение без префикса
/g Всем: ивент на спавне через 5 минут
/global
В поставке local: command l, алиасы local/lc, radius 100, default true, prefix пустой. /l без аргумента пишет channel.switched с ключом local и запоминает канал в ChatHandler до следующего переключения. Следующий обычный чат уходит только игрокам в том же измерении в радиусе 100. /g <текст> сначала переключает на global (алиас global; gc из поставки убрали — конфликтовал с диагностикой /gc), затем постит ServerChatEvent — сообщение уже идёт всем. Префикс ! (у global) одноразовый: !привет уходит в global, постоянный канал не меняется.
Staff-чат перенесён в Chatter#
Понять, почему NeoEssentials не регистрирует /staff, /admin, /mod и /s.
/s Проверьте грифа у спавна
Staff-чат принадлежит Chatter: /s <текст> проверяет neoessentials.chat.staff через LuckPerms и шлёт сообщение только онлайн-игрокам с той же нодой. NeoEssentials не содержит staff-канала и не регистрирует /staff, /admin, /mod или /s; это исключает конфликт обработчиков ServerChatEvent. /ac сюда не относится — это алиас /helpop.
Team-канал без FTB Teams и с модом#
Поставка включает team с teamBased: true — понять, что увидит игрок на паке без FTB Teams и что не сломает ванильный /team.
/team
/t
/teamchat Привет нации
ChannelCommands вешает литералы team, t, teamchat с конфига. Если FTB Teams (ftbteams) не загружен, отправка даёт commands.neoessentials.channel.no_team_provider и никому не рассылает. Мод есть, но игрок не в команде — channel.no_team. Иначе видят онлайн-тиммейты; если на канале ещё и permission, то только тиммейты с этим узлом. radius с teamBased не комбинируется. Важно: литерал team совпадает с ванильной /team (scoreboard). Регистрация канала перехватывает это имя — ванильные подкоманды create/add/join пропадают, пока в конфиге command/aliases содержат team.
Свой канал и Discord без бота#
Добавить donor-канал с отдельным правом и односторонним webhook, не дожидаясь SDLink.
/donor Спасибо за подписку
В chat.channels заведите объект с безопасным ключом (donor, не цвет и не эмодзи): command donor, permission любая строка (например neoessentials.chat.donor), discord.enabled true, webhookUrl URL вебхука Discord, channelId можно оставить пустым. Команды каналов регистрируются только при старте — после правки JSON нужен рестарт, /neoe reload дерево не пересобирает. Routing (prefix, radius, permission, discord.enabled, webhookUrl) ChatHandler и WebhookAdapter читают с диска на каждое сообщение: радиус и webhook подхватятся после reload, новые /donor — нет. broadcastPlayerChat вызывается только если discord.enabled true; пустой channelId отдаёт мосту дефолтный канал, webhook при этом всё равно уйдёт, если URL задан.
Тонкие места#
Команды каналов динамические и их нет в инвентаре#
ChannelCommands.register при старте обходит chat.channels, пропускает ключи enabled и *-description, выключенные каналы (enabled: false) и вешает Commands.literal(command) — если поля command нет, литерал = ключ канала — плюс каждый элемент aliases. Ключ staff пропускается намеренно: его команда /s принадлежит Chatter. В _inventory.json этих имён нет: генератор смотрит registerCommand*, не конфиг. Поэтому commands этой страницы пустой: ни /local, ни /g инвентарь не знает. /ac — helpop, не switch канала. Мастер-выключатель chat.channels.enabled: false (или modules.chatEnabled) пропускает регистрацию целиком; обычный чат тогда идёт как глобальная рассылка без радиуса.
Что делать: Список слеш-команд берите из своего chat.channels (поставка: /l//local//lc, /g//global, /team//t//teamchat). Смена command/aliases/включение канала — рестарт. Prefix/radius/permission/discord — достаточно, чтобы конфиг перечитали (сообщения читают его живьём; дерево команд — нет).
Право канала: switch, доставка и Discord — три разных места#
Поле permission — произвольная строка. Switch (/local, /g, команда или алиас кастомного канала) проверяет её у отправителя через PermissionValidator. Доставка в ChatHandler проверяет ту же строку у каждого получателя (для team — поверх членства в команде). Discord: если у отправителя строки нет, broadcastPlayerChat пропускается, даже когда игроки с правом уже увидели сообщение. Узлы neoessentials.chat.channel.local и neoessentials.chat.channel.global в PermissionRegistry есть, но поставка на local/global поля permission не ставит — эти узлы сами по себе ничего не закрывают. Отдельного neoessentials.chat.bypass нет.
Что делать: Хотите закрыть local или donor-канал — пропишите permission в объекте канала, не надейтесь на channel.local. Staff-чат не является каналом NeoEssentials: его право neoessentials.chat.staff проверяет Chatter.
Один сломанный канал больше не убивает остальные#
Каждый канал в своём try/catch: цвет/эмодзи в ключе или в command дают ошибку в лог и skip только этого канала. Раньше общий catch обрывал цикл. Ключ канала — идентификатор routing, Discord и fallback-литерал; цветное имя — только displayName для {channel}/{neoessentials_channel}.
Что делать: Ключи local/staff/donor, не §cStaff. После правки битого command — рестарт, чтобы литерал появился.
Discord: enabled, пустой channelId, webhookUrl#
ChatHandler ретранслирует, только если discord.enabled true. Пустой channelId становится null — адаптер моста шлёт в свой дефолтный канал (у SDLink при native playerMessages NeoEssentials дефолтный маршрут ещё и пропускает, чтобы не двоить; override с непустым id шлется всегда). WebhookAdapter читает chat.channels.<ключ>.discord.webhookUrl сам, но до него очередь не доходит, пока handler не вызвал broadcastPlayerChat — то есть enabled всё равно нужен.
Что делать: Для отдельного Discord-канала задайте непустой channelId. Только webhook: enabled true + URL, мост не обязателен, обратного Discord→MC нет.
Радиус, измерение, teamBased#
Канал с radius шлёт тем, у кого то же dimension и distanceTo <= radius. Permission на таком канале дополнительно фильтрует получателей. teamBased: true радиус игнорирует. Нет ни radius, ни permission, ни teamBased — глобальный broadcast. default: true используется, когда нет persistent-канала и не сработал prefix; если ни у кого default — getEffectiveChannel возвращает строку global, даже если такого ключа нет.
Что делать: В поставке default — local, не global. Для «только проксимити» выключите global.enabled или не давайте игрокам /g.
Алиас gc и ванильный /team#
Комментарий в config.json: бывший алиас gc у global молча проигрывал диагностической /gc. В поставке его уже нет. Литерал team у team-канала перекрывает ванильную scoreboard-команду /team — ChannelCommands не проверяет, занято ли имя. Короткие алиасы s, t, admin, mod так же могут столкнуться с другими модами; регистрация идёт без isCommandRegistered.
Что делать: Нужна ванильная /team — смените command/aliases канала team (например teamchat только) и рестарт. Не возвращайте gc в aliases global.
/neoe reload не перерегистрирует команды каналов#
Дерево строится один раз из NeoEssentials → ChannelCommands.register. Reload перечитывает JSON для следующего сообщения (prefix, radius, discord, permission на доставку), но новые литералы не появляются и старые не снимаются. modules.chatEnabled / chat.channels.enabled тоже только на старте. Staff-команда /s регистрируется Chatter при запуске сервера.
Что делать: После добавления канала, смены command/aliases или выключателя системы — полный рестарт. После смены radius/prefix/discord — достаточно reload, без новых слеш-команд.
Известные проблемы#
Поставочный command team ломает ванильную /team#
chat.channels.team.command = team. dispatcher.register(Commands.literal("team")) перехватывает имя ванильного scoreboard /team. Это не баг парсера — конфликт заложен в дефолтном конфиге. Алиасы t и teamchat от ванили свободны.
Что делать: Переименуйте command канала (оставьте teamchat) либо выставьте team.enabled: false, если FTB Teams не используете, и перезапустите.
Комментарий в ChatHandler про общий try/catch устарел#
Javadoc getChannelDisplayName всё ещё утверждает, что один сломанный литерал абортит регистрацию всех каналов после него. В ChannelCommands уже per-channel try/catch. На поведение это не влияет, на чтение исходников — да.
Что делать: Ориентируйтесь на ChannelCommands.java, не на этот комментарий.
Подробное руководство#
Раздел ниже — исходная документация проекта (docs/Wiki/ChatChannels.md). Ссылки на другие страницы вики переписаны на этот сайт; текст самих руководств не редактируется, чтобы синк с апстримом не давал конфликтов.
Version: 1.0.5+build.54 · Config: config.json → chat.channels section
Overview#
Chat channels allow messages to be scoped to a specific audience — local proximity, a permission-gated group, or global. Each channel is a freeform entry under chat.channels with its own command, aliases, and behavior; there is no explicit "type" field — a channel's behavior is inferred purely from which optional keys it defines:
- Has a
radius key → proximity-based (only players within that many blocks, same dimension, receive the message)
- Has
teamBased: true → only reaches players on the sender's FTB Teams (or similar supported mod) team — see Team Channel below
- Has a
permission key (and no radius/teamBased) → permission-gated (only players holding that permission receive the message, and it can also gate a Discord relay)
- None of the above → treated as a plain global broadcast channel
Config (config.json → chat.channels)#
chat.channels.enabled is the master on/off switch for the whole channel system (defaults to true; if false, no channel commands are registered and chat falls back to plain vanilla formatting).
Each channel entry (keyed by an arbitrary name, e.g. "local", "global", "staff") supports:
| Key |
Description |
enabled |
Enable/disable this channel |
command |
Primary command to switch to (and optionally speak directly in) this channel |
aliases |
List of additional command aliases |
radius |
If present, makes this a proximity/local channel (block radius, same dimension) |
teamBased |
If true, only reaches players on the sender's team (FTB Teams or another supported team mod). See Team Channel. |
permission |
If present, only players with this permission receive messages sent to the channel; also gates the Discord relay for this channel. Combined with teamBased, it additionally restricts who among the sender's teammates receives the message. |
prefix |
If a chat message starts with this literal prefix, it's routed to this channel for that one message (e.g. typing !hello in the default config routes to global) |
default |
If true, this channel is used when a player hasn't explicitly switched channels and their message has no matching prefix |
displayName |
Optional styled text shown wherever the {channel}/{neoessentials_channel} placeholder is used (see Chat Format Placeholders), e.g. "&d⚙ Staff". Falls back to the raw channel key if unset. See below for why this exists as a separate field. |
discord.enabled |
Relay messages sent in this channel to Discord |
discord.channelId |
Discord channel ID to relay to (blank = the Discord bridge mod's own default/fallback chat channel). See Discord Interoperability below before relying on this for a channel meant to be private. |
There is no type or format key, and no discord.relayFromDiscord — the current implementation only relays Minecraft chat to Discord, not the reverse.
The channel key must stay a plain, simple string#
The key you give a channel ("local", "staff", etc.) is not just a label — it's the internal
identifier used for prefix routing, the discord.channelId lookup, and — when command isn't
set — the actual slash-command literal Brigadier registers for that channel
(ChannelCommands#register). Don't put color codes, emoji, or other special characters in the
key itself — a command literal Brigadier can't parse will fail to register (and no player could
ever type it anyway even if it somehow registered). Each channel now registers in its own
try/catch, so one broken channel only skips itself and logs an error — it no longer silently
aborts registration for every channel listed after it in the file (a real bug prior to this).
If you want a styled/colored channel name, use displayName instead — it's shown via
{channel} in your chat-format, completely independent of the safe internal key:
"staff": {
"enabled": true,
"command": "staff",
"aliases": ["mod", "admin", "s"],
"prefix": "@",
"permission": "neoessentials.chat.staff",
"displayName": "&d⚙ Staff",
"discord": { "enabled": true, "channelId": "" }
}
"chat-format": {
"default": "&8[{channel}&8]&r {neoessentials_prefix}{neoessentials_username}{neoessentials_suffix}: {MESSAGE}"
}
Typing in the staff channel now shows something like [⚙ Staff] PlayerName: message, with the
gear icon and "Staff" rendered in the color you chose — while the channel's actual identifier
(staff) stays a safe, working command name behind the scenes.
Switching channels with /<command> persists until changed again (ChatHandler.setPlayerChannel); running /<command> <message> switches and sends that one message immediately.
Use {channel} / {neoessentials_channel} in a chat-format template to show which channel a
message was sent in — see Chat Format Placeholders.
Team Channel (FTB Teams or similar)#
Set "teamBased": true on a channel to scope it to the sender's team instead of a radius or
permission node — only players on the same team as the sender receive the message:
"team": {
"enabled": true,
"command": "team",
"aliases": ["t", "teamchat"],
"displayName": "&a⛨ Team",
"teamBased": true,
"discord": { "enabled": false, "channelId": "" }
}
Team membership is resolved via TeamManager (com.zerog.neoessentials.teams), which currently
has one provider, FtbTeamsAdapter for FTB Teams — detected via ModList.isLoaded("ftbteams")
and talked to entirely through reflection (no compile-time dependency), so it's safe to have this
channel configured even on packs without FTB Teams installed:
- No supported team mod installed → sending in the channel replies with an error saying so
(
commands.neoessentials.channel.no_team_provider), nothing is broadcast.
- Team mod installed but the sender isn't on a team → a different error
(
commands.neoessentials.channel.no_team), nothing is broadcast.
- Team mod installed and the sender is on a team → message reaches only online teammates (and,
if
permission is also set, only the teammates who also hold that permission).
Adding support for another team mod (Towns and Nations, SimpleTeams, etc.) means implementing
TeamProviderAdapter and registering it in TeamManager — the channel config and routing logic
don't need to change.
Example Config#
This is (trimmed from) the actual shipped default in config.json:
"channels": {
"enabled": true,
"team": {
"enabled": true,
"command": "team",
"aliases": ["t", "teamchat"],
"displayName": "&a⛨ Team",
"teamBased": true,
"default": false,
"discord": { "enabled": false, "channelId": "" }
},
"local": {
"enabled": true,
"radius": 100,
"command": "l",
"aliases": ["local", "lc"],
"prefix": "",
"default": true,
"discord": { "enabled": false, "channelId": "" }
},
"global": {
"enabled": true,
"command": "g",
"aliases": ["global"],
"prefix": "!",
"default": false,
"discord": { "enabled": true, "channelId": "" }
},
"staff": {
"enabled": true,
"command": "staff",
"aliases": ["mod", "admin", "s"],
"prefix": "@",
"permission": "neoessentials.chat.staff",
"default": false,
"discord": { "enabled": true, "channelId": "" }
}
}
global's alias list used to also include gc, but that collides with the built-in
memory/TPS diagnostics command (/gc) — the diagnostics command wins registration, silently
making gc unusable for switching channels. It was dropped from the shipped default; use
/g//global instead.
With this config, typing @hi sends hi to the staff channel for that message only, !hi sends it to global, and plain unprefixed chat goes to local (the default: true channel) unless the player has run /staff, /g, or /team to switch persistently. The team channel is shipped enabled by default too — see Team Channel below for what happens if no supported team mod is installed.
Discord Interoperability (Avoiding Duplicate / Leaked Messages)#
If you're seeing chat messages posted to Discord twice, or a channel you configured as
private (e.g. staff) still showing up in your server's default/public Discord channel, this is
almost always caused by running NeoEssentials' own per-channel Discord relay at the same time
as the Discord bridge mod's (Simple Discord Link, Mc2Discord, DCIntegration) own independent,
built-in "relay every chat message" feature — two completely separate systems both deciding to
post the same message.
For Simple Discord Link specifically: its own config/simple-discord-link/simple-discord-link.toml
has a [chat] playerMessages setting (true by default) that relays every Minecraft chat
message to its own single configured chatChannelID, completely independent of and unaware of
NeoEssentials' chat.channels.*.discord settings. With both active:
- Any channel using discord.channelId: "" (the default/fallback case — this is global's and
staff's shipped default) gets posted to Discord twice: once by NeoEssentials' own relay, once
by SDLink's native relay.
- A channel you intend to keep private, like staff, still gets relayed in full by SDLink's
native relay regardless of whatever NeoEssentials' own settings say — SDLink's blanket relay has
no concept of NeoEssentials' channels at all, so there is nothing NeoEssentials can configure to
stop it from its side.
Fix: set playerMessages = false under [chat] in SDLink's own config file and restart, so
NeoEssentials' own per-channel relay is the only one active. NeoEssentials logs a startup
warning if it detects playerMessages = true while SDLink is loaded, specifically to catch this.
If you configure a real discord.channelId for a channel (e.g. a dedicated private staff
channel), NeoEssentials will route that specific message there correctly — this was a genuine bug
prior to the fix that shipped alongside this documentation, where a configured channelId was
silently ignored and every relayed message always went to SDLink's own default channel regardless
of what channelId said. That part is now fixed; the "SDLink's own native relay still leaks it
elsewhere" part above is a separate, unavoidable interoperability issue that only the
playerMessages = false fix resolves. For SDLink specifically, a channel-routed message is sent
as a proper Discord embed (player name + skin avatar), built manually via JDA's own embed API to
match SDLink's own chat-message look — SDLink's DiscordMessageBuilder has no channel-override
capability, so this recreates that same visual style rather than settling for a plain text line.
Mc2Discord has the same class of channel-routing fix applied (a configured discord.channelId
is now honored instead of silently ignored). Unlike SDLink's channel-routed messages, Mc2Discord's
are sent as a plain PlayerName: message text line rather than a styled embed — no rich-embed
send path currently exists for it. Mc2Discord's own core purpose is also "relay chat to Discord
automatically," so the same double-post risk for a channel using a blank channelId almost
certainly applies — check Mc2Discord's own config for a way to disable its automatic relay for
channels NeoEssentials already handles explicitly, the same way playerMessages = false does for
SDLink. NeoEssentials does not currently ship a startup diagnostic for Mc2Discord's config the way
it does for SDLink's.
DCIntegration works differently: it relays chat entirely through its own vanilla-level mixins,
with no supported hook for NeoEssentials to intercept or suppress. Because of that,
onPlayerChat only ever acts for a DCIntegration setup when a channel has a specific
discord.channelId configured — a channel left blank gets no explicit relay from
NeoEssentials at all (trusting DCIntegration's own native relay to handle it, avoiding a
duplicate). This means a channel with a real channelId set (e.g. a private staff channel) is
correctly posted there by NeoEssentials, but DCIntegration's own native relay may also still
post that same message to its own configured channel (general.botChannel /
advanced.chatOutputChannelID) regardless, since it has no concept of NeoEssentials channels
either — check DCIntegration's own config if you see a private channel still leaking elsewhere.
Customizing the SDLink Embed (discordEmbedTemplate)#
The rich embed SDLink builds for a channel-routed message is fully customizable via a top-level
discordEmbedTemplate section in config.json (or its own templates/discord_embed.json split
file — see Split Configs):
"discordEmbedTemplate": {
"enabled": true,
"authorName": "{player}",
"authorIconUrl": "https://mc-heads.net/avatar/{uuid}",
"description": "{message}",
"color": "#5865F2",
"footerText": "",
"footerIconUrl": "",
"showTimestamp": false
}
| Key |
Description |
enabled |
true = build the rich embed described below. false = fall back to a plain PlayerName: message line instead. |
authorName |
Text shown as the embed author (top-left, bold) |
authorIconUrl |
Small icon shown next to the author name — defaults to the sender's Minecraft skin avatar |
description |
The embed's main body text |
color |
Hex color for the embed's left-side bar, e.g. "#5865F2" (Discord blurple). Leave "" for Discord's default. |
footerText / footerIconUrl |
Optional footer row. Leave both empty ("") to omit the footer entirely. |
showTimestamp |
Adds the current time to the embed's footer area |
Every text field (authorName, authorIconUrl, description, footerText) supports these
placeholders:
| Placeholder |
Value |
{player} |
The sender's Minecraft username |
{uuid} |
The sender's UUID (handy for building a custom avatar URL) |
{message} |
The chat message content |
{channel} |
The NeoEssentials chat channel name (local, global, staff, etc.) |
This customization is currently SDLink-specific — Mc2Discord's and DCIntegration's
channel-routed messages are always plain text, since neither has a rich-embed send path built
yet (see above).
Mc2Discord's and DCIntegration's channel-routing fixes are based on reading their compiled
public API directly, not live-tested against a running instance of either mod (unlike SDLink,
which was verified live) — if you hit anything unexpected with either, please report it.
Permissions#
| Node |
Description |
neoessentials.chat.staff |
Used by the shipped default config to gate the staff channel (channel permissions are arbitrary strings you set per-channel in config — this is just the convention the default config uses) |
neoessentials.chat.channel.local, neoessentials.chat.channel.global |
Registered permission-registry entries for the built-in local/global channels (informational/tab-completion; the local and global channels have no permission key by default, so these aren't actually enforced out of the box) |
There is no generic neoessentials.chat.bypass permission — access to a channel is controlled solely by whether that channel's config defines a permission key.
Back to Wiki Home