diff options
author | nopo <nopotheemail@gmail.com> | 2023-03-12 11:00:47 +1100 |
---|---|---|
committer | nopo <nopotheemail@gmail.com> | 2023-03-12 11:00:47 +1100 |
commit | 61dea4f655bdd6d938c890e09d76724fe32ef047 (patch) | |
tree | 4b8ddc4447cdf73ad8fa9d35415158dada877a84 /src/main/kotlin/io | |
parent | 0960bc9aa9faeb558124ee62b1c1e65983bbff69 (diff) | |
parent | 193ba468e43bd4db5b5534d17472078708783349 (diff) | |
download | NotEnoughUpdates-61dea4f655bdd6d938c890e09d76724fe32ef047.tar.gz NotEnoughUpdates-61dea4f655bdd6d938c890e09d76724fe32ef047.tar.bz2 NotEnoughUpdates-61dea4f655bdd6d938c890e09d76724fe32ef047.zip |
Merge branch 'master' into scrolling
Diffstat (limited to 'src/main/kotlin/io')
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 +} + |