diff options
Diffstat (limited to 'src/main/java/at/hannibal2/skyhanni/features')
-rw-r--r-- | src/main/java/at/hannibal2/skyhanni/features/mining/TunnelsMaps.kt | 451 |
1 files changed, 451 insertions, 0 deletions
diff --git a/src/main/java/at/hannibal2/skyhanni/features/mining/TunnelsMaps.kt b/src/main/java/at/hannibal2/skyhanni/features/mining/TunnelsMaps.kt new file mode 100644 index 000000000..32e5864af --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/features/mining/TunnelsMaps.kt @@ -0,0 +1,451 @@ +package at.hannibal2.skyhanni.features.mining + +import at.hannibal2.skyhanni.SkyHanniMod +import at.hannibal2.skyhanni.data.IslandType +import at.hannibal2.skyhanni.data.model.Graph +import at.hannibal2.skyhanni.data.model.GraphNode +import at.hannibal2.skyhanni.data.model.findShortestDistance +import at.hannibal2.skyhanni.data.model.findShortestPathAsGraphWithDistance +import at.hannibal2.skyhanni.events.ConfigLoadEvent +import at.hannibal2.skyhanni.events.GuiContainerEvent +import at.hannibal2.skyhanni.events.GuiRenderEvent +import at.hannibal2.skyhanni.events.InventoryFullyOpenedEvent +import at.hannibal2.skyhanni.events.LorenzKeyPressEvent +import at.hannibal2.skyhanni.events.LorenzRenderWorldEvent +import at.hannibal2.skyhanni.events.LorenzTickEvent +import at.hannibal2.skyhanni.events.LorenzToolTipEvent +import at.hannibal2.skyhanni.events.LorenzWarpEvent +import at.hannibal2.skyhanni.events.RepositoryReloadEvent +import at.hannibal2.skyhanni.test.command.ErrorManager +import at.hannibal2.skyhanni.utils.ChatUtils +import at.hannibal2.skyhanni.utils.CollectionUtils.filterNotNullKeys +import at.hannibal2.skyhanni.utils.ColorUtils.getFirstColorCode +import at.hannibal2.skyhanni.utils.ColorUtils.toChromaColor +import at.hannibal2.skyhanni.utils.ConditionalUtils.onToggle +import at.hannibal2.skyhanni.utils.DelayedRun +import at.hannibal2.skyhanni.utils.ItemUtils.getLore +import at.hannibal2.skyhanni.utils.ItemUtils.itemName +import at.hannibal2.skyhanni.utils.LocationUtils +import at.hannibal2.skyhanni.utils.LocationUtils.distanceSqToPlayer +import at.hannibal2.skyhanni.utils.LorenzColor +import at.hannibal2.skyhanni.utils.LorenzColor.Companion.toLorenzColor +import at.hannibal2.skyhanni.utils.LorenzUtils +import at.hannibal2.skyhanni.utils.LorenzUtils.isInIsland +import at.hannibal2.skyhanni.utils.NEUInternalName.Companion.asInternalName +import at.hannibal2.skyhanni.utils.RenderUtils +import at.hannibal2.skyhanni.utils.RenderUtils.draw3DPathWithWaypoint +import at.hannibal2.skyhanni.utils.RenderUtils.drawDynamicText +import at.hannibal2.skyhanni.utils.RenderUtils.renderRenderables +import at.hannibal2.skyhanni.utils.SimpleTimeMark +import at.hannibal2.skyhanni.utils.SimpleTimeMark.Companion.fromNow +import at.hannibal2.skyhanni.utils.StringUtils.anyMatches +import at.hannibal2.skyhanni.utils.StringUtils.matchFirst +import at.hannibal2.skyhanni.utils.StringUtils.matches +import at.hannibal2.skyhanni.utils.StringUtils.removeColor +import at.hannibal2.skyhanni.utils.renderables.Renderable +import at.hannibal2.skyhanni.utils.repopatterns.RepoPattern +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import java.awt.Color +import kotlin.math.roundToInt +import kotlin.time.Duration.Companion.seconds + +class TunnelsMaps { + + private val config get() = SkyHanniMod.feature.mining.tunnelMaps + + private var graph: Graph = Graph(emptyList()) + private lateinit var campfire: GraphNode + + private var goalReached = false + private var prevGoal: GraphNode? = null + private var goal: GraphNode? = null + set(value) { + prevGoal = field + field = value + } + + private var closedNote: GraphNode? = null + private var path: Pair<Graph, Double>? = null + + private var possibleLocations = mapOf<String, List<GraphNode>>() + private val cooldowns = mutableMapOf<GraphNode, SimpleTimeMark>() + private var active: String = "" + + private lateinit var fairySouls: Map<String, GraphNode> + private lateinit var newGemstones: Map<String, List<GraphNode>> + private lateinit var oldGemstones: Map<String, List<GraphNode>> + private lateinit var normalLocations: Map<String, List<GraphNode>> + + private var locationDisplay: List<Renderable> = emptyList() + + private fun getNext(name: String = active): GraphNode? { + fairySouls[name]?.let { + goalReached = false + return it + } + + val closed = closedNote ?: return null + val list = possibleLocations[name] ?: return null + + val offCooldown = list.filter { cooldowns[it]?.isInPast() != false } + val best = offCooldown.minByOrNull { graph.findShortestDistance(closed, it) } ?: list.minBy { + cooldowns[it] ?: SimpleTimeMark.farPast() + } + if (cooldowns[best]?.isInPast() != false) { + cooldowns[best] = 5.0.seconds.fromNow() + } + goalReached = false + return best + } + + private fun hasNext(name: String = active): Boolean { + val list = possibleLocations[name] ?: return false + return list.size > 1 + } + + private val oldGemstonePattern by RepoPattern.pattern( + "mining.tunnels.maps.gem.old", ".*(?:Ruby|Amethyst|Jade|Sapphire|Amber|Topaz).*" + ) + private val newGemstonePattern by RepoPattern.pattern( + "mining.tunnels.maps.gem.new", ".*(?:Aquamarine|Onyx|Citrine|Peridot).*" + ) + private val commissionInvPattern by RepoPattern.pattern( + "mining.commission.inventory", "Commissions" + ) + private val glacitePattern by RepoPattern.pattern( + "mining.commisson.reward.glacite", + "§7- §b\\d+ Glacite Powder" + ) + private val collectorCommissionPattern by RepoPattern.pattern( + "mining.commisson.collector", + "§9(?<what>\\w+(?: \\w+)?) Collector" + ) + private val invalidGoalPattern by RepoPattern.pattern( + "mining.commisson.collector.invalid", + "Glacite|Scrap" + ) + private val completedPattern by RepoPattern.pattern( + "mining.commisson.completed", + "§a§lCOMPLETED" + ) + + private val translateTable = mutableMapOf<String, String>() + + /** @return Errors with an empty String */ + private fun getGenericName(input: String): String = translateTable.getOrPut(input) { + possibleLocations.keys.firstOrNull() { it.uppercase().removeColor().contains(input.uppercase()) } ?: "" + } + + private var clickTranslate = mapOf<Int, String>() + + @SubscribeEvent + fun onInventoryFullyOpened(event: InventoryFullyOpenedEvent) { + if (!isEnabled()) return + clickTranslate = mapOf() + if (!commissionInvPattern.matches(event.inventoryName)) return + clickTranslate = event.inventoryItems.mapNotNull { (slotId, item) -> + val lore = item.getLore() + if (!glacitePattern.anyMatches(lore)) return@mapNotNull null + if (completedPattern.anyMatches(lore)) return@mapNotNull null + val type = lore.matchFirst(collectorCommissionPattern) { + group("what") + } ?: return@mapNotNull null + if (invalidGoalPattern.matches(type)) return@mapNotNull null + val mapName = getGenericName(type) + if (mapName.isEmpty()) { + ErrorManager.logErrorStateWithData( + "Unknown Collection Commission: $type", "$type can't be found in the graph.", + "type" to type, + "graphNames" to possibleLocations.keys, + "lore" to lore + ) + null + } else { + slotId to getGenericName(type) + } + }.toMap() + if (config.autoCommission) { + clickTranslate.values.firstOrNull()?.let { + setActiveAndGoal(it) + } + } + } + + @SubscribeEvent + fun onRenderItemTooltip(event: LorenzToolTipEvent) { + if (!isEnabled()) return + clickTranslate[event.slot.slotIndex]?.let { + event.toolTip.add("§e§lRight Click §r§eto for Tunnel Maps.") + } + } + + @SubscribeEvent + fun onGuiContainerSlotClick(event: GuiContainerEvent.SlotClickEvent) { + if (!isEnabled()) return + if (event.clickedButton != 1) return + clickTranslate[event.slotId]?.let { + setActiveAndGoal(it) + } + } + + @SubscribeEvent + fun onRepoReload(event: RepositoryReloadEvent) { + graph = event.getConstant<Graph>("TunnelsGraph", gson = Graph.gson) + possibleLocations = graph.groupBy { it.name }.filterNotNullKeys().mapValues { (_, value) -> + value + } + val fairy = mutableMapOf<String, GraphNode>() + val oldGemstone = mutableMapOf<String, List<GraphNode>>() + val newGemstone = mutableMapOf<String, List<GraphNode>>() + val other = mutableMapOf<String, List<GraphNode>>() + possibleLocations.forEach { (key, value) -> + when { + key.contains("Campfire") -> campfire = value.first() + key.contains("Fairy") -> fairy[key] = value.first() + newGemstonePattern.matches(key) -> newGemstone[key] = value + oldGemstonePattern.matches(key) -> oldGemstone[key] = value + else -> other[key] = value + } + } + fairySouls = fairy + this.newGemstones = newGemstone + this.oldGemstones = oldGemstone + normalLocations = other + translateTable.clear() + DelayedRun.runNextTick { // Needs to be delayed since the config may not be loaded + locationDisplay = generateLocationsDisplay() + } + } + + @SubscribeEvent + fun onConfigLoad(event: ConfigLoadEvent) { + onToggle( + config.compactGemstone, + config.excludeFairy + ) { + locationDisplay = generateLocationsDisplay() + } + } + + @SubscribeEvent + fun onRenderDisplay(event: GuiRenderEvent.ChestGuiOverlayRenderEvent) { + if (!isEnabled()) return + val display = buildList<Renderable> { + if (active.isNotEmpty()) { + if (goal == campfire && active != campfire.name) { + add(Renderable.string("§6Override for ${campfire.name}")) + add(Renderable.clickable(Renderable.string("§eMake §f$active §eactive"), onClick = { + goal = getNext() + })) + } else { + add( + Renderable.clickAndHover( + Renderable.string("§6Active: §f$active"), + listOf("§eClick to disable current Waypoint"), + onClick = ::clearPath + ) + ) + if (hasNext()) { + add(Renderable.clickable(Renderable.string("§eNext Spot"), onClick = { + goal = getNext() + })) + } else { + add(Renderable.string("")) + } + } + } else { + add(Renderable.string("")) + add(Renderable.string("")) + } + addAll(locationDisplay) + } + config.position.renderRenderables(display, posLabel = "TunnelsMaps") + } + + private fun generateLocationsDisplay() = buildList { + add(Renderable.string("§6Loactions:")) + add( + Renderable.multiClickAndHover( + campfire.name!!, listOf( + "§eLeft Click to set active", "§eRight Click for override" + ), click = mapOf( + 0 to guiSetActive(campfire.name!!), 1 to ::campfireOverride + ) + ) + ) + if (!config.excludeFairy.get()) { + add(Renderable.hoverable(Renderable.horizontalContainer(listOf(Renderable.string("§dFairy Souls")) + fairySouls.map { + val name = it.key.removePrefix("§dFairy Soul ") + Renderable.clickable(Renderable.string("§d[${name}]"), onClick = guiSetActive(it.key)) + }), Renderable.string("§dFairy Souls"))) + } + if (config.compactGemstone.get()) { + add( + Renderable.table( + listOf( + newGemstones.map(::toCompactGemstoneName), oldGemstones.map(::toCompactGemstoneName) + ) + ) + ) + } else { + addAll(newGemstones.map { + Renderable.clickable(Renderable.string(it.key), onClick = guiSetActive(it.key)) + }) + addAll(oldGemstones.map { + Renderable.clickable(Renderable.string(it.key), onClick = guiSetActive(it.key)) + }) + } + addAll(normalLocations.map { + Renderable.clickable(Renderable.string(it.key), onClick = guiSetActive(it.key)) + }) + } + + private fun toCompactGemstoneName(it: Map.Entry<String, List<GraphNode>>): Renderable = Renderable.clickAndHover( + Renderable.string((it.key.getFirstColorCode()?.let { "§$it" } ?: "") + ("ROUGH_".plus( + it.key.removeColor().removeSuffix("stone") + ).asInternalName().itemName.takeWhile { it != ' ' }.removeColor()), + horizontalAlign = RenderUtils.HorizontalAlignment.CENTER + ), + tips = listOf(it.key), + onClick = guiSetActive(it.key), + ) + + private fun campfireOverride() { + goalReached = false + goal = campfire + } + + private fun setActiveAndGoal(it: String) { + active = it + goal = getNext() + } + + private fun guiSetActive(it: String): () -> Unit = { + setActiveAndGoal(it) + } + + @SubscribeEvent + fun onTick(event: LorenzTickEvent) { + if (!isEnabled()) return + if (checkGoalReached()) return + val prevClosed = closedNote + closedNote = graph.minBy { it.position.distanceSqToPlayer() } + val closest = closedNote ?: return + val goal = goal ?: return + if (closest == prevClosed && goal == prevGoal) return + val (path, distance) = graph.findShortestPathAsGraphWithDistance(closest, goal) + val first = path.firstOrNull() + val second = path.getOrNull(1) + + val playerPosition = LocationUtils.playerLocation() + val nodeDistance = first?.let { playerPosition.distance(it.position) } ?: 0.0 + if (first != null && second != null) { + val direct = playerPosition.distance(second.position) + val firstPath = first.neighbours[second] ?: 0.0 + val around = nodeDistance + firstPath + if (direct < around) { + this.path = Graph(path.drop(1)) to (distance - firstPath + direct) + return + } + } + this.path = path to (distance + nodeDistance) + } + + private fun checkGoalReached(): Boolean { + if (goalReached) return true + val goal = goal ?: return false + val distance = goal.position.distanceSqToPlayer() + goalReached = distance < if (goal == campfire) { + 15.0 * 15.0 + } else { + 6.0 * 6.0 + } + if (goalReached) { + if (goal == campfire && active != campfire.name) { + this.goal = getNext() + } else { + cooldowns[goal] = 60.0.seconds.fromNow() + clearPath() + } + return true + } + return false + } + + private fun clearPath() { + path = null + goal = null + } + + @SubscribeEvent + fun onRenderWorld(event: LorenzRenderWorldEvent) { + if (!isEnabled()) return + val path = path?.takeIf { it.first.isNotEmpty() } ?: return + event.draw3DPathWithWaypoint( + path.first, + getPathColor(), + config.pathWidth.toInt(), + true, + bezierPoint = 2.0, + textSize = config.textSize.toDouble() + ) + event.drawDynamicText( + if (config.distanceFirst) { + path.first.first() + } else { + path.first.last() + }.position, + "§e${path.second.roundToInt()}m", + config.textSize.toDouble(), + yOff = 10f + ) + } + + private fun getPathColor(): Color = if (config.dynamicPathColour) { + goal?.name?.getFirstColorCode()?.toLorenzColor()?.takeIf { it != LorenzColor.WHITE }?.toColor() + } else { + null + } ?: config.pathColour.toChromaColor() + + @SubscribeEvent + fun onKeyPress(event: LorenzKeyPressEvent) { + if (!isEnabled()) return + campfireKey(event) + nextSpotKey(event) + } + + private fun campfireKey(event: LorenzKeyPressEvent) { + if (event.keyCode != config.campfireKey) return + if (config.travelScroll) { + ChatUtils.sendMessageToServer("/warp basecamp") + } else { + campfireOverride() + } + } + + @SubscribeEvent + fun onLorenzWarp(event: LorenzWarpEvent) { + if (!isEnabled()) return + if (goal != null) { + DelayedRun.runNextTick { + goal = getNext() + } + } + } + + private var nextSpotDelay = SimpleTimeMark.farPast() + + private fun nextSpotKey(event: LorenzKeyPressEvent) { + if (event.keyCode != config.nextSpotHotkey) return + if (!nextSpotDelay.isInPast()) return + nextSpotDelay = 0.5.seconds.fromNow() + goal = getNext() + } + + val areas = setOf( + "Glacite Tunnels", "Dwarven Base Camp", "Glacite Lake", "Fossil Research Center" + ) + + private fun isEnabled() = + IslandType.DWARVEN_MINES.isInIsland() && config.enable && areas.contains(LorenzUtils.skyBlockArea) +} |