From 3d9a03ff4f1e542339950be6a77cb4c59a46ab77 Mon Sep 17 00:00:00 2001 From: hannibal2 <24389977+hannibal002@users.noreply.github.com> Date: Mon, 13 Feb 2023 14:03:22 +0100 Subject: Fix Muscle Memory (#581) * Added old SkyBlock Menu. * Execution! * Add cache. * Bingo has already enough to do. * Typo. * Revert "Typo." This reverts commit b4a1c385e0c410b1e111797b8d39e7ff64b09ef5. * Revert "Bingo has already enough to do." This reverts commit 6e004d2d65dff47ea3bee5c5789cb725724df6ed. * I am lazy. * The map is lazy too. * Hypixel moving features behind paywall. * Add red text to the setting too. * SEALED * Fixed Booster Cookie checks. Reworked CookieWarning, kept same logic but accidentally added profile switch support. * /trades does not require booster cookie. * Allowing middle clicks (and any other mouse click combination) --------- Co-authored-by: hannibal2 <24389977+hannibal00212@users.noreply.github.com> --- .../miscfeatures/OldSkyBlockMenu.kt | 195 +++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 src/main/kotlin/io/github/moulberry/notenoughupdates/miscfeatures/OldSkyBlockMenu.kt (limited to 'src/main/kotlin/io') diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/miscfeatures/OldSkyBlockMenu.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/miscfeatures/OldSkyBlockMenu.kt new file mode 100644 index 00000000..b871a672 --- /dev/null +++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/miscfeatures/OldSkyBlockMenu.kt @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2023 NotEnoughUpdates contributors + * + * This file is part of NotEnoughUpdates. + * + * NotEnoughUpdates is free software: you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * NotEnoughUpdates is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with NotEnoughUpdates. If not, see . + */ + +package io.github.moulberry.notenoughupdates.miscfeatures + +import io.github.moulberry.notenoughupdates.NotEnoughUpdates +import io.github.moulberry.notenoughupdates.autosubscribe.NEUAutoSubscribe +import io.github.moulberry.notenoughupdates.events.ReplaceItemEvent +import io.github.moulberry.notenoughupdates.events.SlotClickEvent +import io.github.moulberry.notenoughupdates.util.Utils +import net.minecraft.client.player.inventory.ContainerLocalMenu +import net.minecraft.init.Items +import net.minecraft.item.Item +import net.minecraft.item.ItemStack +import net.minecraftforge.fml.common.eventhandler.EventPriority +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent + +@NEUAutoSubscribe +object OldSkyBlockMenu { + + val map: Map by lazy { + val map = mutableMapOf() + for (button in SkyBlockButton.values()) { + map[button.slot] = button + } + map + } + + @SubscribeEvent + fun replaceItem(event: ReplaceItemEvent) { + if (!isRightInventory()) return + if (event.inventory !is ContainerLocalMenu) return + + val skyBlockButton = map[event.slotNumber] ?: return + + if (skyBlockButton.requiresBoosterCookie && !CookieWarning.hasActiveBoosterCookie()) { + event.replaceWith(skyBlockButton.itemWithCookieWarning) + } else { + event.replaceWith(skyBlockButton.itemWithoutCookieWarning) + } + } + + @SubscribeEvent(priority = EventPriority.HIGH) + fun onStackClick(event: SlotClickEvent) { + if (!isRightInventory()) return + + val skyBlockButton = map[event.slotId] ?: return + event.isCanceled = true + + if (!skyBlockButton.requiresBoosterCookie || CookieWarning.hasActiveBoosterCookie()) { + NotEnoughUpdates.INSTANCE.sendChatMessage("/" + skyBlockButton.command) + } + } + + private fun isRightInventory(): Boolean { + return NotEnoughUpdates.INSTANCE.hasSkyblockScoreboard() && + NotEnoughUpdates.INSTANCE.config.misc.oldSkyBlockMenu && + Utils.getOpenChestName() == "SkyBlock Menu" + } + + enum class SkyBlockButton( + val command: String, + val slot: Int, + private val displayName: String, + private vararg val displayDescription: String, + private val itemData: ItemData, + val requiresBoosterCookie: Boolean = true, + ) { + TRADES( + "trades", 40, + "Trades", + "View your available trades.", + "These trades are always", + "available and accessible through", + "the SkyBlock Menu.", + itemData = NormalItemData(Items.emerald), + requiresBoosterCookie = false + ), + ACCESSORY( + "accessories", 53, + "Accessory Bag", + "A special bag which can hold", + "Talismans, Rings, Artifacts, Relics, and", + "Orbs within it. All will still", + "work while in this bag!", + itemData = SkullItemData( + "2b73dd76-5fc1-4ac3-8139-6a8992f8ce80", + "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOTYxYTkxOGMw" + + "YzQ5YmE4ZDA1M2U1MjJjYjkxYWJjNzQ2ODkzNjdiNGQ4YWEwNmJmYzFiYTkxNTQ3MzA5ODVmZiJ9fX0=" + ) + ), + POTION( + "potionbag", 52, + "Potion Bag", + "A handy bag for holding your", + "Potions in.", + itemData = SkullItemData( + "991c4a18-3283-4629-b0fc-bbce23cd658c", + "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOWY4Yjg" + + "yNDI3YjI2MGQwYTYxZTY0ODNmYzNiMmMzNWE1ODU4NTFlMDhhOWE5ZGYzNzI1NDhiNDE2OGNjODE3YyJ9fX0=" + ) + ), + QUIVER( + "quiver", 44, + "Quiver", + "A masterfully crafted Quiver", + "which holds any kind of", + "projectile you can think of!", + itemData = SkullItemData( + "41758912-e6b1-4700-9de5-04f2cfb9c422", + "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNGNiM2FjZ" + + "GMxMWNhNzQ3YmY3MTBlNTlmNGM4ZTliM2Q5NDlmZGQzNjRjNjg2OTgzMWNhODc4ZjA3NjNkMTc4NyJ9fX0=" + ) + ), + FISHING( + "fishingbag", 43, + "Fishing Bag", + "A useful bag which can hold all", + "types of fish, baits, and fishing", + "loot!", + itemData = SkullItemData( + "508c01d6-eabe-430b-9811-874691ee7ee4", + "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZWI4ZT" + + "I5N2RmNmI4ZGZmY2YxMzVkYmE4NGVjNzkyZDQyMGFkOGVjYjQ1OGQxNDQyODg1NzJhODQ2MDNiMTYzMSJ9fX0=" + ) + ), + SACK_OF_SACKS( + "sacks", 35, + "Sack of Sacks", + "A sack which contains other", + "sacks. Sackception!", + itemData = SkullItemData( + "a206a7eb-70fc-4f9f-8316-c3f69d6ba2ca", + "ewogICJ0aW1lc3RhbXAiIDogMTU5MTMxMDU4NTYwOSwKICAicHJvZmlsZUlkIiA6ICI0MWQzYWJjMmQ3NDk0MDBjOTA5MGQ1NDM0" + + "ZDAzODMxYiIsCiAgInByb2ZpbGVOYW1lIiA6ICJNZWdha2xvb24iLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVl" + + "LAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5l" + + "Y3JhZnQubmV0L3RleHR1cmUvODBhMDc3ZTI0OGQxNDI3NzJlYTgwMDg2NGY4YzU3OGI5ZDM2ODg1YjI5ZGFmODM2YjY0" + + "YTcwNjg4MmI2ZWMxMCIKICAgIH0KICB9Cn0=" + ), + requiresBoosterCookie = false + ), + ; + + val itemWithCookieWarning: ItemStack by lazy { createItem(true) } + val itemWithoutCookieWarning: ItemStack by lazy { createItem(false) } + + private fun createItem(showCookieWarning: Boolean): ItemStack { + val lore = mutableListOf() + for (line in displayDescription) { + lore.add("§7$line") + } + lore.add("") + + if (showCookieWarning) { + lore.add("§cYou need a booster cookie active") + lore.add("§cto use this shortcut!") + } else { + lore.add("§eClick to execute /${command}") + } + val array = lore.toTypedArray() + val name = "§a${displayName}" + return when (itemData) { + is NormalItemData -> Utils.createItemStackArray(itemData.displayIcon, name, array) + is SkullItemData -> Utils.createSkull( + name, + itemData.uuid, + itemData.value, + array + ) + } + } + } + + sealed interface ItemData + + class NormalItemData(val displayIcon: Item) : ItemData + + class SkullItemData(val uuid: String, val value: String) : ItemData +} -- cgit From 5c58e19436f2935ae20cfb351ac6661b2dab1992 Mon Sep 17 00:00:00 2001 From: hannibal2 <24389977+hannibal002@users.noreply.github.com> Date: Wed, 15 Feb 2023 18:48:11 +0100 Subject: API errors in console (#610) * Print api errors in console. * No reason not to just append. * Censor API Key --------- Co-authored-by: hannibal2 <24389977+hannibal00212@users.noreply.github.com> Co-authored-by: nea --- .../core/config/annotations/ConfigOption.java | 1 + .../moulberry/notenoughupdates/util/ApiUtil.java | 7 +++- .../moulberry/notenoughupdates/util/ErrorUtil.kt | 43 ++++++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/io/github/moulberry/notenoughupdates/util/ErrorUtil.kt (limited to 'src/main/kotlin/io') diff --git a/src/main/java/io/github/moulberry/notenoughupdates/core/config/annotations/ConfigOption.java b/src/main/java/io/github/moulberry/notenoughupdates/core/config/annotations/ConfigOption.java index 920cb326..ddd1e71f 100644 --- a/src/main/java/io/github/moulberry/notenoughupdates/core/config/annotations/ConfigOption.java +++ b/src/main/java/io/github/moulberry/notenoughupdates/core/config/annotations/ConfigOption.java @@ -30,6 +30,7 @@ public @interface ConfigOption { String name(); String desc(); + String[] searchTags() default ""; int subcategoryId() default -1; diff --git a/src/main/java/io/github/moulberry/notenoughupdates/util/ApiUtil.java b/src/main/java/io/github/moulberry/notenoughupdates/util/ApiUtil.java index 023be060..45522329 100644 --- a/src/main/java/io/github/moulberry/notenoughupdates/util/ApiUtil.java +++ b/src/main/java/io/github/moulberry/notenoughupdates/util/ApiUtil.java @@ -213,7 +213,12 @@ public class ApiUtil { } catch (IOException e) { throw new RuntimeException(e); // We can rethrow, since supplyAsync catches exceptions. } - }, executorService); + }, executorService).handle((obj, t) -> { + if (t != null) { + System.err.println(ErrorUtil.printStackTraceWithoutApiKey(t)); + } + return obj; + }); } public CompletableFuture requestJson() { diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/ErrorUtil.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/ErrorUtil.kt new file mode 100644 index 00000000..f849a40d --- /dev/null +++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/ErrorUtil.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2023 NotEnoughUpdates contributors + * + * This file is part of NotEnoughUpdates. + * + * NotEnoughUpdates is free software: you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * NotEnoughUpdates is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with NotEnoughUpdates. If not, see . + */ + +package io.github.moulberry.notenoughupdates.util + +import io.github.moulberry.notenoughupdates.NotEnoughUpdates +import java.io.ByteArrayOutputStream +import java.io.PrintStream +import java.nio.charset.StandardCharsets + +object ErrorUtil { + @JvmStatic + fun printCensoredStackTrace(e: Throwable, toCensor: List): String { + val baos = ByteArrayOutputStream() + e.printStackTrace(PrintStream(baos, true, StandardCharsets.UTF_8.name())) + var string = String(baos.toByteArray(), StandardCharsets.UTF_8) + toCensor.forEach { + string = string.replace(it, "*".repeat(it.length)) + } + return string + } + + @JvmStatic + fun printStackTraceWithoutApiKey(e: Throwable): String { + return printCensoredStackTrace(e, listOf(NotEnoughUpdates.INSTANCE.config.apiData.apiKey)) + } +} -- cgit From d3ca199f904cd72e419c6320eda261f023c71937 Mon Sep 17 00:00:00 2001 From: Roman / Linnea Gräf Date: Wed, 15 Feb 2023 18:50:56 +0100 Subject: ApiUtil: Add cache with per request timeout and per class histogram (#592) * ApiUtil: Add cache with per request timeout and per class histogram * MinionHelper: Only load minion helper data when needed * Api: Make api response processing more async. * Lower cache for /pv to 30 seconds and rename cacheDuration to max age * Disk cache for the API --- .../notenoughupdates/auction/APIManager.java | 14 +- .../commands/dev/DevTestCommand.java | 20 ++ .../commands/misc/PronounsCommand.java | 2 +- .../notenoughupdates/cosmetics/CapeManager.java | 4 +- .../loaders/MinionHelperApiLoader.java | 7 +- .../options/customtypes/NEUDebugFlag.java | 5 + .../profileviewer/ProfileViewer.java | 2 + .../moulberry/notenoughupdates/util/ApiUtil.java | 38 +++- .../notenoughupdates/util/MinecraftExecutor.java | 37 ---- .../moulberry/notenoughupdates/util/ApiCache.kt | 215 +++++++++++++++++++++ .../notenoughupdates/util/MinecraftExecutor.kt | 47 +++++ 11 files changed, 335 insertions(+), 56 deletions(-) delete mode 100644 src/main/java/io/github/moulberry/notenoughupdates/util/MinecraftExecutor.java create mode 100644 src/main/kotlin/io/github/moulberry/notenoughupdates/util/ApiCache.kt create mode 100644 src/main/kotlin/io/github/moulberry/notenoughupdates/util/MinecraftExecutor.kt (limited to 'src/main/kotlin/io') diff --git a/src/main/java/io/github/moulberry/notenoughupdates/auction/APIManager.java b/src/main/java/io/github/moulberry/notenoughupdates/auction/APIManager.java index 5ec3724a..ac60ffd9 100644 --- a/src/main/java/io/github/moulberry/notenoughupdates/auction/APIManager.java +++ b/src/main/java/io/github/moulberry/notenoughupdates/auction/APIManager.java @@ -292,7 +292,7 @@ public class APIManager { .newMoulberryRequest("lowestbin.json.gz") .gunzip() .requestJson() - .thenAccept(jsonObject -> { + .thenAcceptAsync(jsonObject -> { if (lowestBins == null) { lowestBins = new JsonObject(); } @@ -465,12 +465,12 @@ public class APIManager { }; manager.apiUtils.newMoulberryRequest("auctionLast.json.gz") - .gunzip().requestJson().thenAccept(process); + .gunzip().requestJson().thenAcceptAsync(process); manager.apiUtils .newMoulberryRequest("auction.json.gz") .gunzip().requestJson() - .thenAccept(jsonObject -> { + .thenAcceptAsync(jsonObject -> { if (jsonObject.get("success").getAsBoolean()) { long apiUpdate = (long) jsonObject.get("time").getAsFloat(); if (lastApiUpdate == apiUpdate) { @@ -683,7 +683,7 @@ public class APIManager { manager.apiUtils .newAnonymousHypixelApiRequest("skyblock/auctions") .requestJson() - .thenAccept(jsonObject -> { + .thenAcceptAsync(jsonObject -> { if (jsonObject == null) return; if (jsonObject.get("success").getAsBoolean()) { @@ -733,7 +733,7 @@ public class APIManager { manager.apiUtils .newAnonymousHypixelApiRequest("skyblock/bazaar") .requestJson() - .thenAccept(jsonObject -> { + .thenAcceptAsync(jsonObject -> { if (!jsonObject.get("success").getAsBoolean()) return; craftCost.clear(); @@ -789,7 +789,7 @@ public class APIManager { public void updateAvgPrices() { manager.apiUtils .newMoulberryRequest("auction_averages/3day.json.gz") - .gunzip().requestJson().thenAccept((jsonObject) -> { + .gunzip().requestJson().thenAcceptAsync((jsonObject) -> { craftCost.clear(); auctionPricesJson = jsonObject; lastAuctionAvgUpdate = System.currentTimeMillis(); @@ -797,7 +797,7 @@ public class APIManager { manager.apiUtils .newMoulberryRequest("auction_averages_lbin/1day.json.gz") .gunzip().requestJson() - .thenAccept((jsonObject) -> { + .thenAcceptAsync((jsonObject) -> { auctionPricesAvgLowestBinJson = jsonObject; }); } diff --git a/src/main/java/io/github/moulberry/notenoughupdates/commands/dev/DevTestCommand.java b/src/main/java/io/github/moulberry/notenoughupdates/commands/dev/DevTestCommand.java index 35474ff3..8dda864a 100644 --- a/src/main/java/io/github/moulberry/notenoughupdates/commands/dev/DevTestCommand.java +++ b/src/main/java/io/github/moulberry/notenoughupdates/commands/dev/DevTestCommand.java @@ -33,11 +33,13 @@ import io.github.moulberry.notenoughupdates.miscfeatures.customblockzones.Locati import io.github.moulberry.notenoughupdates.miscfeatures.customblockzones.SpecialBlockZone; import io.github.moulberry.notenoughupdates.miscgui.GuiPriceGraph; import io.github.moulberry.notenoughupdates.miscgui.minionhelper.MinionHelperManager; +import io.github.moulberry.notenoughupdates.util.ApiCache; import io.github.moulberry.notenoughupdates.util.PronounDB; import io.github.moulberry.notenoughupdates.util.SBInfo; import io.github.moulberry.notenoughupdates.util.TabListUtils; import io.github.moulberry.notenoughupdates.util.Utils; import io.github.moulberry.notenoughupdates.util.hypixelapi.ProfileCollectionInfo; +import lombok.var; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiScreen; import net.minecraft.command.CommandException; @@ -126,6 +128,24 @@ public class DevTestCommand extends ClientCommandBase { Utils.addChatMessage(EnumChatFormatting.RED + DEV_FAIL_STRINGS[devFailIndex++]); return; } + if (args.length == 1 && args[0].equalsIgnoreCase("dumpapihistogram")) { + synchronized (ApiCache.INSTANCE) { + Utils.addChatMessage("§e[NEU] API Request Histogram"); + Utils.addChatMessage("§e[NEU] §bClass Name§e: §aCached§e/§cNonCached§e/§dTotal"); + ApiCache.INSTANCE.getHistogramTotalRequests().forEach((className, totalRequests) -> { + var nonCachedRequests = ApiCache.INSTANCE.getHistogramNonCachedRequests().getOrDefault(className, 0); + var cachedRequests = totalRequests - nonCachedRequests; + Utils.addChatMessage( + String.format( + "§e[NEU] §b%s §a%d§e/§c%d§e/§d%d", + className, + cachedRequests, + nonCachedRequests, + totalRequests + )); + }); + } + } if (args.length == 1 && args[0].equalsIgnoreCase("testprofile")) { NotEnoughUpdates.INSTANCE.manager.apiUtils.newHypixelApiRequest("skyblock/profiles") .queryArgument( diff --git a/src/main/java/io/github/moulberry/notenoughupdates/commands/misc/PronounsCommand.java b/src/main/java/io/github/moulberry/notenoughupdates/commands/misc/PronounsCommand.java index 5a4f1400..cf0d0c56 100644 --- a/src/main/java/io/github/moulberry/notenoughupdates/commands/misc/PronounsCommand.java +++ b/src/main/java/io/github/moulberry/notenoughupdates/commands/misc/PronounsCommand.java @@ -88,7 +88,7 @@ public class PronounsCommand extends ClientCommandBase { "§e[NEU] Pronouns for §b" + user + " §eon §b" + platform + "§e:"), id); betterPronounChoice.render().forEach(it -> nc.printChatMessage(new ChatComponentText("§e[NEU] §a" + it))); return null; - }, MinecraftExecutor.INSTANCE); + }, MinecraftExecutor.OnThread); } } diff --git a/src/main/java/io/github/moulberry/notenoughupdates/cosmetics/CapeManager.java b/src/main/java/io/github/moulberry/notenoughupdates/cosmetics/CapeManager.java index 4a7c1939..984a7931 100644 --- a/src/main/java/io/github/moulberry/notenoughupdates/cosmetics/CapeManager.java +++ b/src/main/java/io/github/moulberry/notenoughupdates/cosmetics/CapeManager.java @@ -150,7 +150,7 @@ public class CapeManager { NotEnoughUpdates.INSTANCE.manager.apiUtils .newMoulberryRequest("activecapes.json") .requestJson() - .thenAccept(jsonObject -> { + .thenAcceptAsync(jsonObject -> { if (jsonObject.get("success").getAsBoolean()) { lastJsonSync = jsonObject; @@ -171,7 +171,7 @@ public class CapeManager { NotEnoughUpdates.INSTANCE.manager.apiUtils .newMoulberryRequest("permscapes.json") .requestJson() - .thenAccept(jsonObject -> { + .thenAcceptAsync(jsonObject -> { if (!jsonObject.get("success").getAsBoolean()) return; permSyncTries = 0; diff --git a/src/main/java/io/github/moulberry/notenoughupdates/miscgui/minionhelper/loaders/MinionHelperApiLoader.java b/src/main/java/io/github/moulberry/notenoughupdates/miscgui/minionhelper/loaders/MinionHelperApiLoader.java index aaa398f4..ecf02236 100644 --- a/src/main/java/io/github/moulberry/notenoughupdates/miscgui/minionhelper/loaders/MinionHelperApiLoader.java +++ b/src/main/java/io/github/moulberry/notenoughupdates/miscgui/minionhelper/loaders/MinionHelperApiLoader.java @@ -47,7 +47,6 @@ import java.util.Map; public class MinionHelperApiLoader { private final MinionHelperManager manager; private boolean dirty = true; - private int ticks = 0; private boolean collectionApiEnabled = true; private boolean ignoreWorldSwitches = false; private boolean readyToUse = false; @@ -72,11 +71,7 @@ public class MinionHelperApiLoader { if (Minecraft.getMinecraft().thePlayer == null) return; if (!NotEnoughUpdates.INSTANCE.hasSkyblockScoreboard()) return; if (!NotEnoughUpdates.INSTANCE.config.minionHelper.gui) return; - ticks++; - - if (ticks % 20 != 0) return; - - if (dirty) { + if (dirty && "Crafted Minions".equals(Utils.getOpenChestName())) { load(); } else { if (System.currentTimeMillis() > lastLoaded + 60_000 * 3) { diff --git a/src/main/java/io/github/moulberry/notenoughupdates/options/customtypes/NEUDebugFlag.java b/src/main/java/io/github/moulberry/notenoughupdates/options/customtypes/NEUDebugFlag.java index 50f459c0..90ef93bb 100644 --- a/src/main/java/io/github/moulberry/notenoughupdates/options/customtypes/NEUDebugFlag.java +++ b/src/main/java/io/github/moulberry/notenoughupdates/options/customtypes/NEUDebugFlag.java @@ -31,6 +31,7 @@ public enum NEUDebugFlag { WISHING("Wishing Compass Solver"), MAP("Dungeon Map Player Information"), SEARCH("SearchString Matches"), + API_CACHE("Api Cache"), ; private final String description; @@ -43,6 +44,10 @@ public enum NEUDebugFlag { return description; } + public void log(String message) { + NEUDebugLogger.log(this, message); + } + public boolean isSet() { return NEUDebugLogger.isFlagEnabled(this); } diff --git a/src/main/java/io/github/moulberry/notenoughupdates/profileviewer/ProfileViewer.java b/src/main/java/io/github/moulberry/notenoughupdates/profileviewer/ProfileViewer.java index 17a14d1f..6f8427ae 100644 --- a/src/main/java/io/github/moulberry/notenoughupdates/profileviewer/ProfileViewer.java +++ b/src/main/java/io/github/moulberry/notenoughupdates/profileviewer/ProfileViewer.java @@ -42,6 +42,7 @@ import net.minecraft.util.EnumChatFormatting; import javax.annotation.Nullable; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; @@ -537,6 +538,7 @@ public class ProfileViewer { manager.apiUtils .newHypixelApiRequest("player") .queryArgument("name", nameF) + .maxCacheAge(Duration.ofSeconds(30)) .requestJson() .thenAccept(jsonObject -> { if ( diff --git a/src/main/java/io/github/moulberry/notenoughupdates/util/ApiUtil.java b/src/main/java/io/github/moulberry/notenoughupdates/util/ApiUtil.java index 45522329..28298fe0 100644 --- a/src/main/java/io/github/moulberry/notenoughupdates/util/ApiUtil.java +++ b/src/main/java/io/github/moulberry/notenoughupdates/util/ApiUtil.java @@ -48,18 +48,26 @@ import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; +import java.time.Duration; import java.util.ArrayList; import java.util.Collection; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.zip.GZIPInputStream; public class ApiUtil { private static final Gson gson = new Gson(); + + private static final Comparator nameValuePairComparator = Comparator + .comparing(NameValuePair::getName) + .thenComparing(NameValuePair::getValue); + private static final ExecutorService executorService = Executors.newFixedThreadPool(3); private static String getUserAgent() { if (NotEnoughUpdates.INSTANCE.config.hidden.customUserAgent != null) { @@ -110,6 +118,7 @@ public class ApiUtil { private final List queryArguments = new ArrayList<>(); private String baseUrl = null; private boolean shouldGunzip = false; + private Duration maxCacheAge = Duration.ofSeconds(500); private String method = "GET"; private String postData = null; private String postContentType = null; @@ -119,6 +128,15 @@ public class ApiUtil { return this; } + /** + * Specify a cache timeout of {@code null} to signify an uncacheable request. + * Non {@code GET} requests are always uncacheable. + */ + public Request maxCacheAge(Duration maxCacheAge) { + this.maxCacheAge = maxCacheAge; + return this; + } + public Request url(String baseUrl) { this.baseUrl = baseUrl; return this; @@ -160,7 +178,17 @@ public class ApiUtil { return fut; } - public CompletableFuture requestString() { + public String getBaseUrl() { + return baseUrl; + } + + private ApiCache.CacheKey getCacheKey() { + if (!"GET".equals(method)) return null; + queryArguments.sort(nameValuePairComparator); + return new ApiCache.CacheKey(baseUrl, queryArguments, shouldGunzip); + } + + private CompletableFuture requestString0() { return buildUrl().thenApplyAsync(url -> { try { InputStream inputStream = null; @@ -183,7 +211,7 @@ public class ApiUtil { conn.setDoOutput(true); OutputStream os = conn.getOutputStream(); try { - os.write(this.postData.getBytes("utf-8")); + os.write(this.postData.getBytes(StandardCharsets.UTF_8)); } finally { os.close(); } @@ -221,12 +249,16 @@ public class ApiUtil { }); } + public CompletableFuture requestString() { + return ApiCache.INSTANCE.cacheRequest(this, getCacheKey(), this::requestString0, maxCacheAge); + } + public CompletableFuture requestJson() { return requestJson(JsonObject.class); } public CompletableFuture requestJson(Class clazz) { - return requestString().thenApply(str -> gson.fromJson(str, clazz)); + return requestString().thenApplyAsync(str -> gson.fromJson(str, clazz)); } } diff --git a/src/main/java/io/github/moulberry/notenoughupdates/util/MinecraftExecutor.java b/src/main/java/io/github/moulberry/notenoughupdates/util/MinecraftExecutor.java deleted file mode 100644 index bf973b76..00000000 --- a/src/main/java/io/github/moulberry/notenoughupdates/util/MinecraftExecutor.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2022 NotEnoughUpdates contributors - * - * This file is part of NotEnoughUpdates. - * - * NotEnoughUpdates is free software: you can redistribute it - * and/or modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. - * - * NotEnoughUpdates is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with NotEnoughUpdates. If not, see . - */ - -package io.github.moulberry.notenoughupdates.util; - -import net.minecraft.client.Minecraft; -import org.jetbrains.annotations.NotNull; - -import java.util.concurrent.Executor; - -public class MinecraftExecutor implements Executor { - - public static MinecraftExecutor INSTANCE = new MinecraftExecutor(); - - private MinecraftExecutor() {} - - @Override - public void execute(@NotNull Runnable runnable) { - Minecraft.getMinecraft().addScheduledTask(runnable); - } -} diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/ApiCache.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/ApiCache.kt new file mode 100644 index 00000000..c14df425 --- /dev/null +++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/ApiCache.kt @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2023 NotEnoughUpdates contributors + * + * This file is part of NotEnoughUpdates. + * + * NotEnoughUpdates is free software: you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * NotEnoughUpdates is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with NotEnoughUpdates. If not, see . + */ + +package io.github.moulberry.notenoughupdates.util + +import io.github.moulberry.notenoughupdates.NotEnoughUpdates +import io.github.moulberry.notenoughupdates.options.customtypes.NEUDebugFlag +import io.github.moulberry.notenoughupdates.util.ApiUtil.Request +import org.apache.http.NameValuePair +import java.nio.file.Files +import java.nio.file.Path +import java.time.Duration +import java.util.* +import java.util.concurrent.CompletableFuture +import java.util.function.Supplier +import kotlin.io.path.deleteIfExists +import kotlin.io.path.readText +import kotlin.io.path.writeText +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.seconds +import kotlin.time.ExperimentalTime +import kotlin.time.TimeSource +import kotlin.time.toKotlinDuration + +@OptIn(ExperimentalTime::class) + +object ApiCache { + data class CacheKey( + val baseUrl: String, + val requestParameters: List, + val shouldGunzip: Boolean, + ) + + data class CacheResult( + private var future: CompletableFuture?, + val firedAt: TimeSource.Monotonic.ValueTimeMark, + private var file: Path? = null, + private var disposed: Boolean = false, + ) { + init { + future!!.thenAcceptAsync { text -> + synchronized(this) { + if (disposed) { + return@synchronized + } + future = null + val f = Files.createTempFile(cacheBaseDir, "api-cache", ".bin") + log("Writing cache to disk: $f") + f.toFile().deleteOnExit() + f.writeText(text) + file = f + } + } + } + + val isAvailable get() = file != null && !disposed + + fun getCachedFuture(): CompletableFuture { + synchronized(this) { + if (disposed) { + return CompletableFuture.supplyAsync { + throw IllegalStateException("Attempting to read from a disposed future at $file. Most likely caused by non synchronized access to ApiCache.cachedRequests") + } + } + val fut = future + if (fut != null) { + return fut + } else { + val text = file!!.readText() + return CompletableFuture.completedFuture(text) + } + } + } + + /** + * Should be called when removing / replacing a request from [cachedRequests]. + * Should only be called while holding a lock on [ApiCache]. + * This deletes the disk cache and smashes the internal state for it to be GCd. + * After calling this method no other method may be called on this object. + */ + internal fun dispose() { + synchronized(this) { + if (disposed) return + log("Disposing cache for $file") + disposed = true + file?.deleteIfExists() + future = null + } + } + } + + private val cacheBaseDir by lazy { + val d = Files.createTempDirectory("neu-cache") + d.toFile().deleteOnExit() + d + } + private val cachedRequests = mutableMapOf() + val histogramTotalRequests: MutableMap = mutableMapOf() + val histogramNonCachedRequests: MutableMap = mutableMapOf() + + private val timeout = 10.seconds + private val globalMaxCacheAge = 1.hours + + private fun log(message: String) { + NEUDebugFlag.API_CACHE.log(message) + } + + private fun traceApiRequest( + request: Request, + failReason: String?, + ) { + if (!NotEnoughUpdates.INSTANCE.config.hidden.dev) return + val callingClass = Thread.currentThread().stackTrace + .find { + !it.className.startsWith("java.") && + !it.className.startsWith("kotlin.") && + it.className != ApiCache::class.java.name && + it.className != ApiUtil::class.java.name && + it.className != Request::class.java.name + } + val callingClassText = callingClass?.let { + "${it.className}.${it.methodName} (${it.fileName}:${it.lineNumber})" + } ?: "no calling class found" + callingClass?.className?.let { + histogramTotalRequests[it] = (histogramTotalRequests[it] ?: 0) + 1 + if (failReason != null) + histogramNonCachedRequests[it] = (histogramNonCachedRequests[it] ?: 0) + 1 + } + if (failReason != null) { + log("Executing api request for url ${request.baseUrl} by $callingClassText: $failReason") + } else { + log("Cache hit for api request for url ${request.baseUrl} by $callingClassText.") + } + } + + private fun evictCache() { + synchronized(this) { + val it = cachedRequests.iterator() + while (it.hasNext()) { + val next = it.next() + if (next.value.firedAt.elapsedNow() >= globalMaxCacheAge) { + next.value.dispose() + it.remove() + } + } + } + } + + fun cacheRequest( + request: Request, + cacheKey: CacheKey?, + futureSupplier: Supplier>, + maxAge: Duration? + ): CompletableFuture { + evictCache() + if (cacheKey == null) { + traceApiRequest(request, "uncacheable request (probably POST)") + return futureSupplier.get() + } + if (maxAge == null) { + traceApiRequest(request, "manually specified as uncacheable") + return futureSupplier.get() + } + fun recache(): CompletableFuture { + return futureSupplier.get().also { + cachedRequests[cacheKey]?.dispose() // Safe to dispose like this because this function is always called in a synchronized block + cachedRequests[cacheKey] = CacheResult(it, TimeSource.Monotonic.markNow()) + } + } + synchronized(this) { + val cachedRequest = cachedRequests[cacheKey] + if (cachedRequest == null) { + traceApiRequest(request, "no cache found") + return recache() + } + + return if (cachedRequest.isAvailable) { + if (cachedRequest.firedAt.elapsedNow() > maxAge.toKotlinDuration()) { + traceApiRequest(request, "outdated cache") + recache() + } else { + // Using local cached request + traceApiRequest(request, null) + cachedRequest.getCachedFuture() + } + } else { + if (cachedRequest.firedAt.elapsedNow() > timeout) { + traceApiRequest(request, "suspiciously slow api response") + recache() + } else { + // Joining ongoing request + traceApiRequest(request, null) + cachedRequest.getCachedFuture() + } + } + } + } + +} diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/MinecraftExecutor.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/MinecraftExecutor.kt new file mode 100644 index 00000000..bb0bc8b4 --- /dev/null +++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/MinecraftExecutor.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2023 NotEnoughUpdates contributors + * + * This file is part of NotEnoughUpdates. + * + * NotEnoughUpdates is free software: you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * NotEnoughUpdates is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with NotEnoughUpdates. If not, see . + */ + +package io.github.moulberry.notenoughupdates.util + +import net.minecraft.client.Minecraft +import java.util.concurrent.Executor +import java.util.concurrent.ForkJoinPool + +object MinecraftExecutor { + + @JvmField + val OnThread = Executor { + val mc = Minecraft.getMinecraft() + if (mc.isCallingFromMinecraftThread) { + it.run() + } else { + Minecraft.getMinecraft().addScheduledTask(it) + } + } + + @JvmField + val OffThread = Executor { + val mc = Minecraft.getMinecraft() + if (mc.isCallingFromMinecraftThread) { + ForkJoinPool.commonPool().execute(it) + } else { + it.run() + } + } +} -- cgit From 0d281d483909d71272783033b2aba8f33dcbce36 Mon Sep 17 00:00:00 2001 From: Roman / Linnea Gräf Date: Sat, 18 Feb 2023 15:24:52 +0100 Subject: ApiCache: Fix apparent NullPointerException (#619) `CacheResult` should in theory have either a `file != null` a `future != null` or be `disposed`. Apparently this invariant of `CacheResult` is either being violated somewhere, or the `synchronized` blocks arent as synchronized as id hoped they were. In fact, `dispose()` does not even delete the file, so i can really only see this happening because the first `synchronized` block that writes the file and the second `synchronized` block that reads from the file hold the same lock. I have no idea how this would happen, but hopefully this fixes it (since the dispose didn't have a threading issue reported so far, i feel more confident leaving the .deleteOnExit in there, but I'm also wrapping any potential IOExceptions during access, because I am just so confused how the internal state was broken. --- .../moulberry/notenoughupdates/util/ApiCache.kt | 52 +++++++++++----------- .../util/kotlin/completablefuture.kt | 33 ++++++++++++++ 2 files changed, 60 insertions(+), 25 deletions(-) create mode 100644 src/main/kotlin/io/github/moulberry/notenoughupdates/util/kotlin/completablefuture.kt (limited to 'src/main/kotlin/io') diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/ApiCache.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/ApiCache.kt index c14df425..59fc2dd5 100644 --- a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/ApiCache.kt +++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/ApiCache.kt @@ -22,6 +22,7 @@ package io.github.moulberry.notenoughupdates.util import io.github.moulberry.notenoughupdates.NotEnoughUpdates import io.github.moulberry.notenoughupdates.options.customtypes.NEUDebugFlag import io.github.moulberry.notenoughupdates.util.ApiUtil.Request +import io.github.moulberry.notenoughupdates.util.kotlin.supplyImmediate import org.apache.http.NameValuePair import java.nio.file.Files import java.nio.file.Path @@ -47,43 +48,45 @@ object ApiCache { val shouldGunzip: Boolean, ) - data class CacheResult( - private var future: CompletableFuture?, + data class CacheResult internal constructor( + var cacheState: CacheState, val firedAt: TimeSource.Monotonic.ValueTimeMark, - private var file: Path? = null, - private var disposed: Boolean = false, ) { - init { - future!!.thenAcceptAsync { text -> + constructor(future: CompletableFuture, firedAt: TimeSource.Monotonic.ValueTimeMark) : this( + CacheState.WaitingForFuture(future), + firedAt + ) { + future.thenAccept { text -> synchronized(this) { - if (disposed) { - return@synchronized - } - future = null val f = Files.createTempFile(cacheBaseDir, "api-cache", ".bin") log("Writing cache to disk: $f") f.toFile().deleteOnExit() f.writeText(text) - file = f + cacheState = CacheState.FileCached(f) } } } - val isAvailable get() = file != null && !disposed + sealed interface CacheState { + object Disposed : CacheState + data class WaitingForFuture(val future: CompletableFuture) : CacheState + data class FileCached(val file: Path) : CacheState + } + + val isAvailable get() = cacheState is CacheState.FileCached fun getCachedFuture(): CompletableFuture { synchronized(this) { - if (disposed) { - return CompletableFuture.supplyAsync { - throw IllegalStateException("Attempting to read from a disposed future at $file. Most likely caused by non synchronized access to ApiCache.cachedRequests") + return when (val cs = cacheState) { + CacheState.Disposed -> supplyImmediate { + throw IllegalStateException("Attempting to read from a disposed future. Most likely caused by non synchronized access to ApiCache.cachedRequests") } - } - val fut = future - if (fut != null) { - return fut - } else { - val text = file!!.readText() - return CompletableFuture.completedFuture(text) + + is CacheState.FileCached -> supplyImmediate { + cs.file.readText() + } + + is CacheState.WaitingForFuture -> cs.future } } } @@ -96,11 +99,10 @@ object ApiCache { */ internal fun dispose() { synchronized(this) { - if (disposed) return + val file = (cacheState as? CacheState.FileCached)?.file log("Disposing cache for $file") - disposed = true + cacheState = CacheState.Disposed file?.deleteIfExists() - future = null } } } diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/kotlin/completablefuture.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/kotlin/completablefuture.kt new file mode 100644 index 00000000..de45c1e3 --- /dev/null +++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/kotlin/completablefuture.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2023 NotEnoughUpdates contributors + * + * This file is part of NotEnoughUpdates. + * + * NotEnoughUpdates is free software: you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * NotEnoughUpdates is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with NotEnoughUpdates. If not, see . + */ + +package io.github.moulberry.notenoughupdates.util.kotlin + +import java.util.concurrent.CompletableFuture + +inline fun supplyImmediate(block: () -> R): CompletableFuture { + val cf = CompletableFuture() + try { + cf.complete(block()) + } catch (t: Throwable) { + cf.completeExceptionally(t) + } + return cf +} + -- cgit