summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThunderblade73 <85900443+Thunderblade73@users.noreply.github.com>2024-05-07 22:43:17 +0200
committerGitHub <noreply@github.com>2024-05-07 22:43:17 +0200
commitd0345f39230e4f07f061b6c82a381c863e9787d8 (patch)
treebeaf9d162d1ee0b891fefefeb1fb4815d9adb89b
parent912c8d8a8c9ad412b2f94827e09a9cf262e5b69a (diff)
downloadskyhanni-d0345f39230e4f07f061b6c82a381c863e9787d8.tar.gz
skyhanni-d0345f39230e4f07f061b6c82a381c863e9787d8.tar.bz2
skyhanni-d0345f39230e4f07f061b6c82a381c863e9787d8.zip
Feature: Tunnels maps (#1546)
Co-authored-by: hannibal2 <24389977+hannibal00212@users.noreply.github.com>
-rw-r--r--src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt2
-rw-r--r--src/main/java/at/hannibal2/skyhanni/config/ConfigManager.kt11
-rw-r--r--src/main/java/at/hannibal2/skyhanni/config/features/mining/MiningConfig.java3
-rw-r--r--src/main/java/at/hannibal2/skyhanni/config/features/mining/TunnelMapsConfig.java80
-rw-r--r--src/main/java/at/hannibal2/skyhanni/data/EntityMovementData.kt23
-rw-r--r--src/main/java/at/hannibal2/skyhanni/data/model/Graph.kt180
-rw-r--r--src/main/java/at/hannibal2/skyhanni/events/LorenzWarpEvent.kt3
-rw-r--r--src/main/java/at/hannibal2/skyhanni/events/RepositoryReloadEvent.kt2
-rw-r--r--src/main/java/at/hannibal2/skyhanni/features/mining/TunnelsMaps.kt451
-rw-r--r--src/main/java/at/hannibal2/skyhanni/test/SkyHanniDebugsAndTests.kt16
-rw-r--r--src/main/java/at/hannibal2/skyhanni/utils/CollectionUtils.kt24
-rw-r--r--src/main/java/at/hannibal2/skyhanni/utils/ColorUtils.kt2
-rw-r--r--src/main/java/at/hannibal2/skyhanni/utils/LorenzColor.kt8
-rw-r--r--src/main/java/at/hannibal2/skyhanni/utils/LorenzVec.kt1
-rw-r--r--src/main/java/at/hannibal2/skyhanni/utils/RenderUtils.kt204
15 files changed, 970 insertions, 40 deletions
diff --git a/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt b/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt
index b615238db..5caf50566 100644
--- a/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt
+++ b/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt
@@ -298,6 +298,7 @@ import at.hannibal2.skyhanni.features.mining.HighlightMiningCommissionMobs
import at.hannibal2.skyhanni.features.mining.KingTalismanHelper
import at.hannibal2.skyhanni.features.mining.MiningCommissionsBlocksColor
import at.hannibal2.skyhanni.features.mining.MiningNotifications
+import at.hannibal2.skyhanni.features.mining.TunnelsMaps
import at.hannibal2.skyhanni.features.mining.crystalhollows.CrystalHollowsNamesInCore
import at.hannibal2.skyhanni.features.mining.crystalhollows.CrystalHollowsWalls
import at.hannibal2.skyhanni.features.mining.eventtracker.MiningEventDisplay
@@ -610,6 +611,7 @@ class SkyHanniMod {
loadModule(DungeonMilestonesDisplay)
loadModule(DungeonDeathCounter())
loadModule(DungeonCleanEnd())
+ loadModule(TunnelsMaps())
loadModule(DungeonBossMessages())
loadModule(DungeonBossHideDamageSplash())
loadModule(UniqueGiftingOpportunitiesFeatures)
diff --git a/src/main/java/at/hannibal2/skyhanni/config/ConfigManager.kt b/src/main/java/at/hannibal2/skyhanni/config/ConfigManager.kt
index c3b011d0e..5c85c97ec 100644
--- a/src/main/java/at/hannibal2/skyhanni/config/ConfigManager.kt
+++ b/src/main/java/at/hannibal2/skyhanni/config/ConfigManager.kt
@@ -159,6 +159,17 @@ class ConfigManager {
.create()
var configDirectory = File("config/skyhanni")
+
+ inline fun <reified T> GsonBuilder.registerTypeAdapter(
+ crossinline write: (JsonWriter, T) -> Unit,
+ crossinline read: (JsonReader) -> T,
+ ): GsonBuilder {
+ this.registerTypeAdapter(T::class.java, object : TypeAdapter<T>() {
+ override fun write(out: JsonWriter, value: T) = write(out, value)
+ override fun read(reader: JsonReader) = read(reader)
+ }.nullSafe())
+ return this
+ }
}
val features get() = jsonHolder[ConfigFileType.FEATURES] as Features
diff --git a/src/main/java/at/hannibal2/skyhanni/config/features/mining/MiningConfig.java b/src/main/java/at/hannibal2/skyhanni/config/features/mining/MiningConfig.java
index b529a7c91..6d8bd06ab 100644
--- a/src/main/java/at/hannibal2/skyhanni/config/features/mining/MiningConfig.java
+++ b/src/main/java/at/hannibal2/skyhanni/config/features/mining/MiningConfig.java
@@ -48,6 +48,9 @@ public class MiningConfig {
public MiningNotificationsConfig notifications = new MiningNotificationsConfig();
@Expose
+ @Category(name = "Tunnel Maps", desc = "Settings for the Tunnel Maps")
+ public TunnelMapsConfig tunnelMaps = new TunnelMapsConfig();
+ @Expose
@ConfigOption(name = "Commissions Blocks Color", desc = "")
@Accordion
public CommissionsBlocksColorConfig commissionsBlocksColor = new CommissionsBlocksColorConfig();
diff --git a/src/main/java/at/hannibal2/skyhanni/config/features/mining/TunnelMapsConfig.java b/src/main/java/at/hannibal2/skyhanni/config/features/mining/TunnelMapsConfig.java
new file mode 100644
index 000000000..99f36a429
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/config/features/mining/TunnelMapsConfig.java
@@ -0,0 +1,80 @@
+package at.hannibal2.skyhanni.config.features.mining;
+
+import at.hannibal2.skyhanni.config.FeatureToggle;
+import at.hannibal2.skyhanni.config.core.config.Position;
+import com.google.gson.annotations.Expose;
+import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorBoolean;
+import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorColour;
+import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorKeybind;
+import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorSlider;
+import io.github.notenoughupdates.moulconfig.annotations.ConfigLink;
+import io.github.notenoughupdates.moulconfig.annotations.ConfigOption;
+import io.github.notenoughupdates.moulconfig.observer.Property;
+import org.lwjgl.input.Keyboard;
+
+public class TunnelMapsConfig {
+
+ @Expose
+ @ConfigOption(name = "Enable", desc = "Enables the tunnel maps, which give you a path to any location you want. Open the Inventory to select a destination.")
+ @ConfigEditorBoolean
+ @FeatureToggle
+ public boolean enable = true;
+
+ @ConfigLink(owner = TunnelMapsConfig.class, field = "enable")
+ public Position position = new Position(20, 20);
+
+ @Expose
+ @ConfigOption(name = "Auto Commission", desc = "Takes the first collector commission as target when opening the commissions inventory, also works when completing commissions.")
+ @ConfigEditorBoolean
+ public boolean autoCommission = false;
+
+ @Expose
+ @ConfigOption(name = "Campfire Hotkey", desc = "Hotkey to warp to the campfire, if the travel scroll is not unlocked shows a path to the campfire.")
+ @ConfigEditorKeybind(defaultKey = Keyboard.KEY_NONE)
+ public int campfireKey = Keyboard.KEY_NONE;
+
+ @Expose
+ @ConfigOption(name = "Travel Scroll", desc = "Lets the mod know that you have unlocked the travel scroll to basecamp.")
+ @ConfigEditorBoolean
+ public boolean travelScroll = false;
+
+ @Expose
+ @ConfigOption(name = "Next Spot Hotkey", desc = "Hotkey to select the next spot.")
+ @ConfigEditorKeybind(defaultKey = Keyboard.KEY_NONE)
+ public int nextSpotHotkey = Keyboard.KEY_NONE;
+
+ @Expose
+ @ConfigOption(name = "Dynamic Path Colour", desc = "Instead of the selected color use the color of the target as line colour.")
+ @ConfigEditorBoolean
+ public boolean dynamicPathColour = true;
+
+ @Expose
+ @ConfigOption(name = "Path Colour", desc = "The colour for the paths, if the dynamic colour option is turned off.")
+ @ConfigEditorColour
+ public String pathColour = "0:255:0:255:0";
+
+ @Expose
+ @ConfigOption(name = "Text Size", desc = "Size of the waypoint texts.")
+ @ConfigEditorSlider(minValue = 0.5f, maxValue = 2.5f, minStep = 0.1f)
+ public float textSize = 1.0f;
+
+ @Expose
+ @ConfigOption(name = "Path width", desc = "Size of the path lines.")
+ @ConfigEditorSlider(minValue = 1f, maxValue = 15f, minStep = 1f)
+ public float pathWidth = 4.0f;
+
+ @Expose
+ @ConfigOption(name = "Distance at First", desc = "Shows the distance at the first edge instead of the end.")
+ @ConfigEditorBoolean
+ public boolean distanceFirst = false;
+
+ @Expose
+ @ConfigOption(name = "Compact Gemstone", desc = "Only shows the icon for gemstones in the selection list.")
+ @ConfigEditorBoolean
+ public Property<Boolean> compactGemstone = Property.of(false);
+
+ @Expose
+ @ConfigOption(name = "Exclude Fairy", desc = "Excludes the fairy soul spots from the selection list.")
+ @ConfigEditorBoolean
+ public Property<Boolean> excludeFairy = Property.of(false);
+}
diff --git a/src/main/java/at/hannibal2/skyhanni/data/EntityMovementData.kt b/src/main/java/at/hannibal2/skyhanni/data/EntityMovementData.kt
index ae5a03878..97039022a 100644
--- a/src/main/java/at/hannibal2/skyhanni/data/EntityMovementData.kt
+++ b/src/main/java/at/hannibal2/skyhanni/data/EntityMovementData.kt
@@ -1,11 +1,16 @@
package at.hannibal2.skyhanni.data
import at.hannibal2.skyhanni.events.EntityMoveEvent
+import at.hannibal2.skyhanni.events.LorenzChatEvent
import at.hannibal2.skyhanni.events.LorenzTickEvent
+import at.hannibal2.skyhanni.events.LorenzWarpEvent
import at.hannibal2.skyhanni.events.LorenzWorldChangeEvent
+import at.hannibal2.skyhanni.utils.DelayedRun
import at.hannibal2.skyhanni.utils.LorenzUtils
import at.hannibal2.skyhanni.utils.LorenzVec
+import at.hannibal2.skyhanni.utils.StringUtils.matches
import at.hannibal2.skyhanni.utils.getLorenzVec
+import at.hannibal2.skyhanni.utils.repopatterns.RepoPattern
import net.minecraft.client.Minecraft
import net.minecraft.entity.Entity
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
@@ -41,6 +46,24 @@ class EntityMovementData {
}
}
+ private val warpingPattern by RepoPattern.pattern(
+ "warping",
+ "§7Warping...|" +
+ "§7Warping you to your SkyBlock island...|" +
+ "§7Warping using transfer token...|" +
+ "§7Finding player...|" +
+ "§7Sending a visit request..."
+ )
+
+ @SubscribeEvent
+ fun onChat(event: LorenzChatEvent) {
+ if (!LorenzUtils.inSkyBlock) return
+ if (!warpingPattern.matches(event.message)) return
+ DelayedRun.runNextTick {
+ LorenzWarpEvent().postAndCatch()
+ }
+ }
+
@SubscribeEvent
fun onWorldChange(event: LorenzWorldChangeEvent) {
entityLocation.clear()
diff --git a/src/main/java/at/hannibal2/skyhanni/data/model/Graph.kt b/src/main/java/at/hannibal2/skyhanni/data/model/Graph.kt
new file mode 100644
index 000000000..b6e6f7b8e
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/data/model/Graph.kt
@@ -0,0 +1,180 @@
+package at.hannibal2.skyhanni.data.model
+
+import at.hannibal2.skyhanni.config.ConfigManager.Companion.registerTypeAdapter
+import at.hannibal2.skyhanni.utils.LorenzVec
+import at.hannibal2.skyhanni.utils.fromJson
+import com.google.gson.GsonBuilder
+import com.google.gson.JsonElement
+import com.google.gson.annotations.Expose
+import java.util.PriorityQueue
+
+@JvmInline
+value class Graph(
+ @Expose
+ val graph: List<GraphNode>,
+) : List<GraphNode> {
+ override val size
+ get() = graph.size
+
+ override fun contains(element: GraphNode) = graph.contains(element)
+
+ override fun containsAll(elements: Collection<GraphNode>) = graph.containsAll(elements)
+
+ override fun get(index: Int) = graph.get(index)
+
+ override fun isEmpty() = graph.isEmpty()
+
+ override fun indexOf(element: GraphNode) = graph.indexOf(element)
+
+ override fun iterator(): Iterator<GraphNode> = graph.iterator()
+ override fun listIterator() = graph.listIterator()
+
+ override fun listIterator(index: Int) = graph.listIterator(index)
+
+ override fun subList(fromIndex: Int, toIndex: Int) = graph.subList(fromIndex, toIndex)
+
+ override fun lastIndexOf(element: GraphNode) = graph.lastIndexOf(element)
+
+ companion object {
+ val gson = GsonBuilder().setPrettyPrinting()
+ /* ConfigManager.createBaseGsonBuilder() */.registerTypeAdapter<Graph>({ out, value ->
+ out.beginObject()
+ value.forEach {
+ out.name(it.id.toString()).beginObject()
+ out.name("Position").value(with(it.position) { "$x:$y:$z" })
+ if (it.name != null) {
+ out.name("Name").value(it.name)
+ }
+ out.name("Neighbours")
+ out.beginObject()
+ it.neighbours.forEach { (node, weight) ->
+ val id = node.id.toString()
+ out.name(id).value(weight)
+ }
+ out.endObject()
+ out.endObject()
+ }
+ out.endObject()
+ }, { reader ->
+ reader.beginObject()
+ val list = mutableListOf<GraphNode>()
+ val neigbourMap = mutableMapOf<GraphNode, List<Pair<Int, Double>>>()
+ while (reader.hasNext()) {
+ val id = reader.nextName().toInt()
+ reader.beginObject()
+ var position: LorenzVec? = null
+ var name: String? = null
+ var neighbors = mutableListOf<Pair<Int, Double>>()
+ while (reader.hasNext()) {
+ when (reader.nextName()) {
+ "Position" -> {
+ position = reader.nextString().split(":").let { parts ->
+ LorenzVec(parts[0].toDouble(), parts[1].toDouble(), parts[2].toDouble())
+ }
+ }
+
+ "Neighbours" -> {
+ reader.beginObject()
+ while (reader.hasNext()) {
+ val nId = reader.nextName().toInt()
+ val distance = reader.nextDouble()
+ neighbors.add(nId to distance)
+ }
+ reader.endObject()
+ }
+
+ "Name" -> {
+ name = reader.nextString()
+ }
+
+ }
+ }
+ val node = GraphNode(id, position!!, name)
+ list.add(node)
+ neigbourMap[node] = neighbors
+ reader.endObject()
+ }
+ neigbourMap.forEach { (node, edge) ->
+ node.neighbours = edge.associate { (id, distance) ->
+ list.first { it.id == id } to distance
+ }
+ }
+ reader.endObject()
+ Graph(list)
+ }).create()
+
+ fun fromJson(json: String): Graph = gson.fromJson<Graph>(json)
+ fun fromJson(json: JsonElement): Graph = gson.fromJson<Graph>(json)
+ }
+}
+
+class GraphNode(val id: Int, val position: LorenzVec, val name: String? = null) {
+
+ /** Keys are the neighbours and value the edge weight (e.g. Distance) */
+ lateinit var neighbours: Map<GraphNode, Double>
+
+ override fun hashCode(): Int {
+ return id
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as GraphNode
+
+ if (id != other.id) return false
+
+ return true
+ }
+}
+
+fun Graph.findShortestPathAsGraph(start: GraphNode, end: GraphNode): Graph =
+ this.findShortestPathAsGraphWithDistance(start, end).first
+
+fun Graph.findShortestPathAsGraphWithDistance(start: GraphNode, end: GraphNode): Pair<Graph, Double> {
+ val distances = mutableMapOf<GraphNode, Double>()
+ val previous = mutableMapOf<GraphNode, GraphNode>()
+ val visited = mutableSetOf<GraphNode>()
+ val queue = PriorityQueue<GraphNode>(compareBy { distances.getOrDefault(it, Double.MAX_VALUE) })
+
+ distances[start] = 0.0
+ queue.add(start)
+
+ while (queue.isNotEmpty()) {
+ val current = queue.poll()
+ if (current == end) break
+
+ visited.add(current)
+
+ current.neighbours.forEach { (neighbour, weight) ->
+ if (neighbour !in visited) {
+ val newDistance = distances.getValue(current) + weight
+ if (newDistance < distances.getOrDefault(neighbour, Double.MAX_VALUE)) {
+ distances[neighbour] = newDistance
+ previous[neighbour] = current
+ queue.add(neighbour)
+ }
+ }
+ }
+ }
+
+ return Graph(buildList {
+ var current = end
+ while (current != start) {
+ add(current)
+ current = previous[current] ?: return Graph(emptyList()) to 0.0
+ }
+ add(start)
+ }.reversed()) to distances[end]!!
+}
+
+fun Graph.findShortestPath(start: GraphNode, end: GraphNode): List<LorenzVec> =
+ this.findShortestPathAsGraph(start, end).toPositionsList()
+
+fun Graph.findShortestDistance(start: GraphNode, end: GraphNode): Double =
+ this.findShortestPathAsGraphWithDistance(start, end).second
+
+fun Graph.toPositionsList() = this.map { it.position }
+
+fun Graph.toJson(): String = Graph.gson.toJson(this)
diff --git a/src/main/java/at/hannibal2/skyhanni/events/LorenzWarpEvent.kt b/src/main/java/at/hannibal2/skyhanni/events/LorenzWarpEvent.kt
new file mode 100644
index 000000000..170dcabe9
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/events/LorenzWarpEvent.kt
@@ -0,0 +1,3 @@
+package at.hannibal2.skyhanni.events
+
+class LorenzWarpEvent : LorenzEvent()
diff --git a/src/main/java/at/hannibal2/skyhanni/events/RepositoryReloadEvent.kt b/src/main/java/at/hannibal2/skyhanni/events/RepositoryReloadEvent.kt
index 3b2b426fe..fc006b049 100644
--- a/src/main/java/at/hannibal2/skyhanni/events/RepositoryReloadEvent.kt
+++ b/src/main/java/at/hannibal2/skyhanni/events/RepositoryReloadEvent.kt
@@ -9,7 +9,7 @@ import java.lang.reflect.Type
class RepositoryReloadEvent(val repoLocation: File, val gson: Gson) : LorenzEvent() {
- inline fun <reified T : Any> getConstant(constant: String, type: Type? = null): T = try {
+ inline fun <reified T : Any> getConstant(constant: String, type: Type? = null, gson: Gson = this.gson): T = try {
RepoManager.setlastConstant(constant)
if (!repoLocation.exists()) throw RepoError("Repo folder does not exist!")
RepoUtils.getConstant(repoLocation, constant, gson, T::class.java, type)
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 {