From 08da9f51266d8f514a047cd3f9ffb5adfd7b29e8 Mon Sep 17 00:00:00 2001 From: hannibal2 <24389977+hannibal002@users.noreply.github.com> Date: Fri, 20 Sep 2024 22:37:38 +0200 Subject: Improvement: Area Navigation updates (#2537) Co-authored-by: nea Co-authored-by: hannibal2 <24389977+hannibal00212@users.noreply.github.com> --- .../hannibal2/skyhanni/config/commands/Commands.kt | 2 +- .../skyhanni/config/features/dev/GraphConfig.java | 5 + .../at/hannibal2/skyhanni/data/IslandGraphs.kt | 3 +- .../java/at/hannibal2/skyhanni/data/model/Graph.kt | 85 ++- .../hannibal2/skyhanni/data/model/GraphNodeTag.kt | 13 +- .../skyhanni/features/misc/IslandAreas.kt | 17 +- .../mixins/transformers/MixinKeyBinding.java | 2 +- .../java/at/hannibal2/skyhanni/test/GraphEditor.kt | 639 -------------------- .../at/hannibal2/skyhanni/test/GraphNodeEditor.kt | 178 ------ .../skyhanni/test/SkyHanniDebugsAndTests.kt | 33 +- .../hannibal2/skyhanni/test/graph/GraphEditor.kt | 666 +++++++++++++++++++++ .../skyhanni/test/graph/GraphEditorBugFinder.kt | 95 +++ .../skyhanni/test/graph/GraphNodeEditor.kt | 232 +++++++ .../java/at/hannibal2/skyhanni/utils/GraphUtils.kt | 68 +++ .../java/at/hannibal2/skyhanni/utils/LorenzVec.kt | 3 + .../at/hannibal2/skyhanni/utils/RaycastUtils.kt | 73 +++ 16 files changed, 1233 insertions(+), 881 deletions(-) delete mode 100644 src/main/java/at/hannibal2/skyhanni/test/GraphEditor.kt delete mode 100644 src/main/java/at/hannibal2/skyhanni/test/GraphNodeEditor.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/test/graph/GraphEditor.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/test/graph/GraphEditorBugFinder.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/test/graph/GraphNodeEditor.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/utils/GraphUtils.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/utils/RaycastUtils.kt (limited to 'src/main/java/at') diff --git a/src/main/java/at/hannibal2/skyhanni/config/commands/Commands.kt b/src/main/java/at/hannibal2/skyhanni/config/commands/Commands.kt index d0f58f082..3f2ddace6 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/commands/Commands.kt +++ b/src/main/java/at/hannibal2/skyhanni/config/commands/Commands.kt @@ -77,7 +77,6 @@ import at.hannibal2.skyhanni.features.rift.area.westvillage.VerminTracker import at.hannibal2.skyhanni.features.rift.everywhere.PunchcardHighlight import at.hannibal2.skyhanni.features.slayer.SlayerProfitTracker import at.hannibal2.skyhanni.test.DebugCommand -import at.hannibal2.skyhanni.test.GraphEditor import at.hannibal2.skyhanni.test.PacketTest import at.hannibal2.skyhanni.test.SkyBlockIslandTest import at.hannibal2.skyhanni.test.SkyHanniConfigSearchResetCommand @@ -92,6 +91,7 @@ import at.hannibal2.skyhanni.test.command.CopyScoreboardCommand import at.hannibal2.skyhanni.test.command.TestChatCommand import at.hannibal2.skyhanni.test.command.TrackParticlesCommand import at.hannibal2.skyhanni.test.command.TrackSoundsCommand +import at.hannibal2.skyhanni.test.graph.GraphEditor import at.hannibal2.skyhanni.utils.APIUtil import at.hannibal2.skyhanni.utils.ChatUtils import at.hannibal2.skyhanni.utils.ExtendedChatColor diff --git a/src/main/java/at/hannibal2/skyhanni/config/features/dev/GraphConfig.java b/src/main/java/at/hannibal2/skyhanni/config/features/dev/GraphConfig.java index acf9ac67d..dda1ddb1e 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/features/dev/GraphConfig.java +++ b/src/main/java/at/hannibal2/skyhanni/config/features/dev/GraphConfig.java @@ -32,6 +32,11 @@ public class GraphConfig { @ConfigEditorKeybind(defaultKey = -98) // Middle Mouse public int selectKey = -98; + @Expose + @ConfigOption(name = "Select near look", desc = "Select the node closest to where you are looking.") + @ConfigEditorKeybind(defaultKey = Keyboard.KEY_NONE) + public int selectRaycastKey = Keyboard.KEY_NONE; + @Expose @ConfigOption(name = "Connect Key", desc = "Connect the nearest node with the active node. If the nodes are already connected removes the connection.") @ConfigEditorKeybind(defaultKey = Keyboard.KEY_C) diff --git a/src/main/java/at/hannibal2/skyhanni/data/IslandGraphs.kt b/src/main/java/at/hannibal2/skyhanni/data/IslandGraphs.kt index 0b173db46..ca3143b95 100644 --- a/src/main/java/at/hannibal2/skyhanni/data/IslandGraphs.kt +++ b/src/main/java/at/hannibal2/skyhanni/data/IslandGraphs.kt @@ -45,13 +45,14 @@ import kotlin.math.abs * 12 starter NPC's * diana * farming: - * pelt farming + * pelt farming area * rift: * enigma souls * eyes * big quests * montezuma souls * blood effigies + * avoid area around enderman * spider: * relicts + throw spot * dwarven mines: diff --git a/src/main/java/at/hannibal2/skyhanni/data/model/Graph.kt b/src/main/java/at/hannibal2/skyhanni/data/model/Graph.kt index 8c63be891..b9ad1251c 100644 --- a/src/main/java/at/hannibal2/skyhanni/data/model/Graph.kt +++ b/src/main/java/at/hannibal2/skyhanni/data/model/Graph.kt @@ -10,6 +10,7 @@ import com.google.gson.annotations.Expose import com.google.gson.stream.JsonToken import java.util.PriorityQueue +// TODO: This class should be disambiguated into a NodePath and a Graph class @JvmInline value class Graph( @Expose val nodes: List, @@ -49,7 +50,7 @@ value class Graph( out.name("Name").value(it) } - it.tagNames?.takeIf { it.isNotEmpty() }?.let { + it.tagNames.takeIf { list -> list.isNotEmpty() }?.let { out.name("Tags") out.beginArray() for (tagName in it) { @@ -79,8 +80,8 @@ value class Graph( reader.beginObject() var position: LorenzVec? = null var name: String? = null - var tags: List? = null - var neighbors = mutableListOf>() + var tags = emptyList() + val neighbors = mutableListOf>() while (reader.hasNext()) { if (reader.peek() != JsonToken.NAME) { reader.skipValue() @@ -140,10 +141,10 @@ value class Graph( } // The node object that gets parsed from/to json -class GraphNode(val id: Int, val position: LorenzVec, val name: String? = null, val tagNames: List? = null) { +class GraphNode(val id: Int, val position: LorenzVec, val name: String? = null, val tagNames: List = emptyList()) { val tags: List by lazy { - tagNames?.mapNotNull { GraphNodeTag.byId(it) } ?: emptyList() + tagNames.mapNotNull { GraphNodeTag.byId(it) } } /** Keys are the neighbours and value the edge weight (e.g. Distance) */ @@ -167,18 +168,50 @@ class GraphNode(val id: Int, val position: LorenzVec, val name: String? = null, fun Graph.findShortestPathAsGraph(start: GraphNode, end: GraphNode): Graph = this.findShortestPathAsGraphWithDistance(start, end).first -fun Graph.findShortestPathAsGraphWithDistance(start: GraphNode, end: GraphNode): Pair { +data class DijkstraTree( + val origin: GraphNode, + /** + * A map of distances between the [origin] and each node in a graph. This distance map is only accurate for nodes closer to the + * origin than the [lastVisitedNode]. In case there is no early bailout, this map will be accurate for all nodes in the graph. + */ + val distances: Map, + /** + * A map of nodes to the neighbouring node that is the quickest path towards the origin (the neighbouring node that has the lowest value + * in [distances]) + */ + val towardsOrigin: Map, + /** + * This is either the furthest away node in the graph, or the node that was bailed out on early because it fulfilled the search + * condition. In case the search condition matches nothing, this will *still* be the furthest away node, so an additional check might be + * necessary. + */ + val lastVisitedNode: GraphNode, +) + +/** + * Find a tree of distances to the [start] node using dijkstra's algorithm. + */ +fun Graph.findDijkstraDistances( + start: GraphNode, + /** + * Bail out early before collecting all the distances to all nodes in the graph. This will not collect valid distance data for *all* + * nodes for which bailout matches, but only the closest one. + */ + bailout: (GraphNode) -> Boolean, +): DijkstraTree { val distances = mutableMapOf() val previous = mutableMapOf() val visited = mutableSetOf() val queue = PriorityQueue(compareBy { distances.getOrDefault(it, Double.MAX_VALUE) }) + var lastVisitedNode: GraphNode = start distances[start] = 0.0 queue.add(start) while (queue.isNotEmpty()) { val current = queue.poll() - if (current == end) break + lastVisitedNode = current + if (bailout(current)) break visited.add(current) @@ -194,16 +227,34 @@ fun Graph.findShortestPathAsGraphWithDistance(start: GraphNode, end: GraphNode): } } - 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]!! + return DijkstraTree( + start, + distances, + previous, + lastVisitedNode, + ) +} + +fun Graph.findAllShortestDistances(start: GraphNode): DijkstraTree { + return findDijkstraDistances(start) { false } +} + +fun DijkstraTree.findPathToDestination(end: GraphNode): Pair { + val distances = this + val reversePath = buildList { + var current = end + while (true) { + add(current) + if (current == distances.origin) break + current = distances.towardsOrigin[current] ?: return Graph(emptyList()) to 0.0 + } + } + return Graph(reversePath.reversed()) to distances.distances[end]!! +} + +fun Graph.findShortestPathAsGraphWithDistance(start: GraphNode, end: GraphNode): Pair { + val distances = findDijkstraDistances(start) { it == end } + return distances.findPathToDestination(end) } fun Graph.findShortestPath(start: GraphNode, end: GraphNode): List = this.findShortestPathAsGraph(start, end).toPositionsList() diff --git a/src/main/java/at/hannibal2/skyhanni/data/model/GraphNodeTag.kt b/src/main/java/at/hannibal2/skyhanni/data/model/GraphNodeTag.kt index e1fa3317a..e70786137 100644 --- a/src/main/java/at/hannibal2/skyhanni/data/model/GraphNodeTag.kt +++ b/src/main/java/at/hannibal2/skyhanni/data/model/GraphNodeTag.kt @@ -6,7 +6,7 @@ import at.hannibal2.skyhanni.utils.LorenzColor enum class GraphNodeTag( val internalName: String, val color: LorenzColor, - val cleanName: String, + cleanName: String, val description: String, val onlyIsland: IslandType? = null, ) { @@ -17,13 +17,17 @@ enum class GraphNodeTag( AREA("area", LorenzColor.DARK_GREEN, "Area", "A big SkyBlock area."), SMALL_AREA("small_area", LorenzColor.GREEN, "Small Area", "A small SkyBlock area, e.g. a house."), POI("poi", LorenzColor.WHITE, "PoI", "Point of interest."), - LAUNCH_PAD("launch", LorenzColor.WHITE, "Launch Pad", "Slime blocks sending you to another server."), +// LAUNCH_PAD("launch", LorenzColor.WHITE, "Launch Pad", "Slime blocks sending you to another server."), + TELEPORT("teleport", LorenzColor.BLUE, "Teleport", "A spot from/to teleport."), // on multiple islands ROMEO("romeo", LorenzColor.WHITE, "Romeo & Juliette Quest", "Blocks related to the Romeo and Juliette/Ring of Love quest line."), RACE("race", LorenzColor.WHITE, "Race Start/Stop", "A race start or stop point."), - SLAYER("slayer", LorenzColor.WHITE, "Slayer", "A Slayer area"), + SLAYER("slayer", LorenzColor.RED, "Slayer", "A Slayer area"), HOPPITY("hoppity", LorenzColor.AQUA, "Hoppity Egg", "An egg location in Hoppity's Hunt."), + GRIND_MOBS("grind_mobs", LorenzColor.RED, "Grind Mobs", "A spot where combat mobs spawn that can be killed."), + GRIND_ORES("grind_ores", LorenzColor.DARK_AQUA, "Grind Ores", "A spot where mining ores spawn that can be mines."), + GRIND_CROPS("grind_crops", LorenzColor.DARK_GREEN, "Grind Crops", "A spot where farming crops spawn that can be farmed."), // hoppity // Hub @@ -52,6 +56,9 @@ enum class GraphNodeTag( // Crimson Isles CRIMSON_MINIBOSS("crimson_miniboss", LorenzColor.RED, "Crimson Miniboss", "Mini bosses in Crimson Isle.", onlyIsland = IslandType.CRIMSON_ISLE), + // The End + END_GOLEM("end_golem", LorenzColor.RED, "Golem Spawn", "A spot where the golem in the end spawns", onlyIsland = IslandType.THE_END), + ; val displayName: String = color.getChatColor() + cleanName diff --git a/src/main/java/at/hannibal2/skyhanni/features/misc/IslandAreas.kt b/src/main/java/at/hannibal2/skyhanni/features/misc/IslandAreas.kt index d85b34cc6..311af056e 100644 --- a/src/main/java/at/hannibal2/skyhanni/features/misc/IslandAreas.kt +++ b/src/main/java/at/hannibal2/skyhanni/features/misc/IslandAreas.kt @@ -6,7 +6,6 @@ import at.hannibal2.skyhanni.data.model.Graph import at.hannibal2.skyhanni.data.model.GraphNode import at.hannibal2.skyhanni.data.model.GraphNodeTag import at.hannibal2.skyhanni.data.model.TextInput -import at.hannibal2.skyhanni.data.model.findShortestPathAsGraphWithDistance import at.hannibal2.skyhanni.events.ConfigLoadEvent import at.hannibal2.skyhanni.events.EntityMoveEvent import at.hannibal2.skyhanni.events.GuiRenderEvent @@ -19,6 +18,7 @@ import at.hannibal2.skyhanni.utils.CollectionUtils.addSearchString import at.hannibal2.skyhanni.utils.CollectionUtils.sorted import at.hannibal2.skyhanni.utils.ColorUtils.toChromaColor import at.hannibal2.skyhanni.utils.ConditionalUtils +import at.hannibal2.skyhanni.utils.GraphUtils import at.hannibal2.skyhanni.utils.LocationUtils.canBeSeen import at.hannibal2.skyhanni.utils.LocationUtils.distanceToPlayer import at.hannibal2.skyhanni.utils.LorenzColor @@ -65,19 +65,10 @@ object IslandAreas { val graph = IslandGraphs.currentIslandGraph ?: return val closedNote = IslandGraphs.closedNote ?: return - val paths = mutableMapOf() - - val map = mutableMapOf() - for (graphNode in graph.nodes) { - if (graphNode.getAreaTag() == null) continue - val (path, distance) = graph.findShortestPathAsGraphWithDistance(closedNote, graphNode) - paths[graphNode] = path - map[graphNode] = distance - } + val (paths, map) = GraphUtils.findFastestPaths(graph, closedNote) { it.getAreaTag() != null } this.paths = paths val finalNodes = mutableMapOf() - val alreadyFoundAreas = mutableListOf() for ((node, distance) in map.sorted()) { val areaName = node.name ?: continue @@ -271,8 +262,8 @@ object IslandAreas { private val allAreas = listOf(GraphNodeTag.AREA, GraphNodeTag.SMALL_AREA) private val onlyLargeAreas = listOf(GraphNodeTag.AREA) - private fun GraphNode.getAreaTag(): GraphNodeTag? = tags.firstOrNull { - it in (if (config.includeSmallAreas) allAreas else onlyLargeAreas) + fun GraphNode.getAreaTag(ignoreConfig: Boolean = false): GraphNodeTag? = tags.firstOrNull { + it in (if (config.includeSmallAreas || ignoreConfig) allAreas else onlyLargeAreas) } private fun setTarget(node: GraphNode) { diff --git a/src/main/java/at/hannibal2/skyhanni/mixins/transformers/MixinKeyBinding.java b/src/main/java/at/hannibal2/skyhanni/mixins/transformers/MixinKeyBinding.java index a070f5e89..01efda5ce 100644 --- a/src/main/java/at/hannibal2/skyhanni/mixins/transformers/MixinKeyBinding.java +++ b/src/main/java/at/hannibal2/skyhanni/mixins/transformers/MixinKeyBinding.java @@ -2,7 +2,7 @@ package at.hannibal2.skyhanni.mixins.transformers; import at.hannibal2.skyhanni.data.model.TextInput; import at.hannibal2.skyhanni.features.garden.farming.GardenCustomKeybinds; -import at.hannibal2.skyhanni.test.GraphEditor; +import at.hannibal2.skyhanni.test.graph.GraphEditor; import net.minecraft.client.settings.KeyBinding; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; diff --git a/src/main/java/at/hannibal2/skyhanni/test/GraphEditor.kt b/src/main/java/at/hannibal2/skyhanni/test/GraphEditor.kt deleted file mode 100644 index d4038162f..000000000 --- a/src/main/java/at/hannibal2/skyhanni/test/GraphEditor.kt +++ /dev/null @@ -1,639 +0,0 @@ -package at.hannibal2.skyhanni.test - -import at.hannibal2.skyhanni.SkyHanniMod -import at.hannibal2.skyhanni.data.IslandGraphs -import at.hannibal2.skyhanni.data.model.Graph -import at.hannibal2.skyhanni.data.model.GraphNode -import at.hannibal2.skyhanni.data.model.GraphNodeTag -import at.hannibal2.skyhanni.data.model.TextInput -import at.hannibal2.skyhanni.data.model.findShortestPathAsGraph -import at.hannibal2.skyhanni.data.model.toJson -import at.hannibal2.skyhanni.events.GuiRenderEvent -import at.hannibal2.skyhanni.events.LorenzRenderWorldEvent -import at.hannibal2.skyhanni.events.LorenzTickEvent -import at.hannibal2.skyhanni.skyhannimodule.SkyHanniModule -import at.hannibal2.skyhanni.test.command.ErrorManager -import at.hannibal2.skyhanni.utils.ChatUtils -import at.hannibal2.skyhanni.utils.ColorUtils -import at.hannibal2.skyhanni.utils.KeyboardManager -import at.hannibal2.skyhanni.utils.KeyboardManager.isKeyClicked -import at.hannibal2.skyhanni.utils.KeyboardManager.isKeyHeld -import at.hannibal2.skyhanni.utils.LocationUtils -import at.hannibal2.skyhanni.utils.LocationUtils.distanceToPlayer -import at.hannibal2.skyhanni.utils.LocationUtils.playerLocation -import at.hannibal2.skyhanni.utils.LorenzColor -import at.hannibal2.skyhanni.utils.LorenzUtils -import at.hannibal2.skyhanni.utils.LorenzVec -import at.hannibal2.skyhanni.utils.NumberUtil.addSeparators -import at.hannibal2.skyhanni.utils.OSUtils -import at.hannibal2.skyhanni.utils.RenderUtils.draw3DLine_nea -import at.hannibal2.skyhanni.utils.RenderUtils.drawDynamicText -import at.hannibal2.skyhanni.utils.RenderUtils.drawWaypointFilled -import at.hannibal2.skyhanni.utils.RenderUtils.renderStrings -import kotlinx.coroutines.runBlocking -import net.minecraft.client.settings.KeyBinding -import net.minecraftforge.fml.common.eventhandler.EventPriority -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable -import java.awt.Color - -@SkyHanniModule -object GraphEditor { - - val config get() = SkyHanniMod.feature.dev.devTool.graph - - fun isEnabled() = config != null && config.enabled - - private var id = 0 - - val nodes = mutableListOf() - private val edges = mutableListOf() - - var activeNode: GraphingNode? = null - set(value) { - field = value - selectedEdge = findEdgeBetweenActiveAndClosed() - checkDissolve() - } - private var closedNode: GraphingNode? = null - set(value) { - field = value - selectedEdge = findEdgeBetweenActiveAndClosed() - } - - private var selectedEdge: GraphingEdge? = null - private var ghostPosition: LorenzVec? = null - - private var seeThroughBlocks = true - - private var inEditMode = false - private var inTextMode = false - set(value) { - field = value - if (value) { - activeNode?.name?.let { - textBox.textBox = it - } - - textBox.makeActive() - } else { - textBox.clear() - textBox.disable() - } - } - - private var inTutorialMode = false - - private val textBox = TextInput() - - private val nodeColor = LorenzColor.BLUE.addOpacity(200) - private val activeColor = LorenzColor.GREEN.addOpacity(200) - private val closedColor = LorenzColor.YELLOW.addOpacity(200) - private val dijkstraColor = LorenzColor.LIGHT_PURPLE.addOpacity(200) - - private val edgeColor = LorenzColor.GOLD.addOpacity(150) - private val edgeDijkstraColor = LorenzColor.DARK_BLUE.addOpacity(150) - private val edgeSelectedColor = LorenzColor.DARK_RED.addOpacity(150) - - @SubscribeEvent(priority = EventPriority.HIGHEST) - fun onRender(event: LorenzRenderWorldEvent) { - if (!isEnabled()) return - nodes.forEach { event.drawNode(it) } - edges.forEach { event.drawEdge(it) } - drawGhostPosition(event) - } - - private fun drawGhostPosition(event: LorenzRenderWorldEvent) { - val ghostPosition = ghostPosition ?: return - if (ghostPosition.distanceToPlayer() >= config.maxNodeDistance) return - - event.drawWaypointFilled( - ghostPosition, - if (activeNode == null) Color.RED else Color.GRAY, - seeThroughBlocks = seeThroughBlocks, - minimumAlpha = 0.2f, - inverseAlphaScale = true, - ) - } - - @SubscribeEvent - fun onOverlay(event: GuiRenderEvent.GuiOverlayRenderEvent) { - if (!isEnabled()) return - config.infoDisplay.renderStrings(buildDisplay(), posLabel = "Graph Info") - } - - private fun buildDisplay(): List = buildList { - add("§eExit: §6${KeyboardManager.getKeyName(config.exitKey)}") - if (!inEditMode && !inTextMode) { - add("§ePlace: §6${KeyboardManager.getKeyName(config.placeKey)}") - add("§eSelect: §6${KeyboardManager.getKeyName(config.selectKey)}") - add("§eConnect: §6${KeyboardManager.getKeyName(config.connectKey)}") - add("§eTest: §6${KeyboardManager.getKeyName(config.dijkstraKey)}") - add("§eVision: §6${KeyboardManager.getKeyName(config.throughBlocksKey)}") - add("§eSave: §6${KeyboardManager.getKeyName(config.saveKey)}") - add("§eLoad: §6${KeyboardManager.getKeyName(config.loadKey)}") - add("§eClear: §6${KeyboardManager.getKeyName(config.clearKey)}") - add("§eTutorial: §6${KeyboardManager.getKeyName(config.tutorialKey)}") - add("§eToggle Ghost Position: §6${KeyboardManager.getKeyName(config.toggleGhostPosition)}") - add(" ") - if (activeNode != null) { - add("§eText: §6${KeyboardManager.getKeyName(config.textKey)}") - if (dissolvePossible) add("§eDissolve: §6${KeyboardManager.getKeyName(config.dissolveKey)}") - if (selectedEdge != null) add("§eSplit: §6${KeyboardManager.getKeyName(config.splitKey)}") - } - } - - if (!inTextMode) { - if (activeNode != null) { - add("§eEdit active node: §6${KeyboardManager.getKeyName(config.editKey)}") - } else if (ghostPosition != null) { - add("Edit Ghost Position: §6${KeyboardManager.getKeyName(config.editKey)}") - } - } - - if (inEditMode) { - add("§ex+ §6${KeyboardManager.getKeyName(KeyboardManager.WasdInputMatrix.w.keyCode)}") - add("§ex- §6${KeyboardManager.getKeyName(KeyboardManager.WasdInputMatrix.s.keyCode)}") - add("§ez+ §6${KeyboardManager.getKeyName(KeyboardManager.WasdInputMatrix.a.keyCode)}") - add("§ez- §6${KeyboardManager.getKeyName(KeyboardManager.WasdInputMatrix.d.keyCode)}") - add("§ey+ §6${KeyboardManager.getKeyName(KeyboardManager.WasdInputMatrix.up.keyCode)}") - add("§ey- §6${KeyboardManager.getKeyName(KeyboardManager.WasdInputMatrix.down.keyCode)}") - } - if (inTextMode) { - add("§eFormat: ${textBox.finalText()}") - add("§eRaw: ${textBox.editText(textColor = LorenzColor.YELLOW)}") - } - } - - private var dissolvePossible = false - - private fun findEdgeBetweenActiveAndClosed(): GraphingEdge? = getEdgeIndex(activeNode, closedNode)?.let { edges[it] } - - private fun checkDissolve() { - if (activeNode == null) { - dissolvePossible = false - return - } - dissolvePossible = edges.count { it.isInEdge(activeNode) } == 2 - } - - private fun feedBackInTutorial(text: String) { - if (inTutorialMode) { - ChatUtils.chat(text) - } - } - - @SubscribeEvent - fun onTick(event: LorenzTickEvent) { - if (!isEnabled()) return - input() - if (nodes.isEmpty()) return - closedNode = nodes.minBy { it.position.distanceSqToPlayer() } - } - - private fun LorenzRenderWorldEvent.drawNode(node: GraphingNode) { - if (node.position.distanceToPlayer() > config.maxNodeDistance) return - this.drawWaypointFilled( - node.position, - node.getNodeColor(), - seeThroughBlocks = seeThroughBlocks, - minimumAlpha = 0.2f, - inverseAlphaScale = true, - ) - - val nodeName = node.name ?: return - this.drawDynamicText( - node.position, - nodeName, - 0.8, - ignoreBlocks = seeThroughBlocks || node.position.distanceSqToPlayer() < 100, - smallestDistanceVew = 12.0, - ignoreY = true, - yOff = -15f, - maxDistance = 80, - ) - - val tags = node.tags - if (tags.isEmpty()) return - val tagText = tags.map { it.displayName }.joinToString(" §f+ ") - this.drawDynamicText( - node.position, - tagText, - 0.8, - ignoreBlocks = seeThroughBlocks || node.position.distanceSqToPlayer() < 100, - smallestDistanceVew = 12.0, - ignoreY = true, - yOff = 0f, - maxDistance = 80, - ) - } - - private fun LorenzRenderWorldEvent.drawEdge(edge: GraphingEdge) { - if (edge.node1.position.distanceToPlayer() > config.maxNodeDistance) return - this.draw3DLine_nea( - edge.node1.position.add(0.5, 0.5, 0.5), - edge.node2.position.add(0.5, 0.5, 0.5), - when { - selectedEdge == edge -> edgeSelectedColor - edge in highlightedEdges -> edgeDijkstraColor - else -> edgeColor - }, - 7, - !seeThroughBlocks, - ) - } - - private fun GraphingNode.getNodeColor() = when (this) { - activeNode -> if (this == closedNode) ColorUtils.blendRGB(activeColor, closedColor, 0.5) else activeColor - closedNode -> closedColor - in highlightedNodes -> dijkstraColor - else -> nodeColor - } - - fun commandIn() { - config.enabled = !config.enabled - if (config.enabled) { - ChatUtils.chat("Graph Editor is now active.") - } else { - chatAtDisable() - } - } - - private fun chatAtDisable() = ChatUtils.clickableChat("Graph Editor is now inactive. §lClick to activate.", ::commandIn) - - private fun input() { - if (LorenzUtils.isAnyGuiActive()) return - if (config.exitKey.isKeyClicked()) { - if (inTextMode) { - inTextMode = false - feedBackInTutorial("Exited Text Mode.") - return - } - if (inEditMode) { - inEditMode = false - feedBackInTutorial("Exited Edit Mode.") - return - } - config.enabled = false - chatAtDisable() - } - if (inTextMode) { - textBox.handle() - val text = textBox.finalText() - if (text.isEmpty()) { - activeNode?.name = null - } else { - activeNode?.name = text - } - return - } - if (activeNode != null && config.textKey.isKeyClicked()) { - inTextMode = true - feedBackInTutorial("Entered Text Mode.") - return - } - if (inEditMode) { - editModeClicks() - inEditMode = false - } - if ((activeNode != null || ghostPosition != null) && config.editKey.isKeyHeld()) { - inEditMode = true - return - } - if (config.saveKey.isKeyClicked()) { - save() - return - } - if (config.loadKey.isKeyClicked()) { - runBlocking { - OSUtils.readFromClipboard()?.let { - try { - Graph.fromJson(it) - } catch (e: Exception) { - ErrorManager.logErrorWithData( - e, - "Import of graph failed.", - "json" to it, - ignoreErrorCache = true, - ) - null - } - }?.let { - import(it) - ChatUtils.chat("Loaded Graph from clip board.") - } - } - return - } - if (config.clearKey.isKeyClicked()) { - val json = compileGraph().toJson() - OSUtils.copyToClipboard(json) - ChatUtils.chat("Copied Graph to Clipboard and cleared the graph.") - clear() - } - if (config.placeKey.isKeyClicked()) { - addNode() - } - if (config.toggleGhostPosition.isKeyClicked()) { - toggleGhostPosition() - } - if (config.selectKey.isKeyClicked()) { - activeNode = if (activeNode == closedNode) { - feedBackInTutorial("De-selected active node.") - null - } else { - feedBackInTutorial("Selected new active node.") - closedNode - } - } - if (activeNode != closedNode && config.connectKey.isKeyClicked()) { - val edge = getEdgeIndex(activeNode, closedNode) - if (edge == null) { - addEdge(activeNode, closedNode) - feedBackInTutorial("Added new edge.") - } else { - this.edges.removeAt(edge) - checkDissolve() - selectedEdge = findEdgeBetweenActiveAndClosed() - feedBackInTutorial("Removed edge.") - } - } - if (config.throughBlocksKey.isKeyClicked()) { - seeThroughBlocks = !seeThroughBlocks - feedBackInTutorial( - if (seeThroughBlocks) "Graph is visible though walls." else "Graph is invisible behind walls.", - ) - } - if (config.dijkstraKey.isKeyClicked()) { - feedBackInTutorial("Calculated shortest route and cleared active node.") - testDijkstra() - } - if (config.tutorialKey.isKeyClicked()) { - inTutorialMode = !inTutorialMode - ChatUtils.chat("Tutorial mode is now ${if (inTutorialMode) "active" else "inactive"}.") - } - if (selectedEdge != null && config.splitKey.isKeyClicked()) { - val edge = selectedEdge ?: return - feedBackInTutorial("Split Edge into a Node and two edges.") - val middle = edge.node1.position.middle(edge.node2.position).roundLocationToBlock() - val node = GraphingNode(id++, middle) - nodes.add(node) - edges.remove(edge) - addEdge(node, edge.node1) - addEdge(node, edge.node2) - activeNode = node - } - if (dissolvePossible && config.dissolveKey.isKeyClicked()) { - feedBackInTutorial("Dissolved the node, now it is gone.") - val edgePair = edges.filter { it.isInEdge(activeNode) } - val edge1 = edgePair[0] - val edge2 = edgePair[1] - val neighbors1 = if (edge1.node1 == activeNode) edge1.node2 else edge1.node1 - val neighbors2 = if (edge2.node1 == activeNode) edge2.node2 else edge2.node1 - edges.removeAll(edgePair) - nodes.remove(activeNode) - activeNode = null - addEdge(neighbors1, neighbors2) - } - } - - private fun save() { - if (nodes.isEmpty()) { - ChatUtils.chat("Copied nothing since the graph is empty.") - return - } - val compileGraph = compileGraph() - if (config.useAsIslandArea) { - IslandGraphs.setNewGraph(compileGraph) - } - val json = compileGraph.toJson() - OSUtils.copyToClipboard(json) - ChatUtils.chat("Copied Graph to Clipboard.") - if (config.showsStats) { - val length = edges.sumOf { it.node1.position.distance(it.node2.position) }.toInt().addSeparators() - ChatUtils.chat( - "§lStats\n" + "§eNamed Nodes: ${ - nodes.count { it.name != null }.addSeparators() - }\n" + "§eNodes: ${nodes.size.addSeparators()}\n" + "§eEdges: ${edges.size.addSeparators()}\n" + "§eLength: $length", - ) - } - } - - private fun editModeClicks() { - var vector = LocationUtils.calculatePlayerFacingDirection() - KeyboardManager.WasdInputMatrix.w.handleEditClicks(vector) - KeyboardManager.WasdInputMatrix.a.handleEditClicks(vector.rotateXZ(Math.toRadians(90.0))) - KeyboardManager.WasdInputMatrix.s.handleEditClicks(vector.rotateXZ(Math.toRadians(180.0))) - KeyboardManager.WasdInputMatrix.d.handleEditClicks(vector.rotateXZ(Math.toRadians(270.0))) - - KeyboardManager.WasdInputMatrix.up.handleEditClicks(LorenzVec(0, 1, 0)) - KeyboardManager.WasdInputMatrix.down.handleEditClicks(LorenzVec(0, -1, 0)) - } - - private fun KeyBinding.handleEditClicks(vector: LorenzVec) { - if (this.keyCode.isKeyClicked()) { - activeNode?.let { - it.position = it.position + vector - } ?: run { - ghostPosition?.let { - ghostPosition = it + vector - } - } - } - } - - fun onMinecraftInput(keyBinding: KeyBinding, cir: CallbackInfoReturnable) { - if (!isEnabled()) return - if (!inEditMode) return - if (keyBinding !in KeyboardManager.WasdInputMatrix) return - cir.returnValue = false - } - - private fun addNode() { - val closedNode = closedNode - if (closedNode != null && closedNode.position.distanceSqToPlayer() < 9.0) { - if (closedNode == activeNode) { - feedBackInTutorial("Removed node, since you where closer than 3 blocks from a the active node.") - nodes.remove(closedNode) - edges.removeIf { it.isInEdge(closedNode) } - if (closedNode == activeNode) activeNode = null - this.closedNode = null - return - } - } - - val position = ghostPosition ?: LocationUtils.playerEyeLocation().roundLocationToBlock() - if (nodes.any { it.position == position }) { - feedBackInTutorial("Can't create node, here is already another one.") - return - } - val node = GraphingNode(id++, position) - nodes.add(node) - feedBackInTutorial("Added graph node.") - if (activeNode == null) return - addEdge(activeNode, node) - } - - fun toggleGhostPosition() { - if (ghostPosition != null) { - ghostPosition = null - feedBackInTutorial("Disabled Ghost Position.") - } else { - ghostPosition = LocationUtils.playerEyeLocation().roundLocationToBlock() - feedBackInTutorial("Enabled Ghost Position.") - } - } - - private fun getEdgeIndex(node1: GraphingNode?, node2: GraphingNode?) = - if (node1 != null && node2 != null && node1 != node2) GraphingEdge( - node1, - node2, - ).let { e -> edges.indexOfFirst { it == e }.takeIf { it != -1 } } - else null - - private fun addEdge(node1: GraphingNode?, node2: GraphingNode?) = if (node1 != null && node2 != null && node1 != node2) { - val edge = GraphingEdge(node1, node2) - if (edge.isInEdge(activeNode)) { - checkDissolve() - selectedEdge = findEdgeBetweenActiveAndClosed() - } - edges.add(edge) - } else false - - /** Has a side effect on the graphing graph, since it runs [prune] on the graphing graph*/ - private fun compileGraph(): Graph { - prune() - val indexedTable = nodes.mapIndexed { index, node -> node.id to index }.toMap() - val nodes = nodes.mapIndexed { index, it -> GraphNode(index, it.position, it.name, it.tags.mapNotNull { it.internalName }) } - val neighbours = this.nodes.map { node -> - edges.filter { it.isInEdge(node) }.map { edge -> - val otherNode = if (node == edge.node1) edge.node2 else edge.node1 - nodes[indexedTable[otherNode.id]!!] to node.position.distance(otherNode.position) - }.sortedBy { it.second } - } - nodes.forEachIndexed { index, it -> it.neighbours = neighbours[index].toMap() } - return Graph(nodes) - } - - fun import(graph: Graph) { - clear() - nodes.addAll( - graph.map { - GraphingNode( - it.id, - it.position, - it.name, - it.tagNames?.mapNotNull { GraphNodeTag.byId(it) }?.toMutableList() ?: mutableListOf(), - ) - }, - ) - val translation = graph.mapIndexed { index, it -> it to nodes[index] }.toMap() - edges.addAll( - graph.map { node -> - node.neighbours.map { GraphingEdge(translation[node]!!, translation[it.key]!!) } - }.flatten().distinct(), - ) - id = nodes.lastOrNull()?.id?.plus(1) ?: 0 - checkDissolve() - selectedEdge = findEdgeBetweenActiveAndClosed() - } - - private val highlightedNodes = mutableSetOf() - private val highlightedEdges = mutableSetOf() - - private fun testDijkstra() { - - val savedCurrent = closedNode ?: return - val savedActive = activeNode ?: return - - val compiled = compileGraph() - import(compiled) - highlightedEdges.clear() - highlightedNodes.clear() - - val current = compiled.firstOrNull { it.position == savedCurrent.position } ?: return - val goal = compiled.firstOrNull { it.position == savedActive.position } ?: return - - val path = compiled.findShortestPathAsGraph(current, goal) - - val inGraph = path.map { nodes[it.id] } - highlightedNodes.addAll(inGraph) - - val edge = edges.filter { highlightedNodes.contains(it.node1) && highlightedNodes.contains(it.node2) } - highlightedEdges.addAll(edge) - } - - private fun clear() { - id = 0 - nodes.clear() - edges.clear() - activeNode = null - closedNode = null - dissolvePossible = false - ghostPosition = null - } - - private fun prune() { //TODO fix - val hasNeighbours = nodes.associateWith { false }.toMutableMap() - edges.forEach { - hasNeighbours[it.node1] = true - hasNeighbours[it.node2] = true - } - nodes.removeIf { hasNeighbours[it] == false } - } - - fun LorenzVec.distanceSqToPlayer(): Double = ghostPosition?.let { distanceSq(it) } ?: distanceSq(playerLocation()) -} - -// The node object the graph editor is working with -class GraphingNode( - val id: Int, - var position: LorenzVec, - var name: String? = null, - var tags: MutableList = mutableListOf(), -) { - - 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 GraphingNode - - if (id != other.id) return false - - return true - } -} - -private class GraphingEdge(val node1: GraphingNode, val node2: GraphingNode) { - - fun isInEdge(node: GraphingNode?) = node1 == node || node2 == node - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as GraphingEdge - - return (this.node1 == other.node1 && this.node2 == other.node2) || (this.node1 == other.node2 && this.node2 == other.node1) - } - - override fun hashCode(): Int { - val hash1 = node1.hashCode() - val hash2 = node2.hashCode() - - var result: Int - if (hash1 <= hash2) { - result = hash1 - result = 31 * result + hash2 - } else { - result = hash2 - result = 31 * result + hash1 - } - return result - } -} diff --git a/src/main/java/at/hannibal2/skyhanni/test/GraphNodeEditor.kt b/src/main/java/at/hannibal2/skyhanni/test/GraphNodeEditor.kt deleted file mode 100644 index f541b16e3..000000000 --- a/src/main/java/at/hannibal2/skyhanni/test/GraphNodeEditor.kt +++ /dev/null @@ -1,178 +0,0 @@ -package at.hannibal2.skyhanni.test - -import at.hannibal2.skyhanni.data.model.GraphNodeTag -import at.hannibal2.skyhanni.data.model.TextInput -import at.hannibal2.skyhanni.events.GuiRenderEvent -import at.hannibal2.skyhanni.skyhannimodule.SkyHanniModule -import at.hannibal2.skyhanni.test.GraphEditor.distanceSqToPlayer -import at.hannibal2.skyhanni.utils.CollectionUtils.addString -import at.hannibal2.skyhanni.utils.KeyboardManager -import at.hannibal2.skyhanni.utils.LorenzUtils -import at.hannibal2.skyhanni.utils.NumberUtil.addSeparators -import at.hannibal2.skyhanni.utils.RenderUtils.renderRenderables -import at.hannibal2.skyhanni.utils.SimpleTimeMark -import at.hannibal2.skyhanni.utils.renderables.Renderable -import at.hannibal2.skyhanni.utils.renderables.ScrollValue -import at.hannibal2.skyhanni.utils.renderables.Searchable -import at.hannibal2.skyhanni.utils.renderables.buildSearchableScrollable -import at.hannibal2.skyhanni.utils.renderables.toSearchable -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent -import kotlin.math.sqrt -import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.Duration.Companion.seconds - -@SkyHanniModule -object GraphNodeEditor { - - private val scrollValueNodes = ScrollValue() - private val scrollValueTags = ScrollValue() - private val textInput = TextInput() - private var nodesDisplay = emptyList() - private var lastUpdate = SimpleTimeMark.farPast() - - @SubscribeEvent - fun onGuiRender(event: GuiRenderEvent) { - if (!isEnabled()) return - - - config.namedNodesList.renderRenderables( - getNodeNames(), - posLabel = "Graph Nodes List", - ) - } - - private fun getNodeNames(): List { - if (lastUpdate.passedSince() > 250.milliseconds) { - updateNodeNames() - } - return nodesDisplay - } - - private fun updateNodeNames() { - lastUpdate = SimpleTimeMark.now() - nodesDisplay = buildList { - val list = drawNodeNames() - val size = list.size - addString("§eGraph Nodes: $size") - val height = (size * 10).coerceAtMost(250) - if (list.isNotEmpty()) { - add(list.buildSearchableScrollable(height, textInput, scrollValueNodes, velocity = 10.0)) - } - } - } - - private fun updateTagView(node: GraphingNode) { - lastUpdate = SimpleTimeMark.now() + 60.seconds - nodesDisplay = buildList { - val list = drawTagNames(node) - val size = list.size - addString("§eGraph Nodes: $size") - val height = (size * 10).coerceAtMost(250) - if (list.isNotEmpty()) { - add(Renderable.scrollList(list, height, scrollValueTags, velocity = 10.0)) - } - } - } - - private fun drawTagNames(node: GraphingNode): List = buildList { - addString("§eChange tag for node '${node.name}§e'") - addString("") - - for (tag in GraphNodeTag.entries.filter { it in node.tags || checkIsland(it) }) { - val state = if (tag in node.tags) "§aYES" else "§cNO" - val name = state + " §r" + tag.displayName - add(createTagName(name, tag, node)) - } - addString("") - add( - Renderable.clickAndHover( - "§cGo Back!", - tips = listOf("§eClick to go back to the node list!"), - onClick = { - updateNodeNames() - }, - ), - ) - } - - private fun checkIsland(tag: GraphNodeTag): Boolean = tag.onlyIsland?.let { - it == LorenzUtils.skyBlockIsland - } ?: true - - private fun createTagName( - name: String, - tag: GraphNodeTag, - node: GraphingNode, - ) = Renderable.clickAndHover( - name, - tips = listOf( - "Tag ${tag.name}", - "§7${tag.description}", - "", - "§eClick to set tag for ${node.name} to ${tag.name}!", - ), - onClick = { - if (tag in node.tags) { - node.tags.remove(tag) - } else { - node.tags.add(tag) - } - updateTagView(node) - }, - ) - - private fun drawNodeNames(): List = buildList { - for ((node, distance: Double) in GraphEditor.nodes.map { it to it.position.distanceSqToPlayer() }.sortedBy { it.second }) { - val name = node.name?.takeIf { !it.isBlank() } ?: continue - val color = if (node == GraphEditor.activeNode) "§a" else "§7" - val distanceFormat = sqrt(distance).toInt().addSeparators() - val tagText = node.tags.let { tags -> - if (tags.isEmpty()) { - " §cNo tag§r" - } else { - val text = node.tags.joinToString(", ") { it.internalName } - " §f($text)" - } - } - - val text = "${color}Node §r$name$tagText §7[$distanceFormat]" - add(createNodeTextLine(text, name, node)) - } - } - - private fun createNodeTextLine( - text: String, - name: String, - node: GraphingNode, - ): Searchable = Renderable.clickAndHover( - text, - tips = buildList { - add("Node '$name'") - add("") - - if (node.tags.isNotEmpty()) { - add("Tags: ") - for (tag in node.tags) { - add(" §8- §r${tag.displayName}") - } - add("") - } - - add("§eClick to select/deselect this node!") - add("§eControl-Click to edit the tags for this node!") - - }, - onClick = { - if (KeyboardManager.isModifierKeyDown()) { - updateTagView(node) - } else { - GraphEditor.activeNode = node - updateNodeNames() - } - }, - ).toSearchable(name) - - fun isEnabled() = GraphEditor.isEnabled() - private val config get() = GraphEditor.config - -} diff --git a/src/main/java/at/hannibal2/skyhanni/test/SkyHanniDebugsAndTests.kt b/src/main/java/at/hannibal2/skyhanni/test/SkyHanniDebugsAndTests.kt index 5b7333189..045ce5bce 100644 --- a/src/main/java/at/hannibal2/skyhanni/test/SkyHanniDebugsAndTests.kt +++ b/src/main/java/at/hannibal2/skyhanni/test/SkyHanniDebugsAndTests.kt @@ -131,36 +131,13 @@ object SkyHanniDebugsAndTests { } fun testCommand(args: Array) { + SkyHanniMod.coroutineScope.launch { + asyncTest() + } + } + private fun asyncTest() { -// val a = Thread { OSUtils.copyToClipboard("123") } -// val b = Thread { OSUtils.copyToClipboard("456") } -// a.start() -// b.start() - -// for ((i, s) in ScoreboardData.siedebarLinesFormatted().withIndex()) { -// println("$i: '$s'") -// } - -// val name = args[0] -// val pitch = args[1].toFloat() -// val sound = SoundUtils.createSound("note.harp", 1.35f) -// val sound = SoundUtils.createSound("random.orb", 11.2f) -// SoundUtils.createSound(name, pitch).playSound() - -// a = args[0].toDouble() -// b = args[1].toDouble() -// c = args[2].toDouble() - -// for (line in getPlayerTabOverlay().footer.unformattedText -// .split("\n")) { -// println("footer: '$line'") -// } -// -// -// for (line in TabListUtils.getTabList()) { -// println("tablist: '$line'") -// } } fun findNullConfig(args: Array) { diff --git a/src/main/java/at/hannibal2/skyhanni/test/graph/GraphEditor.kt b/src/main/java/at/hannibal2/skyhanni/test/graph/GraphEditor.kt new file mode 100644 index 000000000..1aefeda52 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/test/graph/GraphEditor.kt @@ -0,0 +1,666 @@ +package at.hannibal2.skyhanni.test.graph + +import at.hannibal2.skyhanni.SkyHanniMod +import at.hannibal2.skyhanni.data.IslandGraphs +import at.hannibal2.skyhanni.data.model.Graph +import at.hannibal2.skyhanni.data.model.GraphNode +import at.hannibal2.skyhanni.data.model.GraphNodeTag +import at.hannibal2.skyhanni.data.model.TextInput +import at.hannibal2.skyhanni.data.model.findShortestPathAsGraph +import at.hannibal2.skyhanni.data.model.toJson +import at.hannibal2.skyhanni.events.GuiRenderEvent +import at.hannibal2.skyhanni.events.LorenzRenderWorldEvent +import at.hannibal2.skyhanni.events.LorenzTickEvent +import at.hannibal2.skyhanni.skyhannimodule.SkyHanniModule +import at.hannibal2.skyhanni.test.command.ErrorManager +import at.hannibal2.skyhanni.utils.ChatUtils +import at.hannibal2.skyhanni.utils.ColorUtils +import at.hannibal2.skyhanni.utils.KeyboardManager +import at.hannibal2.skyhanni.utils.KeyboardManager.isKeyClicked +import at.hannibal2.skyhanni.utils.KeyboardManager.isKeyHeld +import at.hannibal2.skyhanni.utils.LocationUtils +import at.hannibal2.skyhanni.utils.LocationUtils.distanceToPlayer +import at.hannibal2.skyhanni.utils.LocationUtils.playerLocation +import at.hannibal2.skyhanni.utils.LorenzColor +import at.hannibal2.skyhanni.utils.LorenzUtils +import at.hannibal2.skyhanni.utils.LorenzVec +import at.hannibal2.skyhanni.utils.NumberUtil.addSeparators +import at.hannibal2.skyhanni.utils.OSUtils +import at.hannibal2.skyhanni.utils.RaycastUtils +import at.hannibal2.skyhanni.utils.RenderUtils.draw3DLine_nea +import at.hannibal2.skyhanni.utils.RenderUtils.drawDynamicText +import at.hannibal2.skyhanni.utils.RenderUtils.drawWaypointFilled +import at.hannibal2.skyhanni.utils.RenderUtils.renderStrings +import kotlinx.coroutines.runBlocking +import net.minecraft.client.settings.KeyBinding +import net.minecraftforge.fml.common.eventhandler.EventPriority +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable +import java.awt.Color + +@SkyHanniModule +object GraphEditor { + + val config get() = SkyHanniMod.feature.dev.devTool.graph + + fun isEnabled() = config != null && config.enabled + + private var id = 0 + + val nodes = mutableListOf() + private val edges = mutableListOf() + + var activeNode: GraphingNode? = null + set(value) { + field = value + selectedEdge = findEdgeBetweenActiveAndClosed() + checkDissolve() + } + private var closedNode: GraphingNode? = null + set(value) { + field = value + selectedEdge = findEdgeBetweenActiveAndClosed() + } + + private var selectedEdge: GraphingEdge? = null + private var ghostPosition: LorenzVec? = null + + private var seeThroughBlocks = true + + private var inEditMode = false + private var inTextMode = false + set(value) { + field = value + if (value) { + activeNode?.name?.let { + textBox.textBox = it + } + + textBox.makeActive() + } else { + textBox.clear() + textBox.disable() + } + } + + private var inTutorialMode = false + + private val textBox = TextInput() + + private val nodeColor = LorenzColor.BLUE.addOpacity(200) + private val activeColor = LorenzColor.GREEN.addOpacity(200) + private val closedColor = LorenzColor.YELLOW.addOpacity(200) + private val dijkstraColor = LorenzColor.LIGHT_PURPLE.addOpacity(200) + + private val edgeColor = LorenzColor.GOLD.addOpacity(150) + private val edgeDijkstraColor = LorenzColor.DARK_BLUE.addOpacity(150) + private val edgeSelectedColor = LorenzColor.DARK_RED.addOpacity(150) + + @SubscribeEvent(priority = EventPriority.HIGHEST) + fun onRender(event: LorenzRenderWorldEvent) { + if (!isEnabled()) return + nodes.forEach { event.drawNode(it) } + edges.forEach { event.drawEdge(it) } + drawGhostPosition(event) + } + + private fun drawGhostPosition(event: LorenzRenderWorldEvent) { + val ghostPosition = ghostPosition ?: return + if (ghostPosition.distanceToPlayer() >= config.maxNodeDistance) return + + event.drawWaypointFilled( + ghostPosition, + if (activeNode == null) Color.RED else Color.GRAY, + seeThroughBlocks = seeThroughBlocks, + minimumAlpha = 0.2f, + inverseAlphaScale = true, + ) + } + + @SubscribeEvent + fun onOverlay(event: GuiRenderEvent.GuiOverlayRenderEvent) { + if (!isEnabled()) return + config.infoDisplay.renderStrings(buildDisplay(), posLabel = "Graph Info") + } + + private fun buildDisplay(): List = buildList { + add("§eExit: §6${KeyboardManager.getKeyName(config.exitKey)}") + if (!inEditMode && !inTextMode) { + add("§ePlace: §6${KeyboardManager.getKeyName(config.placeKey)}") + add("§eSelect: §6${KeyboardManager.getKeyName(config.selectKey)}") + add("§eSelect (Look): §6${KeyboardManager.getKeyName(config.selectRaycastKey)}") + add("§eConnect: §6${KeyboardManager.getKeyName(config.connectKey)}") + add("§eTest: §6${KeyboardManager.getKeyName(config.dijkstraKey)}") + add("§eVision: §6${KeyboardManager.getKeyName(config.throughBlocksKey)}") + add("§eSave: §6${KeyboardManager.getKeyName(config.saveKey)}") + add("§eLoad: §6${KeyboardManager.getKeyName(config.loadKey)}") + add("§eClear: §6${KeyboardManager.getKeyName(config.clearKey)}") + add("§eTutorial: §6${KeyboardManager.getKeyName(config.tutorialKey)}") + add("§eToggle Ghost Position: §6${KeyboardManager.getKeyName(config.toggleGhostPosition)}") + add(" ") + if (activeNode != null) { + add("§eText: §6${KeyboardManager.getKeyName(config.textKey)}") + if (dissolvePossible) add("§eDissolve: §6${KeyboardManager.getKeyName(config.dissolveKey)}") + if (selectedEdge != null) add("§eSplit: §6${KeyboardManager.getKeyName(config.splitKey)}") + } + } + + if (!inTextMode) { + if (activeNode != null) { + add("§eEdit active node: §6${KeyboardManager.getKeyName(config.editKey)}") + } else if (ghostPosition != null) { + add("Edit Ghost Position: §6${KeyboardManager.getKeyName(config.editKey)}") + } + } + + if (inEditMode) { + add("§ex+ §6${KeyboardManager.getKeyName(KeyboardManager.WasdInputMatrix.w.keyCode)}") + add("§ex- §6${KeyboardManager.getKeyName(KeyboardManager.WasdInputMatrix.s.keyCode)}") + add("§ez+ §6${KeyboardManager.getKeyName(KeyboardManager.WasdInputMatrix.a.keyCode)}") + add("§ez- §6${KeyboardManager.getKeyName(KeyboardManager.WasdInputMatrix.d.keyCode)}") + add("§ey+ §6${KeyboardManager.getKeyName(KeyboardManager.WasdInputMatrix.up.keyCode)}") + add("§ey- §6${KeyboardManager.getKeyName(KeyboardManager.WasdInputMatrix.down.keyCode)}") + } + if (inTextMode) { + add("§eFormat: ${textBox.finalText()}") + add("§eRaw: ${textBox.editText(textColor = LorenzColor.YELLOW)}") + } + } + + private var dissolvePossible = false + + private fun findEdgeBetweenActiveAndClosed(): GraphingEdge? = getEdgeIndex(activeNode, closedNode)?.let { edges[it] } + + private fun checkDissolve() { + if (activeNode == null) { + dissolvePossible = false + return + } + dissolvePossible = edges.count { it.isInEdge(activeNode) } == 2 + } + + private fun feedBackInTutorial(text: String) { + if (inTutorialMode) { + ChatUtils.chat(text) + } + } + + @SubscribeEvent + fun onTick(event: LorenzTickEvent) { + if (!isEnabled()) return + input() + if (nodes.isEmpty()) return + closedNode = nodes.minBy { it.position.distanceSqToPlayer() } + } + + private fun LorenzRenderWorldEvent.drawNode(node: GraphingNode) { + if (node.position.distanceToPlayer() > config.maxNodeDistance) return + this.drawWaypointFilled( + node.position, + node.getNodeColor(), + seeThroughBlocks = seeThroughBlocks, + minimumAlpha = 0.2f, + inverseAlphaScale = true, + ) + + val nodeName = node.name ?: return + this.drawDynamicText( + node.position, + nodeName, + 0.8, + ignoreBlocks = seeThroughBlocks || node.position.distanceSqToPlayer() < 100, + smallestDistanceVew = 12.0, + ignoreY = true, + yOff = -15f, + maxDistance = 80, + ) + + val tags = node.tags + if (tags.isEmpty()) return + val tagText = tags.map { it.displayName }.joinToString(" §f+ ") + this.drawDynamicText( + node.position, + tagText, + 0.8, + ignoreBlocks = seeThroughBlocks || node.position.distanceSqToPlayer() < 100, + smallestDistanceVew = 12.0, + ignoreY = true, + yOff = 0f, + maxDistance = 80, + ) + } + + private fun LorenzRenderWorldEvent.drawEdge(edge: GraphingEdge) { + if (edge.node1.position.distanceToPlayer() > config.maxNodeDistance) return + this.draw3DLine_nea( + edge.node1.position.add(0.5, 0.5, 0.5), + edge.node2.position.add(0.5, 0.5, 0.5), + when { + selectedEdge == edge -> edgeSelectedColor + edge in highlightedEdges -> edgeDijkstraColor + else -> edgeColor + }, + 7, + !seeThroughBlocks, + ) + } + + private fun GraphingNode.getNodeColor() = when (this) { + activeNode -> if (this == closedNode) ColorUtils.blendRGB(activeColor, closedColor, 0.5) else activeColor + closedNode -> closedColor + in highlightedNodes -> dijkstraColor + else -> nodeColor + } + + fun commandIn() { + config.enabled = !config.enabled + if (config.enabled) { + ChatUtils.chat("Graph Editor is now active.") + } else { + chatAtDisable() + } + } + + private fun chatAtDisable() = ChatUtils.clickableChat("Graph Editor is now inactive. §lClick to activate.", + GraphEditor::commandIn + ) + + private fun input() { + if (LorenzUtils.isAnyGuiActive()) return + if (config.exitKey.isKeyClicked()) { + if (inTextMode) { + inTextMode = false + feedBackInTutorial("Exited Text Mode.") + return + } + if (inEditMode) { + inEditMode = false + feedBackInTutorial("Exited Edit Mode.") + return + } + config.enabled = false + chatAtDisable() + } + if (inTextMode) { + textBox.handle() + val text = textBox.finalText() + if (text.isEmpty()) { + activeNode?.name = null + } else { + activeNode?.name = text + } + return + } + if (activeNode != null && config.textKey.isKeyClicked()) { + inTextMode = true + feedBackInTutorial("Entered Text Mode.") + return + } + if (inEditMode) { + editModeClicks() + inEditMode = false + } + if ((activeNode != null || ghostPosition != null) && config.editKey.isKeyHeld()) { + inEditMode = true + return + } + if (config.saveKey.isKeyClicked()) { + save() + return + } + if (config.loadKey.isKeyClicked()) { + runBlocking { + OSUtils.readFromClipboard()?.let { + try { + Graph.fromJson(it) + } catch (e: Exception) { + ErrorManager.logErrorWithData( + e, + "Import of graph failed.", + "json" to it, + ignoreErrorCache = true, + ) + null + } + }?.let { + import(it) + ChatUtils.chat("Loaded Graph from clip board.") + } + } + return + } + if (config.clearKey.isKeyClicked()) { + val json = compileGraph().toJson() + OSUtils.copyToClipboard(json) + ChatUtils.chat("Copied Graph to Clipboard and cleared the graph.") + clear() + } + if (config.placeKey.isKeyClicked()) { + addNode() + } + if (config.toggleGhostPosition.isKeyClicked()) { + toggleGhostPosition() + } + if (config.selectKey.isKeyClicked()) { + activeNode = if (activeNode == closedNode) { + feedBackInTutorial("De-selected active node.") + null + } else { + feedBackInTutorial("Selected new active node.") + closedNode + } + } + if (config.selectRaycastKey.isKeyClicked()) { + val playerRay = RaycastUtils.createPlayerLookDirectionRay() + var minimumDistance = Double.MAX_VALUE + var minimumNode: GraphingNode? = null + for (node in nodes) { + val nodeCenterPosition = node.position.add(0.5, 0.5, 0.5) + val distance = RaycastUtils.findDistanceToRay(playerRay, nodeCenterPosition) + if (distance > minimumDistance) { + continue + } + if (minimumDistance > 1.0) { + minimumNode = node + minimumDistance = distance + continue + } + if (minimumNode == null || minimumNode.position.distanceSqToPlayer() > node.position.distanceSqToPlayer()) { + minimumNode = node + minimumDistance = distance + } + } + activeNode = minimumNode + } + if (activeNode != closedNode && config.connectKey.isKeyClicked()) { + val edge = getEdgeIndex(activeNode, closedNode) + if (edge == null) { + addEdge(activeNode, closedNode) + feedBackInTutorial("Added new edge.") + } else { + edges.removeAt(edge) + checkDissolve() + selectedEdge = findEdgeBetweenActiveAndClosed() + feedBackInTutorial("Removed edge.") + } + } + if (config.throughBlocksKey.isKeyClicked()) { + seeThroughBlocks = !seeThroughBlocks + feedBackInTutorial( + if (seeThroughBlocks) "Graph is visible though walls." else "Graph is invisible behind walls.", + ) + } + if (config.dijkstraKey.isKeyClicked()) { + feedBackInTutorial("Calculated shortest route and cleared active node.") + testDijkstra() + } + if (config.tutorialKey.isKeyClicked()) { + inTutorialMode = !inTutorialMode + ChatUtils.chat("Tutorial mode is now ${if (inTutorialMode) "active" else "inactive"}.") + } + if (selectedEdge != null && config.splitKey.isKeyClicked()) { + val edge = selectedEdge ?: return + feedBackInTutorial("Split Edge into a Node and two edges.") + val middle = edge.node1.position.middle(edge.node2.position).roundLocationToBlock() + val node = GraphingNode(id++, middle) + nodes.add(node) + edges.remove(edge) + addEdge(node, edge.node1) + addEdge(node, edge.node2) + activeNode = node + } + if (dissolvePossible && config.dissolveKey.isKeyClicked()) { + feedBackInTutorial("Dissolved the node, now it is gone.") + val edgePair = edges.filter { it.isInEdge(activeNode) } + val edge1 = edgePair[0] + val edge2 = edgePair[1] + val neighbors1 = if (edge1.node1 == activeNode) edge1.node2 else edge1.node1 + val neighbors2 = if (edge2.node1 == activeNode) edge2.node2 else edge2.node1 + edges.removeAll(edgePair) + nodes.remove(activeNode) + activeNode = null + addEdge(neighbors1, neighbors2) + } + } + + private fun save() { + if (nodes.isEmpty()) { + ChatUtils.chat("Copied nothing since the graph is empty.") + return + } + val compileGraph = compileGraph() + if (config.useAsIslandArea) { + IslandGraphs.setNewGraph(compileGraph) + GraphEditorBugFinder.runTests() + } + val json = compileGraph.toJson() + OSUtils.copyToClipboard(json) + ChatUtils.chat("Copied Graph to Clipboard.") + if (config.showsStats) { + val length = edges.sumOf { it.node1.position.distance(it.node2.position) }.toInt().addSeparators() + ChatUtils.chat( + "§lStats\n" + "§eNamed Nodes: ${ + nodes.count { it.name != null }.addSeparators() + }\n" + "§eNodes: ${nodes.size.addSeparators()}\n" + "§eEdges: ${edges.size.addSeparators()}\n" + "§eLength: $length", + ) + } + } + + private fun editModeClicks() { + var vector = LocationUtils.calculatePlayerFacingDirection() + KeyboardManager.WasdInputMatrix.w.handleEditClicks(vector) + KeyboardManager.WasdInputMatrix.a.handleEditClicks(vector.rotateXZ(Math.toRadians(90.0))) + KeyboardManager.WasdInputMatrix.s.handleEditClicks(vector.rotateXZ(Math.toRadians(180.0))) + KeyboardManager.WasdInputMatrix.d.handleEditClicks(vector.rotateXZ(Math.toRadians(270.0))) + + KeyboardManager.WasdInputMatrix.up.handleEditClicks(LorenzVec(0, 1, 0)) + KeyboardManager.WasdInputMatrix.down.handleEditClicks(LorenzVec(0, -1, 0)) + } + + private fun KeyBinding.handleEditClicks(vector: LorenzVec) { + if (this.keyCode.isKeyClicked()) { + activeNode?.let { + it.position = it.position + vector + } ?: run { + ghostPosition?.let { + ghostPosition = it + vector + } + } + } + } + + fun onMinecraftInput(keyBinding: KeyBinding, cir: CallbackInfoReturnable) { + if (!isEnabled()) return + if (!inEditMode) return + if (keyBinding !in KeyboardManager.WasdInputMatrix) return + cir.returnValue = false + } + + private fun addNode() { + val closedNode = closedNode + if (closedNode != null && closedNode.position.distanceSqToPlayer() < 9.0) { + if (closedNode == activeNode) { + feedBackInTutorial("Removed node, since you where closer than 3 blocks from a the active node.") + nodes.remove(closedNode) + edges.removeIf { it.isInEdge(closedNode) } + if (closedNode == activeNode) activeNode = null + GraphEditor.closedNode = null + return + } + } + + val position = ghostPosition ?: LocationUtils.playerEyeLocation().roundLocationToBlock() + if (nodes.any { it.position == position }) { + feedBackInTutorial("Can't create node, here is already another one.") + return + } + val node = GraphingNode(id++, position) + nodes.add(node) + feedBackInTutorial("Added graph node.") + if (activeNode == null) return + addEdge(activeNode, node) + } + + fun toggleGhostPosition() { + if (ghostPosition != null) { + ghostPosition = null + feedBackInTutorial("Disabled Ghost Position.") + } else { + ghostPosition = LocationUtils.playerEyeLocation().roundLocationToBlock() + feedBackInTutorial("Enabled Ghost Position.") + } + } + + private fun getEdgeIndex(node1: GraphingNode?, node2: GraphingNode?) = + if (node1 != null && node2 != null && node1 != node2) GraphingEdge( + node1, + node2, + ).let { e -> edges.indexOfFirst { it == e }.takeIf { it != -1 } } + else null + + private fun addEdge(node1: GraphingNode?, node2: GraphingNode?) = if (node1 != null && node2 != null && node1 != node2) { + val edge = GraphingEdge(node1, node2) + if (edge.isInEdge(activeNode)) { + checkDissolve() + selectedEdge = findEdgeBetweenActiveAndClosed() + } + edges.add(edge) + } else false + + /** Has a side effect on the graphing graph, since it runs [prune] on the graphing graph*/ + private fun compileGraph(): Graph { + prune() + val indexedTable = nodes.mapIndexed { index, node -> node.id to index }.toMap() + val nodes = nodes.mapIndexed { index, it -> GraphNode(index, it.position, it.name, it.tags.mapNotNull { it.internalName }) } + val neighbours = GraphEditor.nodes.map { node -> + edges.filter { it.isInEdge(node) }.map { edge -> + val otherNode = if (node == edge.node1) edge.node2 else edge.node1 + nodes[indexedTable[otherNode.id]!!] to node.position.distance(otherNode.position) + }.sortedBy { it.second } + } + nodes.forEachIndexed { index, it -> it.neighbours = neighbours[index].toMap() } + return Graph(nodes) + } + + fun import(graph: Graph) { + clear() + nodes.addAll( + graph.map { + GraphingNode( + it.id, + it.position, + it.name, + it.tagNames.mapNotNull { tag -> GraphNodeTag.byId(tag) }.toMutableList(), + ) + }, + ) + val translation = graph.mapIndexed { index, it -> it to nodes[index] }.toMap() + edges.addAll( + graph.map { node -> + node.neighbours.map { GraphingEdge(translation[node]!!, translation[it.key]!!) } + }.flatten().distinct(), + ) + id = nodes.lastOrNull()?.id?.plus(1) ?: 0 + checkDissolve() + selectedEdge = findEdgeBetweenActiveAndClosed() + } + + private val highlightedNodes = mutableSetOf() + private val highlightedEdges = mutableSetOf() + + private fun testDijkstra() { + + val savedCurrent = closedNode ?: return + val savedActive = activeNode ?: return + + val compiled = compileGraph() + import(compiled) + highlightedEdges.clear() + highlightedNodes.clear() + + val current = compiled.firstOrNull { it.position == savedCurrent.position } ?: return + val goal = compiled.firstOrNull { it.position == savedActive.position } ?: return + + val path = compiled.findShortestPathAsGraph(current, goal) + + val inGraph = path.map { nodes[it.id] } + highlightedNodes.addAll(inGraph) + + val edge = edges.filter { highlightedNodes.contains(it.node1) && highlightedNodes.contains(it.node2) } + highlightedEdges.addAll(edge) + } + + private fun clear() { + id = 0 + nodes.clear() + edges.clear() + activeNode = null + closedNode = null + dissolvePossible = false + ghostPosition = null + } + + private fun prune() { //TODO fix + val hasNeighbours = nodes.associateWith { false }.toMutableMap() + edges.forEach { + hasNeighbours[it.node1] = true + hasNeighbours[it.node2] = true + } + nodes.removeIf { hasNeighbours[it] == false } + } + + fun LorenzVec.distanceSqToPlayer(): Double = ghostPosition?.let { distanceSq(it) } ?: distanceSq(playerLocation()) +} + +// The node object the graph editor is working with +class GraphingNode( + val id: Int, + var position: LorenzVec, + var name: String? = null, + var tags: MutableList = mutableListOf(), +) { + + 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 GraphingNode + + if (id != other.id) return false + + return true + } +} + +private class GraphingEdge(val node1: GraphingNode, val node2: GraphingNode) { + + fun isInEdge(node: GraphingNode?) = node1 == node || node2 == node + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as GraphingEdge + + return (this.node1 == other.node1 && this.node2 == other.node2) || (this.node1 == other.node2 && this.node2 == other.node1) + } + + override fun hashCode(): Int { + val hash1 = node1.hashCode() + val hash2 = node2.hashCode() + + var result: Int + if (hash1 <= hash2) { + result = hash1 + result = 31 * result + hash2 + } else { + result = hash2 + result = 31 * result + hash1 + } + return result + } +} diff --git a/src/main/java/at/hannibal2/skyhanni/test/graph/GraphEditorBugFinder.kt b/src/main/java/at/hannibal2/skyhanni/test/graph/GraphEditorBugFinder.kt new file mode 100644 index 000000000..aee75ec5f --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/test/graph/GraphEditorBugFinder.kt @@ -0,0 +1,95 @@ +package at.hannibal2.skyhanni.test.graph + +import at.hannibal2.skyhanni.SkyHanniMod +import at.hannibal2.skyhanni.data.IslandGraphs +import at.hannibal2.skyhanni.data.model.GraphNode +import at.hannibal2.skyhanni.events.LorenzRenderWorldEvent +import at.hannibal2.skyhanni.features.misc.IslandAreas.getAreaTag +import at.hannibal2.skyhanni.test.graph.GraphEditor.distanceSqToPlayer +import at.hannibal2.skyhanni.utils.GraphUtils +import at.hannibal2.skyhanni.utils.LorenzVec +import at.hannibal2.skyhanni.utils.RenderUtils.drawDynamicText +import kotlinx.coroutines.launch +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import java.awt.Color + +// Trying to find errors in Area Graph for the current graph editor instance +object GraphEditorBugFinder { + private var errorsInWorld = emptyMap() + + fun runTests() { + SkyHanniMod.coroutineScope.launch { + asyncTest() + } + } + + private fun asyncTest() { + val graph = IslandGraphs.currentIslandGraph ?: return + val errorsInWorld: MutableMap = mutableMapOf() + val nodes = graph.nodes + + val nearestArea = mutableMapOf() + for (node in nodes) { + val pathToNearestArea = GraphUtils.findFastestPath(graph, node) { it.getAreaTag(ignoreConfig = true) != null }?.first + if (pathToNearestArea == null) { + continue + } + val areaNode = pathToNearestArea.lastOrNull() ?: error("Empty path to nearest area") + nearestArea[node] = areaNode + } + var bugs = 0 + for (node in nodes) { + val areaNode = nearestArea[node]?.name ?: continue + for (neighbour in node.neighbours.keys) { + val neighbouringAreaNode = nearestArea[neighbour]?.name ?: continue + if (neighbouringAreaNode == areaNode) continue + if ((null == node.getAreaTag(ignoreConfig = true))) { + bugs++ + errorsInWorld[node.position] = "§cConflicting areas $areaNode and $neighbouringAreaNode" + } + } + } + for (node in nodes) { + val nameNull = node.name.isNullOrBlank() + val tagsEmpty = node.tags.isEmpty() + if (nameNull > tagsEmpty) { + errorsInWorld[node.position] = "§cMissing name despite having tags" + bugs++ + } + if (tagsEmpty > nameNull) { + errorsInWorld[node.position] = "§cMissing tags despite having name" + bugs++ + } + } + + val clusters = GraphUtils.findDisjointClusters(graph) + if (clusters.size > 1) { + val closestCluster = clusters.minBy { it.minOf { it.position.distanceSqToPlayer() } } + val foreignClusters = clusters.filter { it !== closestCluster } + val closestForeignNodes = foreignClusters.map { network -> network.minBy { it.position.distanceSqToPlayer() } } + closestForeignNodes.forEach { + errorsInWorld[it.position] = "§cDisjoint node network" + bugs++ + } + val closestForeignNode = closestForeignNodes.minBy { it.position.distanceSqToPlayer() } + val closestNodeToForeignNode = closestCluster.minBy { it.position.distanceSq(closestForeignNode.position) } + IslandGraphs.pathFind(closestNodeToForeignNode.position, Color.RED) + } + + println("found $bugs bugs!") + this.errorsInWorld = errorsInWorld + if (clusters.size <= 1) { + IslandGraphs.pathFind(errorsInWorld.keys.minByOrNull { it.distanceSqToPlayer() } ?: return, Color.RED) + } + } + + @SubscribeEvent + fun onRenderWorld(event: LorenzRenderWorldEvent) { + if (!isEnabled()) return + + for ((location, text) in errorsInWorld) { + event.drawDynamicText(location, text, 1.5) + } + } + fun isEnabled() = GraphEditor.isEnabled() +} diff --git a/src/main/java/at/hannibal2/skyhanni/test/graph/GraphNodeEditor.kt b/src/main/java/at/hannibal2/skyhanni/test/graph/GraphNodeEditor.kt new file mode 100644 index 000000000..e6c4536d8 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/test/graph/GraphNodeEditor.kt @@ -0,0 +1,232 @@ +package at.hannibal2.skyhanni.test.graph + +import at.hannibal2.skyhanni.data.model.GraphNodeTag +import at.hannibal2.skyhanni.data.model.TextInput +import at.hannibal2.skyhanni.events.GuiRenderEvent +import at.hannibal2.skyhanni.skyhannimodule.SkyHanniModule +import at.hannibal2.skyhanni.test.graph.GraphEditor.distanceSqToPlayer +import at.hannibal2.skyhanni.utils.CollectionUtils.addString +import at.hannibal2.skyhanni.utils.KeyboardManager +import at.hannibal2.skyhanni.utils.LorenzUtils +import at.hannibal2.skyhanni.utils.NumberUtil.addSeparators +import at.hannibal2.skyhanni.utils.RenderUtils.renderRenderables +import at.hannibal2.skyhanni.utils.SimpleTimeMark +import at.hannibal2.skyhanni.utils.renderables.Renderable +import at.hannibal2.skyhanni.utils.renderables.ScrollValue +import at.hannibal2.skyhanni.utils.renderables.Searchable +import at.hannibal2.skyhanni.utils.renderables.buildSearchableScrollable +import at.hannibal2.skyhanni.utils.renderables.toSearchable +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import kotlin.math.sqrt +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds + +@SkyHanniModule +object GraphNodeEditor { + + private val scrollValueNodes = ScrollValue() + private val scrollValueTags = ScrollValue() + private val textInput = TextInput() + private var nodesDisplay = emptyList() + private var lastUpdate = SimpleTimeMark.farPast() + private val tagsToShow: MutableList = GraphNodeTag.entries.toMutableList() + + @SubscribeEvent + fun onGuiRender(event: GuiRenderEvent) { + if (!isEnabled()) return + + config.namedNodesList.renderRenderables( + getNodeNames(), + posLabel = "Graph Nodes List", + ) + } + + private fun getNodeNames(): List { + if (lastUpdate.passedSince() > 250.milliseconds) { + updateNodeNames() + } + return nodesDisplay + } + + private fun updateNodeNames() { + lastUpdate = SimpleTimeMark.now() + nodesDisplay = buildList { + val list = drawNodeNames() + val total = GraphEditor.nodes.count { it.name?.isNotBlank() ?: false } + val shown = list.size + add( + Renderable.clickAndHover( + "§eGraph Nodes: $shown/$total", + listOf("§eClick to toggle node tags!"), + onClick = { + updateToggleTags() + }, + ), + ) + val height = (shown * 10).coerceAtMost(250) + if (list.isNotEmpty()) { + add(list.buildSearchableScrollable(height, textInput, scrollValueNodes, velocity = 10.0)) + } + } + } + + private fun updateToggleTags() { + lastUpdate = SimpleTimeMark.now() + 60.seconds + nodesDisplay = buildList { + addString("§eToggle Visible Tags") + for (tag in GraphNodeTag.entries) { + val isVisible = tag in tagsToShow + val nodes = GraphEditor.nodes.count { tag in it.tags } + val visibilityText = if (isVisible) " §aVisible" else " §7Invisible" + val name = " - ${tag.displayName} §8($nodes nodes) $visibilityText" + add( + Renderable.clickAndHover( + name, + listOf("§eClick to " + (if (isVisible) "hide" else "show") + " nodes with this tag!"), + onClick = { + toggleTag(tag) + updateToggleTags() + }, + ), + ) + } + addString("") + add( + Renderable.clickAndHover( + "§cGo Back!", + tips = listOf("§eClick to go back to the node list!"), + onClick = { + updateNodeNames() + }, + ), + ) + } + + } + + private fun toggleTag(tag: GraphNodeTag) { + if (tag in tagsToShow) { + tagsToShow.remove(tag) + } else { + tagsToShow.add(tag) + } + } + + private fun updateTagView(node: GraphingNode) { + lastUpdate = SimpleTimeMark.now() + 60.seconds + nodesDisplay = buildList { + val list = drawTagNames(node) + val size = list.size + addString("§eGraph Nodes: $size") + val height = (size * 10).coerceAtMost(250) + if (list.isNotEmpty()) { + add(Renderable.scrollList(list, height, scrollValueTags, velocity = 10.0)) + } + } + } + + private fun drawTagNames(node: GraphingNode): List = buildList { + addString("§eChange tag for node '${node.name}§e'") + addString("") + + for (tag in GraphNodeTag.entries.filter { it in node.tags || checkIsland(it) }) { + val state = if (tag in node.tags) "§aYES" else "§cNO" + val name = state + " §r" + tag.displayName + add(createTagName(name, tag, node)) + } + addString("") + add( + Renderable.clickAndHover( + "§cGo Back!", + tips = listOf("§eClick to go back to the node list!"), + onClick = { + updateNodeNames() + }, + ), + ) + } + + private fun checkIsland(tag: GraphNodeTag): Boolean = tag.onlyIsland?.let { + it == LorenzUtils.skyBlockIsland + } ?: true + + private fun createTagName( + name: String, + tag: GraphNodeTag, + node: GraphingNode, + ) = Renderable.clickAndHover( + name, + tips = listOf( + "Tag ${tag.name}", + "§7${tag.description}", + "", + "§eClick to set tag for ${node.name} to ${tag.name}!", + ), + onClick = { + if (tag in node.tags) { + node.tags.remove(tag) + } else { + node.tags.add(tag) + } + updateTagView(node) + }, + ) + + private fun drawNodeNames(): List = buildList { + for ((node, distance: Double) in GraphEditor.nodes.map { it to it.position.distanceSqToPlayer() }.sortedBy { it.second }) { + if (node.tags.isNotEmpty()) { + if (!node.tags.any { it in tagsToShow }) continue + } + val name = node.name?.takeIf { it.isNotBlank() } ?: continue + val color = if (node == GraphEditor.activeNode) "§a" else "§7" + val distanceFormat = sqrt(distance).toInt().addSeparators() + val tagText = node.tags.let { tags -> + if (tags.isEmpty()) { + " §cNo tag§r" + } else { + val text = node.tags.joinToString(", ") { it.internalName } + " §f($text)" + } + } + + val text = "${color}Node §r$name$tagText §7[$distanceFormat]" + add(createNodeTextLine(text, name, node)) + } + } + + private fun createNodeTextLine( + text: String, + name: String, + node: GraphingNode, + ): Searchable = Renderable.clickAndHover( + text, + tips = buildList { + add("Node '$name'") + add("") + + if (node.tags.isNotEmpty()) { + add("Tags: ") + for (tag in node.tags) { + add(" §8- §r${tag.displayName}") + } + add("") + } + + add("§eClick to select/deselect this node!") + add("§eControl-Click to edit the tags for this node!") + + }, + onClick = { + if (KeyboardManager.isModifierKeyDown()) { + updateTagView(node) + } else { + GraphEditor.activeNode = node + updateNodeNames() + } + }, + ).toSearchable(name) + + fun isEnabled() = GraphEditor.isEnabled() + private val config get() = GraphEditor.config + +} diff --git a/src/main/java/at/hannibal2/skyhanni/utils/GraphUtils.kt b/src/main/java/at/hannibal2/skyhanni/utils/GraphUtils.kt new file mode 100644 index 000000000..4a245a894 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/utils/GraphUtils.kt @@ -0,0 +1,68 @@ +package at.hannibal2.skyhanni.utils + +import at.hannibal2.skyhanni.data.model.Graph +import at.hannibal2.skyhanni.data.model.GraphNode +import at.hannibal2.skyhanni.data.model.findAllShortestDistances +import at.hannibal2.skyhanni.data.model.findDijkstraDistances +import at.hannibal2.skyhanni.data.model.findPathToDestination +import java.util.Stack + +object GraphUtils { + /** + * Find the fastest path from [closestNode] to *any* node that matches [condition]. + */ + fun findFastestPath( + graph: Graph, + closestNode: GraphNode, + condition: (GraphNode) -> Boolean, + ): Pair? { + val distances = graph.findDijkstraDistances(closestNode, condition) + val entry = distances.lastVisitedNode.takeIf(condition) + return entry?.let { + distances.findPathToDestination(it) + } + } + + /** + * Find the fastest path from [closestNode] to *all* nodes that matches [condition]. + */ + fun findFastestPaths( + graph: Graph, + closestNode: GraphNode, + condition: (GraphNode) -> Boolean = { true }, + ): Pair, MutableMap> { + val paths = mutableMapOf() + + val map = mutableMapOf() + val distances = graph.findAllShortestDistances(closestNode) + for (graphNode in graph.nodes) { + if (!condition(graphNode)) continue + val (path, distance) = distances.findPathToDestination(graphNode) + paths[graphNode] = path + map[graphNode] = distance + } + return Pair(paths, map) + } + + /** + * Find all maximal sub graphs of the given graph which are not connected + */ + fun findDisjointClusters(graph: Graph): List> { + val universe = graph.toMutableSet() + val allClusters = mutableListOf>() + while (universe.isNotEmpty()) { + val cluster = mutableSetOf() + allClusters.add(cluster) + val queue = Stack() + queue.add(universe.first()) + while (queue.isNotEmpty()) { + val next = queue.pop() + universe.remove(next) + cluster.add(next) + queue.addAll(next.neighbours.keys) + queue.retainAll(universe) + } + } + return allClusters + } +} diff --git a/src/main/java/at/hannibal2/skyhanni/utils/LorenzVec.kt b/src/main/java/at/hannibal2/skyhanni/utils/LorenzVec.kt index 51bb8b72c..a573ed78d 100644 --- a/src/main/java/at/hannibal2/skyhanni/utils/LorenzVec.kt +++ b/src/main/java/at/hannibal2/skyhanni/utils/LorenzVec.kt @@ -9,6 +9,7 @@ import net.minecraft.util.BlockPos import net.minecraft.util.Rotations import net.minecraft.util.Vec3 import kotlin.math.abs +import kotlin.math.absoluteValue import kotlin.math.acos import kotlin.math.cos import kotlin.math.max @@ -126,6 +127,8 @@ data class LorenzVec( fun lengthSquared(): Double = x * x + y * y + z * z fun length(): Double = sqrt(this.lengthSquared()) + fun isNormalized(tolerance: Double = 0.01) = (lengthSquared() - 1.0).absoluteValue < tolerance + fun isZero(): Boolean = x == 0.0 && y == 0.0 && z == 0.0 fun clone(): LorenzVec = LorenzVec(x, y, z) diff --git a/src/main/java/at/hannibal2/skyhanni/utils/RaycastUtils.kt b/src/main/java/at/hannibal2/skyhanni/utils/RaycastUtils.kt new file mode 100644 index 000000000..d5aafdcd3 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/utils/RaycastUtils.kt @@ -0,0 +1,73 @@ +package at.hannibal2.skyhanni.utils + +import net.minecraft.client.Minecraft + +object RaycastUtils { + + data class Ray( + val origin: LorenzVec, + val direction: LorenzVec, + ) { + init { + require(direction.isNormalized()) + } + } + + data class Plane( + val origin: LorenzVec, + val normal: LorenzVec, + ) { + init { + require(normal.isNormalized()) + } + } + + fun createPlayerLookDirectionRay(): Ray { + return Ray( + LocationUtils.playerEyeLocation(), + Minecraft.getMinecraft().thePlayer.lookVec.toLorenzVec() + ) + } + + /** + * Create a plane that contains [point] and is orthogonal to [ray]. + */ + fun createOrthogonalPlaneToRayAtPoint( + ray: Ray, + point: LorenzVec, + ): Plane { + return Plane(point, ray.direction) + } + + /** + * Intersect a plane (of any orientation) with a ray. The ray and plane may not be parallel to each other. + */ + fun intersectPlaneWithRay(plane: Plane, ray: Ray): LorenzVec { +// require(plane.normal.dotProduct(ray.direction).absoluteValue != 0.0) + val intersectionPointDistanceAlongRay = + (plane.normal.dotProduct(plane.origin) - plane.normal.dotProduct(ray.origin)) / plane.normal.dotProduct(ray.direction) + return ray.origin + ray.direction.scale(intersectionPointDistanceAlongRay) + } + + /** + * Finds the distance between the given ray and the point. If the point is behind the ray origin (according to the ray's direction), + * returns [Double.MAX_VALUE] instead. + */ + fun findDistanceToRay(ray: Ray, point: LorenzVec): Double { + val plane = createOrthogonalPlaneToRayAtPoint(ray, point) + val intersectionPoint = intersectPlaneWithRay(plane, ray) + if ((intersectionPoint - ray.origin).dotProduct(ray.direction) < 0) return Double.MAX_VALUE + return intersectionPoint.distance(point) + } + + inline fun createDistanceToRayEstimator(ray: Ray, crossinline position: (T) -> LorenzVec): (T) -> Double { + return { + findDistanceToRay(ray, position(it)) + } + } + + fun List.findClosestPointToRay(ray: Ray, positionExtractor: (T) -> LorenzVec): T? { + return minByOrNull(createDistanceToRayEstimator(ray, positionExtractor)) + } + +} -- cgit