Content generated with AI — it may contain mistakes.

Chat module

Chat features

Everything in chat/config.yml that is not the filter: mentions, emojis, renders, whispers, moderation, the announcer and the module's own messages.

chat/config.yml holds the module's switches. The filter has its own file; everything else is here.

SectionKeys
listener-priorityWhere the module takes the chat event: LOWEST, LOW, NORMAL, HIGH, HIGHEST. Default HIGHEST
commandsOne boolean per player command: msg, reply, ignore, ignoreall, spymsg, mutechat, clearchat, broadcast, channel
spyformat, with {sender}, {receiver} and {message}
msgsound, an ExyliaLib effect played for whoever receives a whisper
grammarenabled
emojisenabled
command-replacerenabled, format (%command%), hover
linksenabled
mentionsenabled, format (%player%), max, cooldown-seconds, feedback
renderscache-seconds, then item, inventory and enderchest
cross-serverenabled
console-format{channel}, {name}, {message}. Default [{channel}] {name}: {message}
filtered-markerPut in front of a blocked message for staff who may still read it. Default {error}✖

listener-priority at HIGHEST lets every other plugin have its say before the module takes over. A value that is not an event priority is reported and HIGHEST is used.

commands is read on every reload, not only at startup: a command switched off stops answering without a restart. A command that is off is not registered at all, so another plugin may claim it.

Mentions

mentions:
  enabled: true
  format: '{secondary}@%player%'
  max: 3
  cooldown-seconds: 5.0
  feedback:
    sound: { name: BLOCK_NOTE_BLOCK_PLING, volume: 1.0, pitch: 1.4 }

Writing the @ is a courtesy, not a requirement: a bare name is how people actually talk, and the rewrite puts the @ back. A name inside a longer word is not a mention, so Notchy is one word and not a mention of Notch.

Only players who will actually read the message can be mentioned — the mention is matched against the message's viewers, so a name in a channel somebody cannot read does nothing. Mentions past max in one message stay plain text, and the sender cannot mention themselves.

feedback is an ExyliaLib effect played for the mentioned player, and it ships as a sound alone: a note block pling, not a title across the screen, because a mention is a nudge and a title is an interruption. Write a title, an action-bar or a firework into it if you want one. %mention% in it is the name of the player who said theirs. A player is not nudged twice inside cooldown-seconds, however many times they are named.

The highlight happens at TRANSFORM and the nudge at DELIVERY, so a message the filter stopped in between nudges nobody. The sender needs exyliachatcosmetics.chat.mentions.

Emojis

chat/text/emojis.yml is the table; emojis.enabled is the switch.

plugins/ExyliaChatCosmetics/chat/text/emojis.yml
emojis:
  '<3': '{error}❤'
  ':)': '{success}☺'
  '*star*': '{highlight}★'
 
gated:
  discord:
    trigger: '[discord]'
    replacement: "<click:open_url:'https://discord.exylia.net'><hover:show_text:'{letters}Click to join'>{info}discord.exylia.net</hover></click>"
    permission: exyliachatcosmetics.chat.emoji.discord

emojis.<trigger>: replacement is for everybody. Each gated.<name> names a trigger, a replacement and a permission, which defaults to exyliachatcosmetics.chat.emoji.<name> when it is left out.

A replacement is written like any Exylia line: palette tokens, MiniMessage, its own hover and click, and %placeholders% — though only a replacement that actually names one pays for resolving it.

Every trigger compiles into one alternation, longest first, so :)) is not eaten by :).

Grammar

chat/text/grammar.yml is the table; grammar.enabled is the switch.

rules:
  i: 'I'
  im: "I'm"
  dont: "don't"
  alot: 'a lot'

rules.<wrong>: right. Whole words only, matched in any case, replaced with exactly what you wrote.

Command replacer

A /command written in chat becomes something a reader can click to write. format draws it — %command% is the command without the slash — and hover is what it says. Clicking suggests /command with a trailing space, ready for arguments.

Only the command name is replaced; whatever the player typed after it stays as text.

A URL becomes underlined and clickable, but only for holders of exyliachatcosmetics.chat.links. Everybody else's links were masked or blocked by the filter's url check before this runs, so a link that reached here unmasked belongs to somebody who bypasses the filter anyway.

Renders

Three things a player can show in chat.

WrittenTriggersPermissionShows
[item][item], [i], [hand]exyliachatcosmetics.chat.render.itemWhat they hold, with the vanilla item hover
[inv][inv], [inventory]…chat.render.invTheir inventory, hovering a count
[ec][ec], [enderchest]…chat.render.ecTheir ender chest, hovering a count

Each is drawn at most once per message: a line of twenty [inv] is one window, not twenty snapshots. Triggers are matched in any case, and each kind's format ({item}, {player}) and title (%player%) are configurable.

Clicking opens a read-only copy of what the sender had when they wrote the line. It is a plain container whose clicks and drags are refused, not a menu; there is nothing in it to interact with. [item] opens on one row with the item in the middle, except a held shulker box, which opens on its contents. [inv] opens on a double chest laid out like the inventory screen, with armour and the two hands on the bottom row.

Snapshots live for renders.cache-seconds (default 60) in a bounded cache. After that the click answers renders.expired from chat/messages.yml. The click runs the hidden command /chatcosmeticsrender <id>, and the id is eight random characters rather than a counter, so nobody opens somebody else's window by guessing a number. That command is registered so the click has something to call; there is nothing to type.

The snapshot is taken on the chat thread

The chat event is asynchronous and the message has to be decided inside it, so the inventory is copied where the message is — off the main thread, like every chat plugin that shows items. What is copied is item stacks, which are plain data, and nothing here writes to the player or the world. What Paper guards against off-thread is world and entity mutation, and this does neither.

An empty hand produces no render: the trigger stays as text and the sender is told why, rather than reading [item] in their own message with no explanation.

Private messages, ignores and social spy

/msg (/tell, /w, /whisper), /reply (/r), /ignore <player>, /ignoreall (/msgtoggle, /togglemsg) and /spymsg (/socialspy).

A whisper is two lines, from the sender and receiver blocks of msg.yml. Each is rendered with its own reader as the viewer and the other person as the placeholder target, so a hover about "them" is about the right person on both screens. See Formats.

/ignore toggles one player; /ignoreall closes the inbox to everybody. Both are stored per player. A whisper to a closed or ignoring inbox tells the sender so, rather than pretending to have been delivered. exyliachatcosmetics.chat.bypass.ignore messages through both, and a holder of exyliachatcosmetics.chat.staff cannot be ignored at all.

PrivateMessageEvent fires before a whisper reaches somebody on this server, and cancelling it delivers nothing.

/spymsg toggles reading everybody's whispers, drawn with spy.format. A spy never reads their own whispers, nor the ones they are the receiver of; they would have those anyway.

/reply answers whoever last whispered you or was last whispered by you — both sides remember, so /r works before you have written anything.

With cross-server on, a whisper to somebody who is not on this server is built here, published, and shown there. The other side checks only the closed inbox and the ignore list. If they are on no server at all, the sender is told they are offline.

Moderation

CommandBypass
/mutechatexyliachatcosmetics.chat.bypass.mute
/clearchatexyliachatcosmetics.chat.bypass.clearchat
/broadcast (/bc)—

All three announce themselves with a line naming who did it, and all three follow across servers when cross-server is on — a mute is announced once, on the change, not once per server.

/clearchat pushes a hundred blank lines onto every screen except those that bypass it.

The mute is not stored

It is one flag in memory, read by the gate on every message. A restart unmutes, which is what an owner who muted during a raid an hour ago expects.

The announcer

chat/announcements.yml holds the rotation.

plugins/ExyliaChatCosmetics/chat/announcements.yml
enabled: true
interval-seconds: 300
random: false
 
replacements:
  discord:
    text: '{info}&ldiscord.exylia.net'
    hover: '{warning}➥ Click to join'
    click: 'OPEN_URL:https://discord.exylia.net'
 
announcements:
  discord:
    lines:
      - ' {letters_black}▎ {letters}join us at {discord}{letters}.'
    sound: BLOCK_NOTE_BLOCK_PLING
  cosmetics:
    lines:
      - ' {letters_black}▎ {letters}open {highlight}/cosmetics{letters}.'
    hover: '{warning}➥ Click to open'
    click: 'RUN_COMMAND:/cosmetics'
    permission: ''
KeyMeaning
enabledfalse stops the clock
interval-secondsBetween two announcements. Anything under 5 is raised to 5
randomtrue picks any, false goes down the list
announcements.<id>.linesOne line or a list. Palette tokens, MiniMessage and %placeholders%
announcements.<id>.hoverOne line or a list, shown over every line of the announcement
announcements.<id>.clickRUN_COMMAND:, SUGGEST_COMMAND:, OPEN_URL: or COPY:
announcements.<id>.soundA sound name, or left out for silence
announcements.<id>.permissionOnly holders read it; empty for everybody
replacements.<name>A piece with its own text, hover and click that a line pastes with {name}

A replacement is how one word in a line carries a hover and a click of its own while the rest of the line does not. There is one async timer for all announcements, and reloading swaps the file and restarts the clock from zero.

A line with no %placeholder% in it is built once and sent to everybody; a line with one is built per reader, which is the only time the announcer does work per player.

/cca chat announce <id> sends one now; /cca chat announce next sends whatever is due and advances the rotation.

Announcements are the server speaking, so they keep ExyliaLib's small capitals — unlike the chat line itself. See The chat module.

chat/messages.yml

Every line the module sends, in sections: general, whispers, ignore, staff (spy, filter alerts, the /cca chat results), moderation (mute, clear, broadcast, punished), channels, filter, renders and announcer.

It has its own prefix, and %prefix% in this file is substituted from it before the line reaches the text engine — so the plugin-wide prefix from the cosmetics' messages.yml never applies here.

Why the chat does not borrow the cosmetics' prefix

A chat plugin talks as the chat. A server that runs the module and hides the cosmetics should not read the cosmetics plugin's name in front of a whisper error, so the two prefixes are separate on purpose and neither can leak into the other.

Cross-server

cross-server.enabled: true plus Redis in ExyliaLib's database.yml puts the module on the library's chat channel. Chat lines from channels marked cross-server, whispers, the mute switch, /clearchat and /broadcast all travel; everything is rendered where it was typed and the receiving server only decides who reads it. Without Redis nothing is published and nothing arrives.

The channel side of this is on Channels.

Every command and node named here is listed in full on Commands and Permissions.

Something missing on this page? Tell us on Discord