/* * Skytils - Hypixel Skyblock Quality of Life Mod * Copyright (C) 2022 Skytils * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package gg.skytils.skytilsmod import gg.essential.universal.UChat import gg.essential.universal.UKeyboard import gg.skytils.skytilsmod.commands.impl.* import gg.skytils.skytilsmod.commands.stats.impl.CataCommand import gg.skytils.skytilsmod.commands.stats.impl.SlayerCommand import gg.skytils.skytilsmod.core.* import gg.skytils.skytilsmod.events.impl.MainReceivePacketEvent import gg.skytils.skytilsmod.events.impl.PacketEvent import gg.skytils.skytilsmod.features.impl.dungeons.* import gg.skytils.skytilsmod.features.impl.dungeons.solvers.* import gg.skytils.skytilsmod.features.impl.dungeons.solvers.terminals.* import gg.skytils.skytilsmod.features.impl.events.GriffinBurrows import gg.skytils.skytilsmod.features.impl.events.MayorDiana import gg.skytils.skytilsmod.features.impl.events.MayorJerry import gg.skytils.skytilsmod.features.impl.events.TechnoMayor import gg.skytils.skytilsmod.features.impl.farming.FarmingFeatures import gg.skytils.skytilsmod.features.impl.farming.TreasureHunter import gg.skytils.skytilsmod.features.impl.handlers.* import gg.skytils.skytilsmod.features.impl.mining.MiningFeatures import gg.skytils.skytilsmod.features.impl.mining.StupidTreasureChestOpeningThing import gg.skytils.skytilsmod.features.impl.misc.* import gg.skytils.skytilsmod.features.impl.overlays.AuctionPriceOverlay import gg.skytils.skytilsmod.features.impl.protectitems.ProtectItems import gg.skytils.skytilsmod.features.impl.spidersden.RainTimer import gg.skytils.skytilsmod.features.impl.spidersden.RelicWaypoints import gg.skytils.skytilsmod.features.impl.spidersden.SpidersDenFeatures import gg.skytils.skytilsmod.features.impl.trackers.impl.DupeTracker import gg.skytils.skytilsmod.features.impl.trackers.impl.MayorJerryTracker import gg.skytils.skytilsmod.features.impl.trackers.impl.MythologicalTracker import gg.skytils.skytilsmod.gui.OptionsGui import gg.skytils.skytilsmod.gui.ReopenableGUI import gg.skytils.skytilsmod.listeners.ChatListener import gg.skytils.skytilsmod.listeners.DungeonListener import gg.skytils.skytilsmod.mixins.extensions.ExtensionEntityLivingBase import gg.skytils.skytilsmod.mixins.transformers.accessors.AccessorCommandHandler import gg.skytils.skytilsmod.mixins.transformers.accessors.AccessorGuiStreamUnavailable import gg.skytils.skytilsmod.mixins.transformers.accessors.AccessorSettingsGui import gg.skytils.skytilsmod.utils.* import gg.skytils.skytilsmod.utils.graphics.ScreenRenderer import gg.skytils.skytilsmod.utils.graphics.colors.CustomColor import io.ktor.client.* import io.ktor.client.engine.cio.* import io.ktor.client.plugins.* import io.ktor.client.plugins.cache.* import io.ktor.client.plugins.contentnegotiation.* import io.ktor.serialization.kotlinx.json.* import kotlinx.coroutines.* import kotlinx.serialization.json.Json import kotlinx.serialization.modules.SerializersModule import net.minecraft.client.Minecraft import net.minecraft.client.gui.GuiButton import net.minecraft.client.gui.GuiGameOver import net.minecraft.client.gui.GuiIngameMenu import net.minecraft.client.gui.GuiScreen import net.minecraft.client.settings.KeyBinding import net.minecraft.inventory.ContainerChest import net.minecraft.launchwrapper.Launch import net.minecraft.network.play.client.C01PacketChatMessage import net.minecraft.network.play.server.* import net.minecraftforge.client.ClientCommandHandler import net.minecraftforge.client.event.GuiOpenEvent import net.minecraftforge.client.event.GuiScreenEvent import net.minecraftforge.client.event.RenderGameOverlayEvent import net.minecraftforge.common.MinecraftForge import net.minecraftforge.fml.common.Loader import net.minecraftforge.fml.common.Mod import net.minecraftforge.fml.common.event.FMLInitializationEvent import net.minecraftforge.fml.common.event.FMLLoadCompleteEvent import net.minecraftforge.fml.common.event.FMLPostInitializationEvent import net.minecraftforge.fml.common.event.FMLPreInitializationEvent import net.minecraftforge.fml.common.eventhandler.EventPriority import net.minecraftforge.fml.common.eventhandler.SubscribeEvent import net.minecraftforge.fml.common.gameevent.TickEvent import net.minecraftforge.fml.common.network.FMLNetworkEvent import skytils.hylin.HylinAPI.Companion.createHylinAPI import sun.misc.Unsafe import java.io.File import java.util.* import java.util.concurrent.Executors import java.util.concurrent.ThreadPoolExecutor import kotlin.coroutines.CoroutineContext @Mod( modid = Skytils.MODID, name = Skytils.MOD_NAME, version = Skytils.VERSION, acceptedMinecraftVersions = "[1.8.9]", clientSideOnly = true ) class Skytils { companion object : CoroutineScope { const val MODID = Reference.MODID const val MOD_NAME = Reference.MOD_NAME const val VERSION = Reference.VERSION @JvmStatic val mc: Minecraft by lazy { Minecraft.getMinecraft() } val config by lazy { Config } val modDir by lazy { File(File(mc.mcDataDir, "config"), "skytils").also { it.mkdirs() File(it, "trackers").mkdirs() } } @JvmStatic lateinit var guiManager: GuiManager var ticks = 0 @JvmField val sendMessageQueue = ArrayDeque() @JvmField var usingLabymod = false @JvmField var usingNEU = false @JvmField var usingSBA = false @JvmField var jarFile: File? = null private var lastChatMessage = 0L @JvmField var displayScreen: GuiScreen? = null @JvmField val threadPool = Executors.newFixedThreadPool(10) as ThreadPoolExecutor @JvmField val dispatcher = threadPool.asCoroutineDispatcher() val IO = object : CoroutineScope { override val coroutineContext = Dispatchers.IO + SupervisorJob() + CoroutineName("Skytils IO") } override val coroutineContext: CoroutineContext = dispatcher + SupervisorJob() + CoroutineName("Skytils") val hylinAPI by lazy { createHylinAPI("", false) } val deobfEnvironment by lazy { Launch.blackboard.getOrDefault("fml.deobfuscatedEnvironment", false) as Boolean } val unsafe by lazy { Unsafe::class.java.getDeclaredField("theUnsafe").apply { isAccessible = true }.get(null) as Unsafe } val json = Json { prettyPrint = true isLenient = true ignoreUnknownKeys = true serializersModule = SerializersModule { include(serializersModule) contextual(CustomColor::class, CustomColor.Serializer) contextual(Regex::class, RegexAsString) contextual(UUID::class, UUIDAsString) } } val client = HttpClient(CIO) { install(ContentNegotiation) { json(json) } install(HttpCache) install(HttpRequestRetry) { retryOnServerErrors(maxRetries = 3) exponentialDelay() } install(UserAgent) { agent = "Skytils/$VERSION" } engine { endpoint { connectTimeout = 10000 keepAliveTime = 5000 requestTimeout = 10000 socketTimeout = 10000 } } } val areaRegex = Regex("§r§b§l(?[\\w]+): §r§7(?[\\w ]+)§r") var domain = "api.skytils.gg" const val prefix = "§9§lSkytils §8»" const val successPrefix = "§a§lSkytils §8»" const val failPrefix = "§c§lSkytils §8»" } @Mod.EventHandler fun preInit(event: FMLPreInitializationEvent) { DataFetcher.preload() guiManager = GuiManager jarFile = event.sourceFile mc.framebuffer.enableStencil() } @Mod.EventHandler fun init(event: FMLInitializationEvent) { config.init() hylinAPI.key = config.apiKey UpdateChecker.downloadDeleteTask() arrayOf( this, ChatListener(), DungeonListener, guiManager, MayorInfo, SBInfo, SoundQueue, TickTaskManager, UpdateChecker, AlignmentTaskSolver, ArmorColor, AuctionData(), AuctionPriceOverlay(), BetterStash(), BlazeSolver(), BloodHelper, BossHPDisplays(), BoulderSolver(), ChatTabs, ChangeAllToSameColorSolver, ChestProfit(), ClickInOrderSolver, CreeperSolver(), CommandAliases, ContainerSellValue, CooldownTracker, CustomNotifications, DamageSplash(), DungeonFeatures, DungeonMap(), DungeonTimer(), DupeTracker, EnchantNames, FarmingFeatures(), FavoritePets, Funny, GlintCustomizer, GriffinBurrows, IceFillSolver, IcePathSolver(), ItemFeatures(), KeyShortcuts, LockOrb(), MasterMode7Features, MayorDiana(), MayorJerry(), MayorJerryTracker, MiningFeatures(), MinionFeatures(), MiscFeatures(), MythologicalTracker, PartyFinderStats, PetFeatures(), Ping, PricePaid, ProtectItems, RainTimer(), RandomStuff, RelicWaypoints, ScamCheck, ScoreCalculation, SelectAllColorSolver, ShootTheTargetSolver, SimonSaysSolver, SlayerFeatures(), SpidersDenFeatures(), SpamHider, SpiritLeap, StartsWithSequenceSolver, StupidTreasureChestOpeningThing, TankDisplayStuff(), TechnoMayor(), TeleportMazeSolver(), TerminalFeatures, ThreeWeirdosSolver(), TicTacToeSolver(), TreasureHunter(), TriviaSolver(), WaterBoardSolver(), Waypoints, ).forEach(MinecraftForge.EVENT_BUS::register) } @Mod.EventHandler fun postInit(event: FMLPostInitializationEvent) { usingLabymod = Loader.isModLoaded("labymod") usingNEU = Loader.isModLoaded("notenoughupdates") usingSBA = Loader.isModLoaded("skyblockaddons") MayorInfo.fetchMayorData() PersistentSave.loadData() ModChecker.checkModdedForge() ScreenRenderer.init() } @Mod.EventHandler fun loadComplete(event: FMLLoadCompleteEvent) { val cch = ClientCommandHandler.instance if (cch !is AccessorCommandHandler) throw RuntimeException( "Skytils was unable to mixin to the CommandHandler. Please report this on our Discord at discord.gg/skytils." ) cch.registerCommand(SkytilsCommand) cch.registerCommand(CataCommand) cch.registerCommand(CalcXPCommand) cch.registerCommand(HollowWaypointCommand) cch.registerCommand(LimboCommand) cch.registerCommand(ScamCheckCommand) cch.registerCommand(SlayerCommand) if (!cch.commands.containsKey("armorcolor")) { cch.registerCommand(ArmorColorCommand) } if (!cch.commands.containsKey("glintcustomize")) { cch.registerCommand(GlintCustomizeCommand) } if (!cch.commands.containsKey("protectitem")) { cch.registerCommand(ProtectItemCommand) } if (!cch.commands.containsKey("trackcooldown")) { cch.registerCommand(TrackCooldownCommand) } cch.commandSet.add(RepartyCommand) cch.commandMap["skytilsreparty"] = RepartyCommand if (config.overrideReparty || !cch.commands.containsKey("reparty")) { cch.commandMap["reparty"] = RepartyCommand } if (config.overrideReparty || !cch.commands.containsKey("rp")) { cch.commandMap["rp"] = RepartyCommand } } @SubscribeEvent(priority = EventPriority.HIGHEST) fun onTick(event: TickEvent.ClientTickEvent) { if (event.phase != TickEvent.Phase.START) return ScreenRenderer.refresh() EntityManager.tickEntities() ScoreboardUtil.sidebarLines = ScoreboardUtil.fetchScoreboardLines().map { ScoreboardUtil.cleanSB(it) } TabListUtils.tabEntries = TabListUtils.fetchTabEntries().map { it to it.text } if (displayScreen != null) { if (mc.thePlayer?.openContainer == mc.thePlayer?.inventoryContainer) { mc.displayGuiScreen(displayScreen) displayScreen = null } } if (mc.thePlayer != null && sendMessageQueue.isNotEmpty() && System.currentTimeMillis() - lastChatMessage > 250) { val msg = sendMessageQueue.pollFirst() if (!msg.isNullOrBlank()) mc.thePlayer.sendChatMessage(msg) } if (ticks % 20 == 0) { if (mc.thePlayer != null) { if (deobfEnvironment) { if (DevTools.toggles.getOrDefault("forcehypixel", false)) Utils.isOnHypixel = true if (DevTools.toggles.getOrDefault("forceskyblock", false)) Utils.skyblock = true if (DevTools.toggles.getOrDefault("forcedungeons", false)) Utils.dungeons = true } if (DevTools.getToggle("sprint")) KeyBinding.setKeyBindState(mc.gameSettings.keyBindSprint.keyCode, true) } ticks = 0 } if (Utils.inSkyblock && DevTools.getToggle("copydetails") && UKeyboard.isCtrlKeyDown()) { if (UKeyboard.isKeyDown(UKeyboard.KEY_TAB)) { UChat.chat("Copied tab data to clipboard") GuiScreen.setClipboardString(TabListUtils.tabEntries.map { it.second }.toString()) } if (UKeyboard.isKeyDown(UKeyboard.KEY_CAPITAL)) { UChat.chat("Copied scoreboard data to clipboard") GuiScreen.setClipboardString(ScoreboardUtil.sidebarLines.toString()) } val container = mc.thePlayer?.openContainer if (UKeyboard.isKeyDown(UKeyboard.KEY_LMETA) && container is ContainerChest) { UChat.chat("Copied container data to clipboard") GuiScreen.setClipboardString( "Name: '${container.lowerChestInventory.name}', Items: ${ container.inventorySlots.filter { it.inventory == container.lowerChestInventory } .map { it.stack?.serializeNBT() } }" ) } } ticks++ } @SubscribeEvent fun onConnect(event: FMLNetworkEvent.ClientConnectedToServerEvent) { Utils.isOnHypixel = mc.runCatching { !event.isLocal && (thePlayer?.clientBrand?.lowercase()?.contains("hypixel") ?: currentServerData?.serverIP?.lowercase()?.contains("hypixel") ?: false) }.onFailure { it.printStackTrace() }.getOrDefault(false) } @SubscribeEvent(priority = EventPriority.HIGHEST) fun onPacket(event: MainReceivePacketEvent<*, *>) { if (event.packet is S01PacketJoinGame) { Utils.skyblock = false Utils.dungeons = false } if (!Utils.inSkyblock && Utils.isOnHypixel && event.packet is S3DPacketDisplayScoreboard && event.packet.func_149371_c() == 1) { Utils.skyblock = event.packet.func_149370_d() == "SBScoreboard" printDevMessage("score ${event.packet.func_149370_d()}", "utils") printDevMessage("sb ${Utils.inSkyblock}", "utils") } if (event.packet is S1CPacketEntityMetadata && mc.thePlayer != null) { val nameObj = event.packet.func_149376_c()?.find { it.dataValueId == 2 } ?: return val entity = mc.theWorld.getEntityByID(event.packet.entityId) if (entity is ExtensionEntityLivingBase) { entity.skytilsHook.onNewDisplayName(nameObj.`object` as String) } } if (!Utils.isOnHypixel && event.packet is S3FPacketCustomPayload && event.packet.channelName == "MC|Brand") { if (event.packet.bufferData.readStringFromBuffer(Short.MAX_VALUE.toInt()).lowercase().contains("hypixel")) Utils.isOnHypixel = true } if (Utils.inDungeons || !Utils.isOnHypixel || event.packet !is S38PacketPlayerListItem || (event.packet.action != S38PacketPlayerListItem.Action.UPDATE_DISPLAY_NAME && event.packet.action != S38PacketPlayerListItem.Action.ADD_PLAYER) ) return event.packet.entries.forEach { playerData -> val name = playerData?.displayName?.formattedText ?: playerData?.profile?.name ?: return@forEach areaRegex.matchEntire(name)?.let { result -> Utils.dungeons = Utils.inSkyblock && result.groups["area"]?.value == "Dungeon" printDevMessage("dungeons ${Utils.inDungeons} action ${event.packet.action}", "utils") if (Utils.inDungeons) ScoreCalculation.updateText(ScoreCalculation.totalScore.get()) return@forEach } } } @SubscribeEvent fun onDisconnect(event: FMLNetworkEvent.ClientDisconnectionFromServerEvent) { Utils.isOnHypixel = false Utils.skyblock = false Utils.dungeons = false } @SubscribeEvent fun onSendPacket(event: PacketEvent.SendEvent) { if (event.packet is C01PacketChatMessage) { lastChatMessage = System.currentTimeMillis() } } @SubscribeEvent fun onRenderGameOverlay(event: RenderGameOverlayEvent) { if (mc.currentScreen is OptionsGui && event.type == RenderGameOverlayEvent.ElementType.CROSSHAIRS) { event.isCanceled = true } } @SubscribeEvent fun onGuiInitPost(event: GuiScreenEvent.InitGuiEvent.Post) { if (config.configButtonOnPause && event.gui is GuiIngameMenu) { val x = event.gui.width - 105 val x2 = x + 100 var y = event.gui.height - 22 var y2 = y + 20 val sorted = event.buttonList.sortedWith { a, b -> b.yPosition + b.height - a.yPosition + a.height } for (button in sorted) { val otherX = button.xPosition val otherX2 = button.xPosition + button.width val otherY = button.yPosition val otherY2 = button.yPosition + button.height if (otherX2 > x && otherX < x2 && otherY2 > y && otherY < y2) { y = otherY - 20 - 2 y2 = y + 20 } } event.buttonList.add(GuiButton(6969420, x, 0.coerceAtLeast(y), 100, 20, "Skytils")) } } @SubscribeEvent fun onGuiAction(event: GuiScreenEvent.ActionPerformedEvent.Post) { if (config.configButtonOnPause && event.gui is GuiIngameMenu && event.button.id == 6969420) { displayScreen = OptionsGui() } } @SubscribeEvent fun onGuiChange(event: GuiOpenEvent) { val old = mc.currentScreen if (event.gui == null && config.reopenOptionsMenu) { if (old is ReopenableGUI || (old is AccessorSettingsGui && old.config is Config)) { TickTask(1) { if (mc.thePlayer?.openContainer == mc.thePlayer?.inventoryContainer) displayScreen = OptionsGui() } } } if (old is AccessorGuiStreamUnavailable) { if (config.twitchFix && event.gui == null && !(Utils.skyblock && old.parentScreen is GuiGameOver)) { event.gui = old.parentScreen } } } }