diff options
| author | nea <nea@nea.moe> | 2023-02-17 20:54:04 +0100 |
|---|---|---|
| committer | nea <nea@nea.moe> | 2023-02-17 20:54:04 +0100 |
| commit | 1cb9bbc261891b9283c1e4cbd246c6e4d2bd87df (patch) | |
| tree | d0cd9516c1a9f8ffff4ae314faa79f4badd3ae40 /src/main/kotlin | |
| parent | a851b80bccac8a190b3f1532bcb3de1b7e1c6a3f (diff) | |
| parent | 1d75ac44c20fafd9f834dc7c01066a85a74f89e7 (diff) | |
| download | NotEnoughUpdates-1cb9bbc261891b9283c1e4cbd246c6e4d2bd87df.tar.gz NotEnoughUpdates-1cb9bbc261891b9283c1e4cbd246c6e4d2bd87df.tar.bz2 NotEnoughUpdates-1cb9bbc261891b9283c1e4cbd246c6e4d2bd87df.zip | |
Merge remote-tracking branch 'origin/master' into museumthing
Diffstat (limited to 'src/main/kotlin')
5 files changed, 542 insertions, 0 deletions
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/events/ProfileDataLoadedEvent.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/events/ProfileDataLoadedEvent.kt new file mode 100644 index 00000000..00a535c6 --- /dev/null +++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/events/ProfileDataLoadedEvent.kt @@ -0,0 +1,42 @@ +/* + * 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 <https://www.gnu.org/licenses/>. + */ +package io.github.moulberry.notenoughupdates.events + +import com.google.gson.JsonObject + +//TODO extend the usage of this event (accessory bag and storage data) +class ProfileDataLoadedEvent(val uuid: String, val data: JsonObject?) : NEUEvent() { + val profileInfo: JsonObject? by lazy { readProfileInfo() } + + private fun readProfileInfo(): JsonObject? { + if (data == null) return null + + val skyblockProfiles = data["profiles"].asJsonArray + for (profileEle in skyblockProfiles) { + val profile = profileEle.asJsonObject + if (!profile.has("members")) continue + val members = profile["members"].asJsonObject + if (!members.has(uuid)) continue + if (profile.has("selected") && profile["selected"].asBoolean) { + return members[uuid].asJsonObject + } + } + return null + } +} 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..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 <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 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( + private var future: CompletableFuture<String>?, + 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<String> { + 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<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() + } + } +} |
