diff options
-rw-r--r-- | CHANGELOG.md | 4 | ||||
-rw-r--r-- | README.md | 1 | ||||
-rw-r--r-- | src/main/java/de/cowtipper/cowlection/command/MooCommand.java | 201 | ||||
-rw-r--r-- | src/main/java/de/cowtipper/cowlection/config/MooConfig.java | 9 | ||||
-rw-r--r-- | src/main/java/de/cowtipper/cowlection/data/DataHelper.java | 2 | ||||
-rw-r--r-- | src/main/java/de/cowtipper/cowlection/util/GsonUtils.java | 89 | ||||
-rw-r--r-- | src/main/java/de/cowtipper/cowlection/util/ImageUtils.java | 132 | ||||
-rw-r--r-- | src/main/java/de/cowtipper/cowlection/util/MooChatComponent.java | 21 | ||||
-rw-r--r-- | src/main/resources/assets/cowlection/lang/en_US.lang | 2 |
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' @@ -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` → `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 |