Text rendering and console logging for Paper plugins.
Colorize— turns configured strings into AdventureComponents, with optional support for legacy&colour codes alongside MiniMessage.Logger— a prefixed console logger with levels, an optional daily trace file, and grep-friendly tags.
Requires Paper 1.18.2 or newer. Adventure and MiniMessage come from the server.
repositories {
maven("https://repo.papermc.io/repository/maven-public/")
maven("https://repo.vulpine.top/repository/maven-open/")
}
dependencies {
implementation("top.vulpine:commons:0.1.0")
compileOnly("io.papermc.paper:paper-api:1.18.2-R0.1-SNAPSHOT")
}
tasks.shadowJar {
relocate("top.vulpine.commons", "com.example.myplugin.libs.commons")
}Relocate top.vulpine.commons so two plugins on the same server do not share
configuration. Do not relocate net.kyori.adventure — Paper provides it
unrelocated, and a relocated Component will not satisfy Paper's own method
signatures.
Colorize.color returns a Component, which is what Paper's APIs accept:
Component message = Colorize.color("<green>Welcome to the server");
player.sendMessage(message);
player.sendActionBar(Colorize.color("<yellow>Loading…"));
Bukkit.getConsoleSender().sendMessage(Colorize.color("<gray>Plugin ready"));Lists and arrays are handled too:
List<Component> lines = Colorize.color(config.motd); // List<String> in
Component[] parts = Colorize.color(new String[]{"a", "b"});Call init once during startup to choose which syntax your config accepts:
Colorize.init(Dialect.MODERN); // MiniMessage only
Colorize.init(Dialect.LEGACY); // MiniMessage plus legacy & codes| Dialect | <green>x |
&ax |
&#ff00ffx |
|---|---|---|---|
MODERN |
green | literal text &ax |
literal text |
LEGACY |
green | green | pink |
MODERN is the default and the right choice for a new plugin. Use LEGACY when
server owners already have & codes in their config files from an earlier version.
In LEGACY, both syntaxes can appear in the same string, and legacy codes are
converted the same way regardless of what else the string contains:
Colorize.init(Dialect.LEGACY);
Colorize.color("&aGreen and <bold>bold"); // both apply
Colorize.color("&7Just legacy"); // greyUnknown MiniMessage tags render as literal text rather than throwing, so a message like
"Press <shift> to sneak" is safe.
Note on legacy formatting: legacy treats a colour code as a reset of formatting, so
&l&aXis green and not bold. MiniMessage nests instead, so after conversion<bold><green>Xis bold and green. Strings that rely on the legacy reset will show extra formatting.
Pass dynamic values as TagResolvers rather than concatenating them into the template:
Component greeting = Colorize.color("<gray>Welcome, <name>",
Placeholder.unparsed("name", player.getName()));Substituting a value into the string first means the value is then parsed as markup — a
player whose name contains <red> would change the message's colours, and one
containing a <click:run_command:…> tag would make the line clickable. Passing values
as resolvers keeps them as data.
Where a resolver is impractical, escape the value:
String safe = Colorize.escape(untrustedValue);
Component line = Colorize.color("<gray>Reported: " + safe);Colorize.serialize(component); // Component -> MiniMessage string, lossless
Colorize.plain("&aHello"); // -> "Hello", formatting removed, dialect-aware
Colorize.strip("<green>Hello"); // -> "Hello", removes MiniMessage tags only
Colorize.toLegacy(component); // Component -> §-coded string, lossyplain is what you want for log files and anything else that cannot show colour, since
it renders through the configured dialect first. strip works on the raw string and so
leaves legacy codes in place.
toLegacy is only for APIs that will not take a Component; hover events, click
events and fonts do not survive it.
Logger.builder()
.prefix("<dark_gray>[<white>My<green>Plugin<dark_gray>]</dark_gray> ")
.level(LogLevel.INFO)
.build();| Method | Default | |
|---|---|---|
prefix(String) |
— | required; MiniMessage. Pass "" for no prefix |
level(LogLevel) |
INFO |
DEBUG, INFO, WARN, ERROR |
trace(File) |
off | plugin data folder; see Trace file |
showCaller(boolean) |
true |
include the calling class name |
Then:
Logger.debug("Loaded 3 arenas");
Logger.info("Connected to MySQL");
Logger.warn("Config value out of range, using default");
Logger.error("Could not save data: " + e.getMessage());
Logger.system(""); // no level tag, no caller, ignores the level
Logger.system("<green> MyPlugin v1.0");system is for startup banners — it always prints and adds no decoration beyond the
prefix.
Output looks like:
[MyPlugin] [ArenaManager] Loaded 3 arenas
[MyPlugin] [DEBUG] [SlotManager] Slot 4 released
Message bodies go through Colorize, so if your plugin uses Dialect.LEGACY you can
log with & codes.
To change the level at runtime — a /reload command, for example — without reopening
the trace file:
Logger.setLevel(config.logLevel);An optional tag says what kind of work a line came from, independent of the class
name. Implement LogAction with a private enum:
public final class StorageManager {
private enum Action implements LogAction {
SETUP, CLOSE, QUERY
}
public void connect() {
Logger.info(Action.SETUP, "Connected to MySQL");
}
public void query(final String sql) {
Logger.debug(Action.QUERY, "Running: " + sql);
}
}[MyPlugin] [StorageManager] [SETUP] Connected to MySQL
Enums provide name() already, so there is nothing to implement beyond the constants.
Tags make logs greppable by operation rather than by class, which survives renaming a
class.
Worth skipping where a class only does one kind of work — the caller name is already on every line, so a single-constant tag just repeats it. Use the overloads without a tag there.
Pass the plugin data folder to also write every emitted line to
<dataFolder>/logs/trace-yyyy-MM-dd.log, with colour removed:
Logger.builder()
.prefix("<gray>[MyPlugin]</gray> ")
.level(LogLevel.DEBUG)
.trace(getDataFolder())
.build();[14:32:07] [INFO] [StorageManager] [SETUP] Connected to MySQL
[14:32:09] [DEBUG] [SlotManager] Slot 4 released
The writer stays open, so close it on shutdown:
@Override
public void onDisable() {
Logger.close();
}close() is safe to call when no trace file is open.
public final class MyPlugin extends JavaPlugin {
private Config configuration;
@Override
public void onEnable() {
// Legacy because earlier versions of this plugin shipped & codes
Colorize.init(Dialect.LEGACY);
Logger.builder()
.prefix("<dark_gray>[<white>My<green>Plugin<dark_gray>]</dark_gray> ")
.level(LogLevel.INFO)
.trace(getDataFolder())
.build();
Logger.system("<green> MyPlugin v" + getDescription().getVersion());
this.configuration = loadConfig();
Logger.setLevel(configuration.logLevel);
Logger.info("Enabled");
}
@Override
public void onDisable() {
Logger.close();
}
}public final class JoinListener implements Listener {
private enum Action implements LogAction {
JOIN, QUIT
}
@EventHandler
public void onJoin(final PlayerJoinEvent event) {
Player player = event.getPlayer();
event.joinMessage(Colorize.color("<gray>+ <white><name>",
Placeholder.unparsed("name", player.getName())));
player.sendMessage(Colorize.color(configuration.welcome,
Placeholder.unparsed("player", player.getName())));
Logger.debug(Action.JOIN, "Greeted " + player.getName());
}
}Configuration set through Colorize.init and Logger.builder is process-wide. Because
each plugin relocates the library into its own package, that state is per-plugin rather
than shared across the server.
MIT