aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md4
-rw-r--r--README.md1
-rw-r--r--src/main/java/de/cowtipper/cowlection/command/MooCommand.java201
-rw-r--r--src/main/java/de/cowtipper/cowlection/config/MooConfig.java9
-rw-r--r--src/main/java/de/cowtipper/cowlection/data/DataHelper.java2
-rw-r--r--src/main/java/de/cowtipper/cowlection/util/GsonUtils.java89
-rw-r--r--src/main/java/de/cowtipper/cowlection/util/ImageUtils.java132
-rw-r--r--src/main/java/de/cowtipper/cowlection/util/MooChatComponent.java21
-rw-r--r--src/main/resources/assets/cowlection/lang/en_US.lang2
9 files changed, 448 insertions, 13 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 95a5da9..ef853ee 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Bestiary Overview: enhances tooltips of `/bestiary` ⬌ `/be`
- hover over one of the area/location-items in a *sub*-category of the Bestiary to see an overview of the tiers upgrades you are closest to
- can be ordered by fewest kills or lowest % to next tier by clicking on the area/location item
+- `/moo whatAmILookingAt` (or: `/m waila`)
+ - copy info of "the thing" you're looking at (NPC or mob + nearby "text-only" armor stands; armor stand, placed skull, dropped item, item in item frame, map on wall)
+ - automatically decodes base64 data (e.g. skin details) and unix timestamps
- SkyBlock Dwarven Mines update:
- Added new minions to `/m analyzeIslands` (Mithril + t12)
- `/moo stalkskyblock` additions:
@@ -19,6 +22,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Refined the comma representation of large numbers abbreviated with k, m, b
- Dungeon Party Finder: Parties with specific classes can now *always* be marked as 'unideal' (additionally to the already existing option to mark a party when 2+ members use the same specific class)
- Dungeon Performance Overlay: added an alternative text border option
+- "Copy inventories to clipboard"-feature now automatically decodes base64 data (e.g. skin details) and unix timestamps
### Fixed
- Fixed issue with 'no dung class selected'
diff --git a/README.md b/README.md
index 1a76670..3a480a3 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,7 @@ It is a collection of different features mainly focused on Hypixel SkyBlock.
| Toggle join/leave notifications for friends, guild members or best friends separately | `/moo config` → Notifications |
| Copy chat component | <kbd>ALT</kbd> + <kbd>right click</kbd><br>Hold <kbd>shift</kbd> to copy full component |
| Copy inventories to clipboard as JSON | <kbd>CTRL</kbd> + <kbd>C</kbd> |
+| Copy info of "the thing" you're looking at (NPC or mob + nearby "text-only" armor stands; armor stand, placed skull, dropped item, item in item frame, map on wall) | `/moo whatAmILookingAt` |
| Tab-completable usernames for several commands (e.g. `/party`, `/invite`, ...) | `/moo config` &rarr; `Commands with Tab-completable usernames` for full list of commands |
| Auto-replace `/r` with `/w <latest username>` | `/r `, use `/rr` to avoid auto-replacement |
| Change guiScale to any value | `/moo guiscale [newValue]` |
diff --git a/src/main/java/de/cowtipper/cowlection/command/MooCommand.java b/src/main/java/de/cowtipper/cowlection/command/MooCommand.java
index f3b2962..2f941c0 100644
--- a/src/main/java/de/cowtipper/cowlection/command/MooCommand.java
+++ b/src/main/java/de/cowtipper/cowlection/command/MooCommand.java
@@ -1,5 +1,7 @@
package de.cowtipper.cowlection.command;
+import com.mojang.authlib.GameProfile;
+import com.mojang.authlib.properties.Property;
import com.mojang.realmsclient.util.Pair;
import de.cowtipper.cowlection.Cowlection;
import de.cowtipper.cowlection.command.exception.ApiContactException;
@@ -15,23 +17,32 @@ import de.cowtipper.cowlection.listener.skyblock.DungeonsPartyListener;
import de.cowtipper.cowlection.search.GuiSearch;
import de.cowtipper.cowlection.util.*;
import net.minecraft.client.Minecraft;
+import net.minecraft.client.entity.EntityOtherPlayerMP;
import net.minecraft.client.gui.GuiScreen;
import net.minecraft.client.multiplayer.WorldClient;
import net.minecraft.client.settings.GameSettings;
import net.minecraft.command.*;
import net.minecraft.entity.Entity;
+import net.minecraft.entity.EntityLiving;
import net.minecraft.entity.item.EntityArmorStand;
+import net.minecraft.entity.item.EntityItemFrame;
import net.minecraft.event.ClickEvent;
import net.minecraft.event.HoverEvent;
+import net.minecraft.init.Items;
+import net.minecraft.item.ItemMap;
import net.minecraft.item.ItemSkull;
import net.minecraft.item.ItemStack;
-import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.nbt.*;
+import net.minecraft.tileentity.TileEntity;
+import net.minecraft.tileentity.TileEntitySkull;
import net.minecraft.util.*;
+import net.minecraft.world.storage.MapData;
import net.minecraftforge.common.util.Constants;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import java.awt.*;
+import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.*;
@@ -101,6 +112,16 @@ public class MooCommand extends CommandBase {
handleStalkingSkyBlock(args);
} else if (args[0].equalsIgnoreCase("analyzeIsland")) {
handleAnalyzeIsland(sender);
+ } else if (args[0].equalsIgnoreCase("waila") || args[0].equalsIgnoreCase("whatAmILookingAt")) {
+ boolean showAllInfo = MooConfig.keepFullWailaInfo();
+ if (args.length == 2) {
+ if (args[1].equalsIgnoreCase("all")) {
+ showAllInfo = true;
+ } else if (args[1].equalsIgnoreCase("main")) {
+ showAllInfo = false;
+ }
+ }
+ handleWhatAmILookingAt(sender, showAllInfo);
} else if (args[0].equalsIgnoreCase("dungeon") || args[0].equalsIgnoreCase("dung")
|| /* dungeon party: */ args[0].equalsIgnoreCase("dp")) {
handleDungeon(args);
@@ -717,6 +738,179 @@ public class MooCommand extends CommandBase {
main.getChatHelper().sendMessage(EnumChatFormatting.YELLOW, analysisResults.toString());
}
+ private void handleWhatAmILookingAt(ICommandSender sender, boolean showAllInfo) {
+ MovingObjectPosition lookingAt = Minecraft.getMinecraft().objectMouseOver;
+ if (lookingAt != null) {
+ switch (lookingAt.typeOfHit) {
+ case BLOCK: {
+ TileEntity te = sender.getEntityWorld().getTileEntity(lookingAt.getBlockPos());
+ if (te instanceof TileEntitySkull) {
+ TileEntitySkull skull = (TileEntitySkull) te;
+ if (skull.getSkullType() != 3) {
+ // non-player skull, abort!
+ break;
+ }
+ NBTTagCompound nbt = new NBTTagCompound();
+ skull.writeToNBT(nbt);
+ // is a player head!
+ if (nbt.hasKey("Owner", Constants.NBT.TAG_COMPOUND)) {
+ NBTTagCompound relevantNbt = tldrInfo(nbt, showAllInfo);
+ BlockPos skullPos = skull.getPos();
+ relevantNbt.setTag("__position", new NBTTagIntArray(new int[]{skullPos.getX(), skullPos.getY(), skullPos.getZ()}));
+ GuiScreen.setClipboardString(GsonUtils.toJson(relevantNbt, true));
+ main.getChatHelper().sendMessage(EnumChatFormatting.GREEN, "Copied skull data to clipboard.");
+ return;
+ }
+ }
+ break;
+ }
+ case ENTITY: {
+ Entity entity = lookingAt.entityHit;
+ if (entity instanceof EntityArmorStand) {
+ // looking at non-invisible armor stand (e.g. Minion)
+ EntityArmorStand armorStand = (EntityArmorStand) entity;
+ copyEntityInfoToClipboard(armorStand, showAllInfo);
+ main.getChatHelper().sendMessage(EnumChatFormatting.GREEN, "Copied armor stand '" + armorStand.getName() + EnumChatFormatting.GREEN + "' to clipboard.");
+ return;
+ } else if (entity instanceof EntityOtherPlayerMP) {
+ // looking at NPC or another player
+ EntityOtherPlayerMP otherPlayer = (EntityOtherPlayerMP) entity;
+ copyEntityInfoToClipboard(otherPlayer, showAllInfo);
+ main.getChatHelper().sendMessage(EnumChatFormatting.GREEN, "Copied player/npc '" + otherPlayer.getDisplayNameString() + EnumChatFormatting.GREEN + "' to clipboard.");
+ return;
+ } else if (entity instanceof EntityItemFrame) {
+ EntityItemFrame itemFrame = (EntityItemFrame) entity;
+ copyEntityInfoToClipboard(itemFrame, showAllInfo);
+
+ ItemStack displayedItem = itemFrame.getDisplayedItem();
+ if (displayedItem != null) {
+
+ NBTTagCompound nbt = new NBTTagCompound();
+ if (displayedItem.getItem() == Items.filled_map) {
+ // filled map
+ MapData mapData = ItemMap.loadMapData(displayedItem.getItemDamage(), sender.getEntityWorld());
+ File mapFile = ImageUtils.saveMapToFile(mapData);
+ if (mapFile != null) {
+ main.getChatHelper().sendMessage(new MooChatComponent("Saved map as " + mapFile.getName() + " ").green().setOpenFile(mapFile).appendSibling(new MooChatComponent("[open file]").gold())
+ .appendSibling(new MooChatComponent(" [open folder]").darkAqua().setOpenFile(mapFile.getParentFile())));
+ } else {
+ main.getChatHelper().sendMessage(EnumChatFormatting.RED, "Couldn't save map for some reason");
+ }
+ return;
+ } else {
+ displayedItem.writeToNBT(nbt);
+ }
+ GuiScreen.setClipboardString(GsonUtils.toJson(nbt, true));
+ main.getChatHelper().sendMessage(EnumChatFormatting.GREEN, "Copied item in item frame '" + displayedItem.getDisplayName() + EnumChatFormatting.GREEN + "' to clipboard.");
+ return;
+ }
+ } else if (entity instanceof EntityLiving) {
+ EntityLiving living = (EntityLiving) entity;
+ copyEntityInfoToClipboard(living, showAllInfo);
+ main.getChatHelper().sendMessage(EnumChatFormatting.GREEN, "Copied mob '" + living.getName() + EnumChatFormatting.GREEN + "' to clipboard.");
+ return;
+ }
+ break;
+ }
+ default:
+ // didn't find anything...
+ }
+ }
+ // didn't find anything special; search for all nearby entities
+ double maxDistance = 5; // default 4
+ Entity self = sender.getCommandSenderEntity();
+ Vec3 selfLook = self.getLook(1);
+ float searchRadius = 1.0F;
+ List<Entity> nearbyEntities = sender.getEntityWorld().getEntitiesInAABBexcluding(self, self.getEntityBoundingBox().addCoord(selfLook.xCoord * maxDistance, selfLook.yCoord * maxDistance, selfLook.zCoord * maxDistance).expand(searchRadius, searchRadius, searchRadius), entity1 -> true);
+
+ if (nearbyEntities.size() > 0) {
+ NBTTagList entities = new NBTTagList();
+ for (Entity entity : nearbyEntities) {
+ NBTTagCompound relevantNbt = extractEntityInfo(entity, showAllInfo);
+ // add additional info to make it easier to find the correct entity in the list of entities
+ relevantNbt.setTag("_entityType", new NBTTagString(entity.getClass().getSimpleName()));
+ NBTTagList position = new NBTTagList();
+ position.appendTag(new NBTTagDouble(entity.posX));
+ position.appendTag(new NBTTagDouble(entity.posY));
+ position.appendTag(new NBTTagDouble(entity.posZ));
+ relevantNbt.setTag("_position", position);
+ entities.appendTag(relevantNbt);
+ }
+
+ GuiScreen.setClipboardString(GsonUtils.toJson(entities, true));
+ main.getChatHelper().sendMessage(EnumChatFormatting.GREEN, "Copied " + nearbyEntities.size() + " nearby entities to clipboard.");
+ } else {
+ main.getChatHelper().sendMessage(EnumChatFormatting.RED, "You stare into the void... and see nothing of interest.");
+ }
+ }
+
+ private NBTTagCompound extractEntityInfo(Entity entity, boolean showAllInfo) {
+ NBTTagCompound nbt = new NBTTagCompound();
+ entity.writeToNBT(nbt);
+ NBTTagCompound relevantNbt = tldrInfo(nbt, showAllInfo);
+
+ if (entity instanceof EntityOtherPlayerMP) {
+ EntityOtherPlayerMP otherPlayer = (EntityOtherPlayerMP) entity;
+ relevantNbt.setString("__name", otherPlayer.getName());
+ if (otherPlayer.hasCustomName()) {
+ relevantNbt.setString("__customName", otherPlayer.getCustomNameTag());
+ }
+ GameProfile gameProfile = otherPlayer.getGameProfile();
+ for (Property property : gameProfile.getProperties().get("textures")) {
+ relevantNbt.setString("_skin", property.getValue());
+ }
+ }
+ if (entity instanceof EntityLiving || entity instanceof EntityOtherPlayerMP) {
+ // either EntityLiving (any mob), or EntityOtherPlayerMP => find other nearby "name tag" EntityArmorStands
+ List<Entity> nearbyArmorStands = entity.getEntityWorld().getEntitiesInAABBexcluding(entity, entity.getEntityBoundingBox().expand(0.2d, 3, 0.2d), nearbyEntity -> {
+ if (nearbyEntity instanceof EntityArmorStand) {
+ EntityArmorStand armorStand = (EntityArmorStand) nearbyEntity;
+ if (armorStand.isInvisible() && armorStand.hasCustomName()) {
+ for (ItemStack equipment : armorStand.getInventory()) {
+ if (equipment != null) {
+ // armor stand has equipment, abort!
+ return false;
+ }
+ }
+ // armor stand has a custom name, is invisible and has no equipment -> probably a "name tag"-armor stand
+ return true;
+ }
+ }
+ return false;
+ });
+ if (nearbyArmorStands.size() > 0) {
+ nearbyArmorStands.sort(Comparator.<Entity>comparingDouble(nearbyArmorStand -> nearbyArmorStand.posY).reversed());
+ NBTTagList nearbyText = new NBTTagList();
+ for (int i = 0, maxNearbyArmorStands = Math.min(10, nearbyArmorStands.size()); i < maxNearbyArmorStands; i++) {
+ Entity nearbyArmorStand = nearbyArmorStands.get(i);
+ nearbyText.appendTag(new NBTTagString(nearbyArmorStand.getCustomNameTag()));
+ }
+ relevantNbt.setTag("__nearbyText", nearbyText);
+ }
+ }
+ return relevantNbt;
+ }
+
+ private void copyEntityInfoToClipboard(Entity entity, boolean showAllInfo) {
+ NBTTagCompound relevantNbt = extractEntityInfo(entity, showAllInfo);
+ GuiScreen.setClipboardString(GsonUtils.toJson(relevantNbt, true));
+ }
+
+ private NBTTagCompound tldrInfo(NBTTagCompound nbt, boolean showAllInfo) {
+ if (showAllInfo) {
+ // don't tl;dr!
+ return nbt;
+ }
+ String[] importantTags = new String[]{"CustomName", "id", "Damage", "Count", "Equipment", "Item", "tag", "ExtraAttributes", "Owner"};
+ NBTTagCompound relevantNbt = new NBTTagCompound();
+ for (String tag : importantTags) {
+ if (nbt.hasKey(tag)) {
+ relevantNbt.setTag(tag, nbt.getTag(tag));
+ }
+ }
+ return relevantNbt;
+ }
+
private void handleDungeon(String[] args) throws MooCommandException {
DungeonCache dungeonCache = main.getDungeonCache();
if (args.length == 2 && args[1].equalsIgnoreCase("enter")) {
@@ -851,6 +1045,7 @@ public class MooCommand extends CommandBase {
.appendSibling(createCmdHelpSection(2, "SkyBlock"))
.appendSibling(createCmdHelpEntry("stalkskyblock", "Get info of player's SkyBlock stats §d§l⚷"))
.appendSibling(createCmdHelpEntry("analyzeIsland", "Analyze a SkyBlock private island"))
+ .appendSibling(createCmdHelpEntry("waila", "Copy the 'thing' you're looking at"))
.appendSibling(createCmdHelpEntry("dungeon", "SkyBlock Dungeons: display current dungeon performance"))
.appendSibling(createCmdHelpEntry("dungeon party", "SkyBlock Dungeons: Shows armor and dungeon info about current party members " + EnumChatFormatting.GRAY + "(alias: " + EnumChatFormatting.WHITE + "/" + getCommandName() + " dp" + EnumChatFormatting.GRAY + ") §d§l⚷"))
.appendSibling(createCmdHelpSection(3, "Miscellaneous"))
@@ -889,10 +1084,12 @@ public class MooCommand extends CommandBase {
return getListOfStringsMatchingLastWord(args,
/* help */ "help",
/* Best friends, friends & other players */ "stalk", "add", "remove", "list", "online", "nameChangeCheck",
- /* SkyBlock */ "stalkskyblock", "skyblockstalk", "analyzeIsland", "dungeon",
+ /* SkyBlock */ "stalkskyblock", "skyblockstalk", "analyzeIsland", "waila", "whatAmILookingAt", "dungeon",
/* miscellaneous */ "config", "search", "worldage", "serverage", "guiscale", "rr", "shrug", "apikey",
/* update mod */ "update", "updateHelp", "version", "directory",
/* rarely used aliases */ "askPolitelyWhereTheyAre", "askPolitelyAboutTheirSkyBlockProgress", "year", "whatyearisit");
+ } else if (args.length == 2 && (args[0].equalsIgnoreCase("waila") || args[0].equalsIgnoreCase("whatAmILookingAt"))) {
+ return getListOfStringsMatchingLastWord(args, "all", "main");
} else if (args.length == 2 && args[0].equalsIgnoreCase("remove")) {
return getListOfStringsMatchingLastWord(args, main.getFriendsHandler().getBestFriends());
} else if (args.length == 2 && args[0].equalsIgnoreCase("dungeon")) {
diff --git a/src/main/java/de/cowtipper/cowlection/config/MooConfig.java b/src/main/java/de/cowtipper/cowlection/config/MooConfig.java
index caa392a..533a767 100644
--- a/src/main/java/de/cowtipper/cowlection/config/MooConfig.java
+++ b/src/main/java/de/cowtipper/cowlection/config/MooConfig.java
@@ -53,6 +53,7 @@ public class MooConfig {
public static String mooCmdAlias;
public static boolean fixReplyCmd;
public static boolean enableCopyInventory;
+ private static String wailaLevelOfDetail;
public static String[] tabCompletableNamesCommands;
private static final String CATEGORY_LOGS_SEARCH = "logssearch";
public static String[] logsDirs;
@@ -264,6 +265,8 @@ public class MooConfig {
"fixReplyCmd", true, "Auto-replace /r?"));
Property propEnableCopyInventory = subCat.addConfigEntry(cfg.get(configCat.getConfigName(),
"enableCopyInventory", false, "Enable copy inventory with CTRL + C?"));
+ Property propWailaLevelOfDetail = subCat.addConfigEntry(cfg.get(configCat.getConfigName(),
+ "wailaLevelOfDetail", "main info", "Level of detail of /moo waila", new String[]{"main info", "all info"}));
// Sub-Category: Tab-completable names in commands
subCat = configCat.addSubCategory("Tab-completable usernames");
@@ -574,6 +577,7 @@ public class MooConfig {
mooCmdAlias = propMooCmdAlias.getString();
fixReplyCmd = propFixReplyCmd.getBoolean();
enableCopyInventory = propEnableCopyInventory.getBoolean();
+ wailaLevelOfDetail = propWailaLevelOfDetail.getString();
tabCompletableNamesCommands = propTabCompletableNamesCommands.getStringList();
logsDirs = propLogsDirs.getStringList();
defaultStartDate = propDefaultStartDate.getString().trim();
@@ -639,6 +643,7 @@ public class MooConfig {
propMooCmdAlias.set(mooCmdAlias);
propFixReplyCmd.set(fixReplyCmd);
propEnableCopyInventory.set(enableCopyInventory);
+ propWailaLevelOfDetail.set(wailaLevelOfDetail);
propTabCompletableNamesCommands.set(tabCompletableNamesCommands);
propLogsDirs.set(logsDirs);
propDefaultStartDate.set(defaultStartDate);
@@ -774,6 +779,10 @@ public class MooConfig {
return Setting.get(configGuiExplanations);
}
+ public static boolean keepFullWailaInfo() {
+ return "all info".equals(wailaLevelOfDetail);
+ }
+
// Category: Notifications
/**
diff --git a/src/main/java/de/cowtipper/cowlection/data/DataHelper.java b/src/main/java/de/cowtipper/cowlection/data/DataHelper.java
index 9f529a4..0e98858 100644
--- a/src/main/java/de/cowtipper/cowlection/data/DataHelper.java
+++ b/src/main/java/de/cowtipper/cowlection/data/DataHelper.java
@@ -50,7 +50,7 @@ public final class DataHelper {
}
}
- // TODO replace with api request: https://github.com/HypixelDev/PublicAPI/blob/master/Documentation/misc/GameType.md
+ // GameTypes: https://api.hypixel.net/#section/Introduction/GameTypes
@SuppressWarnings("unused")
public enum GameType {
QUAKECRAFT("Quakecraft"),
diff --git a/src/main/java/de/cowtipper/cowlection/util/GsonUtils.java b/src/main/java/de/cowtipper/cowlection/util/GsonUtils.java
index c0b2735..e97a88d 100644
--- a/src/main/java/de/cowtipper/cowlection/util/GsonUtils.java
+++ b/src/main/java/de/cowtipper/cowlection/util/GsonUtils.java
@@ -3,14 +3,19 @@ package de.cowtipper.cowlection.util;
import com.google.gson.*;
import com.mojang.util.UUIDTypeAdapter;
import de.cowtipper.cowlection.data.HyPlayerData;
-import net.minecraft.nbt.NBTBase;
-import net.minecraft.nbt.NBTTagCompound;
-import net.minecraft.nbt.NBTTagList;
-import net.minecraft.nbt.NBTTagString;
+import net.minecraft.nbt.*;
import net.minecraftforge.common.util.Constants;
+import org.apache.commons.codec.binary.Base64;
import java.io.Reader;
import java.lang.reflect.Type;
+import java.time.DateTimeException;
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Map;
+import java.util.TreeMap;
import java.util.UUID;
public final class GsonUtils {
@@ -29,13 +34,51 @@ public final class GsonUtils {
}
public static String toJson(Object object) {
+ return toJson(object, false);
+ }
+
+ public static String toJson(Object object, boolean sort) {
if (object instanceof NBTBase) {
- return gsonPrettyPrinter.toJson(nbtToJson((NBTBase) object));
+ JsonElement jsonElement = nbtToJson((NBTBase) object);
+ if (sort && (jsonElement instanceof JsonObject || jsonElement instanceof JsonArray)) {
+ jsonElement = sortJsonElement(jsonElement);
+ }
+ return gsonPrettyPrinter.toJson(jsonElement);
} else {
return gson.toJson(object);
}
}
+ private static JsonElement sortJsonElement(JsonElement jsonElement) {
+ if (jsonElement instanceof JsonArray) {
+ // sort each element of array
+ JsonArray sortedJsonArray = new JsonArray();
+ for (JsonElement arrayElement : (JsonArray) jsonElement) {
+ sortedJsonArray.add(sortJsonElement(arrayElement));
+ }
+ return sortedJsonArray;
+ } else if (jsonElement instanceof JsonObject) {
+ // sort json by key
+ TreeMap<String, JsonElement> sortedJsonObject = new TreeMap<>(String::compareToIgnoreCase);
+ for (Map.Entry<String, JsonElement> jsonEntry : ((JsonObject) jsonElement).entrySet()) {
+ JsonElement sortedJsonElement = jsonEntry.getValue();
+ if (sortedJsonElement instanceof JsonObject) {
+ sortedJsonElement = sortJsonElement(sortedJsonElement);
+ }
+ sortedJsonObject.put(jsonEntry.getKey(), sortedJsonElement);
+ }
+ // overwrite jsonElement with json sorted by key alphabetically
+ JsonObject sortedJsonElement = new JsonObject();
+ for (Map.Entry<String, JsonElement> jsonEntrySorted : sortedJsonObject.entrySet()) {
+ sortedJsonElement.add(jsonEntrySorted.getKey(), jsonEntrySorted.getValue());
+ }
+ return sortedJsonElement;
+ } else {
+ // neither array, nor object: return original element
+ return jsonElement;
+ }
+ }
+
private static JsonElement nbtToJson(NBTBase nbtElement) {
if (nbtElement instanceof NBTBase.NBTPrimitive) {
NBTBase.NBTPrimitive nbtNumber = (NBTBase.NBTPrimitive) nbtElement;
@@ -56,7 +99,34 @@ public final class GsonUtils {
return new JsonObject();
}
} else if (nbtElement instanceof NBTTagString) {
- return new JsonPrimitive(((NBTTagString) nbtElement).getString());
+ String str = ((NBTTagString) nbtElement).getString();
+ if (str.length() > 100 && (str.startsWith("eyJ") || str.startsWith("ewo")) && Base64.isBase64(str)) {
+ // base64 decode NBTTagStrings starting with {" or {\n
+ try {
+ JsonElement base64DecodedJson = new JsonParser().parse(new String(Base64.decodeBase64(str)));
+ if (base64DecodedJson.isJsonObject()) {
+ JsonObject jsonObject = base64DecodedJson.getAsJsonObject();
+ JsonElement timestamp = jsonObject.get("timestamp");
+ if (timestamp != null) {
+ // convert unix timestamp to human-readable dates
+ try {
+ ZonedDateTime utcDateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(timestamp.getAsLong()), ZoneOffset.UTC);
+ ZonedDateTime localDateTime = utcDateTime.withZoneSameInstant(ZoneOffset.systemDefault());
+ String zoneOffset = localDateTime.getOffset().toString();
+
+ DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss O");
+ jsonObject.add("timeInUTC", new JsonPrimitive(utcDateTime.format(dateTimeFormatter)));
+ jsonObject.add("timeInLocalZone (UTC" + ("Z".equals(zoneOffset) ? "" : zoneOffset) + ")", new JsonPrimitive(localDateTime.format(dateTimeFormatter)));
+ } catch (DateTimeException | NumberFormatException ignored) {
+ }
+ }
+ }
+ return base64DecodedJson;
+ } catch (JsonParseException ignored) {
+ // failed to parse as json; leaving original string unmodified
+ }
+ }
+ return new JsonPrimitive(str);
} else if (nbtElement instanceof NBTTagList) {
NBTTagList nbtList = (NBTTagList) nbtElement;
JsonArray jsonArray = new JsonArray();
@@ -64,6 +134,13 @@ public final class GsonUtils {
jsonArray.add(nbtToJson(nbtList.get(tagId)));
}
return jsonArray;
+ } else if (nbtElement instanceof NBTTagIntArray) {
+ int[] intArray = ((NBTTagIntArray) nbtElement).getIntArray();
+ JsonArray jsonArray = new JsonArray();
+ for (int number : intArray) {
+ jsonArray.add(new JsonPrimitive(number));
+ }
+ return jsonArray;
} else if (nbtElement instanceof NBTTagCompound) {
NBTTagCompound nbtCompound = (NBTTagCompound) nbtElement;
JsonObject jsonObject = new JsonObject();
diff --git a/src/main/java/de/cowtipper/cowlection/util/ImageUtils.java b/src/main/java/de/cowtipper/cowlection/util/ImageUtils.java
index 69ee561..1d6820f 100644
--- a/src/main/java/de/cowtipper/cowlection/util/ImageUtils.java
+++ b/src/main/java/de/cowtipper/cowlection/util/ImageUtils.java
@@ -1,12 +1,20 @@
package de.cowtipper.cowlection.util;
import com.mojang.authlib.minecraft.MinecraftProfileTexture;
+import de.cowtipper.cowlection.Cowlection;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.ThreadDownloadImageData;
import net.minecraft.util.ResourceLocation;
+import net.minecraft.world.storage.MapData;
import net.minecraftforge.fml.relauncher.ReflectionHelper;
+import javax.imageio.ImageIO;
+import java.awt.*;
import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
public class ImageUtils {
public static int getTierFromTexture(String minionSkinId) {
@@ -62,4 +70,128 @@ public class ImageUtils {
return ordinal();
}
}
+
+ private static final Color[] MAP_COLORS;
+
+ static {
+ // base colors: https://minecraft.gamepedia.com/Map_item_format?oldid=778280#Base_colors
+ MAP_COLORS = new Color[36];
+ MAP_COLORS[0] = new Color(0, 0, 0, 0);
+ MAP_COLORS[1] = new Color(125, 176, 55, 255);
+ MAP_COLORS[2] = new Color(244, 230, 161, 255);
+ MAP_COLORS[3] = new Color(197, 197, 197, 255);
+ MAP_COLORS[4] = new Color(252, 0, 0, 255);
+ MAP_COLORS[5] = new Color(158, 158, 252, 255);
+ MAP_COLORS[6] = new Color(165, 165, 165, 255);
+ MAP_COLORS[7] = new Color(0, 123, 0, 255);
+ MAP_COLORS[8] = new Color(252, 252, 252, 255);
+ MAP_COLORS[9] = new Color(162, 166, 182, 255);
+ MAP_COLORS[10] = new Color(149, 108, 76, 255);
+ MAP_COLORS[11] = new Color(111, 111, 111, 255);
+ MAP_COLORS[12] = new Color(63, 63, 252, 255);
+ MAP_COLORS[13] = new Color(141, 118, 71, 255);
+ MAP_COLORS[14] = new Color(252, 249, 242, 255);
+ MAP_COLORS[15] = new Color(213, 125, 50, 255);
+ MAP_COLORS[16] = new Color(176, 75, 213, 255);
+ MAP_COLORS[17] = new Color(101, 151, 213, 255);
+ MAP_COLORS[18] = new Color(226, 226, 50, 255);
+ MAP_COLORS[19] = new Color(125, 202, 25, 255);
+ MAP_COLORS[20] = new Color(239, 125, 163, 255);
+ MAP_COLORS[21] = new Color(75, 75, 75, 255);
+ MAP_COLORS[22] = new Color(151, 151, 151, 255);
+ MAP_COLORS[23] = new Color(75, 125, 151, 255);
+ MAP_COLORS[24] = new Color(125, 62, 176, 255);
+ MAP_COLORS[25] = new Color(50, 75, 176, 255);
+ MAP_COLORS[26] = new Color(101, 75, 50, 255);
+ MAP_COLORS[27] = new Color(101, 125, 50, 255);
+ MAP_COLORS[28] = new Color(151, 50, 50, 255);
+ MAP_COLORS[29] = new Color(25, 25, 25, 255);
+ MAP_COLORS[30] = new Color(247, 235, 76, 255);
+ MAP_COLORS[31] = new Color(91, 216, 210, 255);
+ MAP_COLORS[32] = new Color(73, 129, 252, 255);
+ MAP_COLORS[33] = new Color(0, 214, 57, 255);
+ MAP_COLORS[34] = new Color(127, 85, 48, 255);
+ MAP_COLORS[35] = new Color(111, 2, 0, 255);
+ }
+
+ public static File saveMapToFile(MapData mapData) {
+ int size = 128;
+ BufferedImage image = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
+
+ int x = 0;
+ int y = 0;
+ for (int i = 0; i < mapData.colors.length; i++) {
+ Color pixelColor = colorIdToColor(mapData.colors[i]);
+ image.setRGB(x, y, pixelColor.getRGB());
+ ++x;
+ if (x >= size) {
+ // new line
+ ++y;
+ x = 0;
+ }
+ }
+ try {
+ File cowlectionImagePath = new File(Minecraft.getMinecraft().mcDataDir, "cowlection_images");
+ if (!cowlectionImagePath.exists() && !cowlectionImagePath.mkdirs()) {
+ // dir didn't exist and couldn't be created
+ return null;
+ }
+ File imageFile = getTimestampedPngFileForDirectory(cowlectionImagePath, "map");
+ ImageIO.write(image, "png", imageFile);
+ return imageFile.getCanonicalFile();
+ } catch (IOException e) {
+ // couldn't save map image
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ private static Color colorIdToColor(byte rawId) {
+ int id = rawId & 255;
+ int baseId = id / 4;
+ int shadeId = id & 3;
+
+ if (baseId >= MAP_COLORS.length) {
+ Cowlection.getInstance().getLogger().warn("Unknown base color id " + baseId + " (id=" + id + ")");
+ return new Color(0xf700d5);
+ }
+
+ Color c = MAP_COLORS[baseId];
+ int shadeMul;
+ switch (shadeId) {
+ case 0:
+ shadeMul = 180;
+ break;
+ case 1:
+ shadeMul = 220;
+ break;
+ case 2:
+ shadeMul = 255;
+ break;
+ case 3:
+ shadeMul = 135;
+ break;
+ default:
+ shadeMul = 180;
+ Cowlection.getInstance().getLogger().warn("Unknown shade id " + shadeId + " (raw: " + id + ")");
+ c = new Color(0xf700d5);
+ }
+ return new Color((shadeMul * c.getRed()) / 255, (shadeMul * c.getGreen()) / 255, (shadeMul * c.getBlue()) / 255, c.getAlpha());
+ }
+
+ /**
+ * Based on ScreenShotHelper#getTimestampedPNGFileForDirectory
+ */
+ private static File getTimestampedPngFileForDirectory(File directory, String prefix) {
+ String currentDateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss"));
+ int i = 1;
+
+ while (true) {
+ File timestampedFile = new File(directory, prefix + "_" + currentDateTime + (i == 1 ? "" : "_" + i) + ".png");
+ if (!timestampedFile.exists()) {
+ return timestampedFile;
+ }
+ ++i;
+ }
+ }
}
diff --git a/src/main/java/de/cowtipper/cowlection/util/MooChatComponent.java b/src/main/java/de/cowtipper/cowlection/util/MooChatComponent.java
index 95415d3..856cbb3 100644
--- a/src/main/java/de/cowtipper/cowlection/util/MooChatComponent.java
+++ b/src/main/java/de/cowtipper/cowlection/util/MooChatComponent.java
@@ -6,6 +6,9 @@ import net.minecraft.util.ChatComponentText;
import net.minecraft.util.EnumChatFormatting;
import net.minecraft.util.IChatComponent;
+import java.io.File;
+
+@SuppressWarnings("unused")
public class MooChatComponent extends ChatComponentText {
public MooChatComponent(String msg) {
super(msg);
@@ -127,13 +130,11 @@ public class MooChatComponent extends ChatComponentText {
}
public MooChatComponent setUrl(String url) {
- setUrl(url, new KeyValueTooltipComponent("Click to visit", url));
- return this;
+ return setUrl(url, new KeyValueTooltipComponent("Click to visit", url));
}
public MooChatComponent setUrl(String url, String hover) {
- setUrl(url, new MooChatComponent(hover).yellow());
- return this;
+ return setUrl(url, new MooChatComponent(hover).yellow());
}
public MooChatComponent setUrl(String url, IChatComponent hover) {
@@ -142,6 +143,12 @@ public class MooChatComponent extends ChatComponentText {
return this;
}
+ public MooChatComponent setOpenFile(File filePath) {
+ setChatStyle(getChatStyle().setChatClickEvent(new ClickEvent(ClickEvent.Action.OPEN_FILE, filePath.getAbsolutePath())));
+ setHover(new MooChatComponent(filePath.isFile() ? "Open " + filePath.getName() : "Open folder: " + filePath.toString()).yellow());
+ return this;
+ }
+
public MooChatComponent setSuggestCommand(String command) {
setSuggestCommand(command, true);
return this;
@@ -155,6 +162,12 @@ public class MooChatComponent extends ChatComponentText {
return this;
}
+ @Override
+ public MooChatComponent appendSibling(IChatComponent component) {
+ super.appendSibling(component);
+ return this;
+ }
+
/**
* Appends the given component in a new line, without inheriting formatting of previous siblings.
*
diff --git a/src/main/resources/assets/cowlection/lang/en_US.lang b/src/main/resources/assets/cowlection/lang/en_US.lang
index 5acf252..e9d9091 100644
--- a/src/main/resources/assets/cowlection/lang/en_US.lang
+++ b/src/main/resources/assets/cowlection/lang/en_US.lang
@@ -8,6 +8,8 @@ cowlection.config.fixReplyCmd=Auto-replace /r?
cowlection.config.fixReplyCmd.tooltip=Auto-replace §e/r §fwith §e/w <latest username> §fto avoid replying to the wrong person.\n\nUse §e/rr <message> §fto prevent the auto-replacement for a message.
cowlection.config.enableCopyInventory=Copy inventories with CTRL + C?
cowlection.config.enableCopyInventory.tooltip=If enabled: copy the items in an inventory as JSON with §eCTRL + C
+cowlection.config.wailaLevelOfDetail=Level of detail of /moo waila
+cowlection.config.wailaLevelOfDetail.tooltip=Should §e/moo whatAmILookingAt §rcopy §oall §rinfo or only the §omost important §rinfo?\n§7§oIn addition, the optional 2nd parameter can also be used: §e§o/moo waila <all|main>
cowlection.config.tabCompletableNamesCommands=Commands with Tab-completable usernames
cowlection.config.tabCompletableNamesCommands.tooltip=List of commands with a username argument that should be Tab-completable.\n§eRequires game restart to take effect!
cowlection.config.gotoLogSearchConfig=Search through your Minecraft logs