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 extends T> 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