Nexal Titles
A comprehensive title system for Minecraft servers featuring unlockable titles that can be displayed in chat and above player heads. Titles can be earned through achievements (both custom statistics and vanilla Minecraft advancements), purchased using Vault economy, or granted via permissions.
Features
- Title Storage - Dual storage system supporting both JSON and SQL (MariaDB/MySQL) for player titles, active selections and statistics
- Config-Based Title Definitions - Define unlimited titles in config.yml with display names, descriptions, rarities, categories, and unlock requirements
- Custom Achievement Tracking - Automatically track player statistics including kills, deaths, playtime, blocks mined/placed, and mobs killed to unlock achievement-based titles
- Vanilla Advancement Integration - Unlock titles when players complete specific Minecraft advancements (e.g., kill Ender Dragon, obtain Elytra)
- Purchasable Titles - Allow players to buy titles using Vault economy with configurable prices and price format ($1,000); every purchase asks for a second click to confirm. Vault is optional: without it everything else works
- Permission-Based Titles - Auto-grant titles based on permissions with periodic checking for VIP/donor ranks
- Interactive GUI Browser - Category-based GUI for browsing and selecting titles with real-time progress tracking
- Title Descriptions - Free text for each title (lore, memes, who won it and when), shown in the GUI for every title, as a chat hover (Paper and Spigot) and through placeholders
- Scoreboard Team Display - Show active titles above player heads using scoreboard teams with configurable prefix/suffix positioning
- PlaceholderAPI Integration - 15+ placeholders for displaying titles, descriptions, prices, stats, and progress in other plugins
- Admin Menu -
/titles adminopens a menu with the settings, every title (edit its name, description and category), every player's titles and statistics (grant and revoke with a click), reload, export and import - Title Categories - Organize titles into categories (Starter, Achievement, Purchase, VIP, Event, Special, Admin)
- Rarity System - Five rarity tiers (Common, Uncommon, Rare, Epic, Legendary) with color-coded display
- Export/Import System - Migrate titles and statistics between JSON and SQL storage with simple export/import commands
- Hot-Reload Support - Config and message files support hot-reload without server restart
Technologies
- Paper API 1.20.1+ - Minecraft server API (Spigot compatible)
- Utilizer v0.21.2 - Configuration, messages, items, tasks, and GUI framework
- ACF (Aikar Command Framework) - Annotation-based command system
- nexal-core - JSON and SQL persistence with HikariCP connection pooling
- Vault API - Economy integration for purchasable titles (optional)
- PlaceholderAPI - Placeholder expansion for cross-plugin integration
Commands
Player Commands
| Command | Description | Permission |
|---|---|---|
/titles | Open title browser GUI (the help from the console) | nexaltitles.use |
/titles select \<title> | Select a title to display | nexaltitles.use |
/titles unequip | Unequip current title | nexaltitles.use |
/titles list | List all owned titles | nexaltitles.use |
/titles stats [player] | View a player's titles and progress, online or offline | nexaltitles.use |
/titles help | Show help information | nexaltitles.use |
Admin Commands
Admin commands are hidden from players without the nexaltitles.admin permission.
| Command | Description | Permission |
|---|---|---|
/titles admin | Open the admin menu (the admin commands from the console) | nexaltitles.admin |
/titles admin help | Show the admin commands | nexaltitles.admin |
/titles admin grant \<player> \<title> | Grant a title to a player, online or offline | nexaltitles.admin |
/titles admin revoke \<player> \<title> | Revoke a title from a player, online or offline | nexaltitles.admin |
/titles admin reload | Reload configuration | nexaltitles.admin |
/titles admin export | Export titles and statistics to JSON files | nexaltitles.admin |
/titles admin import | Import titles and statistics from JSON files | nexaltitles.admin |
Admin menu
/titles admin opens a menu with:
-
Configuration - Chat prefix, above-head display and position, max length, title purchases, achievement tracking and save interval, rank titles and their check interval. Changes are saved to
config.ymland apply at once. -
Titles - Every title with its category, rarity, requirement and how many players own it. Click one to edit it:
- Name - type the new name in chat (MiniMessage or & colour codes);
cancelstops. - Description - left click and type it in chat, one line per message, then
done; right click clears it. - Category - pick one of the seven categories.
Changes are saved to
config.ymland the titles reload at once, names above heads included. - Name - type the new name in chat (MiniMessage or & colour codes);
-
Players - Every stored player, online first. Open one to see their statistics, left click a title to grant it and right click to revoke it.
-
Reload, Export Data and Import Data (shift click), as the commands above.
Permissions
| Permission | Description | Default |
|---|---|---|
nexaltitles.use | Use basic title commands and GUI | true |
nexaltitles.admin | Access admin commands | op |
nexaltitles.title.vip | Unlocks the VIP title | false |
nexaltitles.title.mvp | Unlocks the MVP title | false |
nexaltitles.title.legend | Unlocks the LEGEND title | false |
nexaltitles.title.admin | Unlocks the ADMIN title | false |
nexaltitles.title.developer | Unlocks the DEV title | false |
nexaltitles.title.season3champion | Unlocks the S3 Champion title | false |
nexaltitles.title.lagsurvivor | Unlocks the Lag Survivor title | false |
The nexaltitles.title.* nodes belong to the default permission titles in config.yml. The node
of any permission title you add is registered as default: false too, so operators do not get
rank titles unless you grant the node. Configs from before 1.0.2 used titles.\<name>; the plugin
renames them to nexaltitles.title.\<name> on startup, so grant the new nodes in your permissions
plugin.
Placeholders
Requires PlaceholderAPI.
| Placeholder | Description |
|---|---|
%titles_prefix% | Active title (as prefix) |
%titles_suffix% | Active title (as suffix) |
%titles_display% | Active title with brackets [Title] |
%titles_description% | Active title's description, lines joined with a newline (for a hover) |
%titles_description_\<titleid>% | Any title's description, lines joined with a newline |
%titles_price_\<titleid>% | Price of a purchasable title, as economy.price-format shows it |
%titles_active% | Active title ID or "none" |
%titles_count% | Number of titles owned |
%titles_total% | Total number of titles available |
%titles_percentage% | Percentage of titles owned (e.g., "45.2") |
%titles_stat_player_kills% | Player kill count |
%titles_stat_player_deaths% | Player death count |
%titles_stat_playtime% | Playtime in seconds, the current session included |
%titles_stat_blocks_mined% | Blocks mined count |
%titles_stat_blocks_placed% | Blocks placed count |
%titles_stat_mobs_killed% | Mobs killed count |
%titles_has_\<titleid>% | Check if player has title (true/false) |
%titles_is_active_\<titleid>% | Check if title is active (true/false) |
Title Descriptions
Every title can have a description: free text, one line or a list, in MiniMessage or & colour
codes. Use it for the server's lore, a joke, or the story of the title ("Winner of Season 3",
"Crowned on 30 June 2026"). It shows:
- in the
/titlesGUI, under every title, locked or not, so players can browse them all; - in the unlock announcement (
%description%intitle-unlocked); - as a hover on the title in chat, when
display.chat-prefixis on (see below); - through
%titles_description%(the equipped title) and%titles_description_\<titleid>%.
The chat hover on Paper. The plugin wraps Paper's chat renderer, so the format another chat plugin set is kept and nothing is cancelled or re-sent.
The chat hover on Spigot. A Spigot chat format is plain text and cannot carry a hover, so for a title with a description the plugin sends the chat line itself, as BungeeCord components:
- At
HIGHpriority the title goes in front of the format your chat plugin set, as for every title. - At
HIGHESTthe plugin sends the finished line, the title with its hover, to the message's recipients, and empties the recipient list so the server does not send a plain copy as well. The message is not cancelled: the console, Discord bridges and chat logs still get it, title included.
What this means alongside other chat plugins on Spigot:
- Cancelled messages are left alone. Chat plugins that cancel the message and send their own lines
(ChatControl, VentureChat and similar) do not show the plugin's title; use
%titles_prefix%in their format, as described below. - A listener that runs after Nexal Titles (
MONITOR, or aHIGHESTone registered later) sees an empty recipient list, and a change it makes to the format or message no longer reaches players. - When a plugin replaces the format after the title went in, or fixes the recipient list so it cannot be changed, the line goes out as the server sends it: without the hover, and without the title if the new format dropped it.
- Titles without a description are only put in front of the format; the server sends those lines as usual.
Hover with your own chat plugin. If your chat plugin draws the title itself from
%titles_prefix%, turn display.chat-prefix off and give that part %titles_description% as its
hover text, in chat plugins that support hovers (ChatControl, VentureChat, CarbonChat and similar
tooltip or hover settings). EssentialsX Chat formats plain text and has no hovers.
Admins can edit descriptions in game: /titles admin > Titles > a title > Description.
Configuration
config.yml
# Storage settings
storage:
type: json # json or sql
sql:
host: localhost
port: 3306
database: minecraft
username: root
password: ""
# Achievement tracking settings
achievements:
enabled: true
# Save statistics, add the current session to playtime and check playtime titles (Veteran)
# every 1200 ticks (60 seconds). Statistics are also saved when a player leaves and on shutdown.
save-interval: 1200
# Title display settings
display:
chat-prefix: true # equipped title in front of chat messages (turn off if your chat plugin uses %titles_prefix%)
above-head: true # turning it off removes the titles already shown
above-head-format: prefix # prefix, suffix, or both
max-length: 16 # visible characters above heads; colour codes do not count, 0 = no limit
# Economy settings
economy:
enabled: true # off (or no Vault): purchasable titles show "Purchases are disabled"
# How prices show in the GUI, messages and placeholders
price-format: "$%amount%" # e.g. "%amount% coins" or "€%amount%"
number-format: "#,##0.00" # Java DecimalFormat: , groups thousands, .00 two decimals
hide-zero-decimals: true # $1,000 rather than $1,000.00 (fractions keep them: $1,000.50)
# Permission-based titles
permission-titles:
auto-grant: true
check-interval: 6000 # Check every 5 minutes
# Title categories (display order in GUI)
categories:
- STARTER
- ACHIEVEMENT
- PURCHASE
- VIP
- EVENT
- SPECIAL
- ADMIN
# Title definitions
titles:
warrior:
display-name: "&cWarrior"
description: "&7Kill 100 players"
category: ACHIEVEMENT
rarity: UNCOMMON
requirement:
type: ACHIEVEMENT
achievement: PLAYER_KILLS
amount: 100
dragon_slayer:
display-name: "&5&lDragon Slayer"
# Optional free text: one line, or a list of lines (MiniMessage or & codes)
description:
- "\<dark_purple>Few who enter the End return.\</dark_purple>"
- "\<dark_purple>Fewer return with its breath in a bottle.\</dark_purple>"
category: ACHIEVEMENT
rarity: EPIC
requirement:
type: ADVANCEMENT
advancement: minecraft:end/kill_dragon
display: "Defeat the Ender Dragon" # optional: how the requirement reads to players
season3_champion:
display-name: "&6&lS3 Champion"
description:
- "\<gold>Winner of Season 3\</gold>"
- "\<gray>Crowned on 30 June 2026\</gray>"
category: EVENT
rarity: LEGENDARY
requirement:
type: PERMISSION # nobody has the node: hand it out with /titles admin grant
permission: nexaltitles.title.season3champion
display: "Win Season 3"
rich:
display-name: "&6&lRich"
description: "&7Show off your wealth"
category: PURCHASE
rarity: UNCOMMON
requirement:
type: PURCHASE
price: 10000
vip:
display-name: "&a&lVIP"
description: "&7Very Important Player"
category: VIP
rarity: RARE
requirement:
type: PERMISSION
permission: nexaltitles.title.vip
# Rarity colors
rarity-colors:
COMMON: "&7"
UNCOMMON: "&a"
RARE: "&9"
EPIC: "&5"
LEGENDARY: "&6"
messages.yml
All messages are fully customizable with MiniMessage (<gradient:...>, \<bold>), legacy color
codes (&a-&f), hex colors (&#FFFFFF), sounds, and titles. The colors are listed at the top of the
file for find-and-replace. Key message sections include:
- Title selection and unequip messages
- Title unlock notifications: a single achievement or advancement unlock gets the full announcement
(
title-unlocked, with on-screen title and sound); several at once share one line (titles-unlocked-multiple); rank titles granted from permissions, typically on join, are listed in one line (permission-titles-unlocked) - Purchase confirmation (
confirm-purchase), results and errors.%price%and%balance%already carry the currency fromeconomy.price-format; a$written before them in an oldermessages.ymlis removed on startup - Admin command feedback, export and import
- The admin title editor's chat prompts (
title-edit-*) - GUI titles (
gui-title-browser,gui-title-category) and category names (category-names) - Statistics display
Messages added by a newer version are added to an existing messages.yml on startup.
Example message with title and sound:
title-unlocked:
message:
- ""
- " <gradient:#E91E63:#F06292>\<bold>✦ NEW TITLE UNLOCKED ✦\</bold>\</gradient>"
- ""
- " %title%"
- " \<gray>%description%\</gray>"
- ""
title: "<gradient:#E91E63:#F06292>\<bold>NEW TITLE!\</bold>\</gradient>"
subtitle: "%title%"
sound: "UI_TOAST_CHALLENGE_COMPLETE"
fade-in: 10
stay: 80
fade-out: 20
Database Schema
With JSON storage (storage.type: json) everything lives in data.json, under the keys
titles_player_data and titles_player_stats. With SQL storage (storage.type: sql), the plugin
creates the following tables:
titles_player_data
Stores player title ownership and active selections.
| Column | Type | Key | Description |
|---|---|---|---|
playerId | VARCHAR(36) | PK | Player UUID |
active_title | VARCHAR(64) | Currently equipped title ID (nullable) | |
owned_titles | TEXT | JSON array of owned title IDs | |
created_at | BIGINT | Creation timestamp | |
updated_at | BIGINT | Last modification timestamp |
titles_player_stats
Stores player achievement statistics.
| Column | Type | Key | Description |
|---|---|---|---|
playerId | VARCHAR(36) | PK | Player UUID |
player_kills | INT | Number of players killed | |
player_deaths | INT | Number of deaths | |
playtime_seconds | BIGINT | Total playtime in seconds | |
blocks_mined | INT | Number of blocks mined | |
blocks_placed | INT | Number of blocks placed | |
mobs_killed | INT | Number of mobs killed | |
updated_at | BIGINT | Last update timestamp |
Key Legend: PK = Primary Key
Title Requirement Types
The plugin supports five types of title requirements:
NONE
No requirement - title is available to everyone immediately: any player can equip it without being
granted it. Free titles do not count as owned in /titles list, /titles stats and
%titles_count%.
requirement:
type: NONE
PERMISSION
Requires a specific permission node. Auto-granted when the player joins or at the next periodic
check (permission-titles.check-interval) once they have the node. The node is default: false,
so operators only get it when it is granted to them. Players who try to equip one without the node
are told so (title-no-permission).
requirement:
type: PERMISSION
permission: nexaltitles.title.vip
PURCHASE
Must be purchased with Vault economy. The first click in the GUI asks for confirmation; a second
click within 10 seconds buys it. Nothing is granted if the economy plugin refuses the payment.
Without Vault (or with no economy plugin registered with it) these titles cannot be bought: the GUI
shows "Purchases are disabled" and a click answers with purchases-disabled.
requirement:
type: PURCHASE
price: 10000
ACHIEVEMENT
Unlock by reaching a custom achievement threshold.
requirement:
type: ACHIEVEMENT
achievement: PLAYER_KILLS # PLAYER_KILLS, PLAYTIME, BLOCKS_MINED, BLOCKS_PLACED, MOBS_KILLED, PLAYER_DEATHS
amount: 100
ADVANCEMENT
Unlock by completing a Minecraft advancement. Players see display as the requirement ("Defeat
the Ender Dragon"), not the advancement key. Without display, the first line of the description
is used, as in configs from before descriptions were free text. Any requirement type accepts
display.
requirement:
type: ADVANCEMENT
advancement: minecraft:end/kill_dragon
display: "Defeat the Ender Dragon"
Storage Migration
To migrate from JSON to SQL (or vice versa):
- Current storage: JSON (
storage.type: json) - Run
/titles admin export- exports player titles and statistics to theexport/folder (player_titles.json,player_stats.json) - Change config:
storage.type: sqland configure SQL credentials - Run
/titles admin reload - Run
/titles admin import- imports data from theexport/folder - Done! Now using SQL storage
The export/import system preserves owned titles, active selections and statistics. Both run off the main thread and report back when done.
Changing storage.type connects to the new storage off the main thread. If it cannot be reached
(wrong credentials, database down), the current storage stays in use, the console says why, and
/titles admin reload answers with reload-failed; fix the settings and reload again. A
config.yml or messages.yml that is not valid YAML is reported the same way and not loaded.
Achievement Tracking
The plugin automatically tracks the following player statistics:
- Player Kills - Incremented when a player kills another player
- Player Deaths - Incremented when a player dies
- Playtime - Total time spent on the server (in seconds); the current session counts straight away, and playtime titles unlock while playing
- Blocks Mined - Number of blocks broken
- Blocks Placed - Number of blocks placed
- Mobs Killed - Number of non-player entities killed
Statistics are saved off the main thread every achievements.save-interval (60 seconds by
default), when players log out and when the server stops, including the session of every player
online at a restart. Versions up to 1.0.2 kept statistics in player_stats.json; on startup they
are moved into the current storage and the file is renamed to player_stats.json.migrated.
Installation
- Ensure you have Paper or Spigot 1.20.1+ installed
- (Optional) Install Vault and an economy plugin (e.g., EssentialsX) to sell titles. Without Vault the plugin runs and everything but buying titles works; the console warns once on startup
- (Optional) Install PlaceholderAPI for placeholder support
- Download
NexalTitles-X.X.X.jar - Place in
/pluginsfolder - Start/restart server
- Configure
config.ymlandmessages.yml, or use/titles admin - Reload with
/titles admin reload
Building from Source
# From repository root
cd plugins/nexal-titles
mvn clean package
# Output: target/NexalTitles-X.X.X.jar