package at.hannibal2.skyhanni.data import at.hannibal2.skyhanni.api.event.HandleEvent import at.hannibal2.skyhanni.events.BlockClickEvent import at.hannibal2.skyhanni.events.ColdUpdateEvent import at.hannibal2.skyhanni.events.DebugDataCollectEvent import at.hannibal2.skyhanni.events.IslandChangeEvent import at.hannibal2.skyhanni.events.LorenzChatEvent import at.hannibal2.skyhanni.events.LorenzTickEvent import at.hannibal2.skyhanni.events.LorenzWorldChangeEvent import at.hannibal2.skyhanni.events.PlaySoundEvent import at.hannibal2.skyhanni.events.ScoreboardUpdateEvent import at.hannibal2.skyhanni.events.ServerBlockChangeEvent import at.hannibal2.skyhanni.events.mining.OreMinedEvent import at.hannibal2.skyhanni.events.player.PlayerDeathEvent import at.hannibal2.skyhanni.events.skyblock.ScoreboardAreaChangeEvent import at.hannibal2.skyhanni.features.gui.customscoreboard.ScoreboardPattern import at.hannibal2.skyhanni.features.mining.OreBlock import at.hannibal2.skyhanni.features.mining.isTitanium import at.hannibal2.skyhanni.skyhannimodule.SkyHanniModule import at.hannibal2.skyhanni.utils.CollectionUtils.countBy import at.hannibal2.skyhanni.utils.LocationUtils.distanceToPlayer import at.hannibal2.skyhanni.utils.LorenzUtils import at.hannibal2.skyhanni.utils.LorenzUtils.inAnyIsland import at.hannibal2.skyhanni.utils.LorenzUtils.isInIsland import at.hannibal2.skyhanni.utils.LorenzVec import at.hannibal2.skyhanni.utils.NumberUtil.formatInt import at.hannibal2.skyhanni.utils.RegexUtils.firstMatcher import at.hannibal2.skyhanni.utils.RegexUtils.matchMatcher import at.hannibal2.skyhanni.utils.RegexUtils.matches import at.hannibal2.skyhanni.utils.SimpleTimeMark import at.hannibal2.skyhanni.utils.TimeUtils.format import at.hannibal2.skyhanni.utils.repopatterns.RepoPattern import io.netty.util.internal.ConcurrentSet import net.minecraft.init.Blocks import net.minecraftforge.fml.common.eventhandler.SubscribeEvent import java.util.concurrent.ConcurrentLinkedQueue import kotlin.math.absoluteValue import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds @SkyHanniModule object MiningAPI { private val group = RepoPattern.group("data.miningapi") private val glaciteAreaPattern by group.pattern("area.glacite", "Glacite Tunnels|Great Glacite Lake") private val dwarvenBaseCampPattern by group.pattern("area.basecamp", "Dwarven Base Camp") // TODO add regex test private val coldResetPattern by group.pattern( "cold.reset", "§6The warmth of the campfire reduced your §r§b❄ Cold §r§6to §r§a0§r§6!|§c ☠ §r§7You froze to death§r§7\\.", ) private val pickbobulusGroup = group.group("pickobulus") /** * REGEX-TEST: §aYou used your §r§6Pickobulus §r§aPickaxe Ability! */ private val pickobulusUsePattern by pickbobulusGroup.pattern( "use", "§aYou used your §r§6Pickobulus §r§aPickaxe Ability!", ) // TODO add regex test private val pickobulusEndPattern by pickbobulusGroup.pattern( "end", "§7Your §r§aPickobulus §r§7destroyed §r§e(?[\\d,.]+) §r§7blocks!", ) /** * REGEX-TEST: §7Your §r§aPickobulus §r§7didn't destroy any blocks! */ private val pickobulusFailPattern by pickbobulusGroup.pattern( "fail", "§7Your §r§aPickobulus §r§7didn't destroy any blocks!" ) private data class MinedBlock(val ore: OreBlock, var confirmed: Boolean) { val time: SimpleTimeMark = SimpleTimeMark.now() } // normal mining private val recentClickedBlocks = ConcurrentSet>() private val surroundingMinedBlocks = ConcurrentLinkedQueue>() private var lastInitSound = SimpleTimeMark.farPast() private var initBlockPos: LorenzVec? = null private var waitingForInitSound = true private var waitingForEffMinerSound = false private var waitingForEffMinerBlock = false // pickobulus private var lastPickobulusUse = SimpleTimeMark.farPast() private var lastPickobulusExplosion = SimpleTimeMark.farPast() private var pickobulusExplosionPos: LorenzVec? = null private val pickobulusMinedBlocks = ConcurrentLinkedQueue>() private val pickobulusActive get() = lastPickobulusUse.passedSince() < 2.seconds private var pickobulusWaitingForSound = false private var pickobulusWaitingForBlock = false // oreblock data var inGlacite = false var inTunnels = false var inMineshaft = false var inDwarvenMines = false var inCrystalHollows = false var inCrimsonIsle = false var inEnd = false var inSpidersDen = false var currentAreaOreBlocks = setOf() private set private val allowedSoundNames = setOf("dig.glass", "dig.stone", "dig.gravel", "dig.cloth", "random.orb") var cold: Int = 0 private set var lastColdUpdate = SimpleTimeMark.farPast() private set var lastColdReset = SimpleTimeMark.farPast() private set fun inGlaciteArea() = inGlacialTunnels() || IslandType.MINESHAFT.isInIsland() fun inDwarvenBaseCamp() = IslandType.DWARVEN_MINES.isInIsland() && dwarvenBaseCampPattern.matches(LorenzUtils.skyBlockArea) fun inRegularDwarven() = IslandType.DWARVEN_MINES.isInIsland() && !inGlacialTunnels() fun inCrystalHollows() = IslandType.CRYSTAL_HOLLOWS.isInIsland() fun inMineshaft() = IslandType.MINESHAFT.isInIsland() fun inGlacialTunnels() = IslandType.DWARVEN_MINES.isInIsland() && glaciteAreaPattern.matches(LorenzUtils.skyBlockArea) fun inCustomMiningIsland() = inAnyIsland( IslandType.DWARVEN_MINES, IslandType.MINESHAFT, IslandType.CRYSTAL_HOLLOWS, IslandType.THE_END, IslandType.CRIMSON_ISLE, IslandType.SPIDER_DEN, ) fun inColdIsland() = inAnyIsland(IslandType.DWARVEN_MINES, IslandType.MINESHAFT) @SubscribeEvent fun onScoreboardChange(event: ScoreboardUpdateEvent) { val newCold = ScoreboardPattern.coldPattern.firstMatcher(event.scoreboard) { group("cold").toInt().absoluteValue } ?: return if (newCold != cold) { updateCold(newCold) } } @SubscribeEvent fun onBlockClick(event: BlockClickEvent) { if (!inCustomMiningIsland()) return if (event.clickType != ClickType.LEFT_CLICK) return //println(event.getBlockState.properties) if (OreBlock.getByStateOrNull(event.getBlockState) == null) return recentClickedBlocks += event.position to SimpleTimeMark.now() } @SubscribeEvent fun onChat(event: LorenzChatEvent) { if (!inColdIsland()) return if (coldResetPattern.matches(event.message)) { updateCold(0) lastColdReset = SimpleTimeMark.now() return } if (pickobulusUsePattern.matches(event.message)) { lastPickobulusUse = SimpleTimeMark.now() return } if (pickobulusFailPattern.matches(event.message)) { resetPickobulusEvent() pickobulusMinedBlocks.clear() return } pickobulusEndPattern.matchMatcher(event.message) { val amount = group("amount").formatInt() resetPickobulusEvent() val blocks = pickobulusMinedBlocks.take(amount).countBy { it.second } if (blocks.isNotEmpty()) OreMinedEvent(null, blocks).post() pickobulusMinedBlocks.clear() return } } @SubscribeEvent fun onPlayerDeath(event: PlayerDeathEvent) { if (event.name == LorenzUtils.getPlayerName()) { updateCold(0) lastColdReset = SimpleTimeMark.now() } } @SubscribeEvent fun onPlaySound(event: PlaySoundEvent) { if (!inCustomMiningIsland()) return if (event.soundName == "random.explode" && lastPickobulusUse.passedSince() < 5.seconds) { lastPickobulusExplosion = SimpleTimeMark.now() pickobulusExplosionPos = event.location pickobulusWaitingForSound = true return } if (event.soundName !in allowedSoundNames) return if (pickobulusActive && pickobulusWaitingForSound) { pickobulusWaitingForSound = false pickobulusWaitingForBlock = true return } if (waitingForInitSound) { if (event.soundName != "random.orb" && event.pitch == 0.7936508f) { val pos = event.location.roundLocationToBlock() if (recentClickedBlocks.none { it.first == pos }) return waitingForInitSound = false waitingForEffMinerBlock = true initBlockPos = event.location.roundLocationToBlock() lastInitSound = SimpleTimeMark.now() } return } if (waitingForEffMinerSound) { val lastBlock = surroundingMinedBlocks.lastOrNull()?.first ?: return if (lastBlock.confirmed) return waitingForEffMinerSound = false lastBlock.confirmed = true waitingForEffMinerBlock = true } } @SubscribeEvent fun onBlockChange(event: ServerBlockChangeEvent) { if (!inCustomMiningIsland()) return val oldState = event.oldState val newState = event.newState val oldBlock = oldState.block val newBlock = newState.block if (oldState == newState) return if (oldBlock == Blocks.air || oldBlock == Blocks.bedrock) return if (newBlock != Blocks.air && newBlock != Blocks.bedrock && !isTitanium(newState)) return val pos = event.location if (pickobulusActive && pickobulusWaitingForBlock) { val explosionPos = pickobulusExplosionPos ?: return if (explosionPos.distance(pos) > 15) return val ore = OreBlock.getByStateOrNull(oldState) ?: return if (pickobulusMinedBlocks.any { it.first == pos }) return pickobulusMinedBlocks += pos to ore pickobulusWaitingForBlock = false pickobulusWaitingForSound = true return } if (lastInitSound.passedSince() > 100.milliseconds) return if (pos.distanceToPlayer() > 7) return val ore = OreBlock.getByStateOrNull(oldState) ?: return if (initBlockPos == pos) { surroundingMinedBlocks += MinedBlock(ore, true) to pos runEvent() return } if (waitingForEffMinerBlock) { if (surroundingMinedBlocks.any { it.second == pos }) return waitingForEffMinerBlock = false surroundingMinedBlocks += MinedBlock(ore, false) to pos waitingForEffMinerSound = true return } } @SubscribeEvent fun onTick(event: LorenzTickEvent) { if (!inCustomMiningIsland()) return if (currentAreaOreBlocks.isEmpty()) return // if somehow you take more than 20 seconds to mine a single block, congrats recentClickedBlocks.removeIf { it.second.passedSince() >= 20.seconds } surroundingMinedBlocks.removeIf { it.first.time.passedSince() >= 20.seconds } if (!waitingForInitSound && lastInitSound.passedSince() > 200.milliseconds) { resetOreEvent() } if (!lastPickobulusUse.isFarPast() && lastPickobulusUse.passedSince() > 5.seconds) { resetPickobulusEvent() pickobulusMinedBlocks.clear() } } @HandleEvent fun onAreaChange(event: ScoreboardAreaChangeEvent) { if (!inCustomMiningIsland()) return updateLocation() } @SubscribeEvent fun onIslandChange(event: IslandChangeEvent) { updateLocation() } private fun runEvent() { resetOreEvent() if (surroundingMinedBlocks.isEmpty()) return val originalBlock = surroundingMinedBlocks.firstOrNull { it.first.confirmed }?.first ?: run { surroundingMinedBlocks.clear() recentClickedBlocks.clear() return } val extraBlocks = surroundingMinedBlocks.filter { it.first.confirmed }.countBy { it.first.ore } OreMinedEvent(originalBlock.ore, extraBlocks).post() surroundingMinedBlocks.clear() recentClickedBlocks.removeIf { it.second.passedSince() >= originalBlock.time.passedSince() } } @SubscribeEvent fun onWorldChange(event: LorenzWorldChangeEvent) { if (cold != 0) updateCold(0) lastColdReset = SimpleTimeMark.now() recentClickedBlocks.clear() surroundingMinedBlocks.clear() pickobulusMinedBlocks.clear() currentAreaOreBlocks = setOf() resetOreEvent() resetPickobulusEvent() } private fun resetOreEvent() { lastInitSound = SimpleTimeMark.farPast() waitingForInitSound = true initBlockPos = null waitingForEffMinerSound = false waitingForEffMinerBlock = false } private fun resetPickobulusEvent() { lastPickobulusUse = SimpleTimeMark.farPast() lastPickobulusExplosion = SimpleTimeMark.farPast() pickobulusExplosionPos = null pickobulusWaitingForSound = false pickobulusWaitingForBlock = false } @SubscribeEvent fun onDebugDataCollect(event: DebugDataCollectEvent) { event.title("Mining API") if (!inCustomMiningIsland()) { event.addIrrelevant("not in a mining island") return } event.addData { if (lastInitSound.isFarPast()) { add("lastInitSound: never") } else { add("lastInitSound: ${lastInitSound.passedSince().format()}") } add("waitingForInitSound: $waitingForInitSound") add("waitingForInitBlockPos: $initBlockPos") add("waitingForEffMinerSound: $waitingForEffMinerSound") add("waitingForEffMinerBlock: $waitingForEffMinerBlock") add("recentlyClickedBlocks: ${recentClickedBlocks.joinToString { "(${it.first.toCleanString()}" }}") } } private fun updateCold(newCold: Int) { // Hypixel sends cold data once in scoreboard even after resetting it if (cold == 0 && lastColdUpdate.passedSince() < 1.seconds) return lastColdUpdate = SimpleTimeMark.now() ColdUpdateEvent(newCold).postAndCatch() cold = newCold } private fun updateLocation() { inGlacite = inGlaciteArea() inTunnels = inGlacialTunnels() inMineshaft = inMineshaft() inDwarvenMines = inRegularDwarven() inCrystalHollows = inCrystalHollows() inCrimsonIsle = IslandType.CRIMSON_ISLE.isInIsland() inEnd = IslandType.THE_END.isInIsland() inSpidersDen = IslandType.SPIDER_DEN.isInIsland() currentAreaOreBlocks = OreBlock.entries.filter { it.checkArea() }.toSet() } }