aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin/io
diff options
context:
space:
mode:
authornopo <nopotheemail@gmail.com>2023-03-12 11:00:47 +1100
committernopo <nopotheemail@gmail.com>2023-03-12 11:00:47 +1100
commit61dea4f655bdd6d938c890e09d76724fe32ef047 (patch)
tree4b8ddc4447cdf73ad8fa9d35415158dada877a84 /src/main/kotlin/io
parent0960bc9aa9faeb558124ee62b1c1e65983bbff69 (diff)
parent193ba468e43bd4db5b5534d17472078708783349 (diff)
downloadNotEnoughUpdates-61dea4f655bdd6d938c890e09d76724fe32ef047.tar.gz
NotEnoughUpdates-61dea4f655bdd6d938c890e09d76724fe32ef047.tar.bz2
NotEnoughUpdates-61dea4f655bdd6d938c890e09d76724fe32ef047.zip
Merge branch 'master' into scrolling
Diffstat (limited to 'src/main/kotlin/io')
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/commands/dev/DevTestCommand.kt256
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/commands/dev/DiagCommand.kt78
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/commands/dev/NEUStatsCommand.kt209
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/commands/dev/PackDevCommand.kt165
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/commands/dev/SimpleDevCommands.kt110
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/commands/help/FeaturesCommand.kt65
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/commands/help/HelpCommand.kt95
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/commands/help/LinksCommand.kt52
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/commands/help/SettingsCommand.kt53
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/commands/help/StorageViewerWhyCommand.kt48
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/commands/misc/AhCommand.kt67
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/commands/misc/DungeonCommands.kt144
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/commands/misc/FairySoulsCommand.kt62
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/commands/misc/MiscCommands.kt175
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/commands/misc/PeekCommand.kt318
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/commands/misc/ProfileViewerCommands.kt87
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/commands/misc/ScreenOpenCommands.kt52
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/events/ButtonExclusionZoneEvent.kt69
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/events/RegisterBrigadierCommandEvent.kt52
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/guifeatures/SkyMallDisplay.kt103
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/miscfeatures/OldSkyBlockMenu.kt211
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/miscfeatures/inventory/MuseumCheapestItemOverlay.kt574
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/miscfeatures/inventory/MuseumItemHighlighter.kt121
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/miscgui/DynamicLightItemsEditor.kt255
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/recipes/KatRecipe.kt1
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/util/ApiCache.kt217
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/util/ErrorUtil.kt43
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/util/HotmInformation.kt116
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/util/KotlinStringUtils.kt24
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/util/MinecraftExecutor.kt47
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/util/MuseumUtil.kt113
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/util/Rectangle.kt75
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/util/SkyBlockTime.kt122
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/util/brigadier/BrigadierRoot.kt103
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/util/brigadier/EnumArgumentType.kt64
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/util/brigadier/RestArgumentType.kt31
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/util/brigadier/dsl.kt179
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/util/iterate.kt28
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/util/kotlin/completablefuture.kt33
39 files changed, 4616 insertions, 1 deletions
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/dev/DevTestCommand.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/dev/DevTestCommand.kt
new file mode 100644
index 00000000..a5ff48d2
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/dev/DevTestCommand.kt
@@ -0,0 +1,256 @@
+/*
+ * 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.commands.dev
+
+import com.mojang.brigadier.arguments.StringArgumentType
+import io.github.moulberry.notenoughupdates.BuildFlags
+import io.github.moulberry.notenoughupdates.NotEnoughUpdates
+import io.github.moulberry.notenoughupdates.autosubscribe.NEUAutoSubscribe
+import io.github.moulberry.notenoughupdates.core.config.GuiPositionEditor
+import io.github.moulberry.notenoughupdates.core.util.MiscUtils
+import io.github.moulberry.notenoughupdates.events.RegisterBrigadierCommandEvent
+import io.github.moulberry.notenoughupdates.miscfeatures.FishingHelper
+import io.github.moulberry.notenoughupdates.miscfeatures.customblockzones.CustomBiomes
+import io.github.moulberry.notenoughupdates.miscfeatures.customblockzones.LocationChangeEvent
+import io.github.moulberry.notenoughupdates.miscgui.GuiPriceGraph
+import io.github.moulberry.notenoughupdates.miscgui.minionhelper.MinionHelperManager
+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.brigadier.*
+import net.minecraft.client.Minecraft
+import net.minecraft.client.gui.GuiScreen
+import net.minecraft.command.ICommandSender
+import net.minecraft.entity.player.EntityPlayer
+import net.minecraft.launchwrapper.Launch
+import net.minecraft.util.ChatComponentText
+import net.minecraft.util.EnumChatFormatting.*
+import net.minecraft.util.EnumParticleTypes
+import net.minecraftforge.common.MinecraftForge
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+import java.util.function.Predicate
+import kotlin.math.floor
+
+@NEUAutoSubscribe
+class DevTestCommand {
+ companion object {
+ val DEV_TESTERS: List<String> = mutableListOf(
+ "d0e05de7-6067-454d-beae-c6d19d886191", // moulberry
+ "66502b40-6ac1-4d33-950d-3df110297aab", // lucycoconut
+ "a5761ff3-c710-4cab-b4f4-3e7f017a8dbf", // ironm00n
+ "5d5c548a-790c-4fc8-bd8f-d25b04857f44", // ariyio
+ "53924f1a-87e6-4709-8e53-f1c7d13dc239", // throwpo
+ "d3cb85e2-3075-48a1-b213-a9bfb62360c1", // lrg89
+ "0b4d470f-f2fb-4874-9334-1eaef8ba4804", // dediamondpro
+ "ebb28704-ed85-43a6-9e24-2fe9883df9c2", // lulonaut
+ "698e199d-6bd1-4b10-ab0c-52fedd1460dc", // craftyoldminer
+ "8a9f1841-48e9-48ed-b14f-76a124e6c9df", // eisengolem
+ "a7d6b3f1-8425-48e5-8acc-9a38ab9b86f7", // whalker
+ "0ce87d5a-fa5f-4619-ae78-872d9c5e07fe", // ascynx
+ "a049a538-4dd8-43f8-87d5-03f09d48b4dc", // egirlefe
+ "7a9dc802-d401-4d7d-93c0-8dd1bc98c70d", // efefury
+ "bb855349-dfd8-4125-a750-5fc2cf543ad5" // hannibal2
+ )
+ val SPECIAL_KICK = "SPECIAL_KICK"
+
+ val DEV_FAIL_STRINGS = arrayOf(
+ "No.",
+ "I said no.",
+ "You aren't allowed to use this.",
+ "Are you sure you want to use this? Type 'Yes' in chat.",
+ "Are you sure you want to use this? Type 'Yes' in chat.",
+ "Lmao you thought",
+ "Ok please stop",
+ "What do you want from me?",
+ "This command almost certainly does nothing useful for you",
+ "Ok, this is the last message, after this it will repeat",
+ "No.",
+ "I said no.",
+ "Dammit. I thought that would work. Uhh...",
+ "\u00a7dFrom \u00a7c[ADMIN] Minikloon\u00a77: If you use that command again, I'll have to ban you",
+ SPECIAL_KICK,
+ "Ok, this is actually the last message, use the command again and you'll crash I promise"
+ )
+
+ fun isDeveloper(commandSender: ICommandSender): Boolean {
+ return DEV_TESTERS.contains((commandSender as? EntityPlayer)?.uniqueID?.toString())
+ || Launch.blackboard.get("fml.deobfuscatedEnvironment") as Boolean
+
+ }
+ }
+
+ var devFailIndex = 0
+ fun canPlayerExecute(commandSender: ICommandSender): Boolean {
+ return isDeveloper(commandSender)
+ }
+
+ @SubscribeEvent
+ fun onCommands(event: RegisterBrigadierCommandEvent) {
+ val hook = event.command("neudevtest") {
+ requires {
+ canPlayerExecute(it)
+ }
+ thenLiteralExecute("profileinfo") {
+ val currentProfile = SBInfo.getInstance().currentProfile
+ val gamemode = SBInfo.getInstance().getGamemodeForProfile(currentProfile)
+ reply("${GOLD}You are on Profile $currentProfile with the mode $gamemode")
+ }.withHelp("Display information about your current profile")
+ thenLiteralExecute("buildflags") {
+ reply("BuildFlags: \n" +
+ BuildFlags.getAllFlags().entries
+ .joinToString(("\n")) { (key, value) -> " + $key - $value" })
+ }.withHelp("List the flags with which NEU was built")
+ thenLiteral("exteditor") {
+ thenArgument("editor", StringArgumentType.string()) { newEditor ->
+ thenExecute {
+ NotEnoughUpdates.INSTANCE.config.hidden.externalEditor = this[newEditor]
+ reply("You changed your external editor to: §Z${this[newEditor]}")
+ }
+ }.withHelp("Change the editor used to edit repo files")
+ thenExecute {
+ reply("Your external editor is: §Z${NotEnoughUpdates.INSTANCE.config.hidden.externalEditor}")
+ }
+ }.withHelp("See your current external editor for repo files")
+ thenLiteral("pricetest") {
+ thenArgument("item", StringArgumentType.string()) { item ->
+ thenExecute {
+ NotEnoughUpdates.INSTANCE.openGui = GuiPriceGraph(this[item])
+ }
+ }.withHelp("Display the price graph for an item by id")
+ thenExecute {
+ NotEnoughUpdates.INSTANCE.manager.auctionManager.updateBazaar()
+ }
+ }.withHelp("Update the price data from the bazaar")
+ thenLiteralExecute("zone") {
+ val target = Minecraft.getMinecraft().objectMouseOver.blockPos
+ ?: Minecraft.getMinecraft().thePlayer.position
+ val zone = CustomBiomes.INSTANCE.getSpecialZone(target)
+ listOf(
+ ChatComponentText("Showing Zone Info for: $target"),
+ ChatComponentText("Zone: " + (zone?.name ?: "null")),
+ ChatComponentText("Location: " + SBInfo.getInstance().getLocation()),
+ ChatComponentText("Biome: " + CustomBiomes.INSTANCE.getCustomBiome(target))
+ ).forEach { component ->
+ reply(component)
+ }
+ MinecraftForge.EVENT_BUS.post(
+ LocationChangeEvent(
+ SBInfo.getInstance().getLocation(), SBInfo
+ .getInstance()
+ .getLocation()
+ )
+ )
+ }.withHelp("Display information about the special block zone at your cursor (Custom Texture Regions)")
+ thenLiteralExecute("positiontest") {
+ NotEnoughUpdates.INSTANCE.openGui = GuiPositionEditor()
+ }.withHelp("Open the gui position editor")
+ thenLiteral("pt") {
+ thenArgument("particle", EnumArgumentType.enum<EnumParticleTypes>()) { particle ->
+ thenExecute {
+ FishingHelper.type = this[particle]
+ reply("Fishing particles set to ${FishingHelper.type}")
+ }
+ }
+ }
+ thenLiteralExecute("dev") {
+ NotEnoughUpdates.INSTANCE.config.hidden.dev = !NotEnoughUpdates.INSTANCE.config.hidden.dev
+ reply("Dev mode " + if (NotEnoughUpdates.INSTANCE.config.hidden.dev) "§aenabled" else "§cdisabled")
+ }.withHelp("Toggle developer mode")
+ thenLiteralExecute("saveconfig") {
+ NotEnoughUpdates.INSTANCE.saveConfig()
+ reply("Config saved")
+ }.withHelp("Force sync the config to disk")
+ thenLiteralExecute("searchmode") {
+ NotEnoughUpdates.INSTANCE.config.hidden.firstTimeSearchFocus = true
+ reply(AQUA.toString() + "I would never search")
+ }.withHelp("Reset your search data to redisplay the search tutorial")
+ thenLiteralExecute("bluehair") {
+ PronounDB.test()
+ }.withHelp("Test the pronoundb integration")
+ thenLiteral("opengui") {
+ thenArgumentExecute("class", StringArgumentType.string()) { className ->
+ try {
+ NotEnoughUpdates.INSTANCE.openGui =
+ Class.forName(this[className]).newInstance() as GuiScreen
+ reply("Opening gui: " + NotEnoughUpdates.INSTANCE.openGui)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ reply("Failed to open this GUI.")
+ }
+ }.withHelp("Open a gui by class name")
+ }
+ thenLiteralExecute("center") {
+ val x = floor(Minecraft.getMinecraft().thePlayer.posX) + 0.5f
+ val z = floor(Minecraft.getMinecraft().thePlayer.posZ) + 0.5f
+ Minecraft.getMinecraft().thePlayer.setPosition(x, Minecraft.getMinecraft().thePlayer.posY, z)
+ reply("Literal hacks")
+ }.withHelp("Center yourself on the block you are currently standing (like using AOTE)")
+ thenLiteral("minion") {
+ thenArgumentExecute("args", RestArgumentType) { arg ->
+ MinionHelperManager.getInstance().handleCommand(arrayOf("minion") + this[arg].split(" "))
+ }.withHelp("Minion related commands. Not yet integrated in brigadier")
+ }
+ thenLiteralExecute("copytablist") {
+ val tabList = TabListUtils.getTabList().joinToString("\n", postfix = "\n")
+ MiscUtils.copyToClipboard(tabList)
+ reply("Copied tablist to clipboard!")
+ }.withHelp("Copy the tab list")
+ thenLiteral("useragent") {
+ thenArgumentExecute("newuseragent", RestArgumentType) { userAgent ->
+ reply("Setting your user agent to ${this[userAgent]}")
+ NotEnoughUpdates.INSTANCE.config.hidden.customUserAgent = this[userAgent]
+ }.withHelp("Set a custom user agent for all HTTP requests")
+ thenExecute {
+ reply("Resetting your user agent.")
+ NotEnoughUpdates.INSTANCE.config.hidden.customUserAgent = null
+ }
+ }.withHelp("Reset the custom user agent")
+ }
+ hook.beforeCommand = Predicate {
+ if (!canPlayerExecute(it.context.source)) {
+ if (devFailIndex !in DEV_FAIL_STRINGS.indices) {
+ throw object : Error("L") {
+ @Override
+ fun printStackTrace() {
+ throw Error("L")
+ }
+ }
+ }
+ val text = DEV_FAIL_STRINGS[devFailIndex++]
+ if (text == SPECIAL_KICK) {
+ val component = ChatComponentText("\u00a7cYou are permanently banned from this server!")
+ component.appendText("\n")
+ component.appendText("\n\u00a77Reason: \u00a7rI told you not to run the command - Moulberry")
+ component.appendText("\n\u00a77Find out more: \u00a7b\u00a7nhttps://www.hypixel.net/appeal")
+ component.appendText("\n")
+ component.appendText("\n\u00a77Ban ID: \u00a7r#49871982")
+ component.appendText("\n\u00a77Sharing your Ban ID may affect the processing of your appeal!")
+ Minecraft.getMinecraft().netHandler.networkManager.closeChannel(component)
+ } else {
+ it.context.source.addChatMessage(ChatComponentText("$RED$text"))
+ }
+ false
+ } else {
+ true
+ }
+ }
+ }
+
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/dev/DiagCommand.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/dev/DiagCommand.kt
new file mode 100644
index 00000000..3e5e7b9b
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/dev/DiagCommand.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.commands.dev
+
+import com.mojang.brigadier.arguments.BoolArgumentType.bool
+import io.github.moulberry.notenoughupdates.NotEnoughUpdates
+import io.github.moulberry.notenoughupdates.autosubscribe.NEUAutoSubscribe
+import io.github.moulberry.notenoughupdates.events.RegisterBrigadierCommandEvent
+import io.github.moulberry.notenoughupdates.miscfeatures.CrystalMetalDetectorSolver
+import io.github.moulberry.notenoughupdates.miscfeatures.CrystalWishingCompassSolver
+import io.github.moulberry.notenoughupdates.options.customtypes.NEUDebugFlag
+import io.github.moulberry.notenoughupdates.util.brigadier.*
+import io.github.moulberry.notenoughupdates.util.brigadier.EnumArgumentType.Companion.enum
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+
+// Why is this not merged into /neudevtest
+@NEUAutoSubscribe
+class DiagCommand {
+ @SubscribeEvent
+ fun onCommands(event: RegisterBrigadierCommandEvent) {
+ event.command("neudiag") {
+ thenLiteral("metal") {
+ thenLiteral("center") {
+ thenArgumentExecute("usecenter", bool()) { useCenter ->
+ CrystalMetalDetectorSolver.setDebugDoNotUseCenter(this[useCenter])
+ reply("Center coordinates-based solutions ${if (this[useCenter]) "enabled" else "disabled"}")
+ }.withHelp("Toggle coordinate based solutions")
+ }
+ thenExecute {
+ CrystalMetalDetectorSolver.logDiagnosticData(true)
+ reply("Enabled metal detector diagnostic logging.")
+ }
+ }.withHelp("Enable metal detector diagnostics")
+ thenLiteralExecute("wishing") {
+ CrystalWishingCompassSolver.getInstance().logDiagnosticData(true)
+ reply("Enabled wishing compass diagnostic logging")
+ }.withHelp("Enable wishing compass diagnostic logging")
+ thenLiteral("debug") {
+ thenLiteralExecute("list") {
+ reply("Here are all flags:\n${NEUDebugFlag.getFlagList()}")
+ }.withHelp("List all debug diagnostic logging flags")
+ thenLiteral("setflag") {
+ thenArgument("flag", enum<NEUDebugFlag>()) { flag ->
+ thenArgumentExecute("enable", bool()) { enable ->
+ val debugFlags = NotEnoughUpdates.INSTANCE.config.hidden.debugFlags
+ if (this[enable]) {
+ debugFlags.add(this[flag])
+ } else {
+ debugFlags.remove(this[flag])
+ }
+ reply("${if(this[enable]) "Enabled" else "Disabled"} the flag ${this[flag]}.")
+ }.withHelp("Enable or disable a diagnostic logging stream")
+ }
+ }
+ thenExecute {
+ reply("Effective debug flags: \n${NEUDebugFlag.getEnabledFlags()}")
+ }
+ }.withHelp("Log diagnostic data.")
+ }
+ }
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/dev/NEUStatsCommand.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/dev/NEUStatsCommand.kt
new file mode 100644
index 00000000..7035aaa3
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/dev/NEUStatsCommand.kt
@@ -0,0 +1,209 @@
+/*
+ * 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.commands.dev
+
+import com.mojang.brigadier.context.CommandContext
+import com.sun.management.OperatingSystemMXBean
+import com.sun.management.UnixOperatingSystemMXBean
+import io.github.moulberry.notenoughupdates.NotEnoughUpdates
+import io.github.moulberry.notenoughupdates.autosubscribe.NEUAutoSubscribe
+import io.github.moulberry.notenoughupdates.events.RegisterBrigadierCommandEvent
+import io.github.moulberry.notenoughupdates.util.DiscordMarkdownBuilder
+import io.github.moulberry.notenoughupdates.util.HastebinUploader
+import io.github.moulberry.notenoughupdates.util.SBInfo
+import io.github.moulberry.notenoughupdates.util.brigadier.reply
+import io.github.moulberry.notenoughupdates.util.brigadier.thenExecute
+import io.github.moulberry.notenoughupdates.util.brigadier.thenLiteralExecute
+import io.github.moulberry.notenoughupdates.util.brigadier.withHelp
+import net.minecraft.client.Minecraft
+import net.minecraft.client.renderer.OpenGlHelper
+import net.minecraft.command.ICommandSender
+import net.minecraft.util.EnumChatFormatting.DARK_RED
+import net.minecraft.util.EnumChatFormatting.GREEN
+import net.minecraftforge.common.ForgeVersion
+import net.minecraftforge.fml.client.FMLClientHandler
+import net.minecraftforge.fml.common.Loader
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+import org.lwjgl.opengl.Display
+import org.lwjgl.opengl.GL11
+import java.awt.Toolkit
+import java.awt.datatransfer.StringSelection
+import java.lang.management.ManagementFactory
+import java.util.concurrent.CompletableFuture
+import javax.management.JMX
+import javax.management.ObjectName
+
+@NEUAutoSubscribe
+class NEUStatsCommand {
+ @SubscribeEvent
+ fun onCommands(event: RegisterBrigadierCommandEvent) {
+ event.command("stats", "neustats") {
+ thenLiteralExecute("modlist") {
+ clipboardAndSendMessage(
+ DiscordMarkdownBuilder()
+ .also(::appendModList)
+ .toString()
+ )
+ }.withHelp("Copy the mod list to your clipboard")
+ thenLiteralExecute("full") {
+ clipboardAndSendMessage(
+ DiscordMarkdownBuilder()
+ .also(::appendStats)
+ .also(::appendModList)
+ .toString()
+ )
+ }.withHelp("Copy the full list of all NEU stats and your mod list to your clipboard")
+ thenLiteralExecute("dump") {
+ reply("${GREEN}This will upload a dump of the java classes your game has loaded how big they are and how many there are. This can take a few seconds as it is uploading to HasteBin.")
+ uploadDataUsageDump().thenAccept {
+ clipboardAndSendMessage(it)
+ }
+ }.withHelp("Dump all loaded classes and their memory usage and copy that to your clipboard.")
+ thenExecute {
+ clipboardAndSendMessage(
+ DiscordMarkdownBuilder()
+ .also(::appendStats)
+ .also {
+ if (Loader.instance().activeModList.size <= 15) appendModList(it)
+ }
+ .toString()
+ )
+ }
+ }.withHelp("Copy a list of NEU relevant stats to your clipboard for debugging purposes")
+ }
+ interface DiagnosticCommandMXBean {
+ fun gcClassHistogram(array: Array<String>): String
+ }
+
+ private fun uploadDataUsageDump(): CompletableFuture<String?> {
+ return CompletableFuture.supplyAsync {
+ try {
+ val server =
+ ManagementFactory.getPlatformMBeanServer()
+ val objectName =
+ ObjectName.getInstance("com.sun.management:type=DiagnosticCommand")
+ val proxy = JMX.newMXBeanProxy(
+ server,
+ objectName,
+ DiagnosticCommandMXBean::class.java
+ )
+ HastebinUploader.upload(
+ proxy.gcClassHistogram(emptyArray()).replace("[", "[]"),
+ HastebinUploader.Mode.NORMAL
+ )
+ } catch (e: Exception) {
+ null
+ }
+ }
+
+ }
+
+
+ private fun getMemorySize(): Long {
+ try {
+ return (ManagementFactory.getOperatingSystemMXBean() as OperatingSystemMXBean).totalPhysicalMemorySize
+ } catch (e: java.lang.Exception) {
+ try {
+ return (ManagementFactory.getOperatingSystemMXBean() as UnixOperatingSystemMXBean).totalPhysicalMemorySize
+ } catch (ignored: java.lang.Exception) { /*IGNORE*/
+ }
+ }
+ return -1
+ }
+
+ val ONE_MB = 1024L * 1024L
+ private fun appendStats(builder: DiscordMarkdownBuilder) {
+ val maxMemory = Runtime.getRuntime().maxMemory()
+ val totalMemory = Runtime.getRuntime().totalMemory()
+ val freeMemory = Runtime.getRuntime().freeMemory()
+ val currentMemory = totalMemory - freeMemory
+ builder.category("System Stats")
+ builder.append("OS", System.getProperty("os.name"))
+ builder.append("CPU", OpenGlHelper.getCpu())
+ builder.append(
+ "Display",
+ String.format("%dx%d (%s)", Display.getWidth(), Display.getHeight(), GL11.glGetString(GL11.GL_VENDOR))
+ )
+ builder.append("GPU", GL11.glGetString(GL11.GL_RENDERER))
+ builder.append("GPU Driver", GL11.glGetString(GL11.GL_VERSION))
+ if (getMemorySize() > 0)
+ builder.append(
+ "Maximum Memory",
+ "${getMemorySize() / ONE_MB}MB"
+ )
+ builder.append("Shaders", ("" + OpenGlHelper.isFramebufferEnabled()).uppercase())
+ builder.category("Java Stats")
+ builder.append(
+ "Java",
+ "${System.getProperty("java.version")} ${if (Minecraft.getMinecraft().isJava64bit) 64 else 32}bit",
+ )
+ builder.append(
+ "Memory", String.format(
+ "% 2d%% %03d/%03dMB",
+ currentMemory * 100L / maxMemory,
+ currentMemory / ONE_MB,
+ maxMemory / ONE_MB
+ )
+ )
+ builder.append(
+ "Memory Allocated",
+ String.format("% 2d%% %03dMB", totalMemory * 100L / maxMemory, totalMemory / ONE_MB)
+ )
+ builder.category("Game Stats")
+ builder.append("FPS", Minecraft.getDebugFPS().toString())
+ builder.append("Loaded Mods", Loader.instance().activeModList.size)
+ builder.append("Forge", ForgeVersion.getVersion())
+ builder.append("Optifine", if (FMLClientHandler.instance().hasOptifine()) "TRUE" else "FALSE")
+ builder.category("Neu Settings")
+ builder.append("API Key", if (NotEnoughUpdates.INSTANCE.config.apiData.apiKey.isEmpty()) "FALSE" else "TRUE")
+ builder.append("On SkyBlock", if (NotEnoughUpdates.INSTANCE.hasSkyblockScoreboard()) "TRUE" else "FALSE")
+ builder.append(
+ "Mod Version",
+ Loader.instance().indexedModList[NotEnoughUpdates.MODID]!!.displayVersion
+ )
+ builder.append("SB Profile", SBInfo.getInstance().currentProfile)
+ builder.append("Has Advanced Tab", if (SBInfo.getInstance().hasNewTab) "TRUE" else "FALSE")
+ builder.category("Repo Stats")
+ builder.append("Last Commit", NotEnoughUpdates.INSTANCE.manager.latestRepoCommit)
+ builder.append("Loaded Items", NotEnoughUpdates.INSTANCE.manager.itemInformation.size.toString())
+ }
+
+ private fun appendModList(builder: DiscordMarkdownBuilder): DiscordMarkdownBuilder {
+ builder.category("Mods Loaded")
+ Loader.instance().activeModList.forEach {
+ builder.append(it.name, "${it.source} (${it.displayVersion})")
+ }
+ return builder
+ }
+
+ fun CommandContext<ICommandSender>.clipboardAndSendMessage(data: String?) {
+ if (data == null) {
+ reply("${DARK_RED}Error occurred trying to perform command.")
+ return
+ }
+ try {
+ val clipboard = StringSelection(data)
+ Toolkit.getDefaultToolkit().systemClipboard.setContents(clipboard, null)
+ reply("${GREEN}Dev info copied to clipboard.")
+ } catch (ignored: Exception) {
+ reply("${DARK_RED}Could not copy to clipboard.")
+ }
+ }
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/dev/PackDevCommand.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/dev/PackDevCommand.kt
new file mode 100644
index 00000000..fceacfab
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/dev/PackDevCommand.kt
@@ -0,0 +1,165 @@
+/*
+ * 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.commands.dev
+
+import com.mojang.brigadier.arguments.DoubleArgumentType.doubleArg
+import com.mojang.brigadier.builder.ArgumentBuilder
+import io.github.moulberry.notenoughupdates.NotEnoughUpdates
+import io.github.moulberry.notenoughupdates.autosubscribe.NEUAutoSubscribe
+import io.github.moulberry.notenoughupdates.core.util.MiscUtils
+import io.github.moulberry.notenoughupdates.events.RegisterBrigadierCommandEvent
+import io.github.moulberry.notenoughupdates.util.brigadier.*
+import net.minecraft.client.Minecraft
+import net.minecraft.client.entity.AbstractClientPlayer
+import net.minecraft.command.ICommandSender
+import net.minecraft.entity.Entity
+import net.minecraft.entity.EntityLiving
+import net.minecraft.entity.EntityLivingBase
+import net.minecraft.entity.item.EntityArmorStand
+import net.minecraft.entity.player.EntityPlayer
+import net.minecraft.item.ItemStack
+import net.minecraft.util.EnumChatFormatting
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+
+@NEUAutoSubscribe
+class PackDevCommand {
+
+ fun <T : EntityLivingBase, U : ArgumentBuilder<ICommandSender, U>> U.npcListCommand(
+ name: String,
+ singleCommand: String,
+ multipleCommand: String,
+ clazz: Class<T>,
+ provider: () -> List<Entity>
+ ) {
+ fun getEntities(distance: Double): List<T> {
+ val distanceSquared = distance * distance
+ val thePlayer = Minecraft.getMinecraft().thePlayer
+ return provider()
+ .asSequence()
+ .filterIsInstance(clazz)
+ .filter { it != thePlayer }
+ .filter { it.getDistanceSqToEntity(thePlayer) < distanceSquared }
+ .toList()
+ }
+
+ thenLiteral(singleCommand) {
+ thenArgumentExecute("distance", doubleArg(0.0)) { dist ->
+ val dist = this[dist]
+ val entity = getEntities(dist).minByOrNull { it.getDistanceSqToEntity(Minecraft.getMinecraft().thePlayer) }
+ if (entity == null) {
+ reply("No $name found within $dist blocks")
+ return@thenArgumentExecute
+ }
+ MiscUtils.copyToClipboard(StringBuilder().appendEntityData(entity).toString().trim())
+ reply("Copied data to clipboard")
+ }.withHelp("Find the nearest $name and copy data about them to your clipboard")
+ }
+ thenLiteral(multipleCommand) {
+ thenArgumentExecute("distance", doubleArg(0.0)) { dist ->
+ val dist = this[dist]
+ val entity = getEntities(dist)
+ val sb = StringBuilder()
+ reply("Found ${entity.size} ${name}s")
+ if (entity.isNotEmpty()) {
+ entity.forEach {
+ sb.appendEntityData(it)
+ }
+ MiscUtils.copyToClipboard(sb.toString().trim())
+
+ reply("Copied data to clipboard")
+ }
+ }.withHelp("Find all $name within range and copy data about them to your clipboard")
+ }
+ }
+
+ fun StringBuilder.appendEntityData(entity: EntityLivingBase) {
+ if (entity is EntityPlayer) {
+ append("Player UUID: ")
+ appendLine(entity.uniqueID)
+ if (entity is AbstractClientPlayer) {
+ append("Entity Texture Id: ")
+ appendLine(entity.locationSkin.resourcePath?.replace("skins/", ""))
+ }
+ }
+ append("Custom Name Tag: ")
+ appendLine(entity.customNameTag ?: "null")
+ append("Mob: ")
+ appendLine(entity.name)
+ append("Entity Id: ")
+ appendLine(entity.entityId)
+
+ appendItemData("Item", entity.heldItem)
+
+ for ((slot, name) in listOf("Boots", "Leggings", "Chestplate", "Helmet").withIndex()) {
+ val armorPiece = entity.getCurrentArmor(slot)
+ appendItemData(name, armorPiece)
+ }
+ appendLine()
+ appendLine()
+ }
+
+ fun StringBuilder.appendItemData(name: String, item: ItemStack?) {
+ append("$name: ")
+ if (item != null) {
+ appendLine(item)
+ append("$name Display Name")
+ appendLine(item.displayName)
+ append("$name Tag Compound: ")
+ val compound = item.tagCompound
+ if (compound == null) {
+ appendLine("null")
+ } else {
+ appendLine(compound)
+ append("$name Tag Compound Extra Attributes")
+ appendLine(compound.getTag("ExtraAttributes"))
+ }
+ } else {
+ appendLine("null")
+ }
+
+ }
+
+
+ @SubscribeEvent
+ fun onCommands(event: RegisterBrigadierCommandEvent) {
+ event.command("neupackdev") {
+ npcListCommand("Player", "getplayer", "getplayers", AbstractClientPlayer::class.java) {
+ Minecraft.getMinecraft().theWorld.playerEntities
+ }
+ npcListCommand("NPC", "getnpc", "getnpcs", AbstractClientPlayer::class.java) {
+ Minecraft.getMinecraft().theWorld.playerEntities.filter { it.uniqueID?.version() != 4 }
+ }
+ npcListCommand("mob", "getmob", "getmobs", EntityLiving::class.java) {
+ Minecraft.getMinecraft().theWorld.loadedEntityList
+ }
+ npcListCommand("armor stand", "getarmorstand", "getarmorstands", EntityArmorStand::class.java) {
+ Minecraft.getMinecraft().theWorld.loadedEntityList
+ }
+ thenExecute {
+ NotEnoughUpdates.INSTANCE.packDevEnabled = !NotEnoughUpdates.INSTANCE.packDevEnabled
+ if (NotEnoughUpdates.INSTANCE.packDevEnabled) {
+ reply("${EnumChatFormatting.GREEN}Enabled pack developer mode.")
+ } else {
+ reply("${EnumChatFormatting.RED}Disabled pack developer mode.")
+ }
+ }
+ }.withHelp("Toggle pack developer mode")
+ }
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/dev/SimpleDevCommands.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/dev/SimpleDevCommands.kt
new file mode 100644
index 00000000..95a6500e
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/dev/SimpleDevCommands.kt
@@ -0,0 +1,110 @@
+/*
+ * 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.commands.dev
+
+import com.mojang.brigadier.arguments.FloatArgumentType.floatArg
+import com.mojang.brigadier.arguments.StringArgumentType.string
+import io.github.moulberry.notenoughupdates.NotEnoughUpdates
+import io.github.moulberry.notenoughupdates.autosubscribe.NEUAutoSubscribe
+import io.github.moulberry.notenoughupdates.dungeons.DungeonWin
+import io.github.moulberry.notenoughupdates.events.RegisterBrigadierCommandEvent
+import io.github.moulberry.notenoughupdates.miscfeatures.NullzeeSphere
+import io.github.moulberry.notenoughupdates.util.brigadier.*
+import net.minecraft.client.entity.EntityPlayerSP
+import net.minecraft.event.ClickEvent
+import net.minecraft.util.BlockPos
+import net.minecraft.util.ChatComponentText
+import net.minecraft.util.ResourceLocation
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+import java.util.function.Consumer
+
+@NEUAutoSubscribe
+class SimpleDevCommands {
+ @SubscribeEvent
+ fun onCommands(event: RegisterBrigadierCommandEvent) {
+ event.command("neureloadrepo") {
+ thenExecute {
+ NotEnoughUpdates.INSTANCE.manager.reloadRepository()
+ reply("Reloaded repository.")
+ }
+ }.withHelp("Reload the NEU data repository from disk (not from network)")
+ event.command("neudungeonwintest") {
+ thenArgumentExecute("file", string()) { file ->
+ DungeonWin.TEAM_SCORE = ResourceLocation("notenoughupdates:dungeon_win/${this[file].lowercase()}.png")
+ reply("Changed the dungeon win display")
+ }.withHelp("Change the dungeon win test to load from a file")
+ thenExecute {
+ DungeonWin.displayWin()
+ }
+ }.withHelp("Display the dungeon win pop up")
+ event.command("neuenablestorage") {
+ thenLiteralExecute("disable") {
+ NotEnoughUpdates.INSTANCE.config.storageGUI.enableStorageGUI3 = true
+ NotEnoughUpdates.INSTANCE.saveConfig()
+ reply("Disabled the NEU storage overlay. Click here to enable again") {
+ chatStyle.chatClickEvent = ClickEvent(
+ ClickEvent.Action.SUGGEST_COMMAND,
+ "/neuenablestorage"
+ )
+ }
+ }.withHelp("Disable the neu storage overlay")
+ thenExecute {
+ NotEnoughUpdates.INSTANCE.config.storageGUI.enableStorageGUI3 = true
+ NotEnoughUpdates.INSTANCE.saveConfig()
+ reply("Enabled the NEU storage overlay. Click here to disable again") {
+ chatStyle.chatClickEvent = ClickEvent(
+ ClickEvent.Action.SUGGEST_COMMAND,
+ "/neuenablestorage disable"
+ )
+ }
+ }
+ }.withHelp("Enable the neu storage overlay")
+ event.command("neuzeesphere") {
+ thenLiteralExecute("on") {
+ NullzeeSphere.enabled = true
+ reply("Enabled nullzee sphere")
+ }.withHelp("Enable nullzee sphere")
+ thenLiteralExecute("off") {
+ NullzeeSphere.enabled = false
+ reply("Disabled nullzee sphere")
+ }.withHelp("Disable nullzee sphere")
+ thenLiteralExecute("setcenter") {
+ val p = source as EntityPlayerSP
+ NullzeeSphere.centerPos = BlockPos(p.posX, p.posY, p.posZ)
+ NullzeeSphere.overlayVBO = null
+ reply("Set center to ${NullzeeSphere.centerPos}")
+ }.withHelp("Set the center of the nullzee sphere")
+ thenArgumentExecute("radius", floatArg(0F)) { size ->
+ NullzeeSphere.size = this[size]
+ NullzeeSphere.overlayVBO = null
+ reply("Set size to ${this[size]}")
+ }.withHelp("Set the radius of the nullzee sphere")
+ }
+ event.command("neuresetrepo") {
+ thenExecute {
+ NotEnoughUpdates.INSTANCE.manager
+ .userFacingRepositoryReload()
+ .thenAccept { it
+ it.forEach(::reply)
+ }
+ }
+ }.withHelp("Reload the NEU data repository from network")
+ }
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/help/FeaturesCommand.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/help/FeaturesCommand.kt
new file mode 100644
index 00000000..6cc4e255
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/help/FeaturesCommand.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.commands.help
+
+import io.github.moulberry.notenoughupdates.autosubscribe.NEUAutoSubscribe
+import io.github.moulberry.notenoughupdates.events.RegisterBrigadierCommandEvent
+import io.github.moulberry.notenoughupdates.util.Constants
+import io.github.moulberry.notenoughupdates.util.Utils
+import io.github.moulberry.notenoughupdates.util.brigadier.reply
+import io.github.moulberry.notenoughupdates.util.brigadier.thenExecute
+import io.github.moulberry.notenoughupdates.util.brigadier.withHelp
+import net.minecraft.event.ClickEvent
+import net.minecraft.util.ChatComponentText
+import net.minecraft.util.EnumChatFormatting
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+
+@NEUAutoSubscribe
+class FeaturesCommand {
+ @SubscribeEvent
+ fun onCommands(event: RegisterBrigadierCommandEvent) {
+ event.command("neufeatures") {
+ thenExecute {
+ reply("")
+ val url = Constants.MISC?.get("featureslist")?.asString
+ if (url == null) {
+ Utils.showOutdatedRepoNotification()
+ return@thenExecute
+ }
+
+ if (Utils.openUrl(url)) {
+ reply(
+ EnumChatFormatting.DARK_PURPLE.toString() + "" + EnumChatFormatting.BOLD + "NEU" + EnumChatFormatting.RESET +
+ EnumChatFormatting.GOLD + "> Opening Feature List in browser."
+ )
+ } else {
+ val clickTextFeatures = ChatComponentText(
+ (EnumChatFormatting.DARK_PURPLE.toString() + "" + EnumChatFormatting.BOLD + "NEU" + EnumChatFormatting.RESET +
+ EnumChatFormatting.GOLD + "> Click here to open the Feature List in your browser.")
+ )
+ clickTextFeatures.chatStyle =
+ Utils.createClickStyle(ClickEvent.Action.OPEN_URL, url)
+ reply(clickTextFeatures)
+ }
+ reply("")
+ }
+ }.withHelp("List all of NEUs features")
+ }
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/help/HelpCommand.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/help/HelpCommand.kt
new file mode 100644
index 00000000..1b4f817e
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/help/HelpCommand.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.commands.help
+
+import com.mojang.brigadier.arguments.StringArgumentType.string
+import io.github.moulberry.notenoughupdates.NotEnoughUpdates
+import io.github.moulberry.notenoughupdates.autosubscribe.NEUAutoSubscribe
+import io.github.moulberry.notenoughupdates.events.RegisterBrigadierCommandEvent
+import io.github.moulberry.notenoughupdates.util.brigadier.*
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+
+@NEUAutoSubscribe
+class HelpCommand {
+ val neuHelpMessages = listOf(
+ "§5§lNotEnoughUpdates commands",
+ "§6/neu §7- Opens the main NEU GUI.",
+ "§6/pv §b?{name} §2ⴵ §r§7- Opens the profile viewer",
+ "§6/neusouls {on/off/clear/unclear} §r§7- Shows waypoints to fairy souls.",
+ "§6/neubuttons §r§7- Opens a GUI which allows you to customize inventory buttons.",
+ "§6/neuec §r§7- Opens the enchant colour GUI.",
+ "§6/join {floor} §r§7- Short Command to join a Dungeon. §lNeed a Party of 5 People§r§7 {4/f7/m5}.",
+ "§6/neucosmetics §r§7- Opens the cosmetic GUI.",
+ "§6/neurename §r§7- Opens the NEU Item Customizer.",
+ "§6/cata §b?{name} §2ⴵ §r§7- Opens the profile viewer's Catacombs page.",
+ "§6/neulinks §r§7- Shows links to NEU/Moulberry.",
+ "§6/neuoverlay §r§7- Opens GUI Editor for quickcommands and searchbar.",
+ "§6/neuah §r§7- Opens NEU's custom auction house GUI.",
+ "§6/neucalendar §r§7- Opens NEU's custom calendar GUI.",
+ "§6/neucalc §r§7- Run calculations.",
+ "",
+ "§6§lOld commands:",
+ "§6/peek §b?{user} §2ⴵ §r§7- Shows quick stats for a user.",
+ "",
+ "§6§lDebug commands:",
+ "§6/neustats §r§7- Copies helpful info to the clipboard.",
+ "§6/neustats modlist §r§7- Copies mod list info to clipboard.",
+ "§6/neuresetrepo §r§7- Deletes all repo files.",
+ "§6/neureloadrepo §r§7- Debug command with repo.",
+ "",
+ "§6§lDev commands:",
+ "§6/neupackdev §r§7- pack creator command - getnpc, getmob(s), getarmorstand(s), getall. Optional radius argument for all."
+ )
+ val neuDevHelpMessages = listOf(
+ "§6/neudevtest §r§7- dev test command",
+ "§6/neuzeephere §r§7- sphere",
+ "§6/neudungeonwintest §r§7- displays the dungeon win screen"
+ )
+ val helpInfo = listOf(
+ "",
+ "§7Commands marked with a §2\"ⴵ\"§7 require an api key. You can set your api key via \"/api new\" or by manually putting it in the api field in \"/neu\"",
+ "",
+ "§7Arguments marked with a §b\"?\"§7 are optional.",
+ "",
+ "§6§lScroll up to see everything"
+ )
+
+ @SubscribeEvent
+ fun onCommands(event: RegisterBrigadierCommandEvent) {
+ event.command("neuhelp") {
+ thenArgumentExecute("command", string()) { commandName ->
+ val commandNode = event.dispatcher.root.getChild(this[commandName])
+ if (commandNode == null) {
+ reply("Could not find NEU command with name ${this[commandName]}")
+ return@thenArgumentExecute
+ }
+ reply(event.brigadierRoot.getAllUsages("/${this[commandName]}", commandNode).joinToString("\n"){
+ "${it.path} - ${it.help}"
+ })
+ }.withHelp("Display help for a specific NEU command")
+ thenExecute {
+ neuHelpMessages.forEach(::reply)
+ if (NotEnoughUpdates.INSTANCE.config.hidden.dev)
+ neuDevHelpMessages.forEach(::reply)
+ helpInfo.forEach(::reply)
+ }
+ }.withHelp("Display a list of all NEU commands")
+ }
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/help/LinksCommand.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/help/LinksCommand.kt
new file mode 100644
index 00000000..957948ae
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/help/LinksCommand.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.commands.help
+
+import io.github.moulberry.notenoughupdates.NotEnoughUpdates
+import io.github.moulberry.notenoughupdates.autosubscribe.NEUAutoSubscribe
+import io.github.moulberry.notenoughupdates.events.RegisterBrigadierCommandEvent
+import io.github.moulberry.notenoughupdates.util.Utils
+import io.github.moulberry.notenoughupdates.util.brigadier.reply
+import io.github.moulberry.notenoughupdates.util.brigadier.thenExecute
+import io.github.moulberry.notenoughupdates.util.brigadier.withHelp
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+
+@NEUAutoSubscribe
+class LinksCommand {
+ @SubscribeEvent
+ fun onCommands(event: RegisterBrigadierCommandEvent) {
+ event.command("neulinks") {
+ thenExecute {
+ val manager = NotEnoughUpdates.INSTANCE.manager
+ val updateJsonFile = manager.repoLocation.resolve("update.json")
+ if (!updateJsonFile.exists()) {
+ Utils.showOutdatedRepoNotification()
+ return@thenExecute
+ }
+ try {
+ val updateJson = manager.getJsonFromFile(updateJsonFile)
+ NotEnoughUpdates.INSTANCE.displayLinks(updateJson, 0)
+ } catch (_: Exception) {
+ Utils.showOutdatedRepoNotification()
+ }
+ }
+ }.withHelp("Display links for Moulberry and NEU")
+ }
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/help/SettingsCommand.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/help/SettingsCommand.kt
new file mode 100644
index 00000000..fe58c807
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/help/SettingsCommand.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.commands.help
+
+import io.github.moulberry.notenoughupdates.NotEnoughUpdates
+import io.github.moulberry.notenoughupdates.autosubscribe.NEUAutoSubscribe
+import io.github.moulberry.notenoughupdates.core.GuiScreenElementWrapper
+import io.github.moulberry.notenoughupdates.core.config.struct.ConfigProcessor
+import io.github.moulberry.notenoughupdates.events.RegisterBrigadierCommandEvent
+import io.github.moulberry.notenoughupdates.options.NEUConfigEditor
+import io.github.moulberry.notenoughupdates.util.brigadier.*
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+
+@NEUAutoSubscribe
+class SettingsCommand {
+ @SubscribeEvent
+ fun onCommands(event: RegisterBrigadierCommandEvent) {
+ event.command("neu", "neusettings") {
+ thenArgument("search", RestArgumentType) { search ->
+ suggestsList(ConfigProcessor.create(NotEnoughUpdates.INSTANCE.config).keys.toList())
+ thenExecute {
+ NotEnoughUpdates.INSTANCE.openGui = GuiScreenElementWrapper(
+ NEUConfigEditor(
+ NotEnoughUpdates.INSTANCE.config,
+ this[search]
+ )
+ )
+ }
+ }.withHelp("Search the NEU settings")
+ thenExecute {
+ NotEnoughUpdates.INSTANCE.openGui =
+ GuiScreenElementWrapper(NEUConfigEditor(NotEnoughUpdates.INSTANCE.config))
+ }
+ }.withHelp("Open the NEU settings")
+ }
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/help/StorageViewerWhyCommand.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/help/StorageViewerWhyCommand.kt
new file mode 100644
index 00000000..782eaf3d
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/help/StorageViewerWhyCommand.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.commands.help
+
+import io.github.moulberry.notenoughupdates.autosubscribe.NEUAutoSubscribe
+import io.github.moulberry.notenoughupdates.events.RegisterBrigadierCommandEvent
+import io.github.moulberry.notenoughupdates.util.NotificationHandler
+import io.github.moulberry.notenoughupdates.util.brigadier.thenExecute
+import io.github.moulberry.notenoughupdates.util.brigadier.withHelp
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+
+@NEUAutoSubscribe
+class StorageViewerWhyCommand {
+ @SubscribeEvent
+ fun onCommands(event: RegisterBrigadierCommandEvent) {
+ event.command("neustwhy") {
+ thenExecute {
+ NotificationHandler.displayNotification(
+ listOf(
+ "§eStorage Viewer",
+ "§7Currently, the storage viewer requires you to click twice",
+ "§7in order to switch between pages. This is because Hypixel",
+ "§7has not yet added a shortcut command to go to any enderchest/",
+ "§7storage page.",
+ "§7While it is possible to send the second click",
+ "§7automatically, doing so violates Hypixel's new mod rules."
+ ), true
+ )
+ }
+ }.withHelp("Display information about why you have to click twice in the NEU storage overlay")
+ }
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/misc/AhCommand.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/misc/AhCommand.kt
new file mode 100644
index 00000000..c7d80487
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/misc/AhCommand.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.commands.misc
+
+import io.github.moulberry.notenoughupdates.NotEnoughUpdates
+import io.github.moulberry.notenoughupdates.auction.CustomAHGui
+import io.github.moulberry.notenoughupdates.autosubscribe.NEUAutoSubscribe
+import io.github.moulberry.notenoughupdates.events.RegisterBrigadierCommandEvent
+import io.github.moulberry.notenoughupdates.util.Utils
+import io.github.moulberry.notenoughupdates.util.brigadier.RestArgumentType
+import io.github.moulberry.notenoughupdates.util.brigadier.get
+import io.github.moulberry.notenoughupdates.util.brigadier.reply
+import io.github.moulberry.notenoughupdates.util.brigadier.thenArgumentExecute
+import net.minecraft.util.EnumChatFormatting.RED
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+import java.util.function.Predicate
+
+@NEUAutoSubscribe
+class AhCommand {
+ @SubscribeEvent
+ fun onCommands(event: RegisterBrigadierCommandEvent) {
+ val hook = event.command("neuah") {
+
+ thenArgumentExecute("search", RestArgumentType) { search ->
+ if (NotEnoughUpdates.INSTANCE.config.apiData.apiKey == null ||
+ NotEnoughUpdates.INSTANCE.config.apiData.apiKey.isBlank()
+ ) {
+ reply("${RED}Can't open NEU AH: an api key is not set. Run /api new and put the result in settings.")
+ return@thenArgumentExecute
+ }
+ NotEnoughUpdates.INSTANCE.openGui = CustomAHGui()
+ NotEnoughUpdates.INSTANCE.manager.auctionManager.customAH.lastOpen = System.currentTimeMillis()
+ NotEnoughUpdates.INSTANCE.manager.auctionManager.customAH.clearSearch()
+ NotEnoughUpdates.INSTANCE.manager.auctionManager.customAH.updateSearch()
+
+ val search = this[search]
+
+ NotEnoughUpdates.INSTANCE.manager.auctionManager.customAH.setSearch(
+ if (search.isBlank() && NotEnoughUpdates.INSTANCE.config.neuAuctionHouse.saveLastSearch)
+ null else search
+ )
+ }
+ }
+ hook.beforeCommand = Predicate {
+ if (!NotEnoughUpdates.INSTANCE.hasSkyblockScoreboard())
+ Utils.addChatMessage("${RED}You must be on SkyBlock to use this feature.")
+ NotEnoughUpdates.INSTANCE.hasSkyblockScoreboard()
+ }
+ }
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/misc/DungeonCommands.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/misc/DungeonCommands.kt
new file mode 100644
index 00000000..3b721df5
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/misc/DungeonCommands.kt
@@ -0,0 +1,144 @@
+/*
+ * 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.commands.misc
+
+import com.google.gson.JsonObject
+import com.mojang.brigadier.arguments.StringArgumentType.string
+import io.github.moulberry.notenoughupdates.NotEnoughUpdates
+import io.github.moulberry.notenoughupdates.autosubscribe.NEUAutoSubscribe
+import io.github.moulberry.notenoughupdates.dungeons.GuiDungeonMapEditor
+import io.github.moulberry.notenoughupdates.events.RegisterBrigadierCommandEvent
+import io.github.moulberry.notenoughupdates.util.brigadier.*
+import net.minecraft.block.material.MapColor
+import net.minecraft.client.Minecraft
+import net.minecraft.item.ItemMap
+import net.minecraft.util.EnumChatFormatting.GREEN
+import net.minecraft.util.EnumChatFormatting.RED
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+import java.awt.Color
+
+@NEUAutoSubscribe
+class DungeonCommands {
+ @SubscribeEvent
+ fun onCommands(event: RegisterBrigadierCommandEvent) {
+ event.command("dh") {
+ thenExecute {
+ Minecraft.getMinecraft().thePlayer.sendChatMessage("/warp dungeon_hub")
+ }
+ }.withHelp("Warps to the dungeon hub")
+ event.command("dn") {
+ thenExecute {
+ Minecraft.getMinecraft().thePlayer.sendChatMessage("/warp dungeon_hub")
+ reply("Warping to...")
+ reply("Deez nuts lmao")
+ }
+ }.withHelp("Warps to the dungeon nuts")
+ event.command("join") {
+ thenArgument("floor", string()) { floor ->
+ suggestsList((1..7).flatMap { listOf("f$it", "m$it") })
+ thenExecute {
+ val floor = this[floor]
+ val prefix = if (floor.startsWith("m")) "master_catacombs" else "catacombs"
+ val level = floor.lastOrNull()?.digitToIntOrNull()
+ val cmd = "/joindungeon $prefix ${floor.lastOrNull()}"
+ reply("Running command: $cmd")
+ Minecraft.getMinecraft().thePlayer.sendChatMessage(cmd)
+ }
+ }.withHelp("Join a dungeon floor with a party of 5")
+ }
+ event.command("neumap") {
+ thenLiteral("reset") {
+ thenExecute {
+ NotEnoughUpdates.INSTANCE.colourMap = null
+ reply("Reset color map")
+ }
+ requiresDev()
+ }.withHelp("Reset the colour map")
+ thenLiteral("save") {
+ thenArgument("filename", string()) { fileName ->
+ requiresDev()
+ thenExecute {
+ val stack = Minecraft.getMinecraft().thePlayer.heldItem
+ if (stack == null || stack.item !is ItemMap) {
+ reply("Please hold a map item")
+ return@thenExecute
+ }
+ val map = stack.item as ItemMap
+ val mapData = map.getMapData(stack, Minecraft.getMinecraft().theWorld)
+ if (mapData == null) {
+ reply("Could not grab map data (empty map)")
+ return@thenExecute
+ }
+ val json = JsonObject()
+ for (i in 0 until (128 * 128)) {
+ val x = i % 128
+ val y = i / 128
+ val j = mapData.colors[i].toInt() and 255
+ val c = if (j / 4 == 0) {
+ Color((i + i / 128 and 1) * 8 + 16 shl 24, true)
+ } else {
+ Color(MapColor.mapColorArray[j / 4].getMapColor(j and 3), true)
+ }
+ json.addProperty("$x:$y", c.rgb)
+ }
+ try {
+ NotEnoughUpdates.INSTANCE.manager.configLocation.resolve("maps").mkdirs()
+ NotEnoughUpdates.INSTANCE.manager.writeJson(
+ json,
+ NotEnoughUpdates.INSTANCE.manager.configLocation.resolve("maps/${this[fileName]}.json")
+ )
+ reply("${GREEN}Saved to file.")
+ } catch (e: Exception) {
+ e.printStackTrace()
+ reply("${RED}Failed to save.")
+ }
+ }
+ }.withHelp("Save a colour map from an item")
+ }
+ thenLiteral("load") {
+ thenArgument("filename", string()) { fileName ->
+ requiresDev()
+ thenExecute {
+ val json = NotEnoughUpdates.INSTANCE.manager.getJsonFromFile(
+ NotEnoughUpdates.INSTANCE.manager.configLocation.resolve(
+ "maps/${this[fileName]}.json"
+ )
+ )
+ NotEnoughUpdates.INSTANCE.colourMap = (0 until 128).map { x ->
+ (0 until 128).map { y ->
+ val key = "$x:$y"
+ json[key]?.asInt?.let { Color(it, true) } ?: Color(0, 0, 0, 0)
+ }.toTypedArray()
+ }.toTypedArray()
+ for (x in 0..127) {
+ for (y in 0..127) {
+ NotEnoughUpdates.INSTANCE.colourMap[x][y] = Color(0, 0, 0, 0)
+ }
+ }
+ reply("Loaded colour map from file")
+ }
+ }.withHelp("Load a colour map from a file")
+ }
+ thenExecute {
+ NotEnoughUpdates.INSTANCE.openGui = GuiDungeonMapEditor(null)
+ }
+ }.withHelp("Open the dungeon map editor")
+ }
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/misc/FairySoulsCommand.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/misc/FairySoulsCommand.kt
new file mode 100644
index 00000000..1d766646
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/misc/FairySoulsCommand.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.commands.misc
+
+import io.github.moulberry.notenoughupdates.autosubscribe.NEUAutoSubscribe
+import io.github.moulberry.notenoughupdates.events.RegisterBrigadierCommandEvent
+import io.github.moulberry.notenoughupdates.miscfeatures.FairySouls
+import io.github.moulberry.notenoughupdates.util.brigadier.*
+import net.minecraft.util.EnumChatFormatting.DARK_PURPLE
+import net.minecraft.util.EnumChatFormatting.RED
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+
+@NEUAutoSubscribe
+class FairySoulsCommand {
+
+ @SubscribeEvent
+ fun onCommands(event: RegisterBrigadierCommandEvent) {
+ event.command("neusouls", "fairysouls") {
+ val enable = thenLiteralExecute("enable") {
+ if (!FairySouls.getInstance().isTrackSouls) {
+ reply("${RED}Fairy soul tracking is off, enable it using /neu before using this command")
+ return@thenLiteralExecute
+ }
+ reply("${DARK_PURPLE}Enabled fairy soul waypoints")
+ FairySouls.getInstance().setShowFairySouls(true)
+ }.withHelp("Show fairy soul waypoints")
+ thenLiteral("on") { redirect(enable) }
+ val disable = thenLiteralExecute("disable") {
+ FairySouls.getInstance().setShowFairySouls(false)
+ reply("${DARK_PURPLE}Disabled fairy soul waypoints")
+ }.withHelp("Hide fairy soul waypoints")
+ thenLiteral("off") { redirect(disable) }
+ val clear = thenLiteralExecute("clear") {
+ FairySouls.getInstance().markAllAsFound()
+ // Reply handled by mark all as found
+ }.withHelp("Mark all fairy souls in your current world as found")
+ thenLiteral("markfound") { redirect(clear) }
+ val unclear = thenLiteralExecute("unclear") {
+ FairySouls.getInstance().markAllAsMissing()
+ // Reply handled by mark all as missing
+ }.withHelp("Mark all fairy souls in your current world as not found")
+ thenLiteral("marknotfound") { redirect(unclear) }
+ }
+ }
+}
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
new file mode 100644
index 00000000..caa57909
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/misc/MiscCommands.kt
@@ -0,0 +1,175 @@
+/*
+ * 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.commands.misc
+
+import com.mojang.brigadier.arguments.StringArgumentType.string
+import io.github.moulberry.notenoughupdates.NEUManager
+import io.github.moulberry.notenoughupdates.NotEnoughUpdates
+import io.github.moulberry.notenoughupdates.autosubscribe.NEUAutoSubscribe
+import io.github.moulberry.notenoughupdates.cosmetics.GuiCosmetics
+import io.github.moulberry.notenoughupdates.events.RegisterBrigadierCommandEvent
+import io.github.moulberry.notenoughupdates.miscgui.CalendarOverlay
+import io.github.moulberry.notenoughupdates.miscgui.DynamicLightItemsEditor
+import io.github.moulberry.notenoughupdates.miscgui.GuiItemCustomize
+import io.github.moulberry.notenoughupdates.util.Calculator
+import io.github.moulberry.notenoughupdates.util.Calculator.CalculatorException
+import io.github.moulberry.notenoughupdates.util.MinecraftExecutor
+import io.github.moulberry.notenoughupdates.util.PronounDB
+import io.github.moulberry.notenoughupdates.util.Utils
+import io.github.moulberry.notenoughupdates.util.brigadier.*
+import net.minecraft.client.Minecraft
+import net.minecraft.client.renderer.OpenGlHelper
+import net.minecraft.util.ChatComponentText
+import net.minecraft.util.EnumChatFormatting.*
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+import java.text.DecimalFormat
+import java.util.*
+import java.util.concurrent.CompletableFuture
+
+@NEUAutoSubscribe
+class MiscCommands {
+ @SubscribeEvent
+ fun onCommands(event: RegisterBrigadierCommandEvent) {
+ event.command("neucalc", "neucalculator") {
+ thenArgumentExecute("calculation", RestArgumentType) { calculation ->
+ val calculation = this[calculation]
+ try {
+ val calculate = Calculator.calculate(calculation)
+ val formatter = DecimalFormat("#,##0.##")
+ val formatted = formatter.format(calculate)
+ reply("$WHITE$calculation $YELLOW= $GREEN$formatted")
+ } catch (e: CalculatorException) {
+ reply(
+ "${RED}Error during calculation: ${e.message}\n${WHITE}${calculation.substring(0, e.offset)}" +
+ "${DARK_RED}${calculation.substring(e.offset, e.length + e.offset)}${GRAY}" +
+ calculation.substring(e.length + e.offset)
+ )
+ }
+ }.withHelp("Calculate an expression")
+ thenExecute {
+ reply(
+ "§5It's a calculator.\n" +
+ "§eFor Example §b/neucalc 3m*7k§e.\n" +
+ "§eYou can also use suffixes (k, m, b, t, s)§e.\n" +
+ "§eThe \"s\" suffix acts as 64.\n" +
+ "§eTurn on Sign Calculator in /neu misc to also support this in sign popups and the neu search bar."
+ )
+ }
+ }.withHelp("Display help for NEUs calculator")
+ event.command("neucalendar") {
+ thenExecute {
+ Minecraft.getMinecraft().thePlayer.closeScreen()
+ CalendarOverlay.setEnabled(true)
+ NotEnoughUpdates.INSTANCE.sendChatMessage("/calendar")
+ }
+ }.withHelp("Display NEUs custom calendar overlay")
+ event.command("neucosmetics") {
+ thenExecute {
+ if (!OpenGlHelper.isFramebufferEnabled() && NotEnoughUpdates.INSTANCE.config.notifications.doFastRenderNotif) {
+ reply(
+ "${RED}NEU Cosmetics do not work with OptiFine Fast Render. Go to ESC > Options > Video Settings > Performance > Fast Render to disable it."
+ )
+ }
+ NotEnoughUpdates.INSTANCE.openGui = GuiCosmetics()
+ }
+ }.withHelp("Equip NEU cosmetics")
+ event.command("neucustomize", "neurename") {
+ thenExecute {
+ val held = Minecraft.getMinecraft().thePlayer.heldItem
+ if (held == null) {
+ reply("${RED}You can't customize your hand...")
+ return@thenExecute
+ }
+ val heldUUID = NEUManager.getUUIDForItem(held)
+ if (heldUUID == null) {
+ reply("${RED}This item does not have an UUID, so it cannot be customized.")
+ return@thenExecute
+ }
+
+ NotEnoughUpdates.INSTANCE.openGui = GuiItemCustomize(held, heldUUID)
+ }
+ }.withHelp("Customize your items")
+ event.command("neupronouns", "neuliberals") {
+ thenArgument("user", string()) {user->
+ suggestsList { Minecraft.getMinecraft().theWorld.playerEntities.map { it.name } }
+ thenArgumentExecute("platform", string()) { platform ->
+ fetchPronouns(this[platform], this[user])
+ }.withHelp("Look up someones pronouns using their username on a platform")
+ thenExecute {
+ fetchPronouns("minecraft", this[user])
+ }
+ }.withHelp("Look up someones pronouns using their minecraft username")
+ }
+ event.command("neuupdate", "neuupdates", "enoughupdates") {
+ thenLiteralExecute("check") {
+ NotEnoughUpdates.INSTANCE.autoUpdater.displayUpdateMessageIfOutOfDate()
+ }.withHelp("Check for updates")
+ thenLiteralExecute("scheduledownload") {
+ NotEnoughUpdates.INSTANCE.autoUpdater.scheduleDownload()
+ }.withHelp("Queue a new version of NEU to be downloaded")
+ thenLiteralExecute("updatemodes") {
+ reply("§bTo ensure we do not accidentally corrupt your mod folder, we can only offer support for auto-updates on system with certain capabilities for file deletions (specifically unix systems). You can still manually update your files")
+ }.withHelp("Display an explanation why you cannot auto update")
+ }
+ event.command("neudynamiclights", "neudli", "neudynlights", "neudynamicitems") {
+ thenExecute {
+ NotEnoughUpdates.INSTANCE.openGui = DynamicLightItemsEditor()
+ }
+ }.withHelp("Add items to dynamically emit light")
+ }
+
+ fun fetchPronouns(platform: String, user: String) {
+ val nc = Minecraft.getMinecraft().ingameGUI.chatGUI
+ val id = Random().nextInt()
+ nc.printChatMessageWithOptionalDeletion(ChatComponentText("§e[NEU] Fetching Pronouns..."), id)
+
+ val pronouns = if ("minecraft" == platform) {
+ val c = CompletableFuture<UUID>()
+ NotEnoughUpdates.profileViewer.getPlayerUUID(user) { uuidString ->
+ if (uuidString == null) {
+ c.completeExceptionally(NullPointerException())
+ } else {
+ c.complete(Utils.parseDashlessUUID(uuidString))
+ }
+ }
+ c.thenCompose { minecraftPlayer ->
+ PronounDB.getPronounsFor(minecraftPlayer)
+ }
+ } else {
+ PronounDB.getPronounsFor(platform, user)
+ }
+ pronouns.handleAsync({ pronounChoice, throwable ->
+ if (throwable != null || !pronounChoice.isPresent) {
+ nc.printChatMessageWithOptionalDeletion(ChatComponentText("§e[NEU] §4Failed to fetch pronouns."), id)
+ return@handleAsync null
+ }
+ val betterPronounChoice = pronounChoice.get()
+ nc.printChatMessageWithOptionalDeletion(
+ ChatComponentText("§e[NEU] Pronouns for §b$user §eon §b$platform§e:"), id
+ )
+ betterPronounChoice.render().forEach {
+ nc.printChatMessage(ChatComponentText("§e[NEU] §a$it"))
+ }
+ null
+ }, MinecraftExecutor.OffThread)
+
+ }
+
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/misc/PeekCommand.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/misc/PeekCommand.kt
new file mode 100644
index 00000000..61fa6029
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/misc/PeekCommand.kt
@@ -0,0 +1,318 @@
+/*
+ * 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.commands.misc
+
+import com.mojang.brigadier.arguments.StringArgumentType.string
+import io.github.moulberry.notenoughupdates.NotEnoughUpdates
+import io.github.moulberry.notenoughupdates.autosubscribe.NEUAutoSubscribe
+import io.github.moulberry.notenoughupdates.events.RegisterBrigadierCommandEvent
+import io.github.moulberry.notenoughupdates.profileviewer.ProfileViewer
+import io.github.moulberry.notenoughupdates.util.Utils
+import io.github.moulberry.notenoughupdates.util.brigadier.*
+import net.minecraft.client.Minecraft
+import net.minecraft.util.ChatComponentText
+import net.minecraft.util.EnumChatFormatting.*
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+import org.apache.commons.lang3.text.WordUtils
+import java.util.*
+import java.util.concurrent.*
+
+@NEUAutoSubscribe
+class PeekCommand {
+
+ var future: Future<*>? = null
+ val executor = Executors.newScheduledThreadPool(1)
+
+ fun executePeek(name: String) {
+ val chatGui = Minecraft.getMinecraft().ingameGUI.chatGUI
+ val id = Random().nextInt(Int.MAX_VALUE / 2) + Int.MAX_VALUE / 2
+ fun deleteReply(text: String) {
+ chatGui.printChatMessageWithOptionalDeletion(ChatComponentText(text), id)
+ }
+
+ deleteReply("$YELLOW[PEEK] Getting player information...")
+
+
+ NotEnoughUpdates.profileViewer.getProfileByName(
+ name
+ ) { profile: ProfileViewer.Profile? ->
+ if (profile == null) {
+ deleteReply("$RED[PEEK] Unknown player or the Hypixel API is down.")
+ } else {
+ profile.resetCache()
+ if (future?.isDone != true) {
+ Utils.addChatMessage(
+ "$RED[PEEK] New peek command was run, cancelling old one."
+ )
+ future?.cancel(true)
+ }
+ deleteReply("$YELLOW[PEEK] Getting the player's SkyBlock profile(s)...")
+ val startTime = System.currentTimeMillis()
+ future = ForkJoinPool.commonPool().submit(object : Runnable {
+ override fun run() {
+ if (System.currentTimeMillis() - startTime > 10 * 1000) {
+ deleteReply("$RED[PEEK] Getting profile info took too long, aborting.")
+ return
+ }
+ val g = GRAY.toString()
+ val profileInfo = profile.getProfileInformation(null)
+ if (profileInfo == null) {
+ future = executor.schedule(this, 200, TimeUnit.MILLISECONDS)
+ return
+ }
+ var overallScore = 0f
+ val isMe = name.equals("moulberry", ignoreCase = true)
+ val stats = profile.getStats(null)
+ if (stats == null) {
+ future = executor.schedule(this, 200, TimeUnit.MILLISECONDS)
+ return
+ }
+ val skyblockInfo = profile.getSkyblockInfo(null)
+ if (NotEnoughUpdates.INSTANCE.config.profileViewer.useSoopyNetworth) {
+ deleteReply("$YELLOW[PEEK] Getting the player's Skyblock networth...")
+ val countDownLatch = CountDownLatch(1)
+ profile.getSoopyNetworth(null, Runnable { countDownLatch.countDown() })
+ try { //Wait for async network request
+ countDownLatch.await(10, TimeUnit.SECONDS)
+ } catch (e: InterruptedException) {
+ }
+
+ //Now it's waited for network request the data should be cached (accessed in nw section)
+ }
+ deleteReply(
+ "$GREEN $STRIKETHROUGH-=-$RESET$GREEN ${
+ Utils.getElementAsString(
+ profile.hypixelProfile!!["displayname"],
+ name
+ )
+ }'s Info $STRIKETHROUGH-=-"
+ )
+ if (skyblockInfo == null) {
+ Utils.addChatMessage(YELLOW.toString() + "Skills API disabled!")
+ } else {
+ var totalSkillLVL = 0f
+ var totalSkillCount = 0f
+ val skills: List<String> =
+ mutableListOf(
+ "taming",
+ "mining",
+ "foraging",
+ "enchanting",
+ "farming",
+ "combat",
+ "fishing",
+ "alchemy",
+ "carpentry"
+ )
+ for (skillName in skills) {
+ totalSkillLVL += skyblockInfo[skillName]!!.level
+ totalSkillCount++
+ }
+ var combat = skyblockInfo["combat"]!!.level
+ var zombie = skyblockInfo["zombie"]!!.level
+ var spider = skyblockInfo["spider"]!!.level
+ var wolf = skyblockInfo["wolf"]!!.level
+ var enderman = skyblockInfo["enderman"]!!.level
+ var blaze = skyblockInfo["blaze"]!!.level
+ var avgSkillLVL = totalSkillLVL / totalSkillCount
+ if (isMe) {
+ avgSkillLVL = 6f
+ combat = 4f
+ zombie = 2f
+ spider = 1f
+ wolf = 2f
+ enderman = 0f
+ blaze = 0f
+ }
+ val combatPrefix =
+ if (combat > 20) (if (combat > 35) GREEN else YELLOW) else RED
+ val zombiePrefix =
+ if (zombie > 3) (if (zombie > 6) GREEN else YELLOW) else RED
+ val spiderPrefix =
+ if (spider > 3) (if (spider > 6) GREEN else YELLOW) else RED
+ val wolfPrefix =
+ if (wolf > 3) (if (wolf > 6) GREEN else YELLOW) else RED
+ val endermanPrefix =
+ if (enderman > 3) (if (enderman > 6) GREEN else YELLOW) else RED
+ val blazePrefix =
+ if (blaze > 3) (if (blaze > 6) GREEN else YELLOW) else RED
+ val avgPrefix =
+ if (avgSkillLVL > 20) (if (avgSkillLVL > 35) GREEN else YELLOW) else RED
+ overallScore += zombie * zombie / 81f
+ overallScore += spider * spider / 81f
+ overallScore += wolf * wolf / 81f
+ overallScore += enderman * enderman / 81f
+ overallScore += blaze * blaze / 81f
+ overallScore += avgSkillLVL / 20f
+ val cata = skyblockInfo["catacombs"]!!.level.toInt()
+ val cataPrefix =
+ if (cata > 15) (if (cata > 25) GREEN else YELLOW) else RED
+ overallScore += cata * cata / 2000f
+ Utils.addChatMessage(
+ g + "Combat: " + combatPrefix + Math.floor(combat.toDouble())
+ .toInt() +
+ (if (cata > 0) "$g - Cata: $cataPrefix$cata" else "") +
+ g + " - AVG: " + avgPrefix + Math.floor(avgSkillLVL.toDouble())
+ .toInt()
+ )
+ Utils.addChatMessage(
+ g + "Slayer: " + zombiePrefix + Math.floor(zombie.toDouble())
+ .toInt() + g + "-" +
+ spiderPrefix + Math.floor(spider.toDouble())
+ .toInt() + g + "-" +
+ wolfPrefix + Math.floor(wolf.toDouble()).toInt() + g + "-" +
+ endermanPrefix + Math.floor(enderman.toDouble())
+ .toInt() + g + "-" +
+ blazePrefix + Math.floor(blaze.toDouble()).toInt()
+ )
+ }
+ val health = stats["health"].toInt()
+ val defence = stats["defence"].toInt()
+ val strength = stats["strength"].toInt()
+ val intelligence = stats["intelligence"].toInt()
+ val healthPrefix =
+ if (health > 800) (if (health > 1600) GREEN else YELLOW) else RED
+ val defencePrefix =
+ if (defence > 200) (if (defence > 600) GREEN else YELLOW) else RED
+ val strengthPrefix =
+ if (strength > 100) (if (strength > 300) GREEN else YELLOW) else RED
+ val intelligencePrefix =
+ if (intelligence > 300) (if (intelligence > 900) GREEN else YELLOW) else RED
+ Utils.addChatMessage(
+ g + "Stats : " + healthPrefix + health + RED + "\u2764 " +
+ defencePrefix + defence + GREEN + "\u2748 " +
+ strengthPrefix + strength + RED + "\u2741 " +
+ intelligencePrefix + intelligence + AQUA + "\u270e "
+ )
+ val bankBalance =
+ Utils.getElementAsFloat(
+ Utils.getElement(
+ profileInfo,
+ "banking.balance"
+ ), -1f
+ )
+ val purseBalance =
+ Utils.getElementAsFloat(
+ Utils.getElement(
+ profileInfo,
+ "coin_purse"
+ ), 0f
+ )
+ val networth = if (NotEnoughUpdates.INSTANCE.config.profileViewer.useSoopyNetworth) {
+ val nwData =
+ profile.getSoopyNetworth(null, Runnable {})
+ nwData?.total ?: -2L
+ } else {
+ profile.getNetWorth(null)
+ }
+ val money =
+ Math.max(bankBalance + purseBalance, networth.toFloat())
+ val moneyPrefix =
+ if (money > 50 * 1000 * 1000) (if (money > 200 * 1000 * 1000) GREEN else YELLOW) else RED
+ Utils.addChatMessage(
+ g + "Purse: " + moneyPrefix + Utils.shortNumberFormat(
+ purseBalance.toDouble(),
+ 0
+ ) + g + " - Bank: " +
+ (if (bankBalance == -1f) YELLOW.toString() + "N/A" else moneyPrefix.toString() +
+ if (isMe) "4.8b" else Utils.shortNumberFormat(
+ bankBalance.toDouble(),
+ 0
+ )) +
+ if (networth > 0) "$g - Net: $moneyPrefix" + Utils.shortNumberFormat(
+ networth.toDouble(),
+ 0
+ ) else ""
+ )
+ overallScore += Math.min(2f, money / (100f * 1000 * 1000))
+ val activePet =
+ Utils.getElementAsString(
+ Utils.getElement(
+ profile.getPetsInfo(
+ null
+ ), "active_pet.type"
+ ),
+ "None Active"
+ )
+ val activePetTier =
+ Utils.getElementAsString(
+ Utils.getElement(
+ profile.getPetsInfo(null),
+ "active_pet.tier"
+ ), "UNKNOWN"
+ )
+ var col = NotEnoughUpdates.petRarityToColourMap[activePetTier]
+ if (col == null) col = LIGHT_PURPLE.toString()
+ Utils.addChatMessage(
+ g + "Pet : " + col + WordUtils.capitalizeFully(
+ activePet.replace("_", " ")
+ )
+ )
+ var overall = "Skywars Main"
+ if (isMe) {
+ overall =
+ Utils.chromaString("Literally the best player to exist") // ego much
+ } else if (overallScore < 5 && bankBalance + purseBalance > 500 * 1000 * 1000) {
+ overall = GOLD.toString() + "Bill Gates"
+ } else if (overallScore > 9) {
+ overall =
+ Utils.chromaString("Didn't even think this score was possible")
+ } else if (overallScore > 8) {
+ overall =
+ Utils.chromaString("Mentally unstable")
+ } else if (overallScore > 7) {
+ overall = GOLD.toString() + "Why though 0.0"
+ } else if (overallScore > 5.5) {
+ overall = GOLD.toString() + "Bro stop playing"
+ } else if (overallScore > 4) {
+ overall = GREEN.toString() + "Kinda sweaty"
+ } else if (overallScore > 3) {
+ overall = YELLOW.toString() + "Alright I guess"
+ } else if (overallScore > 2) {
+ overall = YELLOW.toString() + "Ender Non"
+ } else if (overallScore > 1) {
+ overall = RED.toString() + "Played SkyBlock"
+ }
+ Utils.addChatMessage(
+ g + "Overall score: " + overall + g + " (" + Math.round(
+ overallScore * 10
+ ) / 10f + ")"
+ )
+ }
+ })
+ }
+ }
+ }
+
+ @SubscribeEvent
+ fun onCommand(event: RegisterBrigadierCommandEvent) {
+ event.command("peek") {
+ thenArgument("player", string()) { player ->
+ suggestsList { Minecraft.getMinecraft().theWorld.playerEntities.map { it.name } }
+ thenExecute {
+ executePeek(this[player])
+ }
+ }.withHelp("Quickly glance at other peoples stats")
+ thenExecute {
+ executePeek(Minecraft.getMinecraft().thePlayer.name)
+ }
+ }.withHelp("Quickly glance at your own stats")
+ }
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/misc/ProfileViewerCommands.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/misc/ProfileViewerCommands.kt
new file mode 100644
index 00000000..8a2763f7
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/misc/ProfileViewerCommands.kt
@@ -0,0 +1,87 @@
+/*
+ * 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.commands.misc
+
+import com.mojang.brigadier.arguments.StringArgumentType.string
+import com.mojang.brigadier.context.CommandContext
+import io.github.moulberry.notenoughupdates.NotEnoughUpdates
+import io.github.moulberry.notenoughupdates.autosubscribe.NEUAutoSubscribe
+import io.github.moulberry.notenoughupdates.events.RegisterBrigadierCommandEvent
+import io.github.moulberry.notenoughupdates.profileviewer.GuiProfileViewer
+import io.github.moulberry.notenoughupdates.util.brigadier.*
+import net.minecraft.client.Minecraft
+import net.minecraft.client.renderer.OpenGlHelper
+import net.minecraft.command.ICommandSender
+import net.minecraft.util.EnumChatFormatting.RED
+import net.minecraftforge.fml.common.Loader
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+
+@NEUAutoSubscribe
+class ProfileViewerCommands {
+ companion object {
+ fun CommandContext<ICommandSender>.openPv(name: String) {
+ if (!OpenGlHelper.isFramebufferEnabled()) {
+ reply("${RED}Some parts of the profile viewer do not work with OptiFine Fast Render. Go to ESC > Options > Video Settings > Performance > Fast Render to disable it.")
+ }
+
+ if (NotEnoughUpdates.INSTANCE.config.apiData.apiKey.isNullOrBlank()) {
+ reply("${RED}Can't view profile, an API key is not set. Run /api new and put the result in settings.")
+ return
+ }
+
+ NotEnoughUpdates.profileViewer.getProfileByName(name) { profile ->
+ if (profile == null) {
+ reply("${RED}Invalid player name/API key. Maybe the API is down? Try /api new.")
+ } else {
+ profile.resetCache()
+ NotEnoughUpdates.INSTANCE.openGui = GuiProfileViewer(profile)
+ }
+ }
+ }
+ }
+
+
+ @SubscribeEvent
+ fun onCommand(event: RegisterBrigadierCommandEvent) {
+ fun pvCommand(name: String, before: () -> Unit) {
+ event.command(name) {
+ thenExecute {
+ before()
+ openPv(Minecraft.getMinecraft().thePlayer.name)
+ }
+ thenArgument("player", string()) { player ->
+ suggestsList { Minecraft.getMinecraft().theWorld.playerEntities.map { it.name } }
+ thenExecute {
+ before()
+ openPv(this[player])
+ }
+ }.withHelp("Open the profile viewer for a player")
+ }.withHelp("Open the profile viewer for yourself")
+ }
+ pvCommand("pv") {}
+ pvCommand("neuprofile") {}
+ if (!Loader.isModLoaded("skyblockextras"))
+ pvCommand("cata") {
+ GuiProfileViewer.currentPage = GuiProfileViewer.ProfileViewerPage.DUNGEON
+ }
+
+
+ }
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/misc/ScreenOpenCommands.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/misc/ScreenOpenCommands.kt
new file mode 100644
index 00000000..7a9b8d0e
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/misc/ScreenOpenCommands.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.commands.misc
+
+import io.github.moulberry.notenoughupdates.NotEnoughUpdates
+import io.github.moulberry.notenoughupdates.autosubscribe.NEUAutoSubscribe
+import io.github.moulberry.notenoughupdates.events.RegisterBrigadierCommandEvent
+import io.github.moulberry.notenoughupdates.miscgui.GuiEnchantColour
+import io.github.moulberry.notenoughupdates.miscgui.GuiInvButtonEditor
+import io.github.moulberry.notenoughupdates.miscgui.NEUOverlayPlacements
+import io.github.moulberry.notenoughupdates.util.brigadier.thenExecute
+import io.github.moulberry.notenoughupdates.util.brigadier.withHelp
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+
+@NEUAutoSubscribe
+class ScreenOpenCommands {
+ @SubscribeEvent
+ fun onCommands(event: RegisterBrigadierCommandEvent) {
+ event.command("neubuttons") {
+ thenExecute {
+ NotEnoughUpdates.INSTANCE.openGui = GuiInvButtonEditor()
+ }
+ }.withHelp("Open the NEU inventory button editor")
+ event.command("neuec") {
+ thenExecute {
+ NotEnoughUpdates.INSTANCE.openGui = GuiEnchantColour()
+ }
+ }.withHelp("Open the NEU custom enchant colour editor")
+ event.command("neuoverlay") {
+ thenExecute {
+ NotEnoughUpdates.INSTANCE.openGui = NEUOverlayPlacements()
+ }
+ }.withHelp("Open the NEU gui overlay editor")
+ }
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/events/ButtonExclusionZoneEvent.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/events/ButtonExclusionZoneEvent.kt
new file mode 100644
index 00000000..3c8ce418
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/events/ButtonExclusionZoneEvent.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2023 Linnea Gräf
+ *
+ * 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 io.github.moulberry.notenoughupdates.events.ButtonExclusionZoneEvent.PushDirection.*
+import io.github.moulberry.notenoughupdates.util.Rectangle
+import net.minecraft.client.gui.GuiScreen
+import java.util.*
+
+class ButtonExclusionZoneEvent(
+ val gui: GuiScreen,
+ val guiBaseRect: Rectangle,
+) : NEUEvent() {
+ enum class PushDirection {
+ TOWARDS_RIGHT,
+ TOWARDS_LEFT,
+ TOWARDS_TOP,
+ TOWARDS_BOTTOM,
+ }
+
+ data class ExclusionZone(
+ val area: Rectangle,
+ val pushDirection: PushDirection,
+ )
+
+ val occupiedRects = mutableListOf<ExclusionZone>()
+ fun blockArea(area: Rectangle, direction: PushDirection) {
+ occupiedRects.add(ExclusionZone(area, direction))
+ }
+
+ @JvmOverloads
+ fun findButtonPosition(button: Rectangle, margin: Int = 0): Rectangle {
+ val processedAreas = IdentityHashMap<ExclusionZone, Unit>()
+
+ var buttonPosition = button
+ while (true) {
+ val overlappingExclusionZone =
+ occupiedRects.find { it !in processedAreas && it.area.intersects(buttonPosition) } ?: break
+ buttonPosition = when (overlappingExclusionZone.pushDirection) {
+ TOWARDS_RIGHT -> buttonPosition.copy(x = overlappingExclusionZone.area.right + margin)
+ TOWARDS_LEFT -> buttonPosition.copy(x = overlappingExclusionZone.area.left - buttonPosition.width - margin)
+ TOWARDS_TOP -> buttonPosition.copy(y = overlappingExclusionZone.area.top - buttonPosition.height - margin)
+ TOWARDS_BOTTOM -> buttonPosition.copy(y = overlappingExclusionZone.area.bottom + margin)
+ }
+ processedAreas[overlappingExclusionZone] = Unit
+ }
+
+ return buttonPosition
+ }
+
+
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/events/RegisterBrigadierCommandEvent.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/events/RegisterBrigadierCommandEvent.kt
new file mode 100644
index 00000000..d3e0b69a
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/events/RegisterBrigadierCommandEvent.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2023 Linnea Gräf
+ *
+ * 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.mojang.brigadier.builder.LiteralArgumentBuilder
+import io.github.moulberry.notenoughupdates.util.brigadier.BrigadierRoot
+import io.github.moulberry.notenoughupdates.util.brigadier.NEUBrigadierHook
+import io.github.moulberry.notenoughupdates.util.brigadier.literal
+import net.minecraft.command.ICommandSender
+import java.util.function.Consumer
+
+data class RegisterBrigadierCommandEvent(val brigadierRoot: BrigadierRoot) : NEUEvent() {
+ val dispatcher = brigadierRoot.dispatcher
+ val hooks = mutableListOf<NEUBrigadierHook>()
+ fun command(name: String, block: Consumer<LiteralArgumentBuilder<ICommandSender>>): NEUBrigadierHook {
+ return command(name) {
+ block.accept(this)
+ }
+ }
+
+ fun command(
+ name: String,
+ vararg aliases: String,
+ block: LiteralArgumentBuilder<ICommandSender>.() -> Unit
+ ): NEUBrigadierHook {
+ val node = dispatcher.register(literal(name, block))
+ for (alias in aliases) {
+ dispatcher.register(literal(alias) { redirect(node) })
+ }
+ val hook = NEUBrigadierHook(brigadierRoot, node, aliases.toList())
+ hooks.add(hook)
+ return hook
+ }
+
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/guifeatures/SkyMallDisplay.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/guifeatures/SkyMallDisplay.kt
new file mode 100644
index 00000000..2821473b
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/guifeatures/SkyMallDisplay.kt
@@ -0,0 +1,103 @@
+/*
+ * 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.guifeatures
+
+import io.github.moulberry.notenoughupdates.NotEnoughUpdates
+import io.github.moulberry.notenoughupdates.autosubscribe.NEUAutoSubscribe
+import io.github.moulberry.notenoughupdates.util.SBInfo
+import io.github.moulberry.notenoughupdates.util.SkyBlockTime
+import io.github.moulberry.notenoughupdates.util.Utils
+import net.minecraft.init.Items
+import net.minecraft.item.ItemStack
+import net.minecraftforge.client.event.ClientChatReceivedEvent
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+import java.time.Duration
+import java.time.Instant
+import java.util.regex.Pattern
+
+@NEUAutoSubscribe
+class SkyMallDisplay {
+
+ private val pattern = Pattern.compile("§r§eNew buff§r§r§r: (.*)§r")
+
+ @SubscribeEvent(receiveCanceled = true)
+ fun onChatReceive(event: ClientChatReceivedEvent) {
+ if (!NotEnoughUpdates.INSTANCE.hasSkyblockScoreboard()) return
+ if (SBInfo.getInstance().getLocation() != "mining_3") return
+
+ val matcher = pattern.matcher(event.message.formattedText)
+ if (!matcher.matches()) return
+
+ val message = matcher.group(1) ?: return
+ currentPerk = SkyMallPerk.values().find { it.chatMessage == message }
+
+ currentPerk?.let {
+ val manager = NotEnoughUpdates.INSTANCE.manager
+ displayItem = manager.jsonToStack(manager.itemInformation[it.displayItemId])
+ }
+ }
+
+ companion object {
+ private var displayText = ""
+ private var displayItem: ItemStack? = null
+ private var lastUpdated = 0L
+ private var currentPerk: SkyMallPerk? = null
+
+ fun getDisplayText(): String {
+ return if (lastUpdated + 1_000 > System.currentTimeMillis()) {
+ displayText
+ } else {
+ update()
+ displayText
+ }
+ }
+
+ fun getDisplayItem(): ItemStack {
+ return displayItem ?: ItemStack(Items.apple)
+ }
+
+ private fun update() {
+ val nextDayBeginning = SkyBlockTime.now()
+ .let { it.copy(day = it.day + 1, hour = 0, minute = 0, second = 0) }
+ .toInstant()
+ val untilNextDay = Duration.between(Instant.now(), nextDayBeginning)
+ displayText = (currentPerk?.displayName ?: "?") + " §a(${
+ Utils.prettyTime(untilNextDay.toMillis())
+ })"
+ lastUpdated = System.currentTimeMillis()
+ }
+ }
+
+ enum class SkyMallPerk(val displayName: String, val displayItemId: String, val chatMessage: String) {
+ PICKAXE_COOLDOWN(
+ "20% §6Pickaxe Ability cooldown", "DIAMOND_PICKAXE",
+ "§r§fReduce Pickaxe Ability cooldown by §r§a20%§r§f."
+ ),
+ MORE_POWDER("+15% more §6Powder", "MITHRIL_ORE", "§r§fGain §r§a+15% §r§fmore Powder while mining."),
+ MINING_FORTUNE("+50 §6☘ Mining Fortune", "ENCHANTED_RABBIT_FOOT", "§r§fGain §r§a+50 §r§6☘ Mining Fortune§r§f."),
+ MINING_SPEED("+100 §6⸕ Mining Speed", "ENCHANTED_FEATHER", "§r§fGain §r§a+100 §r§6⸕ Mining Speed§r§f."),
+ MORE_GOBLINS("10x §6Goblin chance", "GOBLIN_HELMET", "§r§f§r§a10x §r§fchance to find Goblins while mining."),
+ TITANIUM_DROPS("5x §9Titanium drops", "TITANIUM_ORE", "§r§fGain §r§a5x §r§9Titanium §r§fdrops"),
+
+ // In case hypixel finds some day the missing dot at the end.
+ TITANIUM_DROPS_WITH_DOT("5x §9Titanium drops", "TITANIUM_ORE", "§r§fGain §r§a5x §r§9Titanium §r§fdrops."),
+ ;
+ }
+}
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..4782ab0f
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/miscfeatures/OldSkyBlockMenu.kt
@@ -0,0 +1,211 @@
+/*
+ * 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.ItemUtils
+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
+import java.text.DecimalFormat
+import java.text.DecimalFormatSymbols
+import java.util.*
+
+@NEUAutoSubscribe
+object OldSkyBlockMenu {
+ private val decimalFormat = DecimalFormat("##,##0", DecimalFormatSymbols(Locale.US))
+
+ 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
+ val showWarning = skyBlockButton.requiresBoosterCookie && !CookieWarning.hasActiveBoosterCookie()
+ val item = if (showWarning) skyBlockButton.itemWithCookieWarning else skyBlockButton.itemWithoutCookieWarning
+
+ if (skyBlockButton == SkyBlockButton.ACCESSORY) {
+ val magicalPower = NotEnoughUpdates.INSTANCE.config.profileSpecific?.magicalPower ?: 0
+
+ val lore = ItemUtils.getLore(item)
+ lore.add(4, "")
+ val format = decimalFormat.format(magicalPower)
+ lore.add(5, "§7Magical Power: §6$format")
+
+ val newItem = ItemStack.copyItemStack(item)
+ ItemUtils.setLore(newItem, lore)
+ event.replaceWith(newItem)
+ } else {
+ event.replaceWith(item)
+ }
+ }
+
+ @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/miscfeatures/inventory/MuseumCheapestItemOverlay.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/miscfeatures/inventory/MuseumCheapestItemOverlay.kt
new file mode 100644
index 00000000..8a711230
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/miscfeatures/inventory/MuseumCheapestItemOverlay.kt
@@ -0,0 +1,574 @@
+/*
+ * 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.inventory
+
+import io.github.moulberry.notenoughupdates.NotEnoughUpdates
+import io.github.moulberry.notenoughupdates.core.util.ArrowPagesUtils
+import io.github.moulberry.notenoughupdates.core.util.render.TextRenderUtils
+import io.github.moulberry.notenoughupdates.events.ButtonExclusionZoneEvent
+import io.github.moulberry.notenoughupdates.mixins.AccessorGuiContainer
+import io.github.moulberry.notenoughupdates.options.seperateSections.Museum
+import io.github.moulberry.notenoughupdates.util.*
+import io.github.moulberry.notenoughupdates.util.MuseumUtil.DonationState.MISSING
+import net.minecraft.client.Minecraft
+import net.minecraft.client.gui.GuiScreen
+import net.minecraft.client.gui.ScaledResolution
+import net.minecraft.client.gui.inventory.GuiChest
+import net.minecraft.client.renderer.GlStateManager
+import net.minecraft.client.renderer.RenderHelper
+import net.minecraft.init.Blocks
+import net.minecraft.init.Items
+import net.minecraft.inventory.Slot
+import net.minecraft.item.ItemStack
+import net.minecraft.util.EnumChatFormatting
+import net.minecraft.util.ResourceLocation
+import net.minecraftforge.client.event.GuiScreenEvent
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+import org.lwjgl.input.Mouse
+import org.lwjgl.opengl.GL11
+import kotlin.math.ceil
+
+
+object MuseumCheapestItemOverlay {
+
+ enum class Category {
+ WEAPONS,
+ ARMOUR_SETS,
+ RARITIES,
+ NOT_APPLICABLE; // Either not a valid category or inside the "Special Items" category, which is not useful;
+
+ /**
+ * Convert to readable String to be displayed to the user
+ */
+ override fun toString(): String {
+ return when (this) {
+ WEAPONS -> "Weapons"
+ ARMOUR_SETS -> "Armour Sets"
+ RARITIES -> "Rarities"
+ NOT_APPLICABLE -> "Everything"
+ }
+ }
+ }
+
+ data class MuseumItem(
+ var name: String,
+ var internalNames: List<String>,
+ var value: Double,
+ var priceRefreshedAt: Long,
+ var category: Category
+ )
+
+ private const val ITEMS_PER_PAGE = 10
+
+ private val backgroundResource: ResourceLocation = ResourceLocation("notenoughupdates:minion_overlay.png")
+
+ val config: Museum get() = NotEnoughUpdates.INSTANCE.config.museum
+
+ /**
+ * The top left position of the arrows to be drawn, used by [ArrowPagesUtils]
+ */
+ private var topLeft = intArrayOf(237, 110)
+ private var currentPage: Int = 0
+ private var previousSlots: List<Slot> = emptyList()
+ private var itemsToDonate: MutableList<MuseumItem> = emptyList<MuseumItem>().toMutableList()
+ private var leftButtonRect = Rectangle(0, 0, 0, 0)
+ private var rightButtonRect = Rectangle(0, 0, 0, 0)
+ private var selectedCategory = Category.NOT_APPLICABLE
+ private var totalPages = 0
+
+ /**
+ *category -> was the highest page visited?
+ */
+ private var checkedPages: HashMap<Category, Boolean> = hashMapOf(
+ //this page only shows items when you have already donated them -> there is no useful information to gather
+ Category.WEAPONS to false,
+ Category.ARMOUR_SETS to false,
+ Category.RARITIES to false
+ )
+
+ /**
+ * Draw the overlay and parse items, if applicable
+ */
+ @SubscribeEvent
+ fun onDrawBackground(event: GuiScreenEvent.BackgroundDrawnEvent) {
+ if (!shouldRender(event.gui)) return
+ val chest = event.gui as GuiChest
+
+ val slots = chest.inventorySlots.inventorySlots
+ //check if there is any info to gather only when a category is currently open
+ if (!slots.equals(previousSlots) && Utils.getOpenChestName().startsWith("Museum ➜")) {
+ checkIfHighestPageWasVisited(slots)
+ parseItems(slots)
+ updateOutdatedValues()
+ }
+ previousSlots = slots
+
+ val xSize = (event.gui as AccessorGuiContainer).xSize
+ val guiLeft = (event.gui as AccessorGuiContainer).guiLeft
+ val guiTop = (event.gui as AccessorGuiContainer).guiTop
+
+ drawBackground(guiLeft, xSize, guiTop)
+ drawLines(guiLeft, guiTop)
+ drawButtons(guiLeft, xSize, guiTop)
+ }
+
+ /**
+ * Pass on mouse clicks to [ArrowPagesUtils], if applicable
+ */
+ @SubscribeEvent
+ fun onMouseClick(event: GuiScreenEvent.MouseInputEvent.Pre) {
+ if (!shouldRender(event.gui)) return
+ if (!Mouse.getEventButtonState()) return
+ val guiLeft = (event.gui as AccessorGuiContainer).guiLeft
+ val guiTop = (event.gui as AccessorGuiContainer).guiTop
+ ArrowPagesUtils.onPageSwitchMouse(
+ guiLeft, guiTop, topLeft, currentPage, totalPages
+ ) { pageChange: Int -> currentPage = pageChange }
+ }
+
+ @SubscribeEvent
+ fun onButtonExclusionZones(event: ButtonExclusionZoneEvent) {
+ if (shouldRender(event.gui)) {
+ event.blockArea(
+ Rectangle(
+ event.guiBaseRect.right,
+ event.guiBaseRect.top,
+ 175, 130
+ ), ButtonExclusionZoneEvent.PushDirection.TOWARDS_RIGHT
+ )
+ }
+ }
+
+ @SubscribeEvent
+ fun onMouseInput(event: GuiScreenEvent.MouseInputEvent.Pre) {
+ if (!shouldRender(event.gui)) return
+ val mouseX = Utils.getMouseX()
+ val mouseY = Utils.getMouseY()
+ if (Mouse.getEventButtonState() && leftButtonRect.contains(mouseX, mouseY)) {
+ config.museumCheapestItemOverlayValueSource = 1 - config.museumCheapestItemOverlayValueSource
+ updateAllValues()
+ } else if (Mouse.getEventButtonState() && rightButtonRect.contains(mouseX, mouseY)) {
+ advanceSelectedCategory()
+ }
+ }
+
+ /**
+ * Move the selected category one index forward, or back to the start when already at the end
+ */
+ private fun advanceSelectedCategory() {
+ val nextValueIndex = (selectedCategory.ordinal + 1) % 4
+ selectedCategory = enumValues<Category>()[nextValueIndex]
+ }
+
+ /**
+ * Draw the two clickable buttons on the bottom right and display a tooltip if needed
+ */
+ private fun drawButtons(guiLeft: Int, xSize: Int, guiTop: Int) {
+ RenderHelper.enableGUIStandardItemLighting()
+ val useBIN = config.museumCheapestItemOverlayValueSource == 0
+ val mouseX = Utils.getMouseX()
+ val mouseY = Utils.getMouseY()
+ val scaledResolution = ScaledResolution(Minecraft.getMinecraft())
+ val width = scaledResolution.scaledWidth
+ val height = scaledResolution.scaledHeight
+
+ // Left button
+ val leftItemStack = if (useBIN) {
+ ItemUtils.getCoinItemStack(100000.0)
+ } else {
+ ItemStack(Blocks.crafting_table)
+ }
+ leftButtonRect = Rectangle(
+ guiLeft + xSize + 131,
+ guiTop + 106,
+ 16,
+ 16
+ )
+ Minecraft.getMinecraft().renderItem.renderItemIntoGUI(
+ leftItemStack,
+ leftButtonRect.x,
+ leftButtonRect.y
+ )
+
+ if (leftButtonRect.contains(mouseX, mouseY)) {
+ val tooltip = if (useBIN) {
+ listOf(
+ "${EnumChatFormatting.GREEN}Using ${EnumChatFormatting.BLUE}lowest BIN ${EnumChatFormatting.GREEN}as price source!",
+ "",
+ "${EnumChatFormatting.YELLOW}Click to switch to craft cost!"
+ )
+ } else {
+ listOf(
+ "${EnumChatFormatting.GREEN}Using ${EnumChatFormatting.AQUA}craft cost ${EnumChatFormatting.GREEN}as price source!",
+ "",
+ "${EnumChatFormatting.YELLOW}Click to switch to lowest BIN!"
+ )
+ }
+ Utils.drawHoveringText(
+ tooltip,
+ mouseX,
+ mouseY,
+ width,
+ height,
+ -1,
+ Minecraft.getMinecraft().fontRendererObj
+ )
+ }
+
+ // Right button
+ val rightItemStack = when (selectedCategory) {
+ Category.WEAPONS -> ItemStack(Items.diamond_sword)
+ Category.ARMOUR_SETS -> ItemStack(Items.diamond_chestplate)
+ Category.RARITIES -> ItemStack(Items.emerald)
+ Category.NOT_APPLICABLE -> ItemStack(Items.filled_map)
+ }
+ rightButtonRect = Rectangle(
+ guiLeft + xSize + 150,
+ guiTop + 106,
+ 16,
+ 16
+ )
+ Minecraft.getMinecraft().renderItem.renderItemIntoGUI(
+ rightItemStack,
+ rightButtonRect.x,
+ rightButtonRect.y
+ )
+ if (rightButtonRect.contains(mouseX, mouseY)) {
+ val tooltip = mutableListOf(
+ "${EnumChatFormatting.GREEN}Category Filter",
+ "",
+ )
+ for (category in Category.values()) {
+ tooltip.add(
+ if (category == selectedCategory) {
+ "${EnumChatFormatting.BLUE}>$category"
+ } else {
+ category.toString()
+ }
+ )
+ }
+
+ tooltip.add("")
+ tooltip.add("${EnumChatFormatting.YELLOW}Click to advance!")
+ Utils.drawHoveringText(
+ tooltip,
+ mouseX,
+ mouseY,
+ width,
+ height,
+ -1,
+ Minecraft.getMinecraft().fontRendererObj
+ )
+ }
+ RenderHelper.disableStandardItemLighting()
+ }
+
+ /**
+ * Sort the collected items by their calculated value
+ */
+ private fun sortByValue() {
+ itemsToDonate.sortBy { it.value }
+ }
+
+ /**
+ * Update all values that have not been updated for the last minute
+ */
+ private fun updateOutdatedValues() {
+ val time = System.currentTimeMillis()
+ itemsToDonate.filter { time - it.priceRefreshedAt >= 60000 }
+ .forEach {
+ it.value = calculateValue(it.internalNames)
+ it.priceRefreshedAt = time
+ }
+ }
+
+ /**
+ * Update all values regardless of the time of the last update
+ */
+ private fun updateAllValues() {
+ val time = System.currentTimeMillis()
+ itemsToDonate.forEach {
+ it.value = calculateValue(it.internalNames)
+ it.priceRefreshedAt = time
+ }
+ sortByValue()
+ }
+
+ /**
+ * Calculate the value of an item as displayed in the museum, which may consist of multiple pieces
+ */
+ private fun calculateValue(internalNames: List<String>): Double {
+ var totalValue = 0.0
+ internalNames.forEach {
+ val itemValue: Double =
+ when (config.museumCheapestItemOverlayValueSource) {
+ 0 -> NotEnoughUpdates.INSTANCE.manager.auctionManager.getBazaarOrBin(it, false)
+ 1 -> NotEnoughUpdates.INSTANCE.manager.auctionManager.getCraftCost(it)?.craftCost ?: return@forEach
+ else -> -1.0 //unreachable
+ }
+ if (itemValue == -1.0 || itemValue == 0.0) {
+ totalValue = Double.MAX_VALUE
+ return@forEach
+ } else {
+ totalValue += itemValue
+ }
+ }
+ if (totalValue == 0.0) {
+ totalValue = Double.MAX_VALUE
+ }
+
+ return totalValue
+ }
+
+ /**
+ * Draw the lines containing the displayname and value over the background
+ */
+ private fun drawLines(guiLeft: Int, guiTop: Int) {
+ val mouseX = Utils.getMouseX()
+ val mouseY = Utils.getMouseY()
+ val scaledResolution = ScaledResolution(Minecraft.getMinecraft())
+ val width = scaledResolution.scaledWidth
+ val height = scaledResolution.scaledHeight
+
+ val applicableItems = if (selectedCategory == Category.NOT_APPLICABLE) {
+ itemsToDonate
+ } else {
+ itemsToDonate.toList().filter { it.category == selectedCategory }
+ }
+ val lines = buildLines(applicableItems)
+ totalPages = ceil(applicableItems.size.toFloat() / ITEMS_PER_PAGE.toFloat()).toInt()
+
+ lines.forEachIndexed { index, line ->
+ if (!visitedAllPages() && (index == ITEMS_PER_PAGE || index == lines.size - 1)) {
+ TextRenderUtils.drawStringScaledMaxWidth(
+ "${EnumChatFormatting.RED}Visit all pages for accurate info!",
+ Minecraft.getMinecraft().fontRendererObj,
+ (guiLeft + 185).toFloat(),
+ (guiTop + 95).toFloat(),
+ true,
+ 155,
+ 0
+ )
+ return@forEachIndexed
+ } else {
+ val x = (guiLeft + 187).toFloat()
+ val y = (guiTop + 5 + (index * 10)).toFloat()
+ Utils.renderAlignedString(
+ line.name,
+ if (line.value == Double.MAX_VALUE) "${EnumChatFormatting.RED}Unknown ${if (config.museumCheapestItemOverlayValueSource == 0) "BIN" else "Craft Cost"}" else "${EnumChatFormatting.AQUA}${
+ Utils.shortNumberFormat(
+ line.value,
+ 0
+ )
+ }",
+ x,
+ y,
+ 156
+ )
+
+ if (Utils.isWithinRect(mouseX, mouseY, x.toInt(), y.toInt(), 170, 10)) {
+ val tooltip = mutableListOf(line.name, "")
+ //armor set
+ if (line.internalNames.size > 1) {
+ tooltip.add("${EnumChatFormatting.AQUA}Consists of:")
+ line.internalNames.forEach {
+ val displayname =
+ NotEnoughUpdates.INSTANCE.manager.createItemResolutionQuery().withKnownInternalName(it)
+ .resolveToItemListJson()
+ ?.get("displayname")?.asString ?: "ERROR"
+ val value = calculateValue(listOf(it))
+
+ // Creates:" - displayname (price)" OR " - displayname (No BIN found!)"
+ tooltip.add(
+ " ${EnumChatFormatting.DARK_GRAY}-${EnumChatFormatting.RESET} $displayname${EnumChatFormatting.DARK_GRAY} (${EnumChatFormatting.GOLD}${
+ if (value == Double.MAX_VALUE) {
+ "${EnumChatFormatting.RED}No BIN found!"
+ } else {
+ Utils.shortNumberFormat(
+ value,
+ 0
+ )
+ }
+ }${EnumChatFormatting.DARK_GRAY})"
+ )
+ }
+ tooltip.add("")
+ }
+
+ if (NotEnoughUpdates.INSTANCE.manager.getRecipesFor(line.internalNames[0]).isNotEmpty()) {
+ tooltip.add("${EnumChatFormatting.YELLOW}${EnumChatFormatting.BOLD}Click to open recipe!")
+ } else {
+ tooltip.add("${EnumChatFormatting.RED}${EnumChatFormatting.BOLD}No recipe available!")
+ }
+
+ if (Mouse.getEventButtonState()) {
+ //TODO? this only opens the recipe for one of the armor pieces
+ NotEnoughUpdates.INSTANCE.manager.showRecipe(line.internalNames[0])
+ }
+
+ Utils.drawHoveringText(
+ tooltip,
+ mouseX,
+ mouseY,
+ width,
+ height,
+ -1,
+ Minecraft.getMinecraft().fontRendererObj
+ )
+ }
+ }
+ }
+
+ //no page has been visited yet
+ if (lines.isEmpty()) {
+ TextRenderUtils.drawStringScaledMaxWidth(
+ "${EnumChatFormatting.RED}No items matching filter!",
+ Minecraft.getMinecraft().fontRendererObj,
+ (guiLeft + 200).toFloat(),
+ (guiTop + 128 / 2).toFloat(),
+ true,
+ 155,
+ 0
+ )
+ }
+
+ ArrowPagesUtils.onDraw(guiLeft, guiTop, topLeft, currentPage, totalPages)
+ return
+ }
+
+ /**
+ * Create the list of [MuseumItem]s that should be displayed on the current page
+ */
+ private fun buildLines(applicableItems: List<MuseumItem>): List<MuseumItem> {
+ val list = emptyList<MuseumItem>().toMutableList()
+
+ for (i in (ITEMS_PER_PAGE * currentPage) until ((ITEMS_PER_PAGE * currentPage) + ITEMS_PER_PAGE)) {
+ if (i >= applicableItems.size) {
+ break
+ }
+
+ list.add(applicableItems[i])
+ }
+ return list
+ }
+
+ /**
+ * Parse the not already donated items present in the currently open Museum page
+ */
+ private fun parseItems(slots: List<Slot>) {
+ Thread {
+ val time = System.currentTimeMillis()
+ val category = getCategory()
+ if (category == Category.NOT_APPLICABLE) {
+ return@Thread
+ }
+ val armor = category == Category.ARMOUR_SETS
+ for (i in 0..53) {
+ val stack = slots[i].stack ?: continue
+ val parsedItems = MuseumUtil.findMuseumItem(stack, armor) ?: continue
+ when (parsedItems.state) {
+ MISSING -> {
+ val displayName = if (armor) {
+ // Use the provided displayname for armor sets but change the color to blue (from red)
+ "${EnumChatFormatting.BLUE}${stack.displayName.stripControlCodes()}"
+ } else {
+ // Find out the real displayname and use it for normal items, if possible
+ NotEnoughUpdates.INSTANCE.manager.createItemResolutionQuery()
+ .withKnownInternalName(parsedItems.skyblockItemIds.first())
+ .resolveToItemListJson()
+ ?.get("displayname")?.asString ?: "${EnumChatFormatting.RED}ERROR"
+ }
+
+ //if the list does not already contain it, insert this MuseumItem
+ if (itemsToDonate.none { it.internalNames == parsedItems.skyblockItemIds }) {
+ itemsToDonate.add(
+ MuseumItem(
+ displayName,
+ parsedItems.skyblockItemIds,
+ calculateValue(parsedItems.skyblockItemIds),
+ time,
+ category
+ )
+ )
+ }
+ }
+
+ else -> itemsToDonate.retainAll { it.internalNames != parsedItems.skyblockItemIds }
+ }
+ }
+ sortByValue()
+ }.start()
+ }
+
+ /**
+ * Check if the highest page for the current category is currently open and update [checkedPages] accordingly
+ */
+ private fun checkIfHighestPageWasVisited(slots: List<Slot>) {
+ val category = getCategory()
+ val nextPageSlot = slots[53]
+ // If the "Next Page" arrow is missing, we are at the highest page
+ if ((nextPageSlot.stack ?: return).item != Items.arrow) {
+ checkedPages[category] = true
+ }
+ }
+
+ /**
+ * Draw the background texture to the right side of the open Museum Page
+ */
+ private fun drawBackground(guiLeft: Int, xSize: Int, guiTop: Int) {
+ Minecraft.getMinecraft().textureManager.bindTexture(backgroundResource)
+ GL11.glColor4f(1F, 1F, 1F, 1F)
+ GlStateManager.disableLighting()
+ Utils.drawTexturedRect(
+ (guiLeft + xSize + 4).toFloat(),
+ guiTop.toFloat(),
+ 168f,
+ 128f,
+ 0f,
+ 1f,
+ 0f,
+ 1f,
+ GL11.GL_NEAREST
+ )
+ }
+
+ /**
+ * Determine if the overlay should be active based on the config option and the currently open GuiChest, if applicable
+ */
+ private fun shouldRender(gui: GuiScreen): Boolean =
+ config.museumCheapestItemOverlay && NotEnoughUpdates.INSTANCE.hasSkyblockScoreboard() && (gui is GuiChest && Utils.getOpenChestName()
+ .startsWith("Museum ➜") || Utils.getOpenChestName() == "Your Museum")
+
+ /**
+ * Determine the currently open Museum Category
+ */
+ private fun getCategory(): Category =
+ when (Utils.getOpenChestName().substring(9, Utils.getOpenChestName().length)) {
+ "Weapons" -> Category.WEAPONS
+ "Armor Sets" -> Category.ARMOUR_SETS
+ "Rarities" -> Category.RARITIES
+ else -> Category.NOT_APPLICABLE
+ }
+
+ /**
+ * Determine if all useful pages have been visited
+ */
+ private fun visitedAllPages(): Boolean = !checkedPages.containsValue(false)
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/miscfeatures/inventory/MuseumItemHighlighter.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/miscfeatures/inventory/MuseumItemHighlighter.kt
new file mode 100644
index 00000000..945449ba
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/miscfeatures/inventory/MuseumItemHighlighter.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2022 Linnea Gräf
+ *
+ * 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.inventory
+
+import io.github.moulberry.notenoughupdates.NotEnoughUpdates
+import io.github.moulberry.notenoughupdates.autosubscribe.NEUAutoSubscribe
+import io.github.moulberry.notenoughupdates.core.ChromaColour
+import io.github.moulberry.notenoughupdates.core.util.StringUtils
+import io.github.moulberry.notenoughupdates.events.GuiContainerBackgroundDrawnEvent
+import io.github.moulberry.notenoughupdates.events.ReplaceItemEvent
+import io.github.moulberry.notenoughupdates.events.RepositoryReloadEvent
+import io.github.moulberry.notenoughupdates.util.ItemResolutionQuery
+import io.github.moulberry.notenoughupdates.util.ItemUtils
+import io.github.moulberry.notenoughupdates.util.LRUCache
+import io.github.moulberry.notenoughupdates.util.MuseumUtil
+import net.minecraft.client.gui.Gui
+import net.minecraft.init.Items
+import net.minecraft.inventory.ContainerChest
+import net.minecraft.inventory.IInventory
+import net.minecraft.item.EnumDyeColor
+import net.minecraft.item.ItemStack
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+
+@NEUAutoSubscribe
+object MuseumItemHighlighter {
+
+ private val manager get() = NotEnoughUpdates.INSTANCE.manager
+ private val config get() = NotEnoughUpdates.INSTANCE.config.museum
+
+ private fun getHighlightColor() = ChromaColour.specialToChromaRGB(config.museumItemColor)
+
+
+ private val findRawItemForName = LRUCache.memoize(::findRawItemForName0, 4 * 7 * 2)
+
+ @SubscribeEvent
+ fun onRepositoryReload(event: RepositoryReloadEvent) {
+ findRawItemForName.clearCache()
+ }
+
+ private fun findRawItemForName0(arg: Pair<String, Boolean>): ItemStack? {
+ val (name, armor) = arg
+ return MuseumUtil.findItemsByName(name, armor).firstOrNull()?.let { manager.createItem(it) }
+ }
+
+
+ @SubscribeEvent
+ fun onItemOverride(event: ReplaceItemEvent) {
+ if (!config.museumItemShow) return
+ if (!isMuseumInventory(event.inventory)) return
+ val original = event.original ?: return
+ if (!isCompletedRetrievedItem(original)) return
+ val armor = StringUtils.cleanColour(event.inventory.displayName.unformattedText).endsWith("Armor Sets")
+ val rawItem = findRawItemForName.apply(original.displayName to armor) ?: return
+ val hydratedItem = hydrateMuseumItem(rawItem, original)
+ event.replaceWith(hydratedItem)
+ }
+
+ fun isCompletedRetrievedItem(itemStack: ItemStack): Boolean {
+ return itemStack.hasDisplayName() && itemStack.item == Items.dye && EnumDyeColor.byDyeDamage(itemStack.itemDamage) == EnumDyeColor.LIME
+ }
+
+ fun isMuseumInventory(inventory: IInventory): Boolean {
+ return StringUtils.cleanColour(inventory.displayName.unformattedText).startsWith("Museum ➜")
+ }
+
+ @SubscribeEvent
+ fun onBackgroundDrawn(event: GuiContainerBackgroundDrawnEvent) {
+ val egui = event.container ?: return
+ val chest = egui.inventorySlots as? ContainerChest ?: return
+ if (!config.museumItemShow) return
+ if (!isMuseumInventory(chest.lowerChestInventory)) return
+ val fixedHighlightColor = getHighlightColor()
+ for (slot in chest.inventorySlots) {
+ if (slot == null || slot.stack == null) continue
+ if (isHydratedMuseumItem(slot.stack) || isCompletedRetrievedItem(slot.stack)) {
+ val left = slot.xDisplayPosition
+ val top = slot.yDisplayPosition
+ Gui.drawRect(
+ left, top,
+ left + 16, top + 16,
+ fixedHighlightColor
+ )
+ }
+ }
+ }
+
+ fun hydrateMuseumItem(rawItem: ItemStack, original: ItemStack) = rawItem.copy().apply {
+ setStackDisplayName(original.displayName)
+ val originalLore = ItemUtils.getLore(original).toMutableList()
+ ItemUtils.setLore(this, originalLore)
+ val data = ItemUtils.getOrCreateTag(this)
+ val extraAttributes = data.getCompoundTag("ExtraAttributes")
+ extraAttributes.setByte("donated_museum", 1)
+ data.setTag("ExtraAttributes", extraAttributes)
+ data.setBoolean(MUSEUM_HYDRATED_ITEM_TAG, true)
+ }
+
+ fun isHydratedMuseumItem(stack: ItemStack): Boolean {
+ return ItemUtils.getOrCreateTag(stack).getBoolean(MUSEUM_HYDRATED_ITEM_TAG)
+ }
+
+ const val MUSEUM_HYDRATED_ITEM_TAG = "NEU_HYDRATED_MUSEUM_ITEM"
+
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/miscgui/DynamicLightItemsEditor.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/miscgui/DynamicLightItemsEditor.kt
new file mode 100644
index 00000000..7cebadcb
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/miscgui/DynamicLightItemsEditor.kt
@@ -0,0 +1,255 @@
+/*
+ * 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.miscgui
+
+import io.github.moulberry.notenoughupdates.NotEnoughUpdates
+import io.github.moulberry.notenoughupdates.util.Utils
+import net.minecraft.client.Minecraft
+import net.minecraft.client.gui.GuiScreen
+import net.minecraft.client.renderer.GlStateManager
+import net.minecraft.item.ItemStack
+import net.minecraft.util.ResourceLocation
+import net.minecraftforge.fml.common.registry.GameRegistry
+import org.lwjgl.opengl.GL11
+import java.awt.Color
+import kotlin.math.ceil
+
+class DynamicLightItemsEditor() : GuiScreen() {
+
+ val background = ResourceLocation("notenoughupdates:dynamic_light_items_editor.png")
+ val enabledButton = ResourceLocation("notenoughupdates:enabled_button.png")
+ val disabledButton = ResourceLocation("notenoughupdates:disabled_button.png")
+ val chestGui = ResourceLocation("textures/gui/container/generic_54.png")
+ val widgets = ResourceLocation("textures/gui/widgets.png")
+ val help = ResourceLocation("notenoughupdates:help.png")
+
+ var xSize = 217
+ var ySize = 88
+ var guiLeft = 0
+ var guiTop = 0
+
+ var stackToRender: String? = null
+ var itemSelected: String? = null
+
+ override fun drawScreen(mouseX: Int, mouseY: Int, partialTicks: Float) {
+ drawDefaultBackground()
+
+ val numOfItems = NotEnoughUpdates.INSTANCE.config.hidden.dynamicLightItems.size
+ val numOfRows = if (didApplyMixin) ceil(numOfItems / 9f).toInt() else 0
+ ySize = 70 + 18 * numOfRows
+ guiLeft = (width - xSize) / 2
+ guiTop = (height - ySize) / 2
+
+ // Top and bottom half of gui
+ Minecraft.getMinecraft().textureManager.bindTexture(background)
+ Utils.drawTexturedRect(guiLeft.toFloat(), guiTop.toFloat(), xSize.toFloat(), 24F,
+ 0F, 1F, 0F, 24 / 88f, GL11.GL_NEAREST)
+ Utils.drawTexturedRect(guiLeft.toFloat(), (guiTop + ySize - 46).toFloat(), xSize.toFloat(), 46F,
+ 0F, 1F, 42 / 88f, 1F, GL11.GL_NEAREST)
+
+ fontRendererObj.drawString("Dynamic Light Items Editor", guiLeft + 10, guiTop + 7, 4210752)
+
+ GlStateManager.color(1f, 1f, 1f, 1f)
+ Minecraft.getMinecraft().textureManager.bindTexture(help)
+ Utils.drawTexturedRect((guiLeft + xSize + 3).toFloat(), guiTop.toFloat(), 16F, 16F, GL11.GL_NEAREST)
+ if (mouseX >= guiLeft + xSize + 3 &&
+ mouseX <= guiLeft + xSize + 19 &&
+ mouseY >= guiTop &&
+ mouseY <= guiTop + 16) {
+ val tooltip = listOf(
+ "§bDynamic Light Item Editor",
+ "§eWhat is this?",
+ "§eNEU makes use of OptiFine's feature of certain items",
+ "§eemitting dynamic light. By default OptiFine only implements",
+ "§ethis feature for a select few minecraft items.",
+ "",
+ "§eThis editor however, allows you to add specific skyblock",
+ "§eitems that will emit dynamic light when held. Simply hold the",
+ "§eitem you wish to add, then open this menu again and click",
+ "§e'Add Held Item', now if you have OptiFine installed and the",
+ "§edynamic lights option enabled, the added items will emit light!",
+ "",
+ "§eTo remove an item, click the item in this menu and click",
+ "§ethe 'Remove Item' button in the bottom right.",
+ )
+ Utils.drawHoveringText(tooltip, mouseX, mouseY, width, height, -1)
+ }
+
+ if (!didApplyMixin) {
+ fontRendererObj.drawString("Could not find OptiFine!", guiLeft + 50, guiTop + 22, Color.RED.rgb)
+ fontRendererObj.drawString("Go to #neu-support in", guiLeft + 50, guiTop + 32, Color.RED.rgb)
+ fontRendererObj.drawString("the discord for help", guiLeft + 52, guiTop + 42, Color.RED.rgb)
+ return
+ }
+
+ // Buttons
+ GlStateManager.color(1f, 1f, 1f, 1f)
+ Minecraft.getMinecraft().textureManager.bindTexture(enabledButton)
+ Utils.drawTexturedRect(guiLeft.toFloat() + 15, (guiTop + ySize - 32).toFloat(), 88F, 20F,
+ 0F, 1F, 0F, 1F, GL11.GL_NEAREST)
+
+ if (itemSelected != null) {
+ Minecraft.getMinecraft().textureManager.bindTexture(enabledButton)
+ } else {
+ Minecraft.getMinecraft().textureManager.bindTexture(disabledButton)
+ }
+ Utils.drawTexturedRect(guiLeft.toFloat() + 114, (guiTop + ySize - 32).toFloat(), 88F, 20F,
+ 0F, 1F, 0F, 1F, GL11.GL_NEAREST)
+
+ fontRendererObj.drawString("Add Held Item", guiLeft + 27, guiTop + ySize - 26, 4210752)
+ fontRendererObj.drawString("Remove Item", guiLeft + 130, guiTop + ySize - 26, 4210752)
+
+ GlStateManager.color(1f, 1f, 1f, 1f)
+
+ // Add in some part of the gui for every row
+ Minecraft.getMinecraft().textureManager.bindTexture(background)
+ for (i in 0 until numOfRows) {
+ Utils.drawTexturedRect(guiLeft.toFloat(), ((guiTop + 24) + (i * 18)).toFloat(), xSize.toFloat(), 18f,
+ 0f, 1f, 24 / 88f, 42 / 88f, GL11.GL_NEAREST)
+ }
+
+ var hoveredItem: String? = null
+ var selectedPosition: Pair<Int, Int> = Pair(-999, -999)
+
+ // Draw a slot for each item and the ItemStack
+ for ((index, item) in NotEnoughUpdates.INSTANCE.config.hidden.dynamicLightItems.withIndex()) {
+ val i = index % 9
+ val j = index / 9
+ GlStateManager.color(1f, 1f, 1f, 1f)
+
+ Minecraft.getMinecraft().textureManager.bindTexture(chestGui)
+ drawTexturedModalRect(guiLeft + 27 + i % 9 * 18, guiTop + 24 + j * 18, 7, 17, 18, 18)
+
+ val itemStack = resolveItemStack(item) ?: return
+ Utils.drawItemStack(itemStack, guiLeft + 28 + i % 9 * 18, guiTop + 25 + j * 18)
+
+ if (mouseX >= guiLeft + 27 + i % 9 * 18 && mouseX <= guiLeft + 45 + i % 9 * 18) {
+ if (mouseY >= guiTop + 24 + j * 18 && mouseY <= guiTop + 42 + j * 18) {
+ hoveredItem = item
+ val tooltip = itemStack.getTooltip(Minecraft.getMinecraft().thePlayer, false)
+ Utils.drawHoveringText(tooltip, mouseX, mouseY, width, height, -1)
+ }
+ }
+
+ if (itemSelected != null && itemSelected.equals(item)) {
+ // Save the position, so when we render the selected box its renders on top of everything
+ selectedPosition = Pair(guiLeft + 24 + i % 9 * 18, guiTop + 21 + j * 18)
+ }
+ }
+
+ stackToRender = hoveredItem
+
+ GlStateManager.color(1f, 1f, 1f, 1f)
+ Minecraft.getMinecraft().textureManager.bindTexture(widgets)
+ drawTexturedModalRect(selectedPosition.first, selectedPosition.second, 0, 22, 24, 24)
+
+ super.drawScreen(mouseX, mouseY, partialTicks)
+ }
+
+ override fun mouseClicked(mouseX: Int, mouseY: Int, mouseButton: Int) {
+ if (didApplyMixin) {
+ // Add Held Item button
+ if (mouseX >= guiLeft + 15 &&
+ mouseX <= guiLeft + 103 &&
+ mouseY >= (guiTop + ySize - 32) &&
+ mouseY <= (guiTop + ySize - 12)) {
+
+ val heldItem = Minecraft.getMinecraft().thePlayer.heldItem
+
+ if (heldItem == null) {
+ Utils.addChatMessage("§c[NEU] You can't add your hand to the list of dynamic light items.")
+ return
+ }
+
+ val internalName = resolveInternalName(heldItem)
+ if (internalName == null) {
+ Utils.addChatMessage("§c[NEU] Couldn't resolve an internal name for this item!")
+ return
+ }
+ NotEnoughUpdates.INSTANCE.config.hidden.dynamicLightItems.add(internalName)
+ }
+
+ // Remove Item button
+ if (mouseX >= guiLeft + 114 &&
+ mouseX <= guiLeft + 202 &&
+ mouseY >= guiTop + ySize - 32 &&
+ mouseY <= guiTop + ySize - 12 &&
+ itemSelected != null) {
+ NotEnoughUpdates.INSTANCE.config.hidden.dynamicLightItems.remove(itemSelected)
+ itemSelected = null
+ }
+
+ if (stackToRender != null) {
+ itemSelected = stackToRender
+ }
+ }
+
+ super.mouseClicked(mouseX, mouseY, mouseButton)
+ }
+
+ companion object {
+ @JvmStatic
+ var didApplyMixin = false
+
+ fun resolveItemStack(internalName: String): ItemStack? {
+ var itemStack = NotEnoughUpdates.INSTANCE.manager
+ .createItemResolutionQuery()
+ .withKnownInternalName(internalName)
+ .resolveToItemStack()
+ if (itemStack == null) {
+ // Try resolve the item stack through forge
+ itemStack = GameRegistry.makeItemStack(internalName, 0, 1, null)
+ if (itemStack == null) {
+ Utils.addChatMessage("§c[NEU] Couldn't resolve the ItemStack for $internalName")
+ return null
+ }
+ }
+
+ return itemStack
+ }
+
+ @JvmStatic
+ fun resolveInternalName(itemStack: ItemStack): String? {
+ var internalName =
+ NotEnoughUpdates.INSTANCE.manager.createItemResolutionQuery().withItemStack(itemStack).resolveInternalName()
+ if (internalName == null) {
+ // If resolving internal name failed, the item may be a minecraft item
+ internalName = itemStack.item.registryName
+ if (internalName == null) {
+ // Check if minecraft searching also fails
+ // Leave error handling for caller since this method is also called in MixinOFDynamicLights which
+ // is run every tick, and we don't want to flood the chat
+ return null
+ }
+ }
+
+ return internalName
+ }
+
+ @JvmStatic
+ fun findDynamicLightItems(itemStack: ItemStack): Int {
+ val internalName: String = resolveInternalName(itemStack) ?: return 0
+ if (NotEnoughUpdates.INSTANCE.config.hidden.dynamicLightItems.contains(internalName)) {
+ return 15
+ }
+ return 0
+ }
+ }
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/recipes/KatRecipe.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/recipes/KatRecipe.kt
index e6dc0abc..acb379de 100644
--- a/src/main/kotlin/io/github/moulberry/notenoughupdates/recipes/KatRecipe.kt
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/recipes/KatRecipe.kt
@@ -98,7 +98,6 @@ data class KatRecipe(
)
Utils.drawStringCentered(
Utils.prettyTime(time),
- Minecraft.getMinecraft().fontRendererObj,
gui.guiLeft + textPosition.first.toFloat(), gui.guiTop + textPosition.second.toFloat(),
false, 0xff00ff
)
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/HotmInformation.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/HotmInformation.kt
new file mode 100644
index 00000000..a2a61064
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/HotmInformation.kt
@@ -0,0 +1,116 @@
+/*
+ * 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.autosubscribe.NEUAutoSubscribe
+import io.github.moulberry.notenoughupdates.core.util.StringUtils
+import net.minecraft.client.Minecraft
+import net.minecraft.client.gui.inventory.GuiChest
+import net.minecraft.inventory.ContainerChest
+import net.minecraftforge.client.event.GuiOpenEvent
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+import net.minecraftforge.fml.common.gameevent.TickEvent
+import java.util.regex.Pattern
+
+@NEUAutoSubscribe
+class HotmInformation {
+ private var ticksTillReload = 0
+ private val pattern = Pattern.compile("§[7b]Level (\\d*)(?:§8/.*)?")
+
+ @SubscribeEvent
+ fun onGuiOpen(event: GuiOpenEvent) {
+ val gui = event.gui
+ if (gui !is GuiChest) return
+
+ val containerName = (gui.inventorySlots as ContainerChest).lowerChestInventory.displayName.unformattedText
+ if (containerName == "Heart of the Mountain") {
+ ticksTillReload = 5
+ }
+ }
+
+ @SubscribeEvent
+ fun onTick(event: TickEvent.ClientTickEvent) {
+ if (event.phase != TickEvent.Phase.START) return
+ if (ticksTillReload == 0) return
+ ticksTillReload--
+ if (ticksTillReload == 0) {
+ loadDataFromInventory()
+ }
+ }
+
+ private fun loadDataFromInventory() {
+ val profileSpecific = NotEnoughUpdates.INSTANCE.config.profileSpecific ?: return
+
+ for (slot in Minecraft.getMinecraft().thePlayer.openContainer.inventorySlots) {
+ val stack = slot.stack ?: continue
+ val displayName = stack.displayName
+ val lore = ItemUtils.getLore(stack)
+ if (!lore.any { it.contains("Right click to") }) continue
+
+ val perkName = StringUtils.cleanColour(displayName)
+ profileSpecific.hotmTree[perkName] = getLevel(lore[0])
+ }
+ }
+
+ private fun getLevel(string: String): Int {
+ val matcher = pattern.matcher(string)
+ val level = if (matcher.matches()) matcher.group(1).toInt() else 1
+
+ val withBlueCheeseGoblinOmelette = string.contains("§b")
+ val isNotMaxed = string.contains("§8/")
+ return if (withBlueCheeseGoblinOmelette && (isNotMaxed || level > 1)) level - 1 else level
+ }
+
+ companion object {
+ private val QUICK_FORGE_MULTIPLIERS = intArrayOf(
+ 985,
+ 970,
+ 955,
+ 940,
+ 925,
+ 910,
+ 895,
+ 880,
+ 865,
+ 850,
+ 845,
+ 840,
+ 835,
+ 830,
+ 825,
+ 820,
+ 815,
+ 810,
+ 805,
+ 700
+ )
+
+ /*
+ * 1000 = 100% of the time left
+ * 700 = 70% of the time left
+ * */
+ @JvmStatic
+ fun getQuickForgeMultiplier(level: Int): Int {
+ if (level <= 0) return 1000
+ return if (level > 20) -1 else QUICK_FORGE_MULTIPLIERS[level - 1]
+ }
+ }
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/KotlinStringUtils.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/KotlinStringUtils.kt
new file mode 100644
index 00000000..dc1e800c
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/KotlinStringUtils.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.util
+
+import net.minecraft.util.StringUtils
+
+fun String.stripControlCodes(): String = StringUtils.stripControlCodes(this)
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/MuseumUtil.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/MuseumUtil.kt
new file mode 100644
index 00000000..dd52d175
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/MuseumUtil.kt
@@ -0,0 +1,113 @@
+/*
+ * 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.NEUManager
+import io.github.moulberry.notenoughupdates.NotEnoughUpdates
+import net.minecraft.item.EnumDyeColor
+import net.minecraft.item.ItemDye
+import net.minecraft.item.ItemStack
+
+object MuseumUtil {
+
+ data class MuseumItem(
+ /**
+ * A potentially non-exhaustive list of item ids that are required for this museum donation.
+ */
+ val skyblockItemIds: List<String>,
+ val state: DonationState,
+ )
+
+ enum class DonationState {
+ /**
+ * Donated armor only shows one piece, so we use that for id resolution, which might result in incomplete
+ * results (hence the separate state). This still means that the entire set is donated, but it is guaranteed to
+ * be only a partial result. Other values of this enum do not guarantee a full result, but at least they do not
+ * guarantee a partial one.
+ */
+ DONATED_PRESENT_PARTIAL,
+ DONATED_PRESENT,
+ DONATED_VACANT,
+ MISSING,
+ }
+
+ fun findMuseumItem(stack: ItemStack, isOnArmorPage: Boolean): MuseumItem? {
+ val item = stack.item ?: return null
+ val items by lazy { findItemsByName(stack.displayName, isOnArmorPage)}
+ if (item is ItemDye) {
+ val dyeColor = EnumDyeColor.byDyeDamage(stack.itemDamage)
+ if (dyeColor == EnumDyeColor.LIME) {
+ // Item is donated, but not present in the museum
+ return MuseumItem(items, DonationState.DONATED_VACANT)
+ } else if (dyeColor == EnumDyeColor.GRAY) {
+ // Item is not donated
+ return MuseumItem(items, DonationState.MISSING)
+ }
+ // Otherwise unknown item, try to analyze as normal item.
+ }
+ val skyblockId = NotEnoughUpdates.INSTANCE.manager.createItemResolutionQuery().withItemStack(stack)
+ .resolveInternalName()
+ if (skyblockId != null) {
+ return MuseumItem(
+ listOf(skyblockId),
+ if (isOnArmorPage) DonationState.DONATED_PRESENT_PARTIAL else DonationState.DONATED_PRESENT
+ )
+ }
+ return MuseumItem(
+ items,
+ DonationState.DONATED_PRESENT
+ )
+ }
+
+ fun findItemsByName(displayName: String, armor: Boolean): List<String> {
+ return (if (armor)
+ findMuseumArmorSetByName(displayName)
+ else
+ listOf(findMuseumItemByName(displayName))).filterNotNull()
+
+ }
+
+ fun findMuseumItemByName(displayName: String): String? =
+ ItemResolutionQuery.findInternalNameByDisplayName(displayName, true)
+
+
+ fun findMuseumArmorSetByName(displayName: String): List<String?> {
+ val armorSlots = arrayOf(
+ "HELMET",
+ "LEGGINGS",
+ "CHESTPLATE",
+ "BOOTS"
+ )
+ val monochromeName = NEUManager.cleanForTitleMapSearch(displayName)
+ val results = ItemResolutionQuery.findInternalNameCandidatesForDisplayName(displayName)
+ .asSequence()
+ .filter {
+ val item = NotEnoughUpdates.INSTANCE.manager.createItem(it)
+ val name = NEUManager.cleanForTitleMapSearch(item.displayName)
+ monochromeName.replace("armor", "") in name
+ }
+ .toSet()
+ return armorSlots.map { armorSlot ->
+ results.singleOrNull { armorSlot in it }
+ }
+ }
+
+
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/Rectangle.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/Rectangle.kt
new file mode 100644
index 00000000..d44b7721
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/Rectangle.kt
@@ -0,0 +1,75 @@
+/*
+ * 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
+
+/**
+ * An axis aligned rectangle in the following coordinate space:
+ *
+ * * The top direction is towards y=-INF
+ * * The bottom direction is towards y=+INF
+ * * The right direction is towards x=+INF
+ * * The left direction is towards x=-INF
+ */
+data class Rectangle(
+ val x: Int, val y: Int,
+ val width: Int, val height: Int,
+) {
+ /**
+ * The left edge of this rectangle (Low X)
+ */
+ val left get() = x
+
+ /**
+ * The right edge of this rectangle (High X)
+ */
+ val right get() = x + width
+
+ /**
+ * The top edge of this rectangle (Low X)
+ */
+ val top get() = y
+
+ /**
+ * The bottom edge of this rectangle (High X)
+ */
+ val bottom get() = y + height
+
+ init {
+ require(width >= 0)
+ require(height >= 0)
+ }
+
+ /**
+ * Check for intersections between two rectangles. Two rectangles with perfectly aligned edges do *not* count as
+ * intersecting.
+ */
+ fun intersects(other: Rectangle): Boolean {
+ val intersectsX = !(right <= other.left || left >= other.right)
+ val intersectsY = !(top >= other.bottom || bottom <= other.top)
+ return intersectsX && intersectsY
+ }
+
+ /**
+ * Check if this rectangle contains the given coordinate
+ */
+ fun contains(x1: Int, y1: Int) :Boolean{
+ return left <= x1 && x1 < left + width && top <= y1 && y1 < top + height
+ }
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/SkyBlockTime.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/SkyBlockTime.kt
new file mode 100644
index 00000000..8ceb1c51
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/SkyBlockTime.kt
@@ -0,0 +1,122 @@
+/*
+ * 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 java.time.Instant
+
+data class SkyBlockTime(
+ val year: Int = 1,
+ val month: Int = 1,
+ val day: Int = 1,
+ val hour: Int = 0,
+ val minute: Int = 0,
+ val second: Int = 0,
+) {
+
+ val monthName get() = monthName(month)
+ val dayName get() = "$day${daySuffix(day)}"
+
+
+ fun toInstant(): Instant? {
+ return Instant.ofEpochMilli(toMillis())
+ }
+
+ fun toMillis(): Long {
+ val skyBlockYear = 124 * 60 * 60.0
+ val skyBlockMonth = skyBlockYear / 12
+ val skyBlockDay = skyBlockMonth / 31
+ val skyBlockHour = skyBlockDay / 24
+ val skyBlockMinute = skyBlockHour / 60
+ val skyBlockSecond = skyBlockMinute / 60
+
+ var time = 0.0
+ time += year * skyBlockYear
+ time += (month - 1) * skyBlockMonth
+ time += (day - 1) * skyBlockDay
+ time += hour * skyBlockHour
+ time += minute * skyBlockMinute
+ time += second * skyBlockSecond
+ time += 1559829300
+ return time.toLong() * 1000
+ }
+
+ companion object {
+ fun fromInstant(instant: Instant): SkyBlockTime {
+ val skyBlockTimeZero = 1559829300000 // Day 1, Year 1
+ var realMillis = (instant.toEpochMilli() - skyBlockTimeZero)
+
+ val skyBlockYear = 124 * 60 * 60 * 1000
+ val skyBlockMonth = skyBlockYear / 12
+ val skyBlockDay = skyBlockMonth / 31
+ val skyBlockHour = skyBlockDay / 24
+ val skyBlockMinute = skyBlockHour / 60
+ val skyBlockSecond = skyBlockMinute / 60
+
+ fun getUnit(factor: Int): Int {
+ val result = realMillis / factor
+ realMillis %= factor
+ return result.toInt()
+ }
+
+ val year = getUnit(skyBlockYear)
+ val month = getUnit(skyBlockMonth) + 1
+ val day = getUnit(skyBlockDay) + 1
+ val hour = getUnit(skyBlockHour)
+ val minute = getUnit(skyBlockMinute)
+ val second = getUnit(skyBlockSecond)
+ return SkyBlockTime(year, month, day, hour, minute, second)
+
+ }
+
+ fun now(): SkyBlockTime {
+ return fromInstant(Instant.now())
+ }
+
+ fun monthName(month: Int): String {
+ val prefix = when ((month - 1) % 3) {
+ 0 -> "Early "
+ 1 -> ""
+ 2 -> "Late "
+ else -> "Undefined!"
+ }
+
+ val name = when ((month - 1) / 3) {
+ 0 -> "Spring"
+ 1 -> "Summer"
+ 2 -> "Autumn"
+ 3 -> "Winter"
+ else -> "lol"
+ }
+
+ return prefix + name
+ }
+
+ fun daySuffix(n: Int): String {
+ return if (n in 11..13) {
+ "th"
+ } else when (n % 10) {
+ 1 -> "st"
+ 2 -> "nd"
+ 3 -> "rd"
+ else -> "th"
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/brigadier/BrigadierRoot.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/brigadier/BrigadierRoot.kt
new file mode 100644
index 00000000..66008044
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/brigadier/BrigadierRoot.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2023 Linnea Gräf
+ *
+ * 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.brigadier
+
+import com.mojang.brigadier.CommandDispatcher
+import com.mojang.brigadier.ParseResults
+import com.mojang.brigadier.tree.ArgumentCommandNode
+import com.mojang.brigadier.tree.CommandNode
+import io.github.moulberry.notenoughupdates.autosubscribe.NEUAutoSubscribe
+import io.github.moulberry.notenoughupdates.events.RegisterBrigadierCommandEvent
+import io.github.moulberry.notenoughupdates.util.LRUCache
+import net.minecraft.command.ICommandSender
+import net.minecraftforge.client.ClientCommandHandler
+import java.lang.RuntimeException
+import java.util.*
+
+@NEUAutoSubscribe
+object BrigadierRoot {
+ private val help: MutableMap<CommandNode<DefaultSource>, String> = IdentityHashMap()
+ var dispatcher = CommandDispatcher<DefaultSource>()
+ private set
+ val parseText =
+ LRUCache.memoize<Pair<ICommandSender, String>, ParseResults<DefaultSource>>({ (sender, text) ->
+ dispatcher.parse(text, sender)
+ }, 1)
+
+ fun getHelpForNode(node: CommandNode<DefaultSource>): String? {
+ return help[node]
+ }
+
+ fun setHelpForNode(node: CommandNode<DefaultSource>, helpText: String) {
+ if (node.command == null) {
+ RuntimeException("Warning: Setting help on node that cannot be executed. Will be ignored").printStackTrace()
+ }
+ help[node] = helpText
+ }
+
+
+ fun getAllUsages(
+ path: String,
+ node: CommandNode<ICommandSender>,
+ visited: MutableSet<CommandNode<ICommandSender>> = mutableSetOf()
+ ): Sequence<NEUBrigadierHook.Usage> = sequence {
+ if (node in visited) return@sequence
+ visited.add(node)
+ val redirect = node.redirect
+ if (redirect != null) {
+ yieldAll(getAllUsages(path, node.redirect, visited))
+ visited.remove(node)
+ return@sequence
+ }
+ if (node.command != null)
+ yield(NEUBrigadierHook.Usage(path, getHelpForNode(node)))
+ node.children.forEach {
+ val nodeName = when (it) {
+ is ArgumentCommandNode<*, *> -> "<${it.name}>"
+ else -> it.name
+ }
+ yieldAll(getAllUsages("$path $nodeName", it, visited))
+ }
+ visited.remove(node)
+ }
+
+
+ fun updateHooks() = registerHooks(ClientCommandHandler.instance)
+
+ fun registerHooks(handler: ClientCommandHandler) {
+ val iterator = handler.commands.entries.iterator()
+ while (iterator.hasNext()) {
+ if (iterator.next().value is NEUBrigadierHook)
+ iterator.remove()
+ }
+ dispatcher = CommandDispatcher()
+ help.clear()
+ parseText.clearCache()
+ val event = RegisterBrigadierCommandEvent(this)
+ event.post()
+ event.hooks.forEach {
+ if (handler.commands.containsKey(it.commandName)) {
+ println("Could not register command ${it.commandName}")
+ } else {
+ handler.registerCommand(it)
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/brigadier/EnumArgumentType.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/brigadier/EnumArgumentType.kt
new file mode 100644
index 00000000..14b6ed6e
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/brigadier/EnumArgumentType.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.brigadier
+
+import com.mojang.brigadier.LiteralMessage
+import com.mojang.brigadier.StringReader
+import com.mojang.brigadier.arguments.ArgumentType
+import com.mojang.brigadier.context.CommandContext
+import com.mojang.brigadier.exceptions.SimpleCommandExceptionType
+import com.mojang.brigadier.suggestion.Suggestions
+import com.mojang.brigadier.suggestion.SuggestionsBuilder
+import java.util.concurrent.CompletableFuture
+
+class EnumArgumentType<T : Enum<T>>(
+ val values: List<T>
+) : ArgumentType<T> {
+ companion object {
+ @JvmStatic
+ fun <T : Enum<T>> enum(values: Array<T>) = EnumArgumentType(values.toList())
+
+ inline fun <reified T : Enum<T>> enum() = enum(enumValues<T>())
+ }
+
+ override fun getExamples(): Collection<String> {
+ return values.map { it.name }
+ }
+
+ override fun <S : Any?> listSuggestions(
+ context: CommandContext<S>,
+ builder: SuggestionsBuilder
+ ): CompletableFuture<Suggestions> {
+
+ examples
+ .filter {builder.remaining.isBlank() || it.startsWith(builder.remaining, ignoreCase = true) }
+ .forEach { builder.suggest(it) }
+ return builder.buildFuture()
+ }
+
+ private val invalidEnum =
+ SimpleCommandExceptionType(LiteralMessage("Expected one of: ${values.joinToString(", ")}"))
+
+ override fun parse(reader: StringReader): T {
+ val enumName = reader.readString()
+ return values.find { enumName == it.name }
+ ?: throw invalidEnum.createWithContext(reader)
+ }
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/brigadier/RestArgumentType.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/brigadier/RestArgumentType.kt
new file mode 100644
index 00000000..adfdae6a
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/brigadier/RestArgumentType.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.brigadier
+
+import com.mojang.brigadier.StringReader
+import com.mojang.brigadier.arguments.ArgumentType
+
+object RestArgumentType : ArgumentType<String> {
+ override fun parse(reader: StringReader): String {
+ val remaining = reader.remaining
+ reader.cursor += remaining.length
+ return remaining
+ }
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/brigadier/dsl.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/brigadier/dsl.kt
new file mode 100644
index 00000000..17203a4b
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/brigadier/dsl.kt
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2023 Linnea Gräf
+ *
+ * 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.brigadier
+
+import com.mojang.brigadier.arguments.ArgumentType
+import com.mojang.brigadier.builder.ArgumentBuilder
+import com.mojang.brigadier.builder.LiteralArgumentBuilder
+import com.mojang.brigadier.builder.RequiredArgumentBuilder
+import com.mojang.brigadier.context.CommandContext
+import com.mojang.brigadier.tree.ArgumentCommandNode
+import com.mojang.brigadier.tree.CommandNode
+import com.mojang.brigadier.tree.LiteralCommandNode
+import io.github.moulberry.notenoughupdates.commands.dev.DevTestCommand
+import io.github.moulberry.notenoughupdates.util.iterate
+import net.minecraft.command.ICommandSender
+import net.minecraft.util.ChatComponentText
+import net.minecraft.util.IChatComponent
+import java.lang.reflect.ParameterizedType
+import java.lang.reflect.Type
+import java.lang.reflect.TypeVariable
+
+
+typealias DefaultSource = ICommandSender
+
+
+
+private fun normalizeGeneric(argument: Type): Class<*> {
+ return if (argument is Class<*>) {
+ argument
+ } else if (argument is TypeVariable<*>) {
+ normalizeGeneric(argument.bounds[0])
+ } else if (argument is ParameterizedType) {
+ normalizeGeneric(argument.rawType)
+ } else {
+ Any::class.java
+ }
+}
+
+data class TypeSafeArg<T : Any>(val name: String, val argument: ArgumentType<T>) {
+ val argClass by lazy {
+ argument.javaClass
+ .iterate<Class<in ArgumentType<T>>> {
+ it.superclass
+ }
+ .flatMap {
+ it.genericInterfaces.toList()
+ }
+ .filterIsInstance<ParameterizedType>()
+ .find { it.rawType == ArgumentType::class.java }!!
+ .let {
+ normalizeGeneric(it.actualTypeArguments[0])
+ }
+ }
+
+ @JvmName("getWithThis")
+ fun <S> CommandContext<S>.get(): T =
+ get(this)
+
+
+ fun <S> get(ctx: CommandContext<S>): T {
+ return ctx.getArgument(name, argClass) as T
+ }
+}
+
+fun <T : ICommandSender, C : CommandContext<T>> C.reply(component: IChatComponent) {
+ source.addChatMessage(ChatComponentText("§e[NEU] ").appendSibling(component))
+}
+
+fun <T : ICommandSender, C : CommandContext<T>> C.reply(text: String, block: ChatComponentText.() -> Unit = {}) {
+ source.addChatMessage(ChatComponentText(text.split("\n").joinToString("\n") { "§e[NEU] $it" }).also(block))
+}
+
+operator fun <T : Any, C : CommandContext<*>> C.get(arg: TypeSafeArg<T>): T {
+ return arg.get(this)
+}
+
+
+fun <T : Any> argument(
+ name: String,
+ argument: ArgumentType<T>,
+ block: RequiredArgumentBuilder<DefaultSource, T>.(TypeSafeArg<T>) -> Unit
+): RequiredArgumentBuilder<DefaultSource, T> =
+ RequiredArgumentBuilder.argument<DefaultSource, T>(name, argument).also { block(it, TypeSafeArg(name, argument)) }
+
+fun <T : ArgumentBuilder<DefaultSource, T>, AT : Any> T.thenArgument(
+ name: String,
+ argument: ArgumentType<AT>,
+ block: RequiredArgumentBuilder<DefaultSource, AT>.(TypeSafeArg<AT>) -> Unit
+): ArgumentCommandNode<DefaultSource, AT> = argument(name, argument, block).build().also(::then)
+
+fun <T : ArgumentBuilder<DefaultSource, T>, AT : Any> T.thenArgumentExecute(
+ name: String,
+ argument: ArgumentType<AT>,
+ block: CommandContext<DefaultSource>.(TypeSafeArg<AT>) -> Unit
+): ArgumentCommandNode<DefaultSource, AT> = thenArgument(name, argument) {
+ thenExecute {
+ block(it)
+ }
+}
+
+fun literal(
+ name: String,
+ block: LiteralArgumentBuilder<DefaultSource>.() -> Unit = {}
+): LiteralArgumentBuilder<DefaultSource> =
+ LiteralArgumentBuilder.literal<DefaultSource>(name).also(block)
+
+fun <T : ArgumentBuilder<DefaultSource, T>> T.thenLiteral(
+ name: String,
+ block: LiteralArgumentBuilder<DefaultSource>.() -> Unit
+): LiteralCommandNode<DefaultSource> =
+ then(literal(name), block) as LiteralCommandNode<DefaultSource>
+
+
+fun <T : ArgumentBuilder<DefaultSource, T>> T.thenLiteralExecute(
+ name: String,
+ block: CommandContext<DefaultSource>.() -> Unit
+): LiteralCommandNode<DefaultSource> =
+ thenLiteral(name) {
+ thenExecute(block)
+ }
+
+fun <T : ArgumentBuilder<DefaultSource, T>, U : ArgumentBuilder<DefaultSource, U>> T.then(
+ node: U,
+ block: U.() -> Unit
+): CommandNode<DefaultSource> =
+ node.also(block).build().also(::then)
+
+fun <T : ArgumentBuilder<DefaultSource, T>> T.thenExecute(block: CommandContext<DefaultSource>.() -> Unit): T =
+ executes {
+ block(it)
+ 1
+ }
+
+fun <T : ArgumentBuilder<DefaultSource, T>> T.requiresDev(): T {
+ requires { DevTestCommand.isDeveloper(it) }
+ return this
+}
+
+fun NEUBrigadierHook.withHelp(helpText: String): NEUBrigadierHook {
+ commandNode.withHelp(helpText)
+ return this
+}
+
+fun <T : CommandNode<DefaultSource>> T.withHelp(helpText: String): T {
+ BrigadierRoot.setHelpForNode(this, helpText)
+ return this
+}
+
+fun <A : Any, T : RequiredArgumentBuilder<DefaultSource, A>> T.suggestsList(list: List<String>) {
+ suggestsList { list }
+}
+
+fun <A : Any, T : RequiredArgumentBuilder<DefaultSource, A>> T.suggestsList(list: () -> List<String>) {
+ suggests { context, builder ->
+ list().filter { it.startsWith(builder.remaining, ignoreCase = true) }
+ .forEach { builder.suggest(it) }
+ builder.buildFuture()
+ }
+}
+
+
+
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/iterate.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/iterate.kt
new file mode 100644
index 00000000..bcfe11aa
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/iterate.kt
@@ -0,0 +1,28 @@
+/*
+ * 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
+
+fun <T : Any> T.iterate(evolve: (T) -> T?): Sequence<T> = sequence {
+ var pointer: T? = this@iterate
+ while (pointer != null) {
+ yield(pointer)
+ pointer = evolve(pointer)
+ }
+}
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
+}
+