aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/eu/olli/cowlection/util
diff options
context:
space:
mode:
authorCow <cow@volloeko.de>2020-07-05 05:42:45 +0200
committerCow <cow@volloeko.de>2020-07-05 05:42:45 +0200
commit1b446698398c648b38311975a6cfd54859ea5cfe (patch)
tree521ecc4ce9ad968281094eb8c5453dca606931e3 /src/main/java/eu/olli/cowlection/util
parentedaca1fd41a612c71c526ceb20b89c5dec2d81b3 (diff)
downloadCowlection-1b446698398c648b38311975a6cfd54859ea5cfe.tar.gz
Cowlection-1b446698398c648b38311975a6cfd54859ea5cfe.tar.bz2
Cowlection-1b446698398c648b38311975a6cfd54859ea5cfe.zip
Renamed mod to Cowlection
Bumped version to 1.8.9-0.7.0
Diffstat (limited to 'src/main/java/eu/olli/cowlection/util')
-rw-r--r--src/main/java/eu/olli/cowlection/util/ApiUtils.java138
-rw-r--r--src/main/java/eu/olli/cowlection/util/ChatHelper.java82
-rw-r--r--src/main/java/eu/olli/cowlection/util/GsonUtils.java28
-rw-r--r--src/main/java/eu/olli/cowlection/util/ImageUtils.java76
-rw-r--r--src/main/java/eu/olli/cowlection/util/MooChatComponent.java186
-rw-r--r--src/main/java/eu/olli/cowlection/util/TickDelay.java29
-rw-r--r--src/main/java/eu/olli/cowlection/util/Utils.java250
-rw-r--r--src/main/java/eu/olli/cowlection/util/VersionChecker.java140
8 files changed, 929 insertions, 0 deletions
diff --git a/src/main/java/eu/olli/cowlection/util/ApiUtils.java b/src/main/java/eu/olli/cowlection/util/ApiUtils.java
new file mode 100644
index 0000000..b0a4cce
--- /dev/null
+++ b/src/main/java/eu/olli/cowlection/util/ApiUtils.java
@@ -0,0 +1,138 @@
+package eu.olli.cowlection.util;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonParser;
+import com.mojang.util.UUIDTypeAdapter;
+import eu.olli.cowlection.Cowlection;
+import eu.olli.cowlection.command.exception.ThrowingConsumer;
+import eu.olli.cowlection.config.MooConfig;
+import eu.olli.cowlection.data.Friend;
+import eu.olli.cowlection.data.HySkyBlockStats;
+import eu.olli.cowlection.data.HyStalkingData;
+import eu.olli.cowlection.data.SlothStalkingData;
+import org.apache.http.HttpStatus;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class ApiUtils {
+ public static final String UUID_NOT_FOUND = "UUID-NOT-FOUND";
+ private static final String NAME_TO_UUID_URL = "https://api.mojang.com/users/profiles/minecraft/";
+ private static final String UUID_TO_NAME_URL = "https://api.mojang.com/user/profiles/%s/names";
+ private static final String STALKING_URL_OFFICIAL = "https://api.hypixel.net/status?key=%s&uuid=%s";
+ private static final String SKYBLOCK_STATS_URL_OFFICIAL = "https://api.hypixel.net/skyblock/profiles?key=%s&uuid=%s";
+ private static final String STALKING_URL_UNOFFICIAL = "https://api.slothpixel.me/api/players/%s";
+ private static final ExecutorService pool = Executors.newCachedThreadPool();
+
+ private ApiUtils() {
+ }
+
+ public static void fetchFriendData(String name, ThrowingConsumer<Friend> action) {
+ pool.execute(() -> action.accept(getFriend(name)));
+ }
+
+ private static Friend getFriend(String name) {
+ try (BufferedReader reader = makeApiCall(NAME_TO_UUID_URL + name)) {
+ if (reader == null) {
+ return Friend.FRIEND_NOT_FOUND;
+ } else {
+ return GsonUtils.fromJson(reader, Friend.class);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ public static void fetchCurrentName(Friend friend, ThrowingConsumer<String> action) {
+ pool.execute(() -> action.accept(getCurrentName(friend)));
+ }
+
+ private static String getCurrentName(Friend friend) {
+ try (BufferedReader reader = makeApiCall(String.format(UUID_TO_NAME_URL, UUIDTypeAdapter.fromUUID(friend.getUuid())))) {
+ if (reader == null) {
+ return UUID_NOT_FOUND;
+ } else {
+ JsonArray nameHistoryData = new JsonParser().parse(reader).getAsJsonArray();
+ if (nameHistoryData.size() > 0) {
+ return nameHistoryData.get(nameHistoryData.size() - 1).getAsJsonObject().get("name").getAsString();
+ }
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ public static void fetchPlayerStatus(Friend friend, ThrowingConsumer<HyStalkingData> action) {
+ pool.execute(() -> action.accept(stalkPlayer(friend)));
+ }
+
+ private static HyStalkingData stalkPlayer(Friend friend) {
+ try (BufferedReader reader = makeApiCall(String.format(STALKING_URL_OFFICIAL, MooConfig.moo, UUIDTypeAdapter.fromUUID(friend.getUuid())))) {
+ if (reader != null) {
+ return GsonUtils.fromJson(reader, HyStalkingData.class);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ public static void fetchSkyBlockStats(Friend friend, ThrowingConsumer<HySkyBlockStats> action) {
+ pool.execute(() -> action.accept(stalkSkyBlockStats(friend)));
+ }
+
+ private static HySkyBlockStats stalkSkyBlockStats(Friend friend) {
+ try (BufferedReader reader = makeApiCall(String.format(SKYBLOCK_STATS_URL_OFFICIAL, MooConfig.moo, UUIDTypeAdapter.fromUUID(friend.getUuid())))) {
+ if (reader != null) {
+ return GsonUtils.fromJson(reader, HySkyBlockStats.class);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ public static void fetchPlayerOfflineStatus(Friend stalkedPlayer, ThrowingConsumer<SlothStalkingData> action) {
+ pool.execute(() -> action.accept(stalkOfflinePlayer(stalkedPlayer)));
+ }
+
+ private static SlothStalkingData stalkOfflinePlayer(Friend stalkedPlayer) {
+ try (BufferedReader reader = makeApiCall(String.format(STALKING_URL_UNOFFICIAL, UUIDTypeAdapter.fromUUID(stalkedPlayer.getUuid())))) {
+ if (reader != null) {
+ return GsonUtils.fromJson(reader, SlothStalkingData.class);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ private static BufferedReader makeApiCall(String url) throws IOException {
+ HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
+ connection.setConnectTimeout(5000);
+ connection.setReadTimeout(5000);
+ connection.addRequestProperty("User-Agent", "Forge Mod " + Cowlection.MODNAME + "/" + Cowlection.VERSION + " (" + Cowlection.GITURL + ")");
+
+ connection.getResponseCode();
+ if (connection.getResponseCode() == HttpStatus.SC_NO_CONTENT) { // http status 204
+ return null;
+ } else {
+ BufferedReader reader;
+ InputStream errorStream = connection.getErrorStream();
+ if (errorStream != null) {
+ reader = new BufferedReader(new InputStreamReader(errorStream));
+ } else {
+ reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
+ }
+ return reader;
+ }
+ }
+}
diff --git a/src/main/java/eu/olli/cowlection/util/ChatHelper.java b/src/main/java/eu/olli/cowlection/util/ChatHelper.java
new file mode 100644
index 0000000..202390f
--- /dev/null
+++ b/src/main/java/eu/olli/cowlection/util/ChatHelper.java
@@ -0,0 +1,82 @@
+package eu.olli.cowlection.util;
+
+import net.minecraft.client.Minecraft;
+import net.minecraft.util.ChatComponentText;
+import net.minecraft.util.ChatStyle;
+import net.minecraft.util.EnumChatFormatting;
+import net.minecraft.util.IChatComponent;
+import net.minecraftforge.client.event.ClientChatReceivedEvent;
+import net.minecraftforge.common.MinecraftForge;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class ChatHelper {
+ private static final Pattern USELESS_JSON_CONTENT_PATTERN = Pattern.compile("\"[A-Za-z]+\":false,?");
+ private static final int DISPLAY_DURATION = 5000;
+ private List<IChatComponent> offlineMessages = new ArrayList<>();
+ private String[] aboveChatMessage;
+ private long aboveChatMessageExpiration;
+
+ public ChatHelper() {
+ }
+
+ public void sendMessage(EnumChatFormatting color, String text) {
+ sendMessage(new ChatComponentText(text).setChatStyle(new ChatStyle().setColor(color)));
+ }
+
+ public void sendMessage(IChatComponent chatComponent) {
+ ClientChatReceivedEvent event = new ClientChatReceivedEvent((byte) 1, chatComponent);
+ MinecraftForge.EVENT_BUS.post(event);
+ if (!event.isCanceled()) {
+ if (Minecraft.getMinecraft().thePlayer == null) {
+ offlineMessages.add(event.message);
+ } else {
+ Minecraft.getMinecraft().thePlayer.addChatMessage(event.message);
+ }
+ }
+ }
+
+ public void sendOfflineMessages() {
+ if (Minecraft.getMinecraft().thePlayer != null) {
+ Iterator<IChatComponent> offlineMessages = this.offlineMessages.iterator();
+ if (offlineMessages.hasNext()) {
+ Minecraft.getMinecraft().thePlayer.playSound("random.levelup", 0.4F, 0.8F);
+ }
+ while (offlineMessages.hasNext()) {
+ Minecraft.getMinecraft().thePlayer.addChatMessage(offlineMessages.next());
+ offlineMessages.remove();
+ }
+ }
+ }
+
+ public void sendAboveChatMessage(String... text) {
+ aboveChatMessage = text;
+ aboveChatMessageExpiration = Minecraft.getSystemTime() + DISPLAY_DURATION;
+ }
+
+ public String[] getAboveChatMessage() {
+ if (aboveChatMessageExpiration < Minecraft.getSystemTime()) {
+ // message expired
+ aboveChatMessage = null;
+ }
+ return aboveChatMessage;
+ }
+
+ public String cleanChatComponent(IChatComponent chatComponent) {
+ String component = IChatComponent.Serializer.componentToJson(chatComponent);
+ Matcher jsonMatcher = USELESS_JSON_CONTENT_PATTERN.matcher(component);
+ return jsonMatcher.replaceAll("");
+ }
+
+ public void sendShrug(String... args) {
+ String chatMsg = "\u00AF\\_(\u30C4)_/\u00AF"; // ¯\\_(ツ)_/¯"
+ if (args.length > 0) {
+ chatMsg = String.join(" ", args) + " " + chatMsg;
+ }
+ Minecraft.getMinecraft().thePlayer.sendChatMessage(chatMsg);
+ }
+}
diff --git a/src/main/java/eu/olli/cowlection/util/GsonUtils.java b/src/main/java/eu/olli/cowlection/util/GsonUtils.java
new file mode 100644
index 0000000..ece3faf
--- /dev/null
+++ b/src/main/java/eu/olli/cowlection/util/GsonUtils.java
@@ -0,0 +1,28 @@
+package eu.olli.cowlection.util;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.mojang.util.UUIDTypeAdapter;
+
+import java.io.Reader;
+import java.lang.reflect.Type;
+import java.util.UUID;
+
+public final class GsonUtils {
+ private static final Gson gson = new GsonBuilder().registerTypeAdapter(UUID.class, new UUIDTypeAdapter()).create();
+
+ private GsonUtils() {
+ }
+
+ public static <T> T fromJson(String json, Type clazz) {
+ return gson.fromJson(json, clazz);
+ }
+
+ public static <T> T fromJson(Reader json, Class<T> clazz) {
+ return gson.fromJson(json, clazz);
+ }
+
+ public static String toJson(Object object) {
+ return gson.toJson(object);
+ }
+}
diff --git a/src/main/java/eu/olli/cowlection/util/ImageUtils.java b/src/main/java/eu/olli/cowlection/util/ImageUtils.java
new file mode 100644
index 0000000..d6283bd
--- /dev/null
+++ b/src/main/java/eu/olli/cowlection/util/ImageUtils.java
@@ -0,0 +1,76 @@
+package eu.olli.cowlection.util;
+
+import com.mojang.authlib.minecraft.MinecraftProfileTexture;
+import eu.olli.cowlection.Cowlection;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.ThreadDownloadImageData;
+import net.minecraft.util.ResourceLocation;
+import net.minecraftforge.fml.relauncher.ReflectionHelper;
+
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class ImageUtils {
+ public static int getTierFromTexture(String minionSkinId) {
+ String textureUrl = "http://textures.minecraft.net/texture/" + minionSkinId;
+ MinecraftProfileTexture minionSkinTextureDetails = new MinecraftProfileTexture(textureUrl, null);
+
+ ResourceLocation minionSkinLocation = Minecraft.getMinecraft().getSkinManager().loadSkin(minionSkinTextureDetails, MinecraftProfileTexture.Type.SKIN);
+
+ ThreadDownloadImageData minionSkinTexture = (ThreadDownloadImageData) Minecraft.getMinecraft().getTextureManager().getTexture(minionSkinLocation);
+ BufferedImage minionSkinImage = ReflectionHelper.getPrivateValue(ThreadDownloadImageData.class, minionSkinTexture, "bufferedImage", "field_110560_d");
+
+ // extract part of the minion tier badge (center 2x2 pixel)
+ BufferedImage minionSkinTierBadge = minionSkinImage.getSubimage(43, 3, 2, 2);
+
+ // reference image for tier badges: each tier is 2x2 pixel
+ ResourceLocation tierBadgesLocation = new ResourceLocation(Cowlection.MODID, "minion-tier-badges.png");
+
+ try (InputStream tierBadgesStream = Minecraft.getMinecraft().getResourceManager().getResource(tierBadgesLocation).getInputStream()) {
+ BufferedImage tierBadges = ImageIO.read(tierBadgesStream);
+
+ final int maxTier = 11;
+ for (int tier = 0; tier < maxTier; tier++) {
+ BufferedImage tierBadgeRaw = tierBadges.getSubimage(tier * 2, 0, 2, 2);
+ if (ImageUtils.areEquals(minionSkinTierBadge, tierBadgeRaw)) {
+ return tier + 1;
+ }
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ return -1;
+ }
+ return -5;
+ }
+
+ /**
+ * Compares two images pixel by pixel
+ *
+ * @param imageA the first image
+ * @param imageB the second image
+ * @return whether the images are both the same or not
+ * @see <a href="https://stackoverflow.com/a/29886786">Source</a>
+ */
+ private static boolean areEquals(BufferedImage imageA, BufferedImage imageB) {
+ // images must be the same size
+ if (imageA.getWidth() != imageB.getWidth() || imageA.getHeight() != imageB.getHeight()) {
+ return false;
+ }
+
+ int width = imageA.getWidth();
+ int height = imageB.getHeight();
+
+ // loop over every pixel
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ // compare the pixels for equality
+ if (imageA.getRGB(x, y) != imageB.getRGB(x, y)) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+}
diff --git a/src/main/java/eu/olli/cowlection/util/MooChatComponent.java b/src/main/java/eu/olli/cowlection/util/MooChatComponent.java
new file mode 100644
index 0000000..0c6a141
--- /dev/null
+++ b/src/main/java/eu/olli/cowlection/util/MooChatComponent.java
@@ -0,0 +1,186 @@
+package eu.olli.cowlection.util;
+
+import net.minecraft.event.ClickEvent;
+import net.minecraft.event.HoverEvent;
+import net.minecraft.util.ChatComponentText;
+import net.minecraft.util.EnumChatFormatting;
+import net.minecraft.util.IChatComponent;
+
+public class MooChatComponent extends ChatComponentText {
+ public MooChatComponent(String msg) {
+ super(msg);
+ }
+
+ public MooChatComponent black() {
+ setChatStyle(getChatStyle().setColor(EnumChatFormatting.BLACK));
+ return this;
+ }
+
+ public MooChatComponent darkBlue() {
+ setChatStyle(getChatStyle().setColor(EnumChatFormatting.DARK_BLUE));
+ return this;
+ }
+
+ public MooChatComponent darkGreen() {
+ setChatStyle(getChatStyle().setColor(EnumChatFormatting.DARK_GREEN));
+ return this;
+ }
+
+ public MooChatComponent darkAqua() {
+ setChatStyle(getChatStyle().setColor(EnumChatFormatting.DARK_AQUA));
+ return this;
+ }
+
+ public MooChatComponent darkRed() {
+ setChatStyle(getChatStyle().setColor(EnumChatFormatting.DARK_RED));
+ return this;
+ }
+
+ public MooChatComponent darkPurple() {
+ setChatStyle(getChatStyle().setColor(EnumChatFormatting.DARK_PURPLE));
+ return this;
+ }
+
+ public MooChatComponent gold() {
+ setChatStyle(getChatStyle().setColor(EnumChatFormatting.GOLD));
+ return this;
+ }
+
+ public MooChatComponent gray() {
+ setChatStyle(getChatStyle().setColor(EnumChatFormatting.GRAY));
+ return this;
+ }
+
+ public MooChatComponent darkGray() {
+ setChatStyle(getChatStyle().setColor(EnumChatFormatting.DARK_GRAY));
+ return this;
+ }
+
+ public MooChatComponent blue() {
+ setChatStyle(getChatStyle().setColor(EnumChatFormatting.BLUE));
+ return this;
+ }
+
+ public MooChatComponent green() {
+ setChatStyle(getChatStyle().setColor(EnumChatFormatting.GREEN));
+ return this;
+ }
+
+ public MooChatComponent aqua() {
+ setChatStyle(getChatStyle().setColor(EnumChatFormatting.AQUA));
+ return this;
+ }
+
+ public MooChatComponent red() {
+ setChatStyle(getChatStyle().setColor(EnumChatFormatting.RED));
+ return this;
+ }
+
+ public MooChatComponent lightPurple() {
+ setChatStyle(getChatStyle().setColor(EnumChatFormatting.LIGHT_PURPLE));
+ return this;
+ }
+
+ public MooChatComponent yellow() {
+ setChatStyle(getChatStyle().setColor(EnumChatFormatting.YELLOW));
+ return this;
+ }
+
+ public MooChatComponent white() {
+ setChatStyle(getChatStyle().setColor(EnumChatFormatting.WHITE));
+ return this;
+ }
+
+ public MooChatComponent obfuscated() {
+ setChatStyle(getChatStyle().setObfuscated(true));
+ return this;
+ }
+
+ public MooChatComponent bold() {
+ setChatStyle(getChatStyle().setBold(true));
+ return this;
+ }
+
+ public MooChatComponent strikethrough() {
+ setChatStyle(getChatStyle().setStrikethrough(true));
+ return this;
+ }
+
+ public MooChatComponent underline() {
+ setChatStyle(getChatStyle().setUnderlined(true));
+ return this;
+ }
+
+ public MooChatComponent italic() {
+ setChatStyle(getChatStyle().setItalic(true));
+ return this;
+ }
+
+ public MooChatComponent reset() {
+ setChatStyle(getChatStyle().setParentStyle(null).setBold(false).setItalic(false).setObfuscated(false).setUnderlined(false).setStrikethrough(false));
+ return this;
+ }
+
+ public MooChatComponent setHover(IChatComponent hover) {
+ setChatStyle(getChatStyle().setChatHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, hover)));
+ return this;
+ }
+
+ public MooChatComponent setUrl(String url) {
+ setUrl(url, new KeyValueTooltipComponent("Click to visit", url));
+ return this;
+ }
+
+ public MooChatComponent setUrl(String url, String hover) {
+ setUrl(url, new MooChatComponent(hover).yellow());
+ return this;
+ }
+
+ public MooChatComponent setUrl(String url, IChatComponent hover) {
+ setChatStyle(getChatStyle().setChatClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, url)));
+ setHover(hover);
+ return this;
+ }
+
+ public MooChatComponent setSuggestCommand(String command) {
+ setChatStyle(getChatStyle().setChatClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, command)));
+ setHover(new KeyValueChatComponent("Run", command, " "));
+ return this;
+ }
+
+ /**
+ * Appends the given component in a new line, without inheriting formatting of previous siblings.
+ *
+ * @see ChatComponentText#appendSibling appendSibling
+ */
+ public MooChatComponent appendFreshSibling(IChatComponent sibling) {
+ this.siblings.add(new ChatComponentText("\n").appendSibling(sibling));
+ return this;
+ }
+
+ @Deprecated
+ public MooChatComponent appendKeyValue(String key, String value) {
+ appendSibling(new MooChatComponent("\n").appendFreshSibling(new KeyValueChatComponent(key, value)));
+ return this;
+ }
+
+ public static class KeyValueChatComponent extends MooChatComponent {
+ public KeyValueChatComponent(String key, String value) {
+ this(key, value, ": ");
+ }
+
+ public KeyValueChatComponent(String key, String value, String separator) {
+ super(key);
+ appendText(separator);
+ gold().appendSibling(new MooChatComponent(value).yellow());
+ }
+ }
+
+ public static class KeyValueTooltipComponent extends MooChatComponent {
+ public KeyValueTooltipComponent(String key, String value) {
+ super(key);
+ appendText(": ");
+ gray().appendSibling(new MooChatComponent(value).yellow());
+ }
+ }
+}
diff --git a/src/main/java/eu/olli/cowlection/util/TickDelay.java b/src/main/java/eu/olli/cowlection/util/TickDelay.java
new file mode 100644
index 0000000..9692ce7
--- /dev/null
+++ b/src/main/java/eu/olli/cowlection/util/TickDelay.java
@@ -0,0 +1,29 @@
+package eu.olli.cowlection.util;
+
+import net.minecraftforge.common.MinecraftForge;
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
+import net.minecraftforge.fml.common.gameevent.TickEvent;
+
+public class TickDelay {
+ private Runnable task;
+ private int waitingTicks;
+
+ public TickDelay(Runnable task, int ticks) {
+ this.task = task;
+ this.waitingTicks = ticks;
+
+ MinecraftForge.EVENT_BUS.register(this);
+ }
+
+ @SubscribeEvent
+ public void onTick(TickEvent.ClientTickEvent e) {
+ if (e.phase == TickEvent.Phase.START) {
+ if (waitingTicks < 1) {
+ // we're done waiting! Do stuff and exit.
+ task.run();
+ MinecraftForge.EVENT_BUS.unregister(this);
+ }
+ waitingTicks--;
+ }
+ }
+}
diff --git a/src/main/java/eu/olli/cowlection/util/Utils.java b/src/main/java/eu/olli/cowlection/util/Utils.java
new file mode 100644
index 0000000..92f4c9e
--- /dev/null
+++ b/src/main/java/eu/olli/cowlection/util/Utils.java
@@ -0,0 +1,250 @@
+package eu.olli.cowlection.util;
+
+import com.mojang.realmsclient.util.Pair;
+import net.minecraft.util.EnumChatFormatting;
+import org.apache.commons.lang3.text.WordUtils;
+import org.apache.commons.lang3.time.DateFormatUtils;
+import org.apache.commons.lang3.time.DurationFormatUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.text.DecimalFormat;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+
+public final class Utils {
+ public static final Pattern VALID_UUID_PATTERN = Pattern.compile("^(\\w{8})-(\\w{4})-(\\w{4})-(\\w{4})-(\\w{12})$");
+ private static final Pattern VALID_USERNAME = Pattern.compile("^[\\w]{1,16}$");
+ private static final char[] LARGE_NUMBERS = new char[]{'k', 'm', 'b', 't'};
+
+ private Utils() {
+ }
+
+ public static boolean isValidUuid(String uuid) {
+ return VALID_UUID_PATTERN.matcher(uuid).matches();
+ }
+
+ public static boolean isValidMcName(String username) {
+ return VALID_USERNAME.matcher(username).matches();
+ }
+
+ public static String fancyCase(String string) {
+ return WordUtils.capitalizeFully(string.replace('_', ' '));
+ }
+
+ /**
+ * Turn timestamp into pretty-formatted duration and date details.
+ *
+ * @param timestamp last login/logout
+ * @return 1st: duration between timestamp and now in words; 2nd: formatted date if time differences is >24h, otherwise null
+ */
+ public static Pair<String, String> getDurationAsWords(long timestamp) {
+ long duration = System.currentTimeMillis() - timestamp;
+ long daysPast = TimeUnit.MILLISECONDS.toDays(duration);
+
+ String dateFormatted = null;
+ if (daysPast > 1) {
+ dateFormatted = DateFormatUtils.format(timestamp, "dd-MMM-yyyy");
+ }
+
+ if (daysPast > 31) {
+ return Pair.of(
+ DurationFormatUtils.formatPeriod(timestamp, System.currentTimeMillis(), (daysPast > 365 ? "y 'years' " : "") + "M 'months' d 'days'"),
+ dateFormatted);
+ } else {
+ return Pair.of(
+ DurationFormatUtils.formatDurationWords(duration, true, true),
+ dateFormatted);
+ }
+ }
+
+ public static String getDurationAsWord(long timestamp) {
+ long duration = System.currentTimeMillis() - timestamp;
+ long secondsPast = TimeUnit.MILLISECONDS.toSeconds(duration);
+ if (secondsPast < 60) {
+ return secondsPast + " second" + (secondsPast > 1 ? "s" : "");
+ }
+ long minutesPast = TimeUnit.SECONDS.toMinutes(secondsPast);
+ if (minutesPast < 60) {
+ return minutesPast + " minute" + (minutesPast > 1 ? "s" : "");
+ }
+ long hoursPast = TimeUnit.MINUTES.toHours(minutesPast);
+ if (hoursPast < 24) {
+ return hoursPast + " hour" + (hoursPast > 1 ? "s" : "");
+ }
+ long daysPast = TimeUnit.HOURS.toDays(hoursPast);
+ if (daysPast < 31) {
+ return daysPast + " day" + (daysPast > 1 ? "s" : "");
+ }
+ double monthsPast = daysPast / 30.5d;
+ if (monthsPast < 12) {
+ return new DecimalFormat("0.#").format(monthsPast) + " month" + (monthsPast >= 2 ? "s" : "");
+ }
+ double yearsPast = monthsPast / 12d;
+ return new DecimalFormat("0.#").format(yearsPast) + " year" + (yearsPast >= 2 ? "s" : "");
+ }
+
+ public static String toRealPath(Path path) {
+ try {
+ return path.toRealPath().toString();
+ } catch (IOException e) {
+ e.printStackTrace();
+ return "file not found";
+ }
+ }
+
+ public static String toRealPath(File path) {
+ return toRealPath(path.toPath());
+ }
+
+ /**
+ * Formats a large number with abbreviations for each factor of a thousand (k, m, ...)
+ *
+ * @param number the number to format
+ * @return a String representing the number n formatted in a cool looking way.
+ * @see <a href="https://stackoverflow.com/a/4753866">Source</a>
+ */
+ public static String formatNumberWithAbbreviations(double number) {
+ return formatNumberWithAbbreviations(number, 0);
+ }
+
+ private static String formatNumberWithAbbreviations(double number, int iteration) {
+ @SuppressWarnings("IntegerDivisionInFloatingPointContext") double d = ((long) number / 100) / 10.0;
+ boolean isRound = (d * 10) % 10 == 0; //true if the decimal part is equal to 0 (then it's trimmed anyway)
+ // this determines the class, i.e. 'k', 'm' etc
+ // this decides whether to trim the decimals
+ // (int) d * 10 / 10 drops the decimal
+ return d < 1000 ? // this determines the class, i.e. 'k', 'm' etc
+ (d > 99.9 || isRound || d > 9.99 ? // this decides whether to trim the decimals
+ (int) d * 10 / 10 : d + "" // (int) d * 10 / 10 drops the decimal
+ ) + "" + LARGE_NUMBERS[iteration]
+ : formatNumberWithAbbreviations(d, iteration + 1);
+ }
+
+ /**
+ * Convert Roman numerals to their corresponding Arabic numeral
+ *
+ * @param roman Roman numeral
+ * @return Arabic numeral
+ * @see <a href="https://www.w3resource.com/javascript-exercises/javascript-math-exercise-22.php">Source</a>
+ */
+ public static int convertRomanToArabic(String roman) {
+ if (roman == null) return -1;
+ int number = romanCharToArabic(roman.charAt(0));
+
+ for (int i = 1; i < roman.length(); i++) {
+ int current = romanCharToArabic(roman.charAt(i));
+ int previous = romanCharToArabic(roman.charAt(i - 1));
+ if (current <= previous) {
+ number += current;
+ } else {
+ number = number - previous * 2 + current;
+ }
+ }
+ return number;
+ }
+
+ private static int romanCharToArabic(char c) {
+ switch (c) {
+ case 'I':
+ return 1;
+ case 'V':
+ return 5;
+ case 'X':
+ return 10;
+ case 'L':
+ return 50;
+ case 'C':
+ return 100;
+ case 'D':
+ return 500;
+ case 'M':
+ return 1000;
+ default:
+ return -1;
+ }
+ }
+
+ /**
+ * Convert Arabic numerals to their corresponding Roman numerals
+ *
+ * @param number Arabic numerals
+ * @return Roman numerals
+ * @see <a href="https://stackoverflow.com/a/48357180">Source</a>
+ */
+ public static String convertArabicToRoman(int number) {
+ String romanOnes = arabicToRomanChars(number % 10, "I", "V", "X");
+ number /= 10;
+
+ String romanTens = arabicToRomanChars(number % 10, "X", "L", "C");
+ number /= 10;
+
+ String romanHundreds = arabicToRomanChars(number % 10, "C", "D", "M");
+ number /= 10;
+
+ String romanThousands = arabicToRomanChars(number % 10, "M", "", "");
+
+ return romanThousands + romanHundreds + romanTens + romanOnes;
+ }
+
+ private static String arabicToRomanChars(int n, String one, String five, String ten) {
+ switch (n) {
+ case 1:
+ return one;
+ case 2:
+ return one + one;
+ case 3:
+ return one + one + one;
+ case 4:
+ return one + five;
+ case 5:
+ return five;
+ case 6:
+ return five + one;
+ case 7:
+ return five + one + one;
+ case 8:
+ return five + one + one + one;
+ case 9:
+ return one + ten;
+ }
+ return "";
+ }
+
+ /**
+ * Get the minion tier's color for chat formatting
+ *
+ * @param tier minion tier
+ * @return color code corresponding to the tier
+ */
+ public static EnumChatFormatting getMinionTierColor(int tier) {
+ EnumChatFormatting tierColor;
+ switch (tier) {
+ case 1:
+ tierColor = EnumChatFormatting.WHITE;
+ break;
+ case 2:
+ case 3:
+ case 4:
+ tierColor = EnumChatFormatting.GREEN;
+ break;
+ case 5:
+ case 6:
+ case 7:
+ tierColor = EnumChatFormatting.DARK_PURPLE;
+ break;
+ case 8:
+ case 9:
+ case 10:
+ tierColor = EnumChatFormatting.RED;
+ break;
+ case 11:
+ tierColor = EnumChatFormatting.AQUA;
+ break;
+ default:
+ tierColor = EnumChatFormatting.OBFUSCATED;
+ }
+ return tierColor;
+ }
+}
diff --git a/src/main/java/eu/olli/cowlection/util/VersionChecker.java b/src/main/java/eu/olli/cowlection/util/VersionChecker.java
new file mode 100644
index 0000000..8f05beb
--- /dev/null
+++ b/src/main/java/eu/olli/cowlection/util/VersionChecker.java
@@ -0,0 +1,140 @@
+package eu.olli.cowlection.util;
+
+import eu.olli.cowlection.Cowlection;
+import eu.olli.cowlection.config.MooConfig;
+import net.minecraft.client.Minecraft;
+import net.minecraft.event.ClickEvent;
+import net.minecraft.event.HoverEvent;
+import net.minecraft.util.ChatComponentText;
+import net.minecraft.util.ChatStyle;
+import net.minecraft.util.EnumChatFormatting;
+import net.minecraft.util.IChatComponent;
+import net.minecraftforge.common.ForgeModContainer;
+import net.minecraftforge.common.ForgeVersion;
+import net.minecraftforge.fml.common.Loader;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @see ForgeVersion
+ */
+public class VersionChecker {
+ /**
+ * Cooldown between to update checks in minutes
+ */
+ private static final int CHECK_COOLDOWN = 15;
+ private static final String CHANGELOG_URL = Cowlection.GITURL + "blob/master/CHANGELOG.md";
+ private final Cowlection main;
+ private long lastCheck;
+ private String newVersion;
+ private String downloadUrl;
+
+ public VersionChecker(Cowlection main) {
+ this.main = main;
+ this.lastCheck = Minecraft.getSystemTime();
+ newVersion = "[newVersion]";
+ downloadUrl = Cowlection.GITURL + "releases";
+ }
+
+ public boolean runUpdateCheck(boolean isCommandTriggered) {
+ if (isCommandTriggered || (!ForgeModContainer.disableVersionCheck && MooConfig.doUpdateCheck)) {
+ Runnable handleResults = () -> main.getVersionChecker().handleVersionStatus(isCommandTriggered);
+
+ long now = Minecraft.getSystemTime();
+
+ // only re-run if last check was >CHECK_COOLDOWN minutes ago
+ if (getNextCheck() < 0) { // next allowed check is "in the past", so we're good to go
+ lastCheck = now;
+ ForgeVersion.startVersionCheck();
+
+ // check status after 5 seconds - hopefully that's enough to check
+ new TickDelay(handleResults, 5 * 20);
+ return true;
+ } else {
+ new TickDelay(handleResults, 1);
+ }
+ }
+ return false;
+ }
+
+ public void handleVersionStatus(boolean isCommandTriggered) {
+ ForgeVersion.CheckResult versionResult = ForgeVersion.getResult(Loader.instance().activeModContainer());
+ if (versionResult.target != null) {
+ newVersion = versionResult.target.toString();
+ downloadUrl = Cowlection.GITURL + "releases/download/v" + newVersion + "/" + Cowlection.MODNAME.replace(" ", "") + "-" + newVersion + ".jar";
+ }
+
+ IChatComponent statusMsg = null;
+
+ if (isCommandTriggered) {
+ if (versionResult.status == ForgeVersion.Status.UP_TO_DATE) {
+ // up to date
+ statusMsg = new ChatComponentText("\u2714 You're running the latest version (" + Cowlection.VERSION + ").").setChatStyle(new ChatStyle().setColor(EnumChatFormatting.GREEN));
+ } else if (versionResult.status == ForgeVersion.Status.PENDING) {
+ // pending
+ statusMsg = new ChatComponentText("\u279C " + "Version check either failed or is still running.").setChatStyle(new ChatStyle().setColor(EnumChatFormatting.YELLOW))
+ .appendSibling(new ChatComponentText("\n \u278A Check for results again in a few seconds with " + EnumChatFormatting.GOLD + "/moo version").setChatStyle(new ChatStyle()
+ .setColor(EnumChatFormatting.YELLOW)
+ .setChatClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/moo version"))
+ .setChatHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ChatComponentText(EnumChatFormatting.YELLOW + "Run " + EnumChatFormatting.GOLD + "/moo version")))))
+ .appendSibling(new ChatComponentText("\n \u278B Re-run update check with " + EnumChatFormatting.GOLD + "/moo update").setChatStyle(new ChatStyle()
+ .setColor(EnumChatFormatting.YELLOW)
+ .setChatClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/moo update"))
+ .setChatHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ChatComponentText(EnumChatFormatting.YELLOW + "Run " + EnumChatFormatting.GOLD + "/moo update")))));
+ } else if (versionResult.status == ForgeVersion.Status.FAILED) {
+ // check failed
+ statusMsg = new ChatComponentText("\u2716 Version check failed for an unknown reason. Check again in a few seconds with ").setChatStyle(new ChatStyle().setColor(EnumChatFormatting.RED))
+ .appendSibling(new ChatComponentText("/moo update").setChatStyle(new ChatStyle()
+ .setColor(EnumChatFormatting.GOLD)
+ .setChatClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/moo update"))
+ .setChatHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ChatComponentText(EnumChatFormatting.YELLOW + "Run " + EnumChatFormatting.GOLD + "/moo update")))));
+ }
+ }
+ if (versionResult.status == ForgeVersion.Status.OUTDATED || versionResult.status == ForgeVersion.Status.BETA_OUTDATED) {
+ // outdated
+ IChatComponent spacer = new ChatComponentText(" ").setChatStyle(new ChatStyle().setParentStyle(null));
+
+ IChatComponent text = new ChatComponentText("\u279C New version of " + EnumChatFormatting.DARK_GREEN + Cowlection.MODNAME + " " + EnumChatFormatting.GREEN + "available (" + Cowlection.VERSION + " \u27A1 " + newVersion + ")\n").setChatStyle(new ChatStyle().setColor(EnumChatFormatting.GREEN));
+
+ IChatComponent download = new ChatComponentText("[Download]").setChatStyle(new ChatStyle()
+ .setColor(EnumChatFormatting.DARK_GREEN).setBold(true)
+ .setChatClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, downloadUrl))
+ .setChatHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ChatComponentText(EnumChatFormatting.YELLOW + "Download the latest version of " + Cowlection.MODNAME))));
+
+ IChatComponent changelog = new ChatComponentText("[Changelog]").setChatStyle(new ChatStyle()
+ .setColor(EnumChatFormatting.DARK_AQUA).setBold(true)
+ .setChatClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, CHANGELOG_URL))
+ .setChatHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ChatComponentText(EnumChatFormatting.YELLOW + "View changelog"))));
+
+ IChatComponent updateInstructions = new ChatComponentText("[Update instructions]").setChatStyle(new ChatStyle()
+ .setColor(EnumChatFormatting.GOLD).setBold(true)
+ .setChatClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/moo updateHelp"))
+ .setChatHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ChatComponentText(EnumChatFormatting.YELLOW + "Run " + EnumChatFormatting.GOLD + "/moo updateHelp"))));
+
+ IChatComponent openModsDirectory = new ChatComponentText("\n[Open Mods directory]").setChatStyle(new ChatStyle()
+ .setColor(EnumChatFormatting.GREEN).setBold(true)
+ .setChatClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/moo directory"))
+ .setChatHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ChatComponentText(EnumChatFormatting.YELLOW + "Open mods directory with command " + EnumChatFormatting.GOLD + "/moo directory\n\u279C Click to open mods directory"))));
+
+ statusMsg = text.appendSibling(download).appendSibling(spacer).appendSibling(changelog).appendSibling(spacer).appendSibling(updateInstructions).appendSibling(spacer).appendSibling(openModsDirectory);
+ }
+
+ if (statusMsg != null) {
+ main.getChatHelper().sendMessage(statusMsg);
+ }
+ }
+
+ public long getNextCheck() {
+ long cooldown = TimeUnit.MINUTES.toMillis(CHECK_COOLDOWN);
+ long systemTime = Minecraft.getSystemTime();
+ return cooldown - (systemTime - lastCheck);
+ }
+
+ public String getNewVersion() {
+ return newVersion;
+ }
+
+ public String getDownloadUrl() {
+ return downloadUrl;
+ }
+}