diff --git a/build-mod.gradle.kts b/build-mod.gradle.kts index 7377c0e..6e75bad 100644 --- a/build-mod.gradle.kts +++ b/build-mod.gradle.kts @@ -57,13 +57,13 @@ loom { mixin.defaultRefmapName = refmap_file } -val shade by configurations.registering -val modShade by configurations.registering +val allShade by configurations.registering + +val shade by configurations.registering { allShade.get().extendsFrom(this) } @Suppress("UnstableApiUsage") configurations { implementation.extendsFrom(shade) - modImplementation.extendsFrom(modShade) } sourceSets.main { @@ -73,6 +73,12 @@ sourceSets.main { repositories { mavenLocal() mavenCentral() + maven("https://repo.hypixel.net/repository/Hypixel/") { + name = "Hypixel Maven" + content { + includeGroup("net.hypixel") + } + } maven("https://jitpack.io") { name = "Jitpack Maven" content { @@ -91,7 +97,7 @@ repositories { maven("https://repo.polyfrost.org/releases") { name = "Polyfrost Maven" content { - excludeGroup("com\\.github\\.(.)+") + excludeGroupByRegex("com\\.github\\.(.)+") } } maven("https://cursemaven.com") { @@ -126,6 +132,8 @@ dependencies { compileOnly("cc.polyfrost:oneconfig-1.8.9-forge:0.2.2-alpha223:full") compileOnly("cc.polyfrost:oneconfig-loader-launchwrapper:1.0.0-beta17") + modImplementation("net.hypixel:mod-api-forge:1.0.1.2") + modRuntimeOnly("curse.maven:in-game-account-switcher-232676:3413259a") runtimeOnly("com.github.boopwdn:YqlossClientMixin:master-SNAPSHOT:dev") { isChanging = true } @@ -178,8 +186,13 @@ tasks { shadowJar { archiveClassifier = "dev" - configurations = listOf(shade.get(), modShade.get()) + configurations = listOf(allShade.get()) duplicatesStrategy = DuplicatesStrategy.EXCLUDE + + relocate( + "net.hypixel.modapi.tweaker.HypixelModAPITweaker", + "net.llvg.exec.preload.vanilla_tweaker.HypixelModAPITweaker" + ) } remapJar { diff --git a/src/main/java/net/llvg/exec/mixin/mixin/MixinMinecraft.java b/src/main/java/net/llvg/exec/mixin/mixin/MixinMinecraft.java index 94ea0bb..984e991 100644 --- a/src/main/java/net/llvg/exec/mixin/mixin/MixinMinecraft.java +++ b/src/main/java/net/llvg/exec/mixin/mixin/MixinMinecraft.java @@ -40,6 +40,11 @@ public abstract class MixinMinecraft implements IThreadListener, IPlayerUsage { @Shadow public GameSettings gameSettings; + @Inject (method = "startGame", at = @At ("TAIL")) + private void startGameInject(CallbackInfo ci) { + CallbackMinecraft.postGameStartEventPost(); + } + @Inject (method = "clickMouse", at = @At ("HEAD"), cancellable = true) private void clickMouseInject(CallbackInfo ci) { if (FreeCam.isEnabled()) { @@ -68,6 +73,16 @@ private void runTickInject(CallbackInfo ci) { } } + @Inject (method = "runTick", at = @At (value = "INVOKE", target = "Lnet/minecraft/profiler/Profiler;startSection(Ljava/lang/String;)V", ordinal = 0)) + private void runTickInject1(CallbackInfo ci) { + CallbackMinecraft.postTickEventClientPre(); + } + + @Inject (method = "runTick", at = @At (value = "INVOKE", target = "Lnet/minecraft/profiler/Profiler;endSection()V", ordinal = 1)) + private void runTickInject2(CallbackInfo ci) { + CallbackMinecraft.postTickEventClientPost(); + } + @Inject (method = "loadWorld(Lnet/minecraft/client/multiplayer/WorldClient;Ljava/lang/String;)V", at = @At ("HEAD")) private void loadWorldInject(WorldClient worldClientIn, String loadingMessage, CallbackInfo ci) { CallbackMinecraft.postWorldClientEventLoadPre(worldClientIn); diff --git a/src/main/kotlin/net/llvg/exec/ExeClient.kt b/src/main/kotlin/net/llvg/exec/ExeClient.kt index 927aa16..577e163 100644 --- a/src/main/kotlin/net/llvg/exec/ExeClient.kt +++ b/src/main/kotlin/net/llvg/exec/ExeClient.kt @@ -19,32 +19,45 @@ package net.llvg.exec +import java.io.File import net.llvg.exec.api.command.ExeCCommandManager import net.llvg.exec.api.config.ExeClientConfig import net.llvg.exec.api.event.ExeCEventManager import net.llvg.exec.api.feature.ExeCFeatureManager +import net.llvg.exec.hypixel.HypixelModApiHelper +import net.llvg.exec.preload.vanilla_tweaker.ExeCTweaker import net.llvg.exec.utils.classNameLogger import net.llvg.exec.vanilla.utils.chat_component.ChatColor import net.llvg.exec.vanilla.utils.chat_component.ChatComponentBuildScope import net.llvg.exec.vanilla.utils.chat_component.buildChat -import net.llvg.exec.vanilla.utils.player +import net.llvg.exec.vanilla.utils.mc import net.minecraft.util.IChatComponent object ExeClient { @JvmField val logger = classNameLogger() + @JvmField + val directory: File = File(ExeCTweaker.gameDir, "Exe Client") + + const val MIXIN_ID = "exec" + @JvmStatic fun initialize() { ExeCEventManager - ExeClientConfig ExeCFeatureManager + ExeClientConfig ExeCCommandManager + HypixelModApiHelper } fun send( message: IChatComponent ) { + val player = mc.thePlayer + if (player === null) { + logger.warn("Try to send \"{}\" to player but player is null", message.formattedText) + } buildChat { of( empty() diff --git a/src/main/kotlin/net/llvg/exec/api/command/ExeCCommandManager.kt b/src/main/kotlin/net/llvg/exec/api/command/ExeCCommandManager.kt index 080c481..83b1fab 100644 --- a/src/main/kotlin/net/llvg/exec/api/command/ExeCCommandManager.kt +++ b/src/main/kotlin/net/llvg/exec/api/command/ExeCCommandManager.kt @@ -42,12 +42,18 @@ object ExeCCommandManager : Registry( override fun event( elements: MutableList ): RegisterEvent = - Event.Impl { - elements += it - } + EventImpl(elements) + + interface Event : RegisterEvent - sealed interface Event : RegisterEvent { - fun interface Impl : Event + private data class EventImpl( + private val elements: MutableList + ) : Event { + override fun register( + element: ExeCCommand + ) { + elements += element + } } init { @@ -263,7 +269,7 @@ object ExeCCommandManager : Registry( override fun completeTab( args: Array ): List = if (args.size == 1) { - commandsStartsWith(name) + commandsStartsWith(args[0]) } else { emptyList() } diff --git a/src/main/kotlin/net/llvg/exec/api/config/ExeClientConfig.kt b/src/main/kotlin/net/llvg/exec/api/config/ExeClientConfig.kt index c0bcad5..b5ebc85 100644 --- a/src/main/kotlin/net/llvg/exec/api/config/ExeClientConfig.kt +++ b/src/main/kotlin/net/llvg/exec/api/config/ExeClientConfig.kt @@ -23,6 +23,7 @@ import cc.polyfrost.oneconfig.config.Config import cc.polyfrost.oneconfig.config.annotations.SubConfig import cc.polyfrost.oneconfig.config.data.Mod import cc.polyfrost.oneconfig.config.data.ModType +import net.llvg.exec.feature.catacombs_scan.CatacombsScanConfig import net.llvg.exec.feature.freecam.FreeCamConfig import net.llvg.exec.utils.classNameLogger @@ -37,6 +38,10 @@ object ExeClientConfig : Config( @Transient private val logger = classNameLogger() + @SubConfig + val configCatacombsScan = + CatacombsScanConfig + @SubConfig val configFreeCamera = FreeCamConfig diff --git a/src/main/kotlin/net/llvg/exec/api/feature/ExeCFeatureManager.kt b/src/main/kotlin/net/llvg/exec/api/feature/ExeCFeatureManager.kt index 01bc10e..568b800 100644 --- a/src/main/kotlin/net/llvg/exec/api/feature/ExeCFeatureManager.kt +++ b/src/main/kotlin/net/llvg/exec/api/feature/ExeCFeatureManager.kt @@ -19,11 +19,13 @@ package net.llvg.exec.api.feature +import net.llvg.exec.feature.catacombs_scan.CatacombsScan import net.llvg.exec.feature.freecam.FreeCam import net.llvg.exec.utils.registry.RegisterEvent import net.llvg.exec.utils.registry.Registry object ExeCFeatureManager : Registry>( + CatacombsScan, FreeCam ) { override fun event( diff --git a/src/main/kotlin/net/llvg/exec/feature/catacombs_scan/CatacombsScan.kt b/src/main/kotlin/net/llvg/exec/feature/catacombs_scan/CatacombsScan.kt new file mode 100644 index 0000000..2302d76 --- /dev/null +++ b/src/main/kotlin/net/llvg/exec/feature/catacombs_scan/CatacombsScan.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2025 Water-OR + * + * This file is part of ExeClient + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.llvg.exec.feature.catacombs_scan + +import kotlinx.coroutines.Dispatchers +import net.llvg.exec.api.command.ExeCCommandManager +import net.llvg.exec.api.config.ExeClientConfig +import net.llvg.exec.api.event.onEvent +import net.llvg.exec.api.feature.ExeCFeature +import net.llvg.exec.hypixel.skyblock.catacombs.isInCatacombsBoss +import net.llvg.exec.hypixel.skyblock.catacombs.map.scan.CatacombsScanner +import net.llvg.exec.hypixel.skyblock.isInCatacombs +import net.llvg.exec.vanilla.event.TickEvent + +object CatacombsScan : ExeCFeature { + init { + onEvent(Dispatchers.Default, always = true) { e: ExeCCommandManager.Event -> + e register CatacombsScanCommand + } + + onEvent(Dispatchers.Default) { _: TickEvent.Client.Post -> + if (config.autoScan) { + CatacombsScanner.scan() + } + } + } + + override fun initialize() {} + + fun checkInCatacombs(): Boolean = + !config.onlyInCatacombs || isInCatacombs + + fun checkNotInBoss(): Boolean = + !config.onlyNotInBoss || !isInCatacombsBoss + + override val config: CatacombsScanConfig + get() = ExeClientConfig.configCatacombsScan + + override val active: Boolean + get() = config.active +} \ No newline at end of file diff --git a/src/main/kotlin/net/llvg/exec/feature/catacombs_scan/CatacombsScanCommand.kt b/src/main/kotlin/net/llvg/exec/feature/catacombs_scan/CatacombsScanCommand.kt new file mode 100644 index 0000000..98d19f9 --- /dev/null +++ b/src/main/kotlin/net/llvg/exec/feature/catacombs_scan/CatacombsScanCommand.kt @@ -0,0 +1,360 @@ +/* + * Copyright (C) 2025 Water-OR + * + * This file is part of ExeClient + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +@file:Suppress("ClassName") + +package net.llvg.exec.feature.catacombs_scan + +import com.google.common.collect.ImmutableSortedMap +import kotlinx.coroutines.CancellationException +import net.llvg.exec.ExeClient +import net.llvg.exec.api.command.ExeCCommand +import net.llvg.exec.api.command.ExeCCommandChatComponentScope +import net.llvg.exec.api.command.combineUsages +import net.llvg.exec.api.command.sendUsage +import net.llvg.exec.api.command.sendWrongUsage +import net.llvg.exec.hypixel.skyblock.catacombs.CatacombsInfo +import net.llvg.exec.hypixel.skyblock.catacombs.Floor +import net.llvg.exec.hypixel.skyblock.catacombs.map.scan.CatacombsMap +import net.llvg.exec.hypixel.skyblock.catacombs.map.scan.CatacombsScanner +import net.llvg.exec.vanilla.utils.chat_component.ChatColor +import net.llvg.exec.vanilla.utils.chat_component.buildChat +import net.llvg.exec.vanilla.utils.player +import net.llvg.exec.vanilla.utils.vector.component1 +import net.llvg.exec.vanilla.utils.vector.component2 +import net.llvg.exec.vanilla.utils.vector.component3 +import net.llvg.loliutils.iterator.subArray +import net.llvg.loliutils.time.nanoSecondToMilliSecond +import net.llvg.loliutils.time.systemNanoTime +import net.minecraft.util.IChatComponent + +object CatacombsScanCommand : ExeCCommand { + override val name: String = "catacombs_scan" + + private val commands: Map = + ImmutableSortedMap + .naturalOrder() + .apply { + arrayOf( + `sub-cmd curr`, + `sub-cmd floor`, + `sub-cmd rescan`, + `sub-cmd reset`, + ) + .forEach { + put(it.name, it) + } + } + .build() + + override val usage: IChatComponent = buildChat { + with(ExeCCommandChatComponentScope) { + of( + empty() // " - $name | Display usage of the command + .." " + .."-"() + .`--style split-mark` + .." " + ..CatacombsScanCommand.name() + .`--style command-name` + .." " + .."|"() + .`--style split-mark` + .." " + .."Display usage of the command" + .."\n" + ..commands.combineUsages + ) + } + } + + override fun process( + args: Array + ) { + if (args.isEmpty()) { + sendUsage() + } else { + if (!CatacombsScan.active) return + + if (!CatacombsScan.checkInCatacombs()) { + ExeClient.send { + with(ExeCCommandChatComponentScope) { + "You are not in catacombs"() + .`--style warn` + } + } + return + } + + if (!CatacombsScan.checkNotInBoss()) { + ExeClient.send { + with(ExeCCommandChatComponentScope) { + "You are in catacombs boss"() + .`--style warn` + } + } + return + } + + val command = commands[args[0]] + + if (command === null) { + sendWrongUsage() + return + } + + command.process(args.subArray(1)) + } + } + + override fun completeTab( + args: Array + ): List = + if (args.size == 1) { + val name = args[0] + commands.keys.filter { it.startsWith(name) } + } else { + commands[args[0]]?.completeTab(args.subArray(1)) ?: emptyList() + } +} + +private object `sub-cmd curr` : ExeCCommand { + override val name: String = "curr" + + override val usage: IChatComponent = buildChat { + with(ExeCCommandChatComponentScope) { + of( + empty() // " - $name $name1 | Display name of current room" + .." " + .."-"() + .`--style split-mark` + .." " + ..CatacombsScanCommand.name() + .`--style command-name` + .." " + ..name() + .`--style command-text` + .." " + .."|"() + .`--style split-mark` + .." " + .."Display name of current room" + ) + } + } + + override fun process( + args: Array + ) { + val (x, _, z) = player.position + + val info = CatacombsMap.roomEntryAtPos(x, z)?.info + if (info === null) { + ExeClient.send { + "No information about current room"() + .`--color`(ChatColor.YELLOW) + } + return + } + ExeClient.send { + of( + empty() + .."Current Room: [" + ..info.data.name + .."] r=" + ..info.rotation.name + ..", c=(" + ..(info.corner?.let { (x, y, z) -> + "$x, $y, $z" + } ?: "?, ?, ?") + ..")" + ) + } + } + + override fun completeTab( + args: Array + ): List = + emptyList() +} + +private object `sub-cmd floor` : ExeCCommand { + override val name: String = "floor" + + override val usage: IChatComponent = buildChat { + with(ExeCCommandChatComponentScope) { + of( + empty() // " - $name $name1 | Display name of current floor" + .." " + .."-"() + .`--style split-mark` + .." " + ..CatacombsScanCommand.name() + .`--style command-name` + .." " + ..`sub-cmd floor`.name() + .`--style command-text` + .." " + .."|"() + .`--style split-mark` + .." " + .."Display name of current floor" + ) + } + } + + override fun process( + args: Array + ) { + ExeClient.send { + of( + empty() + .."Current floor " + ..CatacombsInfo.floor.run { + val color: ChatColor = + if (this === Floor.UNKNOWN) { + ChatColor.DARK_RED + } else if (isMasterMode) ChatColor.RED else ChatColor.GREEN + name() + .`--color`(color) + } + ) + } + } + + override fun completeTab( + args: Array + ): List = + emptyList() + +} + +private object `sub-cmd rescan` : ExeCCommand { + override val name: String = "rescan" + + override val usage: IChatComponent = buildChat { + with(ExeCCommandChatComponentScope) { + of( + empty() // " - $name $name1 | Rescan catacombs map" + .." " + .."-"() + .`--style split-mark` + .." " + ..CatacombsScanCommand.name() + .`--style command-name` + .." " + ..name() + .`--style command-text` + .." " + .."|"() + .`--style split-mark` + .." " + .."Scan catacombs map" + ) + } + } + + override fun process( + args: Array + ) { + val start = systemNanoTime + if (CatacombsScanner.roomScanning) { + ExeClient.send { + "Previous scan hasn't finished yet"() + } + return + } + CatacombsMap.reset() + val job = CatacombsScanner.scan() + if (job === null) { + ExeClient.send { + "Previous scan hasn't finished yet"() + } + return + } + job.invokeOnCompletion c@{ e -> + val time = (systemNanoTime - start) / nanoSecondToMilliSecond + if (e !== null) { + if (e is CancellationException) ExeClient.send { + "Catacombs scan has been cancelled"() + .`--color`(ChatColor.YELLOW) + } + return@c + } + ExeClient.send { + of( + empty() + .."Catacombs scan finished in less than " + .."$time"() + .`--color`(ChatColor.GREEN) + .`--bold`(true) + .."ms"() + .`--color`(ChatColor.YELLOW) + ) + } + } + } + + override fun completeTab( + args: Array + ): List = + emptyList() +} + +private object `sub-cmd reset` : ExeCCommand { + override val name: String = "reset" + + override val usage: IChatComponent = buildChat { + with(ExeCCommandChatComponentScope) { + of( + empty() // " - $name $name1 | Reset scan result" + .." " + .."-"() + .`--style split-mark` + .." " + ..CatacombsScanCommand.name() + .`--style command-name` + .." " + ..name() + .`--style command-text` + .." " + .."|"() + .`--style split-mark` + .." " + .."Reset scan result" + ) + } + } + + override fun process( + args: Array + ) { + CatacombsScanner.afterCurrentScan { + ExeClient.send { + "Reset scan result"() + } + CatacombsMap.reset() + } + } + + override fun completeTab( + args: Array + ): List = + emptyList() +} diff --git a/src/main/kotlin/net/llvg/exec/feature/catacombs_scan/CatacombsScanConfig.kt b/src/main/kotlin/net/llvg/exec/feature/catacombs_scan/CatacombsScanConfig.kt new file mode 100644 index 0000000..2ca6b68 --- /dev/null +++ b/src/main/kotlin/net/llvg/exec/feature/catacombs_scan/CatacombsScanConfig.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2025 Water-OR + * + * This file is part of ExeClient + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.llvg.exec.feature.catacombs_scan + +import cc.polyfrost.oneconfig.config.annotations.Checkbox +import net.llvg.exec.api.config.ExeCFeatureConfig +import net.llvg.exec.api.config.ExeClientConfig + +object CatacombsScanConfig : ExeCFeatureConfig( + "Catacombs Scan", + "exec-catacombs_scan-config.json", + enabled = true, + canToggle = false, +) { + override val self: CatacombsScanConfig + get() = this + + @Checkbox( + name = "Only In Catacombs" + ) + var onlyInCatacombs: Boolean = true + + @Checkbox( + name = "Only Not In Boss" + ) + var onlyNotInBoss: Boolean = true + + @Checkbox( + name = "Auto Scan" + ) + var autoScan: Boolean = true + + override fun active(): Boolean = + ExeClientConfig.active() +} \ No newline at end of file diff --git a/src/main/kotlin/net/llvg/exec/hypixel/HypixelLocation.kt b/src/main/kotlin/net/llvg/exec/hypixel/HypixelLocation.kt new file mode 100644 index 0000000..2911018 --- /dev/null +++ b/src/main/kotlin/net/llvg/exec/hypixel/HypixelLocation.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2025 Water-OR + * + * This file is part of ExeClient + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.llvg.exec.hypixel + +import kotlin.jvm.optionals.getOrNull +import kotlinx.coroutines.Dispatchers +import net.hypixel.data.type.ServerType +import net.hypixel.modapi.packet.impl.clientbound.event.ClientboundLocationPacket +import net.llvg.exec.api.event.ExeCEventListenable +import net.llvg.exec.api.event.onEvent +import net.llvg.exec.api.event.post +import net.llvg.exec.hypixel.event.HypixelApiEvent +import net.llvg.exec.hypixel.skyblock.SkyBlockLocation +import net.llvg.exec.vanilla.event.WorldClientEvent + +@Suppress("MemberVisibilityCanBePrivate") +object HypixelLocation : ExeCEventListenable { + var serverName: String? = null + private set + + var serverType: ServerType? = null + private set + + var lobbyName: String? = null + private set + + var mode: String? = null + private set + + var map: String? = null + private set + + fun reset() { + serverName = null + serverType = null + lobbyName = null + mode = null + map = null + } + + init { + onEvent(dispatcher = Dispatchers.Default) { _: WorldClientEvent.Load.Pre -> + reset() + HypixelApiEvent.LocationSet.Impl.post(false) + } + + HypixelModApiHelper.onPacket { p: ClientboundLocationPacket -> + serverName = p.serverName + serverType = p.serverType.getOrNull() + lobbyName = p.lobbyName.getOrNull() + mode = p.mode.getOrNull() + map = p.map.getOrNull() + + HypixelApiEvent.LocationSet.Impl.post(false) + } + + SkyBlockLocation + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/llvg/exec/hypixel/HypixelLocationUtils.kt b/src/main/kotlin/net/llvg/exec/hypixel/HypixelLocationUtils.kt new file mode 100644 index 0000000..d5698e5 --- /dev/null +++ b/src/main/kotlin/net/llvg/exec/hypixel/HypixelLocationUtils.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2025 Water-OR + * + * This file is part of ExeClient + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +@file:JvmName("HypixelLocationUtils") + +package net.llvg.exec.hypixel + +import net.hypixel.data.type.GameType +import net.hypixel.data.type.ServerType + +fun isInServerType( + serverType: ServerType +): Boolean = + HypixelLocation.serverType === serverType + +val isInSkyBlock: Boolean + get() = isInServerType(GameType.SKYBLOCK) \ No newline at end of file diff --git a/src/main/kotlin/net/llvg/exec/hypixel/HypixelModApiHelper.kt b/src/main/kotlin/net/llvg/exec/hypixel/HypixelModApiHelper.kt new file mode 100644 index 0000000..d49c974 --- /dev/null +++ b/src/main/kotlin/net/llvg/exec/hypixel/HypixelModApiHelper.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2025 Water-OR + * + * This file is part of ExeClient + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.llvg.exec.hypixel + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch +import net.hypixel.modapi.HypixelModAPI +import net.hypixel.modapi.packet.EventPacket +import net.llvg.exec.api.event.ExeCEventListenable +import net.llvg.exec.api.event.onEvent +import net.llvg.exec.vanilla.event.GameStartEvent +import net.llvg.loliutils.exception.cast + +@Suppress("MemberVisibilityCanBePrivate") +object HypixelModApiHelper : ExeCEventListenable { + val hypixelModAPI: HypixelModAPI = HypixelModAPI.getInstance() + + private val eventPacketListeners: MutableMap, MutableList<(EventPacket) -> Unit>> = + HashMap() + + init { + HypixelLocation + onEvent(Dispatchers.Default) { _: GameStartEvent.Post -> + val jobs = mutableListOf() + for ((type, listeners) in eventPacketListeners) jobs += launch { + hypixelModAPI.subscribeToEventPacket(type) + listeners.forEach { + hypixelModAPI.createHandler(type, it) + } + } + jobs.joinAll() + } + } + + fun

onPacket( + type: Class

, + action: (P) -> Unit + ) { + val listeners = eventPacketListeners[type] ?: synchronized(eventPacketListeners) { + eventPacketListeners.getOrPut(type) { ArrayList() } + } + + listeners += { action(cast(it)) } + } + + inline fun onPacket( + noinline action: (P) -> Unit + ) { + onPacket(P::class.java, action) + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/llvg/exec/hypixel/event/HypixelApiEvent.kt b/src/main/kotlin/net/llvg/exec/hypixel/event/HypixelApiEvent.kt new file mode 100644 index 0000000..61cb329 --- /dev/null +++ b/src/main/kotlin/net/llvg/exec/hypixel/event/HypixelApiEvent.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2025 Water-OR + * + * This file is part of ExeClient + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.llvg.exec.hypixel.event + +import net.llvg.exec.api.event.ExeCEvent + +sealed interface HypixelApiEvent : ExeCEvent { + sealed interface LocationSet : HypixelApiEvent { + data object Impl : LocationSet + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/llvg/exec/hypixel/skyblock/SkyBlockLocation.kt b/src/main/kotlin/net/llvg/exec/hypixel/skyblock/SkyBlockLocation.kt new file mode 100644 index 0000000..fd26af5 --- /dev/null +++ b/src/main/kotlin/net/llvg/exec/hypixel/skyblock/SkyBlockLocation.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2025 Water-OR + * + * This file is part of ExeClient + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.llvg.exec.hypixel.skyblock + +import kotlinx.coroutines.Dispatchers +import net.llvg.exec.api.event.ExeCEventListenable +import net.llvg.exec.api.event.onEvent +import net.llvg.exec.api.event.post +import net.llvg.exec.hypixel.isInSkyBlock +import net.llvg.exec.hypixel.skyblock.catacombs.CatacombsInfo +import net.llvg.exec.vanilla.event.TickEvent +import net.llvg.exec.vanilla.event.WorldClientEvent +import net.llvg.exec.vanilla.utils.scoreboard.ScoreboardFlags.FLAG_NO_MC_FORMAT +import net.llvg.exec.vanilla.utils.scoreboard.ScoreboardFlags.FLAG_NO_STUPID_CHAR +import net.llvg.exec.vanilla.utils.scoreboard.getScoreboardLines + +object SkyBlockLocation : ExeCEventListenable { + @get:JvmStatic + var locationName: String = "" + private set + + private var tickCounter = 0 + + override val active: Boolean + get() = isInSkyBlock + + init { + onEvent(Dispatchers.Default, always = true) { _: WorldClientEvent.Load.Pre -> + tickCounter = 0 + if (locationName.isNotEmpty()) { + SkyBlockLocationEvent.create("").post(true) + locationName = "" + } + } + + onEvent(Dispatchers.Default) { _: TickEvent.Client.Pre -> + tickCounter = (tickCounter + 1) % 20 + if (tickCounter == 0) { + var newLocationName: String? = null + + for (line in getScoreboardLines(FLAG_NO_MC_FORMAT + FLAG_NO_STUPID_CHAR)) { + if (!line.startsWith(SKY_BLOCK_LOCATION_PREFIX)) continue + newLocationName = line.substring(3) + break + } + + if (newLocationName !== null && newLocationName != locationName) { + SkyBlockLocationEvent.create(newLocationName).post(true) + locationName = newLocationName + } + } + } + + CatacombsInfo + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/llvg/exec/hypixel/skyblock/SkyBlockLocationEvent.kt b/src/main/kotlin/net/llvg/exec/hypixel/skyblock/SkyBlockLocationEvent.kt new file mode 100644 index 0000000..5dd02b1 --- /dev/null +++ b/src/main/kotlin/net/llvg/exec/hypixel/skyblock/SkyBlockLocationEvent.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2025 Water-OR + * + * This file is part of ExeClient + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.llvg.exec.hypixel.skyblock + +import net.llvg.exec.api.event.ExeCEvent + +sealed interface SkyBlockLocationEvent : ExeCEvent { + val newLocationName: String + + companion object { + internal fun create( + newLocationName: String + ): SkyBlockLocationEvent = + Impl(newLocationName) + } +} + +private data class Impl( + override val newLocationName: String +) : SkyBlockLocationEvent \ No newline at end of file diff --git a/src/main/kotlin/net/llvg/exec/hypixel/skyblock/SkyBlockLocationUtils.kt b/src/main/kotlin/net/llvg/exec/hypixel/skyblock/SkyBlockLocationUtils.kt new file mode 100644 index 0000000..2e1ca4b --- /dev/null +++ b/src/main/kotlin/net/llvg/exec/hypixel/skyblock/SkyBlockLocationUtils.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2025 Water-OR + * + * This file is part of ExeClient + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +@file:JvmName("SkyBlockLocationUtils") + +package net.llvg.exec.hypixel.skyblock + +import net.llvg.exec.hypixel.HypixelLocation +import net.llvg.exec.hypixel.isInSkyBlock + +const val SKY_BLOCK_LOCATION_PREFIX = " ⏣ " + +val isInDungeon: Boolean + get() = isInSkyBlock && HypixelLocation.mode == "dungeon" + +val isInCatacombs: Boolean // There's only one type of dungeon called catacombs right now + get() = isInDungeon \ No newline at end of file diff --git a/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/CatacombsInfo.kt b/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/CatacombsInfo.kt new file mode 100644 index 0000000..21a184e --- /dev/null +++ b/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/CatacombsInfo.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2025 Water-OR + * + * This file is part of ExeClient + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.llvg.exec.hypixel.skyblock.catacombs + +import kotlinx.coroutines.Dispatchers +import net.llvg.exec.api.event.ExeCEventListenable +import net.llvg.exec.api.event.onEvent +import net.llvg.exec.hypixel.skyblock.SkyBlockLocationEvent +import net.llvg.exec.hypixel.skyblock.catacombs.map.RoomData +import net.llvg.exec.hypixel.skyblock.catacombs.map.scan.CatacombsMap +import net.llvg.exec.hypixel.skyblock.isInCatacombs +import net.llvg.exec.vanilla.event.WorldClientEvent + +object CatacombsInfo : ExeCEventListenable { + var floor: Floor = Floor.UNKNOWN + private set + + override val active: Boolean + get() = isInCatacombs + + init { + onEvent(Dispatchers.Default, always = true) { _: WorldClientEvent.Load.Pre -> + floor = Floor.UNKNOWN + } + + onEvent(Dispatchers.Default) { e: SkyBlockLocationEvent -> + if (floor !== Floor.UNKNOWN) return@onEvent + + if (!e.newLocationName.startsWith("The Catacombs")) return@onEvent + + if (e.newLocationName.length <= 16) return@onEvent + + floor = when (e.newLocationName[15]) { + 'E' -> Floor.E + 'F' -> Floor.floorF[e.newLocationName[16].digitToInt() - 1] + 'M' -> Floor.floorM[e.newLocationName[16].digitToInt() - 1] + else -> Floor.UNKNOWN + } + } + + CatacombsMap + RoomData + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/CatacombsLocationUtils.kt b/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/CatacombsLocationUtils.kt new file mode 100644 index 0000000..8c1037b --- /dev/null +++ b/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/CatacombsLocationUtils.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2025 Water-OR + * + * This file is part of ExeClient + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +@file:JvmName("CatacombsLocationUtils") + +package net.llvg.exec.hypixel.skyblock.catacombs + +import net.llvg.exec.hypixel.skyblock.isInCatacombs +import net.llvg.exec.vanilla.utils.mc + +val isInCatacombsBoss: Boolean + get() = isInCatacombs && mc.thePlayer?.run { + when (CatacombsInfo.floor.floor) { + 1 -> posX > -71 && posZ > -39 + 2, 3, 4 -> posX > -39 && posZ > -39 + 5, 6 -> posX > -39 && posZ > -7 + 7 -> posX > -7 && posZ > -7 + else -> false + } + } ?: false \ No newline at end of file diff --git a/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/Floor.kt b/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/Floor.kt new file mode 100644 index 0000000..7b084d8 --- /dev/null +++ b/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/Floor.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2025 Water-OR + * + * This file is part of ExeClient + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.llvg.exec.hypixel.skyblock.catacombs + +enum class Floor { + UNKNOWN, + E, + F1, F2, F3, F4, F5, F6, F7, + M1, M2, M3, M4, M5, M6, M7; + + val isMasterMode: Boolean = name.startsWith('M') + + val floor: Int = + when (name[0]) { + 'E' -> 0 + 'F', 'M' -> name[1].digitToInt() + else -> -1 + } + + companion object { + @JvmField + val floorF = arrayOf(F1, F2, F3, F4, F5, F6, F7) + + @JvmField + val floorM = arrayOf(M1, M2, M3, M4, M5, M6, M7) + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/map/RoomData.kt b/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/map/RoomData.kt new file mode 100644 index 0000000..9a84306 --- /dev/null +++ b/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/map/RoomData.kt @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2025-2025 Water-OR + * + * This file is part of ExeClient + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.llvg.exec.hypixel.skyblock.catacombs.map + +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter +import java.io.File +import java.io.FileReader +import java.io.FileWriter +import java.io.InputStreamReader +import java.io.Reader +import net.llvg.exec.ExeClient +import net.llvg.exec.utils.builder.BuilderBase +import net.llvg.exec.utils.builder.notNull +import net.llvg.exec.utils.classNameLogger +import net.llvg.exec.utils.json.nextTyped +import net.llvg.exec.utils.json.value + +data class RoomData( + val name: String, + var type: RoomType, + val cores: Set, + val crypts: Int, + val secrets: Int, + val mimicPoses: Int, +) { + class Builder private constructor() : BuilderBase { + private var name: String? = null + private var type: RoomType? = null + private val cores: MutableSet = LinkedHashSet() + private var crypts: Int = 0 + private var secrets: Int = 0 + private var mimicPoses: Int = 0 + + fun name(name: String): Builder = + apply { this.name = name } + + fun type(type: RoomType): Builder = + apply { this.type = type } + + fun core(core: Int): Builder = + apply { this.cores += core } + + fun crypts(crypts: Int): Builder = + apply { this.crypts = crypts } + + fun secrets(secrets: Int): Builder = + apply { this.secrets = secrets } + + fun mimicPoses(mimicPoses: Int): Builder = + apply { this.mimicPoses = mimicPoses } + + override fun build(): RoomData = RoomData( + notNull(name, "name"), + notNull(type, "type"), + cores, + crypts, + secrets, + mimicPoses + ) + + companion object { + fun create(): Builder = + Builder() + } + } + + companion object { + init { + rooms + } + + @JvmStatic + fun builder(): Builder = + Builder.create() + + @JvmStatic + operator fun get( + hash: Int + ): RoomData? = + rooms.find { hash in it.cores } + } +} + +private val logger = classNameLogger() +private const val roomDataFileName = "catacombs-room-data.json" +private val roomDataFile: File = File(ExeClient.directory, roomDataFileName) +private val rooms: List = run { + val internalPath = "/${ExeClient.MIXIN_ID}/$roomDataFileName" + val internalUrl = RoomData::class.java.getResource(internalPath) + requireNotNull(internalUrl) { "resource '$internalPath' is missing" } + + val input: Reader = + if (roomDataFile.isFile) { + FileReader(roomDataFile) + } else { + roomDataFile.parentFile.mkdirs() + roomDataFile.delete() + + InputStreamReader(internalUrl.openStream()) + } + + + val result: MutableList = ArrayList() + + JsonReader(input).use { + with(it) { + beginArray() + while (hasNext()) try { + result += nextTyped(RoomDataAdapter) + } catch (e: Throwable) { + logger.info("An error occur during reading room data from {}", input, e) + } + endArray() + } + } + + result.sortBy(RoomData::name) + + JsonWriter(FileWriter(roomDataFile)).use { + with(it) { + isLenient = true + serializeNulls = false + setIndent("\t") + beginArray() + for (i in result) { + value(i, RoomDataAdapter) + } + endArray() + } + } + + result +} \ No newline at end of file diff --git a/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/map/RoomDataAdapter.kt b/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/map/RoomDataAdapter.kt new file mode 100644 index 0000000..8eefc9a --- /dev/null +++ b/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/map/RoomDataAdapter.kt @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2025-2025 Water-OR + * + * This file is part of ExeClient + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.llvg.exec.hypixel.skyblock.catacombs.map + +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter +import net.llvg.exec.utils.json.nextTyped +import net.llvg.exec.utils.json.value + +object RoomDataAdapter : TypeAdapter() { + override fun write( + writer: JsonWriter, + value: RoomData? + ) { + with(writer) { + if (value === null) { + nullValue() + return + } + + value.apply { + beginObject() + name("name").value(name) + name("type").value(type, RoomTypeAdapter) + name("cores").apply { + beginArray() + for (core in cores) { + value(core) + } + endArray() + } + name("crypts").value(crypts) + name("secrets").value(secrets) + name("mimicPoses").value(mimicPoses) + endObject() + } + } + } + + override fun read( + reader: JsonReader + ): RoomData = + with(reader) { + RoomData.builder().apply { + beginObject() + while (hasNext()) when (nextName()) { + "name" -> name(nextString()) + "type" -> type(nextTyped(RoomTypeAdapter)) + "cores" -> { + beginArray() + while (hasNext()) { + core(nextInt()) + } + endArray() + } + + "crypts" -> crypts(nextInt()) + "secrets" -> secrets(nextInt()) + "mimicPoses", + "trappedChests" -> mimicPoses(nextInt()) + + else -> skipValue() + } + endObject() + }.build() + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/map/RoomType.kt b/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/map/RoomType.kt new file mode 100644 index 0000000..c3cfc31 --- /dev/null +++ b/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/map/RoomType.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2025-2025 Water-OR + * + * This file is part of ExeClient + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.llvg.exec.hypixel.skyblock.catacombs.map + +enum class RoomType { + NORMAL, + PUZZLE, + ENTRANCE, + FAIRY, + BLOOD, + MINI_BOSS, + TRAP, + RARE; +} \ No newline at end of file diff --git a/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/map/RoomTypeAdapter.kt b/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/map/RoomTypeAdapter.kt new file mode 100644 index 0000000..4cade2f --- /dev/null +++ b/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/map/RoomTypeAdapter.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2025-2025 Water-OR + * + * This file is part of ExeClient + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.llvg.exec.hypixel.skyblock.catacombs.map + +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +object RoomTypeAdapter : TypeAdapter() { + override fun write( + writer: JsonWriter, + value: RoomType? + ) { + with(writer) { + if (value === null) { + nullValue() + return + } + + value(value.name) + } + } + + override fun read( + reader: JsonReader + ): RoomType = + with(reader) { + when (val name = nextString().uppercase()) { + "CHAMPION" -> RoomType.MINI_BOSS + else -> RoomType.valueOf(name) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/map/scan/CatacombsMap.kt b/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/map/scan/CatacombsMap.kt new file mode 100644 index 0000000..718f5f2 --- /dev/null +++ b/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/map/scan/CatacombsMap.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2025-2025 Water-OR + * + * This file is part of ExeClient + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.llvg.exec.hypixel.skyblock.catacombs.map.scan + +import net.llvg.exec.utils.vector.Vec2I + +@Suppress("MemberVisibilityCanBePrivate") +object CatacombsMap { + const val BEGIN = -185 + + @JvmStatic + val roomEntries = Array(36) { + Vec2I( + BEGIN + (it % 6 shl 5), + BEGIN + (it / 6 shl 5) + ).let(::RoomEntry) + } + + fun roomEntryAtPos( + x: Int, + z: Int + ): RoomEntry? = + roomEntryAt( + (x - BEGIN shr 4) + 1 shr 1, + (z - BEGIN shr 4) + 1 shr 1 + ) + + fun roomEntryAt( + x: Int, + z: Int + ): RoomEntry? = + if (x in 0..5 && z in 0..5) { + roomEntries[x + 6 * z] + } else null + + @JvmStatic + val roomInfoSet: MutableSet = HashSet() + + init { + CatacombsScanner + } + + @JvmStatic + fun reset() { + for (i in roomEntries) { + i.info = null + } + roomInfoSet.clear() + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/map/scan/CatacombsScanFailedException.kt b/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/map/scan/CatacombsScanFailedException.kt new file mode 100644 index 0000000..95a5619 --- /dev/null +++ b/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/map/scan/CatacombsScanFailedException.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2025-2025 Water-OR + * + * This file is part of ExeClient + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.llvg.exec.hypixel.skyblock.catacombs.map.scan + +@Suppress("UNUSED_PARAMETER") +class CatacombsScanFailedException(e: Throwable) : RuntimeException() \ No newline at end of file diff --git a/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/map/scan/CatacombsScanner.kt b/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/map/scan/CatacombsScanner.kt new file mode 100644 index 0000000..43f5d82 --- /dev/null +++ b/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/map/scan/CatacombsScanner.kt @@ -0,0 +1,279 @@ +/* + * Copyright (C) 2025-2025 Water-OR + * + * This file is part of ExeClient + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.llvg.exec.hypixel.skyblock.catacombs.map.scan + +import java.util.concurrent.locks.ReadWriteLock +import java.util.concurrent.locks.ReentrantReadWriteLock +import kotlin.concurrent.withLock +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch +import net.llvg.exec.ExeClient +import net.llvg.exec.api.event.ExeCEventListenable +import net.llvg.exec.api.event.onEvent +import net.llvg.exec.feature.catacombs_scan.CatacombsScan +import net.llvg.exec.utils.classNameLogger +import net.llvg.exec.hypixel.skyblock.catacombs.map.RoomData +import net.llvg.exec.hypixel.skyblock.catacombs.map.RoomType +import net.llvg.exec.utils.vector.Vec2I +import net.llvg.exec.vanilla.block.id +import net.llvg.exec.vanilla.event.WorldClientEvent +import net.llvg.exec.vanilla.utils.chat_component.ChatColor +import net.llvg.exec.vanilla.utils.mc +import net.minecraft.init.Blocks +import net.minecraft.util.BlockPos +import net.minecraft.util.EnumFacing +import net.minecraft.world.chunk.Chunk + +@Suppress("MemberVisibilityCanBePrivate") +object CatacombsScanner : ExeCEventListenable { + init { + onEvent(Dispatchers.Default) { _: WorldClientEvent.Load.Pre -> + roomScanJob?.run { + cancel() + } + } + + initialize() + } + + private val scope: CoroutineScope = CoroutineScope(SupervisorJob()) + + val roomScanning: Boolean + get() = roomScanJob !== null + + fun scan(): Job? { + if (!CatacombsScan.checkInCatacombs() || !CatacombsScan.checkNotInBoss()) return null + + roomScanLock.readLock().withLock { + if (roomScanning) return null + } + val job: Job + roomScanLock.writeLock().withLock { + if (roomScanning) return null + job = scope.launch(Dispatchers.Default) c@{ + try { + scanRoomInternalSuspend() + } catch (e: Throwable) { + ExeClient.send { + "An error occur during scanning catacombs rooms, see logs for more information"() + .`--color`(ChatColor.DARK_RED) + } + logger.error("An error occur during scanning catacombs rooms", e) + throw CatacombsScanFailedException(e) + } + } + roomScanJob = job + } + job.invokeOnCompletion { + roomScanLock.writeLock().withLock { + roomScanJob = null + } + } + return job + } + + fun afterCurrentScan( + action: (Throwable?) -> Unit + ) { + if (!CatacombsScan.checkInCatacombs() || !CatacombsScan.checkNotInBoss()) return + + roomScanLock.readLock().withLock { + roomScanJob + ?.invokeOnCompletion { + action(it) + } + ?: action(null) + } + } + + fun scanAndHashPos( + pos: Vec2I + ): Int? { + return StringBuilder(150) + .apply { + val (x, z) = pos + val world = mc.theWorld ?: return null + val chunk = world.getChunkFromChunkCoords(x shr 4, z shr 4) + if (!chunk.isLoaded) return null + + val height = chunk.getHeightValue(x and 15, z and 15).coerceIn(11..140) + append(CharArray(140 - height) { '0' }) + var bedrock = 0 + for (y in height downTo 12) { + val id = chunk.getBlock(x, y, z).id + if (id == 0 && bedrock > 1 && y < 69) { + append(CharArray(y - 11) { '0' }) + break + } + + if (id == 7) { + ++bedrock + } else { + bedrock = 0 + if (id in hashBlockIdException) continue + } + append(id) + } + } + .toString() + .hashCode() + } +} + +private fun initialize() {} + +private val logger = classNameLogger() + +private val hashBlockIdException = intArrayOf( + 5, // planks + 54, // chest + 146, // trapped_chest +) + +private val cornerSideBlockIdInclusion = intArrayOf( + 0, // air + 159, // stained_hardened_clay +) +private var roomScanJob: Job? = null +private var roomScanLock: ReadWriteLock = ReentrantReadWriteLock() + +// do not inline this or the code will be much more stupid +private fun Chunk.getTopYOf( + pos: Vec2I +): Int { + val (x, z) = pos + val y = getHeightValue(x and 15, z and 15) - 1 + + return if (getBlock(x, y, z) == Blocks.gold_block) y - 1 else y +} + +private suspend fun scanRoomInternalSuspend() = coroutineScope { + val dataToEntries: MutableMap> = HashMap() + val jobs = mutableListOf() + + // scan new rooms + for (entry in CatacombsMap.roomEntries) { + if (entry.info !== null) continue + jobs += launch { + CatacombsScanner.scanAndHashPos(entry.pos)?.let(RoomData::get)?.let { data -> + val entries = dataToEntries[data] ?: synchronized(dataToEntries) { + dataToEntries.getOrPut(data) { ArrayList() } + } + + entries += entry + } + } + } + jobs.joinAll() + jobs.clear() + + // process known room-info + for (info in CatacombsMap.roomInfoSet) { + val entries = dataToEntries.remove(info.data) ?: continue + jobs += launch { + for (entry in entries) { + if (entry in info.entries) continue + entry.info = info + info.entries += entry + } + calcRotation(info) + } + } + jobs.joinAll() + jobs.clear() + + // process new room-info + for ((data, entries) in dataToEntries) { + jobs += launch { + val info = RoomInfo(data) + for (entry in entries) { + entry.info = info + info.entries += entry + } + CatacombsMap.roomInfoSet += info + calcRotation(info) + } + } + jobs.joinAll() + jobs.clear() +} + +private fun calcRotation( + info: RoomInfo +) { + if (info.rotation !== Rotation.UNKNOWN) return + + val world = mc.theWorld ?: return + val y: Int + + // actually it won't be null, but who knows :D + val entry1: RoomEntry = info.entries.firstOrNull() ?: return + + entry1.pos.let { + val chunk: Chunk = world.getChunkFromChunkCoords(it.x shr 4, it.y shr 4) ?: return + y = chunk.getTopYOf(it) + } + + if (info.data.type === RoomType.FAIRY) { + val (x, z) = entry1.pos - Vec2I(15, 15) + info.corner = BlockPos(x, y, z) + info.rotation = Rotation.SOUTH + return + } + + if (info.entries.size == 1) { + for (rotation in Rotation.directions) { + val (x, z) = entry1.pos + rotation.offset * 15 + + val chunk: Chunk = world.getChunkFromChunkCoords(x shr 4, z shr 4) ?: continue + if (chunk.getBlock(x, y, z).id != 159) continue + + info.corner = BlockPos(x, y, z) + info.rotation = rotation + return + } + } else for (entry in info.entries) { + for (rotation in Rotation.directions) { + val (x, z) = entry.pos + (rotation.offset * 15) + + val chunk: Chunk = world.getChunkFromChunkCoords(x shr 4, z shr 4) ?: continue + if ( + chunk.getBlock(x, y, z).id != 159 || + EnumFacing.HORIZONTALS.any { + chunk.getBlock( + x + it.frontOffsetX, + y, + z + it.frontOffsetZ + ).id !in cornerSideBlockIdInclusion + } + ) continue + + info.corner = BlockPos(x, y, z) + info.rotation = rotation + return + } + } +} + diff --git a/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/map/scan/RoomEntry.kt b/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/map/scan/RoomEntry.kt new file mode 100644 index 0000000..fceac81 --- /dev/null +++ b/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/map/scan/RoomEntry.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2025-2025 Water-OR + * + * This file is part of ExeClient + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.llvg.exec.hypixel.skyblock.catacombs.map.scan + +import net.llvg.exec.utils.vector.Vec2I + +class RoomEntry( + val pos: Vec2I +) { + var info: RoomInfo? = null +} \ No newline at end of file diff --git a/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/map/scan/RoomInfo.kt b/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/map/scan/RoomInfo.kt new file mode 100644 index 0000000..c15480a --- /dev/null +++ b/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/map/scan/RoomInfo.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2025-2025 Water-OR + * + * This file is part of ExeClient + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.llvg.exec.hypixel.skyblock.catacombs.map.scan + +import net.llvg.exec.hypixel.skyblock.catacombs.map.RoomData +import net.minecraft.util.BlockPos + +data class RoomInfo( + val data: RoomData +) { + var rotation: Rotation = Rotation.UNKNOWN + val entries: MutableSet = LinkedHashSet() + var corner: BlockPos? = null +} \ No newline at end of file diff --git a/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/map/scan/Rotation.kt b/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/map/scan/Rotation.kt new file mode 100644 index 0000000..d6bf39c --- /dev/null +++ b/src/main/kotlin/net/llvg/exec/hypixel/skyblock/catacombs/map/scan/Rotation.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2025-2025 Water-OR + * + * This file is part of ExeClient + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.llvg.exec.hypixel.skyblock.catacombs.map.scan + +import net.llvg.exec.utils.vector.Vec2I + +enum class Rotation( + x: Int, + y: Int +) { + UNKNOWN(0, 0), + NORTH(+1, +1), + SOUTH(-1, -1), + EAST(+1, -1), + WEST(-1, +1); + + val offset = Vec2I(x, y) + + companion object { + @JvmField + val directions: Array = + arrayOf(NORTH, SOUTH, EAST, WEST) + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/llvg/exec/mixin/callback/CallbackMinecraft.kt b/src/main/kotlin/net/llvg/exec/mixin/callback/CallbackMinecraft.kt index da4ae34..58cf547 100644 --- a/src/main/kotlin/net/llvg/exec/mixin/callback/CallbackMinecraft.kt +++ b/src/main/kotlin/net/llvg/exec/mixin/callback/CallbackMinecraft.kt @@ -23,8 +23,22 @@ package net.llvg.exec.mixin.callback import net.llvg.exec.vanilla.event.WorldClientEvent import net.llvg.exec.api.event.post +import net.llvg.exec.vanilla.event.GameStartEvent +import net.llvg.exec.vanilla.event.TickEvent import net.minecraft.client.multiplayer.WorldClient +fun postGameStartEventPost() { + GameStartEvent.Post.Impl.post(true) +} + +fun postTickEventClientPre() { + TickEvent.Client.Pre.Impl.post(true) +} + +fun postTickEventClientPost() { + TickEvent.Client.Post.Impl.post(true) +} + fun postWorldClientEventLoadPre( worldClient: WorldClient? ) { diff --git a/src/main/kotlin/net/llvg/exec/preload/vanilla_tweaker/ExeCTweaker.kt b/src/main/kotlin/net/llvg/exec/preload/vanilla_tweaker/ExeCTweaker.kt index 51885fd..cc49cab 100644 --- a/src/main/kotlin/net/llvg/exec/preload/vanilla_tweaker/ExeCTweaker.kt +++ b/src/main/kotlin/net/llvg/exec/preload/vanilla_tweaker/ExeCTweaker.kt @@ -22,6 +22,9 @@ package net.llvg.exec.preload.vanilla_tweaker import cc.polyfrost.oneconfig.loader.stage0.LaunchWrapperTweaker import java.io.File import net.llvg.exec.utils.classNameLogger +import net.llvg.exec.utils.delegate.InitOnceRef +import net.llvg.loliutils.delegate.getValue +import net.llvg.loliutils.delegate.setValue import net.llvg.loliutils.exception.cast import net.minecraft.launchwrapper.ITweaker import net.minecraft.launchwrapper.Launch @@ -36,14 +39,15 @@ class ExeCTweaker : ITweaker { assetsDir: File?, profile: String? ) { + internalGameDir = gameDir ?: File(".").absoluteFile } override fun injectIntoClassLoader( classLoader: LaunchClassLoader ) { val tweakClasses: MutableList = cast(Launch.blackboard["TweakClasses"]) - tweakClasses.add(LaunchWrapperTweaker::class.java.name) - tweakClasses.add("org.spongepowered.asm.launch.MixinTweaker") + tweakClasses += LaunchWrapperTweaker::class.java.name + tweakClasses += "org.spongepowered.asm.launch.MixinTweaker" } override fun getLaunchTarget(): String = @@ -71,6 +75,14 @@ class ExeCTweaker : ITweaker { MixinBootstrap.getPlatform().addContainer(location.toURI()) } + + companion object { + @JvmStatic + val gameDir: File + get() = internalGameDir + } } +private var internalGameDir by InitOnceRef() + private val logger = classNameLogger() \ No newline at end of file diff --git a/src/main/kotlin/net/llvg/exec/utils/StringUtils.kt b/src/main/kotlin/net/llvg/exec/utils/StringUtils.kt new file mode 100644 index 0000000..ba17055 --- /dev/null +++ b/src/main/kotlin/net/llvg/exec/utils/StringUtils.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2025 Water-OR + * + * This file is part of ExeClient + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +@file:JvmName("StringUtils") + +package net.llvg.exec.utils + +val mcFormatCodeRegex = Regex("\u00a7[0-9a-fk-or]", RegexOption.IGNORE_CASE) + +val String.noMcFormatCode: String + get() = mcFormatCodeRegex.replace(this, "") + +@Suppress("UNUSED") +val String.limitAscii: String + get() = filter { it.code in 21..126 } \ No newline at end of file diff --git a/src/main/kotlin/net/llvg/exec/utils/builder/BuilderBase.kt b/src/main/kotlin/net/llvg/exec/utils/builder/BuilderBase.kt new file mode 100644 index 0000000..43a15f6 --- /dev/null +++ b/src/main/kotlin/net/llvg/exec/utils/builder/BuilderBase.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2025 Water-OR + * + * This file is part of ExeClient + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.llvg.exec.utils.builder + +@BuilderMark +interface BuilderBase { + fun build(): R +} \ No newline at end of file diff --git a/src/main/kotlin/net/llvg/exec/utils/builder/BuilderMark.kt b/src/main/kotlin/net/llvg/exec/utils/builder/BuilderMark.kt new file mode 100644 index 0000000..2e8a13d --- /dev/null +++ b/src/main/kotlin/net/llvg/exec/utils/builder/BuilderMark.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2025 Water-OR + * + * This file is part of ExeClient + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.llvg.exec.utils.builder + +import kotlin.annotation.AnnotationRetention.* +import kotlin.annotation.AnnotationTarget.* + +@DslMarker +@Target( + FUNCTION, + PROPERTY_GETTER, + PROPERTY_SETTER, + CLASS, + TYPE +) +@Retention( + RUNTIME +) +annotation class BuilderMark diff --git a/src/main/kotlin/net/llvg/exec/utils/builder/BuilderUtils.kt b/src/main/kotlin/net/llvg/exec/utils/builder/BuilderUtils.kt new file mode 100644 index 0000000..f95060c --- /dev/null +++ b/src/main/kotlin/net/llvg/exec/utils/builder/BuilderUtils.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2025 Water-OR + * + * This file is part of ExeClient + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +@file:JvmName("BuilderUtils") + +package net.llvg.exec.utils.builder + +@Suppress("UnusedReceiverParameter") +fun BuilderBase<*>.notNull( + value: T?, + name: String +): T { + if (value === null) { + throw IllegalArgumentException("value [$name] should not be null!") + } + + return value +} \ No newline at end of file diff --git a/src/main/kotlin/net/llvg/exec/utils/delegate/InitOnceRef.kt b/src/main/kotlin/net/llvg/exec/utils/delegate/InitOnceRef.kt new file mode 100644 index 0000000..8fd09e5 --- /dev/null +++ b/src/main/kotlin/net/llvg/exec/utils/delegate/InitOnceRef.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2025 Water-OR + * + * This file is part of ExeClient + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.llvg.exec.utils.delegate + +import net.llvg.loliutils.delegate.ValRef +import net.llvg.loliutils.delegate.VarRef +import net.llvg.loliutils.delegate.getValue +import net.llvg.loliutils.delegate.wrapBox + +class InitOnceRef : VarRef { + private var container: ValRef? = null + private val lock = Any() + + override fun get(): T { + val container by container ?: synchronized(lock) { + container ?: throw IllegalArgumentException("Reference hasn't been initialized!") + } + return container + } + + override fun set( + o: T + ) { + if (container !== null) { + throw IllegalArgumentException("Reference has been initialized!") + } + synchronized(lock) { + if (container !== null) { + throw IllegalArgumentException("Reference has been initialized!") + } + container = o.wrapBox + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/llvg/exec/utils/json/JsonUtils.kt b/src/main/kotlin/net/llvg/exec/utils/json/JsonUtils.kt new file mode 100644 index 0000000..77620f3 --- /dev/null +++ b/src/main/kotlin/net/llvg/exec/utils/json/JsonUtils.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2025 Water-OR + * + * This file is part of ExeClient + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +@file:JvmName("JsonUtils") + +package net.llvg.exec.utils.json + +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +inline fun JsonReader.nextTyped( + adapter: TypeAdapter +): T = + adapter.read(this) + +inline fun JsonWriter.value( + value: T, + adapter: TypeAdapter +) { + adapter.write(this, value) +} \ No newline at end of file diff --git a/src/main/kotlin/net/llvg/exec/utils/vector/Vec2I.kt b/src/main/kotlin/net/llvg/exec/utils/vector/Vec2I.kt new file mode 100644 index 0000000..c7eaf51 --- /dev/null +++ b/src/main/kotlin/net/llvg/exec/utils/vector/Vec2I.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2025 Water-OR + * + * This file is part of ExeClient + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.llvg.exec.utils.vector + +import kotlin.math.max +import kotlin.math.min + +@Suppress("UNUSED") +data class Vec2I( + val x: Int, + val y: Int +) { + operator fun unaryPlus(): Vec2I = + if (this == ZERO) ZERO else this + + operator fun unaryMinus(): Vec2I = + if (this == ZERO) ZERO else Vec2I(-x, -y) + + infix operator fun plus( + o: Vec2I + ): Vec2I = + if (o == ZERO) this else if (this == ZERO) o else { + Vec2I(x + o.x, y + o.y) + } + + infix operator fun minus( + o: Vec2I + ): Vec2I = + if (o == ZERO) this else if (this == ZERO) -o else { + Vec2I(x - o.x, y - o.y) + } + + infix fun min( + o: Vec2I + ): Vec2I = + +Vec2I( + min(x, o.x), + min(y, o.y) + ) + + infix fun max( + o: Vec2I + ): Vec2I = + +Vec2I( + max(x, o.x), + max(y, o.y) + ) + + infix operator fun times( + o: Int + ): Vec2I = + if (this == ZERO || o == 0) ZERO else { + Vec2I(x * o, y * o) + } + + companion object { + @JvmField + val ZERO = Vec2I(0, 0) + } +} diff --git a/src/main/kotlin/net/llvg/exec/vanilla/block/BlockUtils.kt b/src/main/kotlin/net/llvg/exec/vanilla/block/BlockUtils.kt new file mode 100644 index 0000000..575f073 --- /dev/null +++ b/src/main/kotlin/net/llvg/exec/vanilla/block/BlockUtils.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2025 Water-OR + * + * This file is part of ExeClient + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +@file:JvmName("BlockUtils") + +package net.llvg.exec.vanilla.block + +import net.minecraft.block.Block + +val Block.id: Int + get() = Block.getIdFromBlock(this) \ No newline at end of file diff --git a/src/main/kotlin/net/llvg/exec/vanilla/event/GameStartEvent.kt b/src/main/kotlin/net/llvg/exec/vanilla/event/GameStartEvent.kt new file mode 100644 index 0000000..2633dbe --- /dev/null +++ b/src/main/kotlin/net/llvg/exec/vanilla/event/GameStartEvent.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2025 Water-OR + * + * This file is part of ExeClient + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.llvg.exec.vanilla.event + +import net.llvg.exec.api.event.ExeCEvent + +interface GameStartEvent : ExeCEvent { + interface Post : GameStartEvent { + data object Impl : Post + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/llvg/exec/vanilla/event/TickEvent.kt b/src/main/kotlin/net/llvg/exec/vanilla/event/TickEvent.kt new file mode 100644 index 0000000..722aed4 --- /dev/null +++ b/src/main/kotlin/net/llvg/exec/vanilla/event/TickEvent.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2025 Water-OR + * + * This file is part of ExeClient + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.llvg.exec.vanilla.event + +import net.llvg.exec.api.event.ExeCEvent + +interface TickEvent : ExeCEvent { + interface Client : TickEvent { + interface Pre : Client { + data object Impl: Pre + } + + interface Post : Client { + data object Impl : Post + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/llvg/exec/vanilla/utils/scoreboard/ScoreboardUtils.kt b/src/main/kotlin/net/llvg/exec/vanilla/utils/scoreboard/ScoreboardUtils.kt new file mode 100644 index 0000000..207a9e7 --- /dev/null +++ b/src/main/kotlin/net/llvg/exec/vanilla/utils/scoreboard/ScoreboardUtils.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2025 Water-OR + * + * This file is part of ExeClient + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +@file:JvmName("ScoreboardUtils") + +package net.llvg.exec.vanilla.utils.scoreboard + +import com.google.common.collect.ImmutableList +import net.llvg.exec.utils.noMcFormatCode +import net.llvg.exec.vanilla.utils.mc +import net.llvg.exec.vanilla.utils.scoreboard.ScoreboardFlags.FLAG_LIMIT_TO_15 +import net.llvg.exec.vanilla.utils.scoreboard.ScoreboardFlags.FLAG_NO_MC_FORMAT +import net.llvg.exec.vanilla.utils.scoreboard.ScoreboardFlags.FLAG_NO_STUPID_CHAR +import net.minecraft.scoreboard.ScorePlayerTeam + +@JvmField +val STUPID_SCOREBOARD_CHARACTER_REGEX = + Regex("[\\ud83c\\udf6b\\ud83d\\udca3\\ud83c\\udf89\\ud83c\\udf6d\\ud83d\\udc7d\\ud83d\\udd2e\\ud83d\\udc0d\\ud83d\\udc7e\\ud83c\\udf20⚽\\ud83c\\udfc0\\ud83d\\udc79\\ud83c\\udf81\\ud83c\\udf82]") + +object ScoreboardFlags { + const val FLAG_LIMIT_TO_15 = 0x01 + const val FLAG_NO_MC_FORMAT = 0x02 + const val FLAG_NO_STUPID_CHAR = 0x04 +} + +fun getScoreboardLines( + flags: Int = 0 +): List { + val world = mc.theWorld ?: return emptyList() + val player = mc.thePlayer ?: return emptyList() + + val scoreboard = world.scoreboard + val objective = + scoreboard.getPlayersTeam(player.name) + ?.run { chatFormat.colorIndex.takeIf { it >= 0 } } + ?.let { scoreboard.getObjectiveInDisplaySlot(3 + it) } + ?: scoreboard.getObjectiveInDisplaySlot(1) + ?: return emptyList() + + val builder = ImmutableList.builder() + + scoreboard + .getSortedScores(objective) + .filter { it.playerName?.startsWith('#') == false } + .run { if (flags and FLAG_LIMIT_TO_15 != 0 && size > 15) subList(size - 15, size) else this } + .forEach { + val name = it.playerName + builder.add( + ScorePlayerTeam.formatPlayerName(scoreboard.getPlayersTeam(name), name) + .run { if (flags and FLAG_NO_MC_FORMAT != 0) noMcFormatCode else this } + .run { if (flags and FLAG_NO_STUPID_CHAR != 0) noStupidScoreboardChar else this } + ) + } + + return builder.build() +} + +val String.noStupidScoreboardChar: String + get() = STUPID_SCOREBOARD_CHARACTER_REGEX.replace(this, "") \ No newline at end of file diff --git a/src/main/kotlin/net/llvg/exec/vanilla/utils/vector/VectorUtils.kt b/src/main/kotlin/net/llvg/exec/vanilla/utils/vector/VectorUtils.kt new file mode 100644 index 0000000..45dd8ee --- /dev/null +++ b/src/main/kotlin/net/llvg/exec/vanilla/utils/vector/VectorUtils.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2025 Water-OR + * + * This file is part of ExeClient + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +@file:JvmName("VectorUtils") + +package net.llvg.exec.vanilla.utils.vector + +import net.minecraft.util.Vec3i + +operator fun Vec3i.component1(): Int = x + +operator fun Vec3i.component2(): Int = y + +operator fun Vec3i.component3(): Int = z \ No newline at end of file diff --git a/src/main/resources/exec/catacombs-room-data.json b/src/main/resources/exec/catacombs-room-data.json new file mode 100644 index 0000000..7bfff72 --- /dev/null +++ b/src/main/resources/exec/catacombs-room-data.json @@ -0,0 +1,1309 @@ +[ + { + "name": "Admin", + "type": "NORMAL", + "cores": [ + -1989372370 + ], + "crypts": 34 + }, + { + "name": "Altar", + "type": "NORMAL", + "cores": [ + 1823952110, + 1004879775, + -1690727070 + ], + "crypts": 3, + "secrets": 6 + }, + { + "name": "Andesite", + "type": "NORMAL", + "cores": [ + -673246822 + ], + "secrets": 2 + }, + { + "name": "Archway", + "type": "NORMAL", + "cores": [ + -1447684689, + 1440798119 + ], + "crypts": 4, + "secrets": 3 + }, + { + "name": "Arrow Trap", + "type": "NORMAL", + "cores": [ + 332584803 + ], + "crypts": 1, + "secrets": 1 + }, + { + "name": "Atlas", + "type": "NORMAL", + "cores": [ + -685489527, + 745093020, + 308515062, + -38388847 + ], + "crypts": 5, + "secrets": 6 + }, + { + "name": "Balcony", + "type": "NORMAL", + "cores": [ + 1989395652, + 559495102 + ], + "secrets": 4 + }, + { + "name": "Banners", + "type": "NORMAL", + "cores": [ + -667603340 + ], + "crypts": 1, + "secrets": 1 + }, + { + "name": "Basement", + "type": "NORMAL", + "cores": [ + -204678789 + ], + "secrets": 1 + }, + { + "name": "Beams", + "type": "NORMAL", + "cores": [ + -2082251638 + ], + "crypts": 2, + "secrets": 2 + }, + { + "name": "Big Red Flag", + "type": "NORMAL", + "cores": [ + -1265317780 + ], + "crypts": 1, + "secrets": 2 + }, + { + "name": "Black Flag", + "type": "NORMAL", + "cores": [ + 153580070 + ], + "crypts": 1, + "secrets": 3 + }, + { + "name": "Blood", + "type": "BLOOD", + "cores": [ + -2130054003 + ] + }, + { + "name": "Blue Skulls", + "type": "NORMAL", + "cores": [ + -999204041 + ], + "crypts": 4, + "secrets": 1 + }, + { + "name": "Bomb Defuse", + "type": "PUZZLE", + "cores": [ + 86014075 + ] + }, + { + "name": "Boulder", + "type": "PUZZLE", + "cores": [ + -671152674, + 307825200 + ] + }, + { + "name": "Bridges", + "type": "NORMAL", + "cores": [ + -1604951086, + -1989950542 + ], + "crypts": 6, + "secrets": 6 + }, + { + "name": "Buttons", + "type": "NORMAL", + "cores": [ + -1359302282, + 160943502, + 823430452, + 2123177620 + ], + "crypts": 21, + "secrets": 5, + "trappedChests": 1 + }, + { + "name": "Cage", + "type": "NORMAL", + "cores": [ + 272954274 + ], + "secrets": 1 + }, + { + "name": "Cages", + "type": "NORMAL", + "cores": [ + -316384390 + ], + "secrets": 2 + }, + { + "name": "Carpets", + "type": "NORMAL", + "cores": [ + 988444684, + 1890757664, + 1186348038, + -1160833644 + ], + "crypts": 3, + "secrets": 1 + }, + { + "name": "Cathedral", + "type": "NORMAL", + "cores": [ + -2043617055, + 789869846, + -1212586959, + 1956475445 + ], + "crypts": 5, + "secrets": 8 + }, + { + "name": "Catwalk", + "type": "NORMAL", + "cores": [ + 1600132124, + -1010346133, + -1694830065 + ], + "crypts": 5, + "secrets": 6 + }, + { + "name": "Cell", + "type": "NORMAL", + "cores": [ + 1751890846 + ], + "secrets": 1 + }, + { + "name": "Chains", + "type": "NORMAL", + "cores": [ + 113272043 + ], + "secrets": 2 + }, + { + "name": "Chambers", + "type": "NORMAL", + "cores": [ + 1756685113, + 368708000, + 252800591 + ], + "crypts": 6, + "secrets": 5 + }, + { + "name": "Cobble Wall Pillar", + "type": "NORMAL", + "cores": [ + 259238824 + ], + "crypts": 1, + "secrets": 2 + }, + { + "name": "Creeper Beams", + "type": "PUZZLE", + "cores": [ + -755321869, + 573221447 + ] + }, + { + "name": "Criss-Cross", + "type": "NORMAL", + "cores": [ + 650161016, + 1002362494 + ], + "crypts": 6, + "secrets": 1 + }, + { + "name": "Crypt", + "type": "NORMAL", + "cores": [ + -845911506, + 331494915 + ], + "crypts": 2, + "secrets": 5 + }, + { + "name": "Deathmite", + "type": "NORMAL", + "cores": [ + -233562612, + -1901273450, + -1408070175 + ], + "crypts": 4, + "secrets": 6 + }, + { + "name": "Default", + "type": "CHAMPION", + "cores": [ + 652570347, + 1956609103 + ] + }, + { + "name": "Diagonal", + "type": "NORMAL", + "cores": [ + 2147431657, + 1144217017, + -464769223 + ], + "crypts": 3, + "secrets": 4 + }, + { + "name": "Dino Site", + "type": "NORMAL", + "cores": [ + -1113939414, + -1425445617, + -609789679 + ], + "crypts": 4, + "secrets": 4 + }, + { + "name": "Dip", + "type": "NORMAL", + "cores": [ + -1691958814 + ], + "crypts": 3, + "secrets": 2 + }, + { + "name": "Dome", + "type": "NORMAL", + "cores": [ + 462413746 + ], + "crypts": 2, + "secrets": 2 + }, + { + "name": "Doors", + "type": "NORMAL", + "cores": [ + 164990589, + 1033794268 + ], + "crypts": 7, + "secrets": 5 + }, + { + "name": "Double Diamond", + "type": "NORMAL", + "cores": [ + 1308341800 + ], + "secrets": 3 + }, + { + "name": "Dragon", + "type": "CHAMPION", + "cores": [ + 84632407 + ] + }, + { + "name": "Drop", + "type": "NORMAL", + "cores": [ + -615308028 + ], + "crypts": 6, + "secrets": 2 + }, + { + "name": "Dueces", + "type": "NORMAL", + "cores": [ + 1046920372 + ], + "crypts": 6, + "secrets": 3, + "trappedChests": 1 + }, + { + "name": "Duncan", + "type": "NORMAL", + "cores": [ + 544418695 + ], + "secrets": 1 + }, + { + "name": "End", + "type": "NORMAL", + "cores": [ + -1897192562 + ], + "crypts": 1, + "secrets": 2 + }, + { + "name": "Entrance", + "type": "ENTRANCE", + "cores": [ + 274652966, + -1092072828, + 1913969999 + ] + }, + { + "name": "Fairy", + "type": "FAIRY", + "cores": [ + 1484567748 + ] + }, + { + "name": "Flags", + "type": "NORMAL", + "cores": [ + -1267948931, + 284566079, + 1157102457, + 67929126 + ], + "crypts": 8, + "secrets": 7 + }, + { + "name": "Gold", + "type": "NORMAL", + "cores": [ + -1035453872, + 1451614295 + ], + "secrets": 1 + }, + { + "name": "Golden Oasis", + "type": "NORMAL", + "cores": [ + -1666473430 + ], + "crypts": 1, + "secrets": 1 + }, + { + "name": "Grand Library", + "type": "NORMAL", + "cores": [ + 749593273, + 1348435369 + ], + "crypts": 2, + "secrets": 4 + }, + { + "name": "Granite", + "type": "NORMAL", + "cores": [ + -456244067 + ], + "secrets": 2 + }, + { + "name": "Grass Ruin", + "type": "NORMAL", + "cores": [ + 568565222, + -786211724 + ], + "crypts": 4, + "secrets": 3 + }, + { + "name": "Gravel", + "type": "NORMAL", + "cores": [ + 841898152, + 1127962661, + 4304131 + ], + "crypts": 2, + "secrets": 6 + }, + { + "name": "Hall", + "type": "NORMAL", + "cores": [ + -2131538192 + ] + }, + { + "name": "Hallway", + "type": "NORMAL", + "cores": [ + -402028497, + 351544339, + -1723662179, + -1644671806 + ], + "crypts": 1, + "secrets": 3 + }, + { + "name": "Higher Blaze", + "type": "PUZZLE", + "cores": [ + 1103121487, + 23134049, + -243302881 + ], + "secrets": 1 + }, + { + "name": "Ice Fill", + "type": "PUZZLE", + "cores": [ + 327081838, + 1328525306, + 161828987 + ] + }, + { + "name": "Ice Path", + "type": "PUZZLE", + "cores": [ + 1073658098 + ] + }, + { + "name": "Jumping Skulls", + "type": "NORMAL", + "cores": [ + -1496765468 + ], + "secrets": 1 + }, + { + "name": "King Midas", + "type": "CHAMPION", + "cores": [ + -1678546839 + ], + "crypts": 1 + }, + { + "name": "Knight", + "type": "NORMAL", + "cores": [ + -1746428299 + ], + "secrets": 3 + }, + { + "name": "Lava Pit", + "type": "RARE", + "cores": [ + -1519947323 + ], + "secrets": 3 + }, + { + "name": "Lava Ravine", + "type": "NORMAL", + "cores": [ + -2040489612, + -294054018, + -2053308786 + ], + "crypts": 4, + "secrets": 6 + }, + { + "name": "Layers", + "type": "NORMAL", + "cores": [ + 1516049261, + 820948152, + 456930870, + -1910037748 + ], + "secrets": 8 + }, + { + "name": "Leaves", + "type": "NORMAL", + "cores": [ + -1967474423 + ], + "crypts": 1, + "secrets": 1 + }, + { + "name": "Locked Away", + "type": "NORMAL", + "cores": [ + 754378401 + ], + "crypts": 1, + "secrets": 1 + }, + { + "name": "Logs", + "type": "NORMAL", + "cores": [ + -1803705489 + ], + "secrets": 4 + }, + { + "name": "Long Hall", + "type": "NORMAL", + "cores": [ + -872480083 + ], + "crypts": 3, + "secrets": 3 + }, + { + "name": "Lots Of Floors", + "type": "NORMAL", + "cores": [ + 1449723216 + ], + "crypts": 1, + "secrets": 3 + }, + { + "name": "Lower Blaze", + "type": "PUZZLE", + "cores": [ + -1092103153, + -2027662369, + -1141175903 + ], + "secrets": 1 + }, + { + "name": "Mage", + "type": "NORMAL", + "cores": [ + -338946136, + 1089356068 + ], + "secrets": 4 + }, + { + "name": "Market", + "type": "NORMAL", + "cores": [ + 270637140, + -402574914, + -880417926 + ], + "crypts": 4, + "secrets": 5 + }, + { + "name": "Melon", + "type": "NORMAL", + "cores": [ + -325025964, + 1964904676, + 158528145 + ], + "crypts": 4, + "secrets": 7 + }, + { + "name": "Mines", + "type": "NORMAL", + "cores": [ + -361911912, + -39488099, + 1363618678, + 1227315161 + ], + "crypts": 11, + "secrets": 10 + }, + { + "name": "Mirror", + "type": "NORMAL", + "cores": [ + -1823353629 + ], + "secrets": 1 + }, + { + "name": "Mossy", + "type": "NORMAL", + "cores": [ + -1169615458, + 1432034198, + 1896208123 + ], + "crypts": 2, + "secrets": 4 + }, + { + "name": "Multicolored", + "type": "NORMAL", + "cores": [ + -671539463 + ], + "secrets": 1 + }, + { + "name": "Mural", + "type": "NORMAL", + "cores": [ + -1027066030 + ], + "secrets": 1 + }, + { + "name": "Museum", + "type": "NORMAL", + "cores": [ + -1957538226, + -2121384577, + -1797804860, + 1514395908 + ], + "crypts": 4, + "secrets": 5 + }, + { + "name": "Mushroom", + "type": "NORMAL", + "cores": [ + 1073109158 + ], + "secrets": 1 + }, + { + "name": "New Trap", + "type": "TRAP", + "cores": [ + -1358669872, + -1989128497 + ], + "crypts": 1, + "secrets": 3, + "trappedChests": 1 + }, + { + "name": "Old Trap", + "type": "TRAP", + "cores": [ + 1128554492 + ], + "crypts": 2, + "secrets": 4 + }, + { + "name": "Overgrown", + "type": "NORMAL", + "cores": [ + 1858897577 + ], + "secrets": 3 + }, + { + "name": "Overgrown Chains", + "type": "NORMAL", + "cores": [ + 1077887433 + ], + "crypts": 1, + "secrets": 2 + }, + { + "name": "Painting", + "type": "NORMAL", + "cores": [ + 474745227 + ], + "secrets": 2 + }, + { + "name": "Pedestal", + "type": "NORMAL", + "cores": [ + -1346033867, + 281551136 + ], + "crypts": 1, + "secrets": 5 + }, + { + "name": "Perch", + "type": "NORMAL", + "cores": [ + 27598620 + ], + "crypts": 1, + "secrets": 2 + }, + { + "name": "Pipes", + "type": "NORMAL", + "cores": [ + 952529534, + -901820944, + -664950032, + -1205767926 + ], + "crypts": 9, + "secrets": 7 + }, + { + "name": "Pirate", + "type": "NORMAL", + "cores": [ + -1175423677, + 1942933815, + 2002468229 + ], + "crypts": 2, + "secrets": 6 + }, + { + "name": "Pit", + "type": "NORMAL", + "cores": [ + -1862968316, + -258250108, + 265221970, + 430079089 + ], + "crypts": 4, + "secrets": 5 + }, + { + "name": "Pressure Plates", + "type": "NORMAL", + "cores": [ + 1172966775, + 681797038 + ], + "crypts": 6, + "secrets": 6 + }, + { + "name": "Prison Cell", + "type": "NORMAL", + "cores": [ + 1172045122 + ], + "crypts": 1, + "secrets": 1 + }, + { + "name": "Purple Flags", + "type": "NORMAL", + "cores": [ + 887024382, + 701310376 + ], + "crypts": 7, + "secrets": 5 + }, + { + "name": "Quad Lava", + "type": "NORMAL", + "cores": [ + 52900193 + ], + "secrets": 2 + }, + { + "name": "Quartz Knight", + "type": "NORMAL", + "cores": [ + -1639566599, + 153753389, + -129760550, + -742870398 + ], + "crypts": 9, + "secrets": 7 + }, + { + "name": "Quiz", + "type": "PUZZLE", + "cores": [ + 1928619293 + ] + }, + { + "name": "Raccoon", + "type": "NORMAL", + "cores": [ + 497881745 + ], + "crypts": 2, + "secrets": 4 + }, + { + "name": "Rail Track", + "type": "RARE", + "cores": [ + -701175845 + ], + "crypts": 3, + "secrets": 3 + }, + { + "name": "Rails", + "type": "NORMAL", + "cores": [ + 526587049, + -214948895, + -1778261714, + 1937419120 + ], + "crypts": 1, + "secrets": 9 + }, + { + "name": "Rare Overgrown", + "type": "RARE", + "cores": [ + -1643528240 + ], + "secrets": 3 + }, + { + "name": "Rare Pillars", + "type": "RARE", + "cores": [ + 1216268340 + ], + "secrets": 1 + }, + { + "name": "Red Blue", + "type": "NORMAL", + "cores": [ + 1011477602, + 1607395895, + -1794981292 + ], + "crypts": 1, + "secrets": 4 + }, + { + "name": "Red Green", + "type": "NORMAL", + "cores": [ + -1085327384 + ], + "crypts": 2, + "secrets": 3 + }, + { + "name": "Redstone Crypt", + "type": "NORMAL", + "cores": [ + -1054702517, + 127199896 + ], + "secrets": 3 + }, + { + "name": "Redstone Key", + "type": "NORMAL", + "cores": [ + 348655632 + ], + "crypts": 4, + "secrets": 3, + "trappedChests": 1 + }, + { + "name": "Redstone Warrior", + "type": "NORMAL", + "cores": [ + 1313090868, + 1819727964 + ], + "crypts": 4, + "secrets": 3 + }, + { + "name": "Ritual", + "type": "NORMAL", + "cores": [ + 758637731 + ], + "crypts": 1, + "secrets": 3 + }, + { + "name": "Sand Dragon", + "type": "RARE", + "cores": [ + -1497998508 + ], + "crypts": 1, + "secrets": 1 + }, + { + "name": "Sarcophagus", + "type": "NORMAL", + "cores": [ + 1986002687 + ], + "crypts": 1, + "secrets": 3 + }, + { + "name": "Scaffolding", + "type": "NORMAL", + "cores": [ + 2091387826 + ], + "secrets": 2 + }, + { + "name": "Shadow Assassin", + "type": "CHAMPION", + "cores": [ + -430117371 + ], + "crypts": 2 + }, + { + "name": "Silver Sword", + "type": "NORMAL", + "cores": [ + -224496952 + ], + "secrets": 1 + }, + { + "name": "Skull", + "type": "NORMAL", + "cores": [ + -46929855, + 1667732153 + ], + "secrets": 2 + }, + { + "name": "Slabs", + "type": "NORMAL", + "cores": [ + -1811244478 + ], + "crypts": 2, + "secrets": 2 + }, + { + "name": "Slime", + "type": "NORMAL", + "cores": [ + 1203689085, + 611794931, + -1050545277 + ], + "crypts": 1, + "secrets": 5, + "trappedChests": 1 + }, + { + "name": "Sloth", + "type": "NORMAL", + "cores": [ + -1390729196 + ], + "crypts": 1, + "secrets": 1 + }, + { + "name": "Small Stairs", + "type": "NORMAL", + "cores": [ + -1250912300 + ], + "crypts": 1, + "secrets": 2 + }, + { + "name": "Small Waterfall", + "type": "NORMAL", + "cores": [ + -1682285647 + ], + "crypts": 5, + "secrets": 2 + }, + { + "name": "Spider", + "type": "NORMAL", + "cores": [ + 365045229, + -1361285742, + 1778566373 + ], + "crypts": 3, + "secrets": 9 + }, + { + "name": "Spikes", + "type": "NORMAL", + "cores": [ + 881396995 + ], + "crypts": 2, + "secrets": 3 + }, + { + "name": "Staircase", + "type": "NORMAL", + "cores": [ + 1386509425 + ], + "crypts": 2, + "secrets": 3 + }, + { + "name": "Stairs", + "type": "NORMAL", + "cores": [ + -330702540, + -283979980, + -697693183, + 606731747 + ], + "crypts": 1, + "secrets": 4 + }, + { + "name": "Steps", + "type": "NORMAL", + "cores": [ + 1350621298 + ], + "crypts": 1, + "secrets": 1 + }, + { + "name": "Stone Window", + "type": "RARE", + "cores": [ + 477318192 + ], + "crypts": 1, + "secrets": 2 + }, + { + "name": "Supertall", + "type": "NORMAL", + "cores": [ + 1522346451, + -1376632689, + 291711773, + -46683467 + ], + "crypts": 6, + "secrets": 6 + }, + { + "name": "Teleport Maze", + "type": "PUZZLE", + "cores": [ + 487124604, + 2089453469 + ] + }, + { + "name": "Temple", + "type": "NORMAL", + "cores": [ + 1663174337, + 1480555517 + ], + "secrets": 3 + }, + { + "name": "Three Floors", + "type": "RARE", + "cores": [ + 633179672 + ], + "secrets": 1 + }, + { + "name": "Three Weirdos", + "type": "PUZZLE", + "cores": [ + -476788643 + ] + }, + { + "name": "Tic Tac Toe", + "type": "PUZZLE", + "cores": [ + 1958698161 + ], + "secrets": 1 + }, + { + "name": "Tombstone", + "type": "RARE", + "cores": [ + 1965783806 + ], + "secrets": 2 + }, + { + "name": "Tomioka", + "type": "NORMAL", + "cores": [ + -195263543 + ] + }, + { + "name": "Trinity", + "type": "RARE", + "cores": [ + 256380076 + ], + "crypts": 3, + "secrets": 4 + }, + { + "name": "Vinny 8 Ball", + "type": "RARE", + "cores": [ + -1169880205 + ], + "secrets": 1 + }, + { + "name": "Water", + "type": "NORMAL", + "cores": [ + -1849552977 + ], + "secrets": 2 + }, + { + "name": "Water Board", + "type": "PUZZLE", + "cores": [ + -109725212, + -353291158 + ] + }, + { + "name": "Waterfall", + "type": "NORMAL", + "cores": [ + 740310812, + 82232284, + 1379043687, + -1971268623 + ], + "crypts": 3, + "secrets": 8 + }, + { + "name": "Well", + "type": "NORMAL", + "cores": [ + 196766004, + 718434953, + 1955671195 + ], + "crypts": 5, + "secrets": 7 + }, + { + "name": "Withermancer", + "type": "NORMAL", + "cores": [ + -499989468, + -406356904, + -1645219026 + ], + "crypts": 6, + "secrets": 4 + }, + { + "name": "Wizard", + "type": "NORMAL", + "cores": [ + -23510667, + 1958624830, + 735485465 + ], + "crypts": 8, + "secrets": 4 + }, + { + "name": "Zodd", + "type": "NORMAL", + "cores": [ + 105458531 + ], + "crypts": 1, + "secrets": 1 + } +]