diff options
Diffstat (limited to 'src/main/kotlin')
6 files changed, 536 insertions, 1 deletions
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/misc/MiscCommands.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/misc/MiscCommands.kt index a21e39b8..b8abb99e 100644 --- a/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/misc/MiscCommands.kt +++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/misc/MiscCommands.kt @@ -162,7 +162,7 @@ class MiscCommands { nc.printChatMessage(ChatComponentText("§e[NEU] §a$it")) } null - }, MinecraftExecutor.INSTANCE) + }, MinecraftExecutor.OnThread) } 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 <https://www.gnu.org/licenses/>. + */ + +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<Int, SkyBlockButton> by lazy { + val map = mutableMapOf<Int, SkyBlockButton>() + 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<String>() + 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 +} 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..59fc2dd5 --- /dev/null +++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/ApiCache.kt @@ -0,0 +1,217 @@ +/* + * 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 <https://www.gnu.org/licenses/>. + */ + +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 +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<NameValuePair>, + val shouldGunzip: Boolean, + ) + + data class CacheResult internal constructor( + var cacheState: CacheState, + val firedAt: TimeSource.Monotonic.ValueTimeMark, + ) { + constructor(future: CompletableFuture<String>, firedAt: TimeSource.Monotonic.ValueTimeMark) : this( + CacheState.WaitingForFuture(future), + firedAt + ) { + future.thenAccept { text -> + synchronized(this) { + val f = Files.createTempFile(cacheBaseDir, "api-cache", ".bin") + log("Writing cache to disk: $f") + f.toFile().deleteOnExit() + f.writeText(text) + cacheState = CacheState.FileCached(f) + } + } + } + + sealed interface CacheState { + object Disposed : CacheState + data class WaitingForFuture(val future: CompletableFuture<String>) : CacheState + data class FileCached(val file: Path) : CacheState + } + + val isAvailable get() = cacheState is CacheState.FileCached + + fun getCachedFuture(): CompletableFuture<String> { + synchronized(this) { + 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") + } + + is CacheState.FileCached -> supplyImmediate { + cs.file.readText() + } + + is CacheState.WaitingForFuture -> cs.future + } + } + } + + /** + * 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) { + val file = (cacheState as? CacheState.FileCached)?.file + log("Disposing cache for $file") + cacheState = CacheState.Disposed + file?.deleteIfExists() + } + } + } + + private val cacheBaseDir by lazy { + val d = Files.createTempDirectory("neu-cache") + d.toFile().deleteOnExit() + d + } + private val cachedRequests = mutableMapOf<CacheKey, CacheResult>() + val histogramTotalRequests: MutableMap<String, Int> = mutableMapOf() + val histogramNonCachedRequests: MutableMap<String, Int> = 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<CompletableFuture<String>>, + maxAge: Duration? + ): CompletableFuture<String> { + 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<String> { + 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/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 <https://www.gnu.org/licenses/>. + */ + +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>): 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)) + } +} 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 <https://www.gnu.org/licenses/>. + */ + +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() + } + } +} 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 <https://www.gnu.org/licenses/>. + */ + +package io.github.moulberry.notenoughupdates.util.kotlin + +import java.util.concurrent.CompletableFuture + +inline fun <R> supplyImmediate(block: () -> R): CompletableFuture<R> { + val cf = CompletableFuture<R>() + try { + cf.complete(block()) + } catch (t: Throwable) { + cf.completeExceptionally(t) + } + return cf +} + |