All source projects
MrUltimateLibrary Source
Standalone source project / 68 files
MrUltimateLibrary is the backbone of all my plugins; it offers features that facilitate plugin creation, maintenance, and updates.
java
MrUltimateLibrary/src/main/java/eu/mrneznamy/mrultimatelibrary/messages/MrLibMessage.java
JAVA · Read-only source
package eu.mrneznamy.mrultimatelibrary.messages;
import eu.mrneznamy.mrultimatelibrary.actionbar.MrLibActionBar;
import eu.mrneznamy.mrultimatelibrary.title.MrLibTitle;
import eu.mrneznamy.mrultimatelibrary.utils.MrLibColors;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.TextComponent;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Advanced message system with multiple display types
*
* Supports:
* (!message!) - Chat message
* (!title!) - Title
* (!subtitle!) - Subtitle
* (!actionbar!) - ActionBar
* (!hover:TEXT!) - Hover message
* (!click:COMMAND!) - Click command
* (!sound:SOUND!) - Play sound to player
* (!close!) - Close player's inventory
*/
public class MrLibMessage {
private final JavaPlugin plugin;
private final MrLibActionBar actionBar;
private final MrLibTitle title;
private static final Pattern TAG_PATTERN = Pattern.compile("\\((!?)([^!)]+)(!?)\\)");
public MrLibMessage(JavaPlugin plugin) {
this.plugin = plugin;
this.actionBar = new MrLibActionBar(plugin);
this.title = new MrLibTitle(plugin);
}
/**
* Send message with tag parsing
*
* @param player Player
* @param message Message with tags
*
* Example:
* "(!message!)Hello (!hover:Click me!)world (!actionbar!)ActionBar text (!title!)Title text"
*/
public void send(Player player, String message) {
if (message == null || message.isEmpty()) {
return;
}
MessageParser parser = new MessageParser(message, player);
parser.parse();
}
/**
* Send to all players
*/
public void sendToAll(String message) {
for (Player player : plugin.getServer().getOnlinePlayers()) {
send(player, message);
}
}
/**
* Send to players with permission
*/
public void sendWithPermission(String message, String permission) {
for (Player player : plugin.getServer().getOnlinePlayers()) {
if (player.hasPermission(permission)) {
send(player, message);
}
}
}
/**
* Sends a smart message to a CommandSender (player or console).
* Automatically loads from messages.yml, prepends prefix, and processes tags.
*/
public void sendSmart(CommandSender sender, String pathOrMessage, String defaultPrefix) {
if (sender == null) return;
if (sender instanceof Player) {
sendSmart((Player) sender, pathOrMessage, defaultPrefix);
} else {
List<String> lines = loadMessageLines(pathOrMessage);
for (String line : lines) {
if (line == null || line.isEmpty()) continue;
if (line.toUpperCase().contains("(!SOUND!:")) {
line = line.replaceAll("(?i)\\(!SOUND!:[^)]+\\)", "");
}
if (line.toUpperCase().contains("(!ACTIONBAR!)")) {
line = line.replaceAll("(?i)\\(!ACTIONBAR!\\)", "");
}
if (line.toUpperCase().contains("(!TITLE!)") || line.toUpperCase().contains("(!SUBTITLE!)")) {
String titleText = "";
String subtitleText = "";
String upperLine = line.toUpperCase();
int titleIndex = upperLine.indexOf("(!TITLE!)");
int subtitleIndex = upperLine.indexOf("(!SUBTITLE!)");
if (titleIndex != -1 && subtitleIndex != -1) {
if (titleIndex < subtitleIndex) {
titleText = line.substring(titleIndex + 9, subtitleIndex).trim();
subtitleText = line.substring(subtitleIndex + 12).trim();
} else {
subtitleText = line.substring(subtitleIndex + 12, titleIndex).trim();
titleText = line.substring(titleIndex + 9).trim();
}
} else if (titleIndex != -1) {
titleText = line.substring(titleIndex + 9).trim();
} else if (subtitleIndex != -1) {
subtitleText = line.substring(subtitleIndex + 12).trim();
}
line = (titleText + " " + subtitleText).trim();
}
boolean noPrefix = false;
if (line.toUpperCase().contains("(!NOPREFIX!)")) {
noPrefix = true;
line = line.replaceAll("(?i)\\(!NOPREFIX!\\)", "");
}
line = line.replaceAll("\\(![^!]*!\\)", "");
if (!noPrefix && defaultPrefix != null && !defaultPrefix.isEmpty()) {
line = defaultPrefix + " " + line;
}
sender.sendMessage(MrLibColors.colorize(line));
}
}
}
/**
* Sends a smart message to a player.
* Automatically loads from messages.yml, prepends prefix, and processes tags.
*/
public void sendSmart(Player player, String pathOrMessage, String defaultPrefix) {
if (player == null || pathOrMessage == null || pathOrMessage.isEmpty()) {
return;
}
List<String> lines = loadMessageLines(pathOrMessage);
for (String line : lines) {
if (line == null || line.isEmpty()) {
continue;
}
Pattern soundPattern = Pattern.compile("(?i)\\(!SOUND!:([^)]+)\\)");
Matcher soundMatcher = soundPattern.matcher(line);
while (soundMatcher.find()) {
String soundName = soundMatcher.group(1);
playSound(player, soundName);
line = line.replace(soundMatcher.group(0), "");
}
boolean isTitle = false;
String titleText = null;
String subtitleText = null;
String upperLine = line.toUpperCase();
if (upperLine.contains("(!TITLE!)") || upperLine.contains("(!SUBTITLE!)")) {
isTitle = true;
int titleIndex = upperLine.indexOf("(!TITLE!)");
int subtitleIndex = upperLine.indexOf("(!SUBTITLE!)");
if (titleIndex != -1 && subtitleIndex != -1) {
if (titleIndex < subtitleIndex) {
titleText = line.substring(titleIndex + 9, subtitleIndex).trim();
subtitleText = line.substring(subtitleIndex + 12).trim();
} else {
subtitleText = line.substring(subtitleIndex + 12, titleIndex).trim();
titleText = line.substring(titleIndex + 9).trim();
}
} else if (titleIndex != -1) {
titleText = line.substring(titleIndex + 9).trim();
} else if (subtitleIndex != -1) {
subtitleText = line.substring(subtitleIndex + 12).trim();
}
}
if (isTitle) {
title.show(player,
titleText != null ? MrLibColors.colorize(titleText, player) : "",
subtitleText != null ? MrLibColors.colorize(subtitleText, player) : "",
10, 70, 20
);
continue;
}
boolean isActionBar = false;
if (line.toUpperCase().contains("(!ACTIONBAR!)")) {
isActionBar = true;
line = line.replaceAll("(?i)\\(!ACTIONBAR!\\)", "");
}
if (isActionBar) {
actionBar.show(player, MrLibColors.colorize(line.trim(), player));
continue;
}
boolean noPrefix = false;
if (line.toUpperCase().contains("(!NOPREFIX!)")) {
noPrefix = true;
line = line.replaceAll("(?i)\\(!NOPREFIX!\\)", "");
}
if (!noPrefix && defaultPrefix != null && !defaultPrefix.isEmpty()) {
if (line.contains("(!message!)")) {
line = line.replace("(!message!)", "(!message!)" + defaultPrefix + " ");
} else {
line = "(!message!)" + defaultPrefix + " " + line;
}
}
send(player, line);
}
}
/**
* Loads message lines from configuration. Supports string, lists, and scoped keys.
* Falls back to pathOrMessage as a raw string if key not found.
*/
public List<String> loadMessageLines(String pathOrMessage) {
List<String> lines = new ArrayList<>();
if (pathOrMessage == null || pathOrMessage.isEmpty()) {
return lines;
}
java.io.File messagesFile = getMessagesFile();
if (messagesFile.exists()) {
org.bukkit.configuration.file.YamlConfiguration messages = org.bukkit.configuration.file.YamlConfiguration.loadConfiguration(messagesFile);
String lookupKey = translateKey(pathOrMessage);
String pluginName = plugin.getName();
String[] paths = {
lookupKey,
pluginName + "." + lookupKey,
pluginName + ".Messages." + lookupKey,
pluginName + ".messages." + lookupKey,
pluginName.toLowerCase() + "." + lookupKey
};
Object val = null;
for (String p : paths) {
if (messages.contains(p)) {
val = messages.get(p);
break;
}
}
if (val instanceof List) {
for (Object o : (List<?>) val) {
if (o != null) lines.add(o.toString());
}
} else if (val != null) {
lines.add(val.toString());
}
}
if (lines.isEmpty()) {
lines.add(pathOrMessage);
}
return lines;
}
/**
* Helper to play sound safely with namespace or Bukkit sound enum.
*/
private void playSound(Player player, String soundName) {
if (soundName == null || soundName.isEmpty()) return;
try {
if (soundName.contains(":")) {
player.playSound(player.getLocation(), soundName, 1.0f, 1.0f);
} else {
try {
org.bukkit.Sound sound = org.bukkit.Sound.valueOf(soundName.toUpperCase().replace(".", "_"));
player.playSound(player.getLocation(), sound, 1.0f, 1.0f);
} catch (IllegalArgumentException ex) {
player.playSound(player.getLocation(), soundName, 1.0f, 1.0f);
}
}
} catch (Exception e) {
}
}
/**
* Get message from config (backward compatibility)
*/
public String getMessage(String path) {
java.io.File messagesFile = getMessagesFile();
if (messagesFile.exists()) {
org.bukkit.configuration.file.YamlConfiguration messages = org.bukkit.configuration.file.YamlConfiguration.loadConfiguration(messagesFile);
String lookupKey = translateKey(path);
String message = messages.getString(lookupKey);
if (message != null) {
return MrLibColors.colorize(message);
}
String pluginName = plugin.getName();
String[] fallbacks = {
pluginName + "." + lookupKey,
pluginName + ".Messages." + lookupKey,
pluginName + ".messages." + lookupKey,
pluginName.toLowerCase() + "." + lookupKey
};
for (String fallback : fallbacks) {
message = messages.getString(fallback);
if (message != null) {
return MrLibColors.colorize(message);
}
}
}
return "&cMessage not found: " + path;
}
/**
* Get message from config with default value (backward compatibility)
*/
public String getMessage(String path, String defaultValue) {
java.io.File messagesFile = getMessagesFile();
if (messagesFile.exists()) {
org.bukkit.configuration.file.YamlConfiguration messages = org.bukkit.configuration.file.YamlConfiguration.loadConfiguration(messagesFile);
String lookupKey = translateKey(path);
String message = messages.getString(lookupKey);
if (message != null) {
return MrLibColors.colorize(message);
}
String pluginName = plugin.getName();
String[] fallbacks = {
pluginName + "." + lookupKey,
pluginName + ".Messages." + lookupKey,
pluginName + ".messages." + lookupKey,
pluginName.toLowerCase() + "." + lookupKey
};
for (String fallback : fallbacks) {
message = messages.getString(fallback);
if (message != null) {
return MrLibColors.colorize(message);
}
}
}
return MrLibColors.colorize(defaultValue);
}
private String translateKey(String path) {
if (!"MrSellChests".equals(plugin.getName())) {
return path;
}
if (path == null) return null;
switch (path) {
case "no_permission":
case "no-permission":
return "General.Missing-Permission";
case "no_permission_place":
case "no-permission-place":
return "General.Missing-Permission-Place";
case "command_usage":
return "General.Command-Usage";
case "unknown_subcommand":
return "General.Unknown-Subcommand";
case "reload_success":
return "General.Reload-Success";
case "invalid_page_number":
return "General.Invalid-Page-Number";
case "chest_created":
return "Chest.Created";
case "chest_limit_reached":
return "Chest.Limit-Reached";
case "not_owner":
case "invite_no_chest_owner":
case "edit_no_chest_owner":
return "Chest.Not-Owner";
case "chest_destroyed":
return "Chest.Destroyed";
case "chest_same_chunk":
return "Chest.Same-Chunk";
case "hologram_disabled":
return "Chest.Hologram-Disabled";
case "hologram_failed":
return "Chest.Hologram-Failed";
case "chest_already_fully_charged":
return "Chest.Already-Fully-Charged";
case "chest_charge_not_enough_money":
return "Chest.Charge-Not-Enough-Money";
case "chest_charged_success":
return "Chest.Charged-Success";
case "chest_type_no_charging":
return "Chest.Type-No-Charging";
case "sell_inventory_not_available":
return "Chest.Sell-Inventory-Not-Available";
case "container_not_available":
return "Chest.Container-Not-Available";
case "banned_item_inventory":
return "Chest.Banned-Item-Inventory";
case "banned_item_added":
return "Chest.Banned-Item-Added";
case "banned_item_removed":
return "Chest.Banned-Item-Removed";
case "banned_item_not_found":
return "Chest.Banned-Item-Not-Found";
case "banned_item_usage":
return "Chest.Banned-Item-Usage";
case "banned_item_no_item":
return "Chest.Banned-Item-No-Item";
case "banned_item_invalid_category":
return "Chest.Banned-Item-Invalid-Category";
case "banned_item_invalid_action":
return "Chest.Banned-Item-Invalid-Action";
case "banned_item_already_banned":
return "Chest.Banned-Item-Already-Banned";
case "banned_item_not_banned":
return "Chest.Banned-Item-Not-Banned";
case "banned_item_add_success":
return "Chest.Banned-Item-Add-Success";
case "banned_item_remove_success":
return "Chest.Banned-Item-Remove-Success";
case "banned_item_save_error":
return "Chest.Banned-Item-Save-Error";
case "chest_link_start":
return "Linking.Start";
case "chest_link_success":
return "Linking.Success";
case "chest_link_cancelled":
return "Linking.Cancelled";
case "chest_unlink_all":
return "Linking.Unlink-All";
case "chest_link_max":
return "Linking.Max";
case "chest_link_max_disabled":
return "Linking.Max-Disabled";
case "chest_link_invalid":
return "Linking.Invalid";
case "chest_link_sell_chest":
return "Linking.Sell-Chest";
case "pickup_link_start":
return "Pickup.Start";
case "pickup_link_success":
return "Pickup.Success";
case "pickup_link_cancelled":
return "Pickup.Cancelled";
case "pickup_unlink_success":
return "Pickup.Unlink-Success";
case "state_linked":
return "State.Linked";
case "state_unlinked":
return "State.Unlinked";
case "state_enabled":
return "State.Enabled";
case "state_disabled":
return "State.Disabled";
case "linked_chests_full":
return "Trasher.Chests-Full";
case "trasher_mode_changed":
return "Trasher.Mode-Changed";
case "trasher_mode_KEEP":
return "Trasher.Mode-KEEP";
case "trasher_mode_TRANSFER":
return "Trasher.Mode-TRANSFER";
case "trasher_mode_REMOVE":
return "Trasher.Mode-REMOVE";
case "boost_set":
return "Booster.Boost-Set";
case "boost_received":
return "Booster.Boost-Received";
case "booster_activated":
return "Booster.Booster-Activated";
case "booster_already_active":
return "Booster.Booster-Already-Active";
case "boost_time_left":
return "Booster.Boost-Time-Left";
case "booster_voucher_name":
return "Booster.Voucher-Name";
case "booster_give_usage":
return "Booster.Give-Usage";
case "booster_give_success":
return "Booster.Give-Success";
case "booster_not_found":
return "Booster.Not-Found";
case "booster_item_not_defined":
return "Booster.Item-Not-Defined";
case "boosters_yml_not_found":
return "Booster.Yml-Not-Found";
case "booster_invalid_amount":
return "Booster.Invalid-Amount";
case "sell_success":
return "Sell.Success";
case "sell_chest_interval_message":
return "Sell.Interval-Message";
case "sell_chest_full":
return "Sell.Chest-Full";
case "invalid_item":
return "Sell.Invalid-Item";
case "not_enough_money":
return "Sell.Not-Enough-Money";
case "give_usage":
return "Command.Give-Usage";
case "give_invalid_amount":
return "Command.Give-Invalid-Amount";
case "give_player_not_found":
return "Command.Give-Player-Not-Found";
case "give_chest_type_not_found":
return "Command.Give-Chest-Type-Not-Found";
case "give_success":
return "Command.Give-Success";
case "boost_usage":
return "Command.Boost-Usage";
case "boost_invalid":
return "Command.Boost-Invalid";
case "help_players_only":
return "Command.Help-Players-Only";
case "invite_already_in_process":
return "Invite.Already-In-Process";
case "invite_max_players_reached":
return "Invite.Max-Players-Reached";
case "invite_process_start":
return "Invite.Process-Start";
case "invite_enter_player_name":
return "Invite.Enter-Player-Name";
case "invite_player_must_be_online":
return "Invite.Player-Must-Be-Online";
case "invite_type_cancel":
return "Invite.Type-Cancel";
case "invite_failed_start":
return "Invite.Failed-Start";
case "invite_cancelled_missing_data":
return "Invite.Cancelled-Missing-Data";
case "invite_cancelled":
return "Invite.Cancelled";
case "invite_player_not_online":
return "Invite.Player-Not-Online";
case "invite_cannot_invite_self":
return "Invite.Cannot-Invite-Self";
case "invite_player_found":
return "Invite.Player-Found";
case "invite_enter_percentage":
return "Invite.Enter-Percentage";
case "invite_percentage_example":
return "Invite.Percentage-Example";
case "invite_percentage_invalid":
return "Invite.Percentage-Invalid";
case "invite_player_offline":
return "Invite.Player-Offline";
case "invite_no_longer_owner":
return "Invite.No-Longer-Owner";
case "invite_player_already_invited":
return "Invite.Player-Already-Invited";
case "invite_success_owner":
return "Invite.Success-Owner";
case "invite_success_target":
return "Invite.Success-Target";
case "invite_failed":
return "Invite.Failed";
case "invite_uninvite_success_online":
return "Invite.Uninvite-Success-Online";
case "invite_uninvite_success_offline":
return "Invite.Uninvite-Success-Offline";
case "invite_uninvite_target":
return "Invite.Uninvite-Target";
case "invite_uninvite_failed":
return "Invite.Uninvite-Failed";
case "invite_menu_open_failed":
return "Invite.Menu-Open-Failed";
case "edit_already_in_process":
return "Edit.Already-In-Process";
case "edit_process_start":
return "Edit.Process-Start";
case "edit_current_player":
return "Edit.Current-Player";
case "edit_current_percentage":
return "Edit.Current-Percentage";
case "edit_enter_new_percentage":
return "Edit.Enter-New-Percentage";
case "edit_type_cancel":
return "Edit.Type-Cancel";
case "edit_failed_start":
return "Edit.Failed-Start";
case "edit_cancelled_missing_data":
return "Edit.Cancelled-Missing-Data";
case "edit_cancelled":
return "Edit.Cancelled";
case "edit_percentage_invalid":
return "Edit.Percentage-Invalid";
case "edit_no_longer_owner":
return "Edit.No-Longer-Owner";
case "edit_player_no_longer_invited":
return "Edit.Player-No-Longer-Invited";
case "edit_success":
return "Edit.Success";
case "edit_notification":
return "Edit.Notification";
case "edit_failed":
return "Edit.Failed";
}
return path;
}
/**
* Message parser
*/
private class MessageParser {
private final String input;
private final Player player;
private String currentMode = "message";
private List<MessagePart> parts = new ArrayList<>();
private String pendingHover = null;
private String pendingClick = null;
private String pendingSound = null;
private boolean shouldCloseInventory = false;
public MessageParser(String input, Player player) {
this.input = input;
this.player = player;
}
public void parse() {
Matcher matcher = TAG_PATTERN.matcher(input);
int lastEnd = 0;
while (matcher.find()) {
if (matcher.start() > lastEnd) {
String text = input.substring(lastEnd, matcher.start());
if (!text.isEmpty()) {
MessagePart part = new MessagePart(currentMode, text, null);
applyPendingEffects(part);
parts.add(part);
}
}
String leftExclamation = matcher.group(1);
String tag = matcher.group(2);
String rightExclamation = matcher.group(3);
parseTag(tag);
lastEnd = matcher.end();
}
if (lastEnd < input.length()) {
String text = input.substring(lastEnd);
if (!text.isEmpty()) {
MessagePart part = new MessagePart(currentMode, text, null);
applyPendingEffects(part);
parts.add(part);
}
}
execute();
}
private void parseTag(String tag) {
String normalizedTag = tag.toLowerCase(java.util.Locale.ROOT);
if (normalizedTag.startsWith("hover:")) {
pendingHover = tag.substring(6);
} else if (normalizedTag.startsWith("click:")) {
pendingClick = tag.substring(6);
} else if (normalizedTag.startsWith("sound:")) {
pendingSound = tag.substring(6);
} else if (tag.equalsIgnoreCase("close")) {
shouldCloseInventory = true;
} else {
currentMode = tag.toLowerCase();
}
}
private void applyPendingEffects(MessagePart part) {
if (pendingHover != null) {
part.hoverText = pendingHover;
pendingHover = null;
}
if (pendingClick != null) {
part.clickCommand = pendingClick;
pendingClick = null;
}
if (pendingSound != null) {
part.soundName = pendingSound;
pendingSound = null;
}
}
private void execute() {
List<TextComponent> chatComponents = new ArrayList<>();
String titleText = null;
String subtitleText = null;
String actionBarText = null;
for (MessagePart part : parts) {
String colored = MrLibColors.colorize(part.text, player);
switch (part.mode) {
case "message":
BaseComponent[] components = TextComponent.fromLegacyText(colored);
if (part.hoverText != null || part.clickCommand != null) {
for (BaseComponent comp : components) {
if (part.hoverText != null) {
String coloredHover = MrLibColors.colorize(part.hoverText, player);
BaseComponent[] hoverComponents = TextComponent.fromLegacyText(coloredHover);
comp.setHoverEvent(new HoverEvent(
HoverEvent.Action.SHOW_TEXT,
hoverComponents
));
}
if (part.clickCommand != null) {
boolean url = part.clickCommand.regionMatches(true, 0, "URL:", 0, 4)
|| part.clickCommand.regionMatches(true, 0, "HTTP", 0, 4);
String target = part.clickCommand.regionMatches(true, 0, "URL:", 0, 4)
? part.clickCommand.substring(4) : part.clickCommand;
comp.setClickEvent(new ClickEvent(
url ? ClickEvent.Action.OPEN_URL : ClickEvent.Action.RUN_COMMAND,
target
));
}
}
}
for (BaseComponent comp : components) {
if (comp instanceof TextComponent) {
chatComponents.add((TextComponent) comp);
}
}
break;
case "title":
titleText = colored;
break;
case "subtitle":
subtitleText = colored;
break;
case "actionbar":
actionBarText = colored;
break;
}
}
if (!chatComponents.isEmpty()) {
BaseComponent[] messageComponents = new BaseComponent[chatComponents.size()];
for (int i = 0; i < chatComponents.size(); i++) {
messageComponents[i] = chatComponents.get(i);
}
player.spigot().sendMessage(messageComponents);
}
if (titleText != null || subtitleText != null) {
title.show(player,
titleText != null ? titleText : "",
subtitleText != null ? subtitleText : "",
10, 70, 20
);
}
if (actionBarText != null) {
actionBar.show(player, actionBarText);
}
for (MessagePart part : parts) {
if (part.soundName != null && !part.soundName.isEmpty()) {
try {
org.bukkit.Sound sound = org.bukkit.Sound.valueOf(part.soundName.toUpperCase());
player.playSound(player.getLocation(), sound, 1.0f, 1.0f);
} catch (IllegalArgumentException e) {
}
}
}
if (shouldCloseInventory) {
player.closeInventory();
}
}
}
private java.io.File getMessagesFile() {
java.io.File messagesFile = new java.io.File(plugin.getDataFolder(), "messages.yml");
if (!messagesFile.exists()) {
String language = plugin.getConfig().getString("MrSellChests.Engine.Language");
if (language == null) language = plugin.getConfig().getString("Engine.Language");
if (language == null) language = plugin.getConfig().getString("Language", "english");
java.io.File langMessages = new java.io.File(plugin.getDataFolder(), "languages/" + language + "/messages.yml");
if (langMessages.exists()) {
return langMessages;
}
java.io.File englishMessages = new java.io.File(plugin.getDataFolder(), "languages/english/messages.yml");
if (englishMessages.exists()) {
return englishMessages;
}
}
return messagesFile;
}
/**
* Message part
*/
private static class MessagePart {
String mode;
String text;
String hoverText;
String clickCommand;
String soundName;
MessagePart(String mode, String text, String hover) {
this.mode = mode;
this.text = text;
this.hoverText = hover;
}
}
}
865 linesGNU GENERAL PUBLIC LICENSE · Version 3, 29 June 2007 · UTF-8 · Read only