Initial commit

This commit is contained in:
Kristian Knarvik 2022-11-06 16:11:45 +01:00
commit 8f0c028bb5
13 changed files with 1086 additions and 0 deletions

113
.gitignore vendored Normal file
View File

@ -0,0 +1,113 @@
# User-specific stuff
.idea/
*.iml
*.ipr
*.iws
# IntelliJ
out/
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar
.flattened-pom.xml
# Common working directory
run/

74
pom.xml Normal file
View File

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>net.knarcraft</groupId>
<artifactId>KnarLib</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>KnarLib</name>
<properties>
<java.version>16</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
<repositories>
<repository>
<id>spigotmc-repo</id>
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
</repository>
<repository>
<id>sonatype</id>
<url>https://oss.sonatype.org/content/groups/public/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.19.2-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,31 @@
package net.knarcraft.knarlib;
import org.bukkit.plugin.java.JavaPlugin;
/**
* KnarLib's main class
*/
public final class KnarLib {
private static JavaPlugin plugin;
/**
* Gets a plugin instance for use with the bukkit scheduler or similar
*
* @return <p>A plugin instance</p>
*/
public static JavaPlugin getPlugin() {
return plugin;
}
/**
* Sets an instance of the plugin used with KnarLib
*
* @param plugin <p>The plugin instance to use</p>
*/
public static void setPlugin(final JavaPlugin plugin) {
KnarLib.plugin = plugin;
}
}

View File

@ -0,0 +1,95 @@
package net.knarcraft.knarlib.formatting;
import net.knarcraft.knarlib.KnarLib;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.command.CommandSender;
/**
* A formatter for formatting displayed messages
*/
public final class StringFormatter {
private final static String pluginName = KnarLib.getPlugin().getDescription().getName();
private StringFormatter() {
}
/**
* Displays a message signifying a successful action
*
* @param sender <p>The command sender to display the message to</p>
* @param message <p>The translatable message to display</p>
*/
public static void displaySuccessMessage(CommandSender sender, TranslatableTimeUnit message) {
sender.sendMessage(ChatColor.GREEN + getFormattedMessage(Translator.getTranslatedMessage(message)));
}
/**
* Displays a message signifying a successful action
*
* @param sender <p>The command sender to display the message to</p>
* @param message <p>The raw message to display</p>
*/
public static void displaySuccessMessage(CommandSender sender, String message) {
sender.sendMessage(ChatColor.GREEN + getFormattedMessage(message));
}
/**
* Displays a message signifying an unsuccessful action
*
* @param sender <p>The command sender to display the message to</p>
* @param message <p>The translatable message to display</p>
*/
public static void displayErrorMessage(CommandSender sender, TranslatableTimeUnit message) {
sender.sendMessage(ChatColor.DARK_RED + getFormattedMessage(Translator.getTranslatedMessage(message)));
}
/**
* Gets the formatted version of any chat message
*
* @param message <p>The message to format</p>
* @return <p>The formatted message</p>
*/
private static String getFormattedMessage(String message) {
return "[" + pluginName + "] " + ChatColor.RESET + translateColors(message);
}
/**
* Translates & color codes to proper colors
*
* @param input <p>The input string to translate colors for</p>
* @return <p>The input with color codes translated</p>
*/
private static String translateColors(String input) {
return ChatColor.translateAlternateColorCodes('&', input);
}
/**
* Replaces a placeholder in a string
*
* @param input <p>The input string to replace in</p>
* @param placeholder <p>The placeholder to replace</p>
* @param replacement <p>The replacement value</p>
* @return <p>The input string with the placeholder replaced</p>
*/
public static String replacePlaceholder(String input, String placeholder, String replacement) {
return input.replace(placeholder, replacement);
}
/**
* Replaces placeholders in a string
*
* @param input <p>The input string to replace in</p>
* @param placeholders <p>The placeholders to replace</p>
* @param replacements <p>The replacement values</p>
* @return <p>The input string with placeholders replaced</p>
*/
public static String replacePlaceholders(String input, String[] placeholders, String[] replacements) {
for (int i = 0; i < Math.min(placeholders.length, replacements.length); i++) {
input = replacePlaceholder(input, placeholders[i], replacements[i]);
}
return input;
}
}

View File

@ -0,0 +1,99 @@
package net.knarcraft.knarlib.formatting;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static net.knarcraft.knarlib.formatting.StringFormatter.replacePlaceholder;
/**
* A utility for formatting a string specifying an amount of time
*/
public final class TimeFormatter {
private static Map<Double, TranslatableTimeUnit[]> timeUnits;
private static List<Double> sortedUnits;
private TimeFormatter() {
}
/**
* Gets the string used for displaying this sign's duration
*
* @return <p>The string used for displaying this sign's duration</p>
*/
public static String getDurationString(int duration) {
if (duration == 0) {
return Translator.getTranslatedMessage(TranslatableTimeUnit.UNIT_NOW);
} else {
if (sortedUnits == null) {
initializeUnits();
}
for (Double unit : sortedUnits) {
if (duration / unit >= 1) {
double units = round(duration / unit);
return formatDurationString(units, timeUnits.get(unit)[units == 1 ? 0 : 1],
(units * 10) % 10 == 0);
}
}
return formatDurationString(duration, TranslatableTimeUnit.UNIT_SECONDS, false);
}
}
/**
* Rounds a number to its last two digits
*
* @param number <p>The number to round</p>
* @return <p>The rounded number</p>
*/
private static double round(double number) {
return Math.round(number * 100.0) / 100.0;
}
/**
* Formats a duration string
*
* @param duration <p>The duration to display</p>
* @param translatableMessage <p>The time unit to display</p>
* @param castToInt <p>Whether to cast the duration to an int</p>
* @return <p>The formatted duration string</p>
*/
private static String formatDurationString(double duration, TranslatableTimeUnit translatableMessage, boolean castToInt) {
String durationFormat = Translator.getTranslatedMessage(TranslatableTimeUnit.DURATION_FORMAT);
durationFormat = replacePlaceholder(durationFormat, "{unit}",
Translator.getTranslatedMessage(translatableMessage));
return replacePlaceholder(durationFormat, "{time}", castToInt ? String.valueOf((int) duration) :
String.valueOf(duration));
}
/**
* Initializes the mapping of available time units for formatting permission sign duration
*/
private static void initializeUnits() {
double minute = 60;
double hour = 60 * minute;
double day = 24 * hour;
double week = 7 * day;
double month = 4 * week;
double year = 365 * day;
double decade = 10 * year;
timeUnits = new HashMap<>();
timeUnits.put(decade, new TranslatableTimeUnit[]{TranslatableTimeUnit.UNIT_DECADE, TranslatableTimeUnit.UNIT_DECADES});
timeUnits.put(year, new TranslatableTimeUnit[]{TranslatableTimeUnit.UNIT_YEAR, TranslatableTimeUnit.UNIT_YEARS});
timeUnits.put(month, new TranslatableTimeUnit[]{TranslatableTimeUnit.UNIT_MONTH, TranslatableTimeUnit.UNIT_MONTHS});
timeUnits.put(week, new TranslatableTimeUnit[]{TranslatableTimeUnit.UNIT_WEEK, TranslatableTimeUnit.UNIT_WEEKS});
timeUnits.put(day, new TranslatableTimeUnit[]{TranslatableTimeUnit.UNIT_DAY, TranslatableTimeUnit.UNIT_DAYS});
timeUnits.put(hour, new TranslatableTimeUnit[]{TranslatableTimeUnit.UNIT_HOUR, TranslatableTimeUnit.UNIT_HOURS});
timeUnits.put(minute, new TranslatableTimeUnit[]{TranslatableTimeUnit.UNIT_MINUTE, TranslatableTimeUnit.UNIT_MINUTES});
timeUnits.put(1D, new TranslatableTimeUnit[]{TranslatableTimeUnit.UNIT_SECOND, TranslatableTimeUnit.UNIT_SECONDS});
sortedUnits = new ArrayList<>(timeUnits.keySet());
Collections.sort(sortedUnits);
Collections.reverse(sortedUnits);
}
}

View File

@ -0,0 +1,26 @@
package net.knarcraft.knarlib.formatting;
/**
* A message which can be translated
*/
public interface TranslatableMessage {
/**
* Gets the name of this translatable message
*
* <p>This is automatically overridden by enums. This should not be overridden manually!</p>
*
* @return <p>The name of this translatable message</p>
*/
String name();
/**
* Gets all translatable messages
*
* <p>This should return Enum.values() for the class. This is basically a workaround to get all enum values.</p>
*
* @return <p>All translatable messages</p>
*/
TranslatableMessage[] getAllMessages();
}

View File

@ -0,0 +1,106 @@
package net.knarcraft.knarlib.formatting;
/**
* An enum containing all translatable time units
*
* <p>These time units must have a translatable message to use the time formatter</p>
*/
public enum TranslatableTimeUnit implements TranslatableMessage {
/**
* The format for displaying the exact duration of a blacksmith's cool-down or delay
*/
DURATION_FORMAT,
/**
* The text to display for 0 seconds
*/
UNIT_NOW,
/**
* The text to display for 1 second
*/
UNIT_SECOND,
/**
* The text to display for a number of seconds
*/
UNIT_SECONDS,
/**
* The text to display for 1 minute
*/
UNIT_MINUTE,
/**
* The text to display for a number of minutes
*/
UNIT_MINUTES,
/**
* The text to display for 1 hour
*/
UNIT_HOUR,
/**
* The text to display for a number of hours
*/
UNIT_HOURS,
/**
* The text to display for 1 day
*/
UNIT_DAY,
/**
* The text to display for a number of days
*/
UNIT_DAYS,
/**
* The text to display for 1 week
*/
UNIT_WEEK,
/**
* The text to display for a number of weeks
*/
UNIT_WEEKS,
/**
* The text to display for 1 month
*/
UNIT_MONTH,
/**
* The text to display for a number of months
*/
UNIT_MONTHS,
/**
* The text to display for 1 year
*/
UNIT_YEAR,
/**
* The text to display for a number of years
*/
UNIT_YEARS,
/**
* The text to display for 1 decade
*/
UNIT_DECADE,
/**
* The text to display for a number of decades
*/
UNIT_DECADES,
;
@Override
public TranslatableMessage[] getAllMessages() {
return TranslatableTimeUnit.values();
}
}

View File

@ -0,0 +1,139 @@
package net.knarcraft.knarlib.formatting;
import net.knarcraft.knarlib.KnarLib;
import net.knarcraft.knarlib.property.ColorConversion;
import net.knarcraft.knarlib.util.ColorHelper;
import net.knarcraft.knarlib.util.FileHelper;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
/**
* A tool to get strings translated to the correct language
*/
public final class Translator {
private static List<TranslatableMessage> messageCategories;
private static Map<TranslatableMessage, String> translatedMessages;
private static Map<TranslatableMessage, String> backupTranslatedMessages;
private Translator() {
}
/**
* Registers all translatable messages in the given message category
*
* <p>This should be run for one enum of every class extending TranslatableMessage. This allows the translator to
* look for translations for any enum in the category.</p>
*
* @param translatableMessage <p>A translatable message in the category to register</p>
*/
public static void registerMessageCategory(TranslatableMessage translatableMessage) {
messageCategories.add(translatableMessage);
}
/**
* Loads the languages used by this translator
*/
public static void loadLanguages(String selectedLanguage) {
backupTranslatedMessages = loadTranslatedMessages("en");
translatedMessages = loadCustomTranslatedMessages(selectedLanguage);
if (translatedMessages == null) {
translatedMessages = loadTranslatedMessages(selectedLanguage);
}
}
/**
* Gets a translated version of the given translatable message
*
* @param translatableMessage <p>The message to translate</p>
* @return <p>The translated message</p>
*/
public static String getTranslatedMessage(TranslatableMessage translatableMessage) {
if (translatedMessages == null) {
return "Translated strings not loaded";
}
String translatedMessage;
if (translatedMessages.containsKey(translatableMessage)) {
translatedMessage = translatedMessages.get(translatableMessage);
} else if (backupTranslatedMessages.containsKey(translatableMessage)) {
translatedMessage = backupTranslatedMessages.get(translatableMessage);
} else {
translatedMessage = translatableMessage.toString();
}
return ColorHelper.translateColorCodes(translatedMessage, ColorConversion.RGB);
}
/**
* Loads all translated messages for the given language
*
* @param language <p>The language chosen by the user</p>
* @return <p>A mapping of all strings for the given language</p>
*/
public static Map<TranslatableMessage, String> loadTranslatedMessages(String language) {
try {
BufferedReader reader = FileHelper.getBufferedReaderForInternalFile("/strings.yml");
return loadTranslatableMessages(language, reader);
} catch (FileNotFoundException e) {
KnarLib.getPlugin().getLogger().log(Level.SEVERE, "Unable to load translated messages");
return null;
}
}
/**
* Tries to load translated messages from a custom strings.yml file
*
* @param language <p>The selected language</p>
* @return <p>The loaded translated strings, or null if no custom language file exists</p>
*/
public static Map<TranslatableMessage, String> loadCustomTranslatedMessages(String language) {
JavaPlugin instance = KnarLib.getPlugin();
File strings = new File(instance.getDataFolder(), "strings.yml");
if (!strings.exists()) {
instance.getLogger().log(Level.FINEST, "Strings file not found");
return null;
}
try {
instance.getLogger().log(Level.INFO, "Loading custom strings...");
return loadTranslatableMessages(language, new BufferedReader(new InputStreamReader(new FileInputStream(strings))));
} catch (FileNotFoundException e) {
instance.getLogger().log(Level.WARNING, "Unable to load custom messages");
return null;
}
}
/**
* Loads translatable messages from the given reader
*
* @param language <p>The selected language</p>
* @param reader <p>The buffered reader to read from</p>
* @return <p>The loaded translated strings</p>
*/
private static Map<TranslatableMessage, String> loadTranslatableMessages(String language, BufferedReader reader) {
Map<TranslatableMessage, String> translatedMessages = new HashMap<>();
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(reader);
for (TranslatableMessage translatableMessageCategories : messageCategories) {
for (TranslatableMessage translatableMessage : translatableMessageCategories.getAllMessages()) {
String translated = configuration.getString(language + "." + translatableMessage.name());
if (translated != null) {
translatedMessages.put(translatableMessage, translated);
}
}
}
return translatedMessages;
}
}

View File

@ -0,0 +1,23 @@
package net.knarcraft.knarlib.property;
/**
* An enum representing the different types of color conversions available
*/
public enum ColorConversion {
/**
* No conversion of colors
*/
NONE,
/**
* Ampersand color codes are converted into colors
*/
NORMAL,
/**
* Ampersand color codes, and hexadecimal color codes are converted into colors
*/
RGB
}

View File

@ -0,0 +1,89 @@
package net.knarcraft.knarlib.util;
import net.knarcraft.knarlib.property.ColorConversion;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.Color;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A helper class for dealing with colors
*/
public final class ColorHelper {
private static boolean requireAmpersandInHexColors = false;
private ColorHelper() {
}
/**
* Inverts the given color
*
* @param color <p>The color to invert</p>
* @return <p>The inverted color</p>
*/
public static Color invert(Color color) {
return color.setRed(255 - color.getRed()).setGreen(255 - color.getGreen()).setBlue(255 - color.getBlue());
}
/**
* Gets the chat color corresponding to the given color
*
* @param color <p>The color to convert into a chat color</p>
* @return <p>The resulting chat color</p>
*/
public static ChatColor fromColor(Color color) {
return ChatColor.of(String.format("#%02X%02X%02X", color.getRed(), color.getGreen(), color.getBlue()));
}
/**
* Sets whether to require ampersand in hex color
*
* <p>If set to true, &#f35336 will be treated as a color code, but #f35336 won't. By default, this is set to false,
* meaning that both are treated as color codes.</p>
*
* @param requireAmpersandInHexColors <p>True if hex colors should require an ampersand</p>
*/
public static void setRequireAmpersandInHexColors(boolean requireAmpersandInHexColors) {
ColorHelper.requireAmpersandInHexColors = requireAmpersandInHexColors;
}
/**
* Translates color codes according to the given color conversion setting
*
* @param message <p>The message to translate color codes for</p>
* @param colorConversion <p>The type of color conversion to apply</p>
* @return <p>The string with color codes applied</p>
*/
public static String translateColorCodes(String message, ColorConversion colorConversion) {
return switch (colorConversion) {
case NONE -> message;
case NORMAL -> ChatColor.translateAlternateColorCodes('&', message);
case RGB -> translateAllColorCodes(message);
};
}
/**
* Translates all found color codes to formatting in a string
*
* @param message <p>The string to search for color codes</p>
* @return <p>The message with color codes translated</p>
*/
private static String translateAllColorCodes(String message) {
message = ChatColor.translateAlternateColorCodes('&', message);
Pattern pattern;
if (requireAmpersandInHexColors) {
pattern = Pattern.compile("(&#[a-fA-F0-9]{6})");
} else {
pattern = Pattern.compile("(&?#[a-fA-F0-9]{6})");
}
Matcher matcher = pattern.matcher(message);
while (matcher.find()) {
message = message.replace(matcher.group(), "" + ChatColor.of(matcher.group()));
}
return message;
}
}

View File

@ -0,0 +1,141 @@
package net.knarcraft.knarlib.util;
import net.md_5.bungee.api.ChatColor;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* A helper class for dealing with files
*/
public final class FileHelper {
private FileHelper() {
}
/**
* Gets a buffered reader for
*
* @return <p>A buffered read for reading the file</p>
* @throws FileNotFoundException <p>If unable to get an input stream for the given file</p>
*/
public static BufferedReader getBufferedReaderForInternalFile(String file) throws FileNotFoundException {
InputStream inputStream = getInputStreamForInternalFile(file);
if (inputStream == null) {
throw new FileNotFoundException("Unable to read the given file");
}
return getBufferedReaderFromInputStream(inputStream);
}
/**
* Gets an input stream from a string pointing to an internal file
*
* <p>This is used for getting an input stream for reading a file contained within the compiled .jar file. The file
* should be in the resources directory, and the file path should start with a forward slash ("/") character.</p>
*
* @param file <p>The file to read</p>
* @return <p>An input stream for the file</p>
*/
public static InputStream getInputStreamForInternalFile(String file) {
return FileHelper.class.getResourceAsStream(file);
}
/**
* Gets a buffered reader from a string pointing to a file
*
* @param file <p>The file to read</p>
* @return <p>A buffered reader reading the file</p>
* @throws FileNotFoundException <p>If the given file does not exist</p>
*/
public static BufferedReader getBufferedReaderFromString(String file) throws FileNotFoundException {
FileInputStream fileInputStream = new FileInputStream(file);
return getBufferedReaderFromInputStream(fileInputStream);
}
/**
* Gets a buffered reader given an input stream
*
* @param inputStream <p>The input stream to read</p>
* @return <p>A buffered reader reading the input stream</p>
*/
public static BufferedReader getBufferedReaderFromInputStream(InputStream inputStream) {
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
return new BufferedReader(inputStreamReader);
}
/**
* Gets a buffered writer from a string pointing to a file
*
* @param file <p>The file to write to</p>
* @return <p>A buffered writer writing to the file</p>
* @throws FileNotFoundException <p>If the file does not exist</p>
*/
public static BufferedWriter getBufferedWriterFromString(String file) throws FileNotFoundException {
FileOutputStream fileOutputStream = new FileOutputStream(file);
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream, StandardCharsets.UTF_8);
return new BufferedWriter(outputStreamWriter);
}
/**
* Reads key/value pairs from an input stream
*
* @param bufferedReader <p>The buffered reader to read</p>
* @return <p>A map containing the read pairs</p>
* @throws IOException <p>If unable to read from the stream</p>
*/
public static Map<String, String> readKeyValuePairs(BufferedReader bufferedReader, String separator, boolean translateColorCodes) throws IOException {
Map<String, String> readPairs = new HashMap<>();
String line = bufferedReader.readLine();
boolean firstLine = true;
while (line != null) {
//Strip UTF BOM from the first line
if (firstLine) {
line = removeUTF8BOM(line);
firstLine = false;
}
//Split at first separator
int separatorIndex = line.indexOf(separator);
if (separatorIndex == -1) {
line = bufferedReader.readLine();
continue;
}
//Read the line
String key = line.substring(0, separatorIndex);
String value = ChatColor.translateAlternateColorCodes('&', line.substring(separatorIndex + 1));
readPairs.put(key, value);
line = bufferedReader.readLine();
}
bufferedReader.close();
return readPairs;
}
/**
* Removes the UTF-8 Byte Order Mark if present
*
* @param string <p>The string to remove the BOM from</p>
* @return <p>A string guaranteed without a BOM</p>
*/
private static String removeUTF8BOM(String string) {
String UTF8_BOM = "\uFEFF";
if (string.startsWith(UTF8_BOM)) {
string = string.substring(1);
}
return string;
}
}

View File

@ -0,0 +1,48 @@
package net.knarcraft.knarlib.util;
import java.util.ArrayList;
import java.util.List;
/**
* Helper class for getting string lists required for auto-completion
*/
public final class TabCompletionHelper {
private TabCompletionHelper() {
}
/**
* Finds tab complete values that contain the typed text
*
* @param values <p>The values to filter</p>
* @param typedText <p>The text the player has started typing</p>
* @return <p>The given string values that contain the player's typed text</p>
*/
public static List<String> filterMatchingContains(List<String> values, String typedText) {
List<String> configValues = new ArrayList<>();
for (String value : values) {
if (value.toLowerCase().contains(typedText.toLowerCase())) {
configValues.add(value);
}
}
return configValues;
}
/**
* Finds tab complete values that match the start of the typed text
*
* @param values <p>The values to filter</p>
* @param typedText <p>The text the player has started typing</p>
* @return <p>The given string values that start with the player's typed text</p>
*/
public static List<String> filterMatchingStartsWith(List<String> values, String typedText) {
List<String> configValues = new ArrayList<>();
for (String value : values) {
if (value.toLowerCase().startsWith(typedText.toLowerCase())) {
configValues.add(value);
}
}
return configValues;
}
}

View File

@ -0,0 +1,102 @@
package net.knarcraft.knarlib.util;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitScheduler;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.logging.Level;
/**
* The update checker is responsible for looking for new updates
*/
public final class UpdateChecker {
private final static String updateNotice = "A new update is available: %s (You are still on %s)";
private UpdateChecker() {
}
/**
* Checks if there's a new update available, and alerts the user if necessary
*
* @param plugin <p>The plugin to check for updates for</p>
* @param apiResourceURL <p>The spigot URL to check for updates. Example: https://api.spigotmc.org/legacy/update.php?resource={resourceId}</p>
* @param getVersionSupplier <p>The supplier used to get the current plugin version</p>
* @param setVersionMethod <p>A method to call with the new version as an argument. Can be used to alert admins about an available update or similar.</p>
*/
public static void checkForUpdate(Plugin plugin, String apiResourceURL, Supplier<String> getVersionSupplier,
Consumer<String> setVersionMethod) {
BukkitScheduler scheduler = plugin.getServer().getScheduler();
scheduler.runTaskAsynchronously(plugin, () -> UpdateChecker.queryAPI(plugin, apiResourceURL, getVersionSupplier,
setVersionMethod));
}
/**
* Queries the spigot API to check for a newer version, and prints to the console if found
*
* @param plugin <p>The plugin to check for updates for</p>
* @param APIResourceURL <p>The spigot URL to check for updates</p>
* @param getVersionMethod <p>The supplier used to get the current plugin version</p>
* @param setVersionMethod <p>A method to call with the new version as an argument. Can be used to alert admins about an available update or similar.</p>
*/
private static void queryAPI(Plugin plugin, String APIResourceURL, Supplier<String> getVersionMethod,
Consumer<String> setVersionMethod) {
try {
InputStream inputStream = new URL(APIResourceURL).openStream();
BufferedReader reader = FileHelper.getBufferedReaderFromInputStream(inputStream);
//There should only be one line of output
String newVersion = reader.readLine();
reader.close();
String oldVersion = getVersionMethod.get();
//If there is a newer version, notify the user
if (isVersionHigher(oldVersion, newVersion)) {
plugin.getLogger().log(Level.INFO, getUpdateAvailableString(newVersion, oldVersion));
if (setVersionMethod != null) {
setVersionMethod.accept(newVersion);
}
}
} catch (IOException e) {
plugin.getLogger().log(Level.WARNING, "Unable to get newest version.");
}
}
/**
* Gets the string to display to a user to alert about a new update
*
* @param newVersion <p>The new available plugin version</p>
* @param oldVersion <p>The old (current) plugin version</p>
* @return <p>The string to display</p>
*/
public static String getUpdateAvailableString(String newVersion, String oldVersion) {
return String.format(updateNotice, newVersion, oldVersion);
}
/**
* Decides whether one version number is higher than another
*
* @param oldVersion <p>The old version to check</p>
* @param newVersion <p>The new version to check</p>
* @return <p>True if the new version is higher than the old one</p>
*/
public static boolean isVersionHigher(String oldVersion, String newVersion) {
String[] oldVersionParts = oldVersion.split("\\.");
String[] newVersionParts = newVersion.split("\\.");
int versionLength = Math.max(oldVersionParts.length, newVersionParts.length);
for (int i = 0; i < versionLength; i++) {
int oldVersionNumber = oldVersionParts.length > i ? Integer.parseInt(oldVersionParts[i]) : 0;
int newVersionNumber = newVersionParts.length > i ? Integer.parseInt(newVersionParts[i]) : 0;
if (newVersionNumber != oldVersionNumber) {
return newVersionNumber > oldVersionNumber;
}
}
return false;
}
}