Content generated with AI — it may contain mistakes.

The catalogue

The catalogue

How the files under cosmetics/ work: one per type, the fields every entry shares, the colour grammar, and what happens when an entry is wrong.

Everything a player can wear is declared in plugins/ExyliaChatCosmetics/cosmetics/, one file per type. They are ordinary YAML, they are meant to be edited by hand, and /cca reload reads them again.

They are deliberately not schema-managed the way config.yml is. A catalogue is curated: it has section banners, a description written as a sentence in one entry and as a list in the next, comments explaining why an id is spelled the way it is. A schema writer round-trips the file through a loader that keeps none of that. config.yml belongs to the schema; these files belong to whoever edits them.

One file per type

Type idFileSectionWorn
tagtags.ymltagsone
nick_colornick-colors.ymlnick_colorsone
rank_colorrank-colors.ymlrank_colorsone
chat_colorchat-colors.ymlchat_colorsone
shadow_colorshadow-colors.ymlshadow_colorsone
fontfonts.ymlfontsone
modifiermodifiers.ymlmodifiersseveral at once
customtag— player rows—one
customcolor— player rows—one
customnick— player rows—one
customrank— player rows—one

The four custom* types have no file: they are what players write for themselves, one database row each, and they are documented in Custom cosmetics.

cosmetics/animations.yml sits in the same folder but is not a type. It is a library of movement any other entry names through animation: — see Animations.

How a cosmetic is named

One cosmetic is type:id everywhere — in a grant, a favourite, a loadout, a placeholder, a command: tag:mvp, chat_color:aurora, customtag:42. One shape means a value written by any of them is readable by all of them.

Both halves are normalised on the way in: trimmed, lower-cased, spaces turned into underscores, and anything that is not a-z, 0-9, _ or - dropped. So MVP and mvp are the same id, and a row an admin wrote years ago as Tag:MVP still resolves. An id that normalises to nothing at all is reported and skipped — there is no cosmetic left to name.

Tabs

Every file opens with a categories section. Each entry there is one tab of the menu that browses that type, and priority is the order the tabs sit in, lowest first.

plugins/ExyliaChatCosmetics/cosmetics/tags.yml
categories:
  symbols:
    name: '{primary}&lSYMBOLS'
    icon: NAME_TAG
    priority: 1
  premium:
    name: '{primary}&lPREMIUM'
    icon: NETHER_STAR
    priority: 7

A category id is normalised like every other key. icon accepts material: as its other spelling, and falls back to PAPER. An entry that names a category no one declared is still loaded — it is simply not reachable from a tab, and a command can still grant it — but the mistake is reported so you can see it.

What every entry declares

Whatever the type, these are the keys the menu, the ownership check and the placeholders read without knowing what kind of cosmetic they are looking at.

KeyDefaultMeans
categoryotherthe tab it lives in
namethe idwhat the menu calls it
iconNAME_TAGthe item drawn in the menu; material: is accepted for it
descriptionnoneone line or a list; lore: is accepted for it
priority999order inside the tab, lowest first; ties break on the id
permissiontruewhether exyliachatcosmetics.<type>.<id> owns it
requirementnoneread from the file and carried on the definition, but nothing evaluates it yet — see below
hiddenfalsekeeps it out of the menu; a grant still equips it
metadatanonefree key: value pairs another plugin can read
tags:
  mvp:
    category: premium
    name: '{primary}&lMVP'
    icon: GOLD_INGOT
    description: 'Reserved for the {highlight}top players.'
    priority: 1
    permission: true
    hidden: false
    metadata:
      season: '2026'
`requirement` does nothing yet

Every entry may carry a requirement, it is read without complaint and it reaches the definition — but no code path evaluates it in 1.0.0. What actually keeps a cosmetic out of a menu is hidden, and what decides whether somebody may wear one is ownership. Write the key if you like; expect nothing from it until a release says otherwise.

name and description are parsed like any other Exylia text: palette tokens, & codes and MiniMessage all work in them.

On top of these, each type adds the keys that make it that type — display and head for a tag, color or gradient for a colour, alphabet for a font, decoration for a modifier. Those live on the pages for tags, colours, fonts and custom cosmetics.

permission reads the other way in rank-colors.yml

permission defaults to true in every file but rank-colors.yml, where a rank colour is usually owned by the rank rather than bought. Written out in Colours.

Ownership by permission reads exyliachatcosmetics.<type>.<id>, exyliachatcosmetics.<type>.category.<category>, exyliachatcosmetics.<type>.* or exyliachatcosmetics.*. Setting permission: false leaves only grants — see Entitlements.

The colour grammar

Wherever a colour is written — color:, a stop inside gradient:, a colour a player mixes, chat.default-color — it is read the same way.

WrittenMeans
#8a51c4one colour, as six hex digits after a hash
&#8a51c4, 8a51c4, <#8a51c4>the same colour, in the other shapes people type it
gold, red, dark_aquaone of the sixteen named colours
{primary}, {accent}, {highlight}an ExyliaLib palette token, which follows your colors.yml
#a:#ba gradient from one colour to the other
#a:#b:#ca gradient through three stops, and so on
gradient:#a:#bthe same, spelled out
<gradient:#a:#b>the same again, for anyone who writes MiniMessage by habit

In a file, color: takes one of those as a string and gradient: takes a list of them; a list of one is read as a solid colour rather than refused. A hash has to be followed by exactly six digits — #12 is not a short hex, it is a mistake, and it is reported as one.

Because palette tokens are resolved when the file is read, a cosmetic written with {primary} follows your server's palette rather than a fixed hex value: change colors.yml, reload, and it changes with it.

Shadows read their own grammar

shadow-colors.yml is the exception. A shadow is not painted with a gradient, and it can be asked to follow the message rather than name a colour of its own, so its color: accepts #rrggbbaa, auto, auto:0.5 and none as well. The full table is in Colours.

When an entry is wrong

Nothing in a catalogue is thrown. Every problem is reported with the path it came from and the entry is skipped, because one mistyped tag must not cost a server its other two hundred.

A report names the file, the entry and the field:

cosmetics/tags.yml tags.mvp.display: is missing, so there is nothing to draw
cosmetics/rank-colors.yml rank_colors.azure.color: cannot be read: 'blurple'
cosmetics/tags.yml tags.event: names the category 'seasonal', which is not declared

What each kind of problem costs you:

ProblemWhat happens
A field an entry cannot do without is missing or unreadablethat entry is skipped, the rest of the file loads
An entry is not a section, or its id normalises to nothingthe same
It names a category nobody declaredit is kept and loaded, but no tab lists it
The categories section is missingthe file loads with no tabs
The entries section is missingthe server has none of that type

A type's catalogue is published as one snapshot and replaced whole, so a reload never leaves a menu showing half a file.

Reloading

/cca reload re-reads the configuration, every catalogue, the animations and the menus. Players stay online, what they are wearing stays on, and a menu that was open picks up the new palette.

Read the console after a reload

The problems above are warnings, not errors: the server keeps running and the file keeps most of its entries. If a cosmetic you just wrote is not in the menu, the reason is one line in the console.

Per type

Something missing on this page? Tell us on Discord