Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/main/java/at/hannibal2/skyhanni/test/graph/GraphEditor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,22 @@ object GraphEditor {
category = CommandCategory.DEVELOPER_TEST
simpleCallback { GraphEditorIO.loadThisIsland() }
}
event.registerBrigadier("shgraphcopynetwork") {
description = "Copies the closest network to the clipboard."
category = CommandCategory.DEVELOPER_TEST
simpleCallback { GraphEditorNetworks.copyClosestNetwork() }
}
event.registerBrigadier("shgraphmerge") {
description = "Merges graph data from the clipboard into the current graph."
category = CommandCategory.DEVELOPER_TEST
simpleCallback {
if (!isEnabled()) {
ChatUtils.userError("Graph Editor is not active!")
return@simpleCallback
}
GraphEditorIO.mergeFromClipboard()
}
}
}

var bypassTempRemoveTimer = SimpleTimeMark.farPast()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,30 +33,11 @@ object GraphEditorBugFinder {
checkConflictingTags(graph, errorsInWorld)
checkConflictingAreas(graph, errorsInWorld)
checkMissingData(graph, errorsInWorld)
val clusters = checkDisjointClusters(graph, errorsInWorld)

this.errorsInWorld = errorsInWorld
if (clusters.size <= 1) {
errorsInWorld.keys.minByOrNull {
it.distanceSqToPlayer()
}?.pathFind("Graph Editor Bug", Color.RED, condition = { isEnabled() })
}
}

private fun checkDisjointClusters(graph: Graph, errorsInWorld: MutableMap<GraphNode, String>): List<Set<GraphNode>> {
val clusters = GraphUtils.findDisjointClusters(graph)
if (clusters.size <= 1) return clusters

val closestCluster = clusters.minBy { cluster -> cluster.minOf { it.distanceSqToPlayer() } }
val foreignClusters = clusters.filter { it !== closestCluster }
val closestForeignNodes = foreignClusters.map { network -> network.minBy { it.distanceSqToPlayer() } }
closestForeignNodes.forEach {
errorsInWorld[it] = "§cDisjoint node network"
}
val closestForeignNode = closestForeignNodes.minBy { it.distanceSqToPlayer() }
val closestNodeToForeignNode = closestCluster.minBy { it.position.distanceSq(closestForeignNode.position) }
closestNodeToForeignNode.pathFind("Graph Editor Bug", Color.RED, condition = { isEnabled() })
return clusters
errorsInWorld.keys.minByOrNull {
it.distanceSqToPlayer()
}?.pathFind("Graph Editor Bug", Color.RED, condition = { isEnabled() })
}

private fun checkMissingData(graph: Graph, errorsInWorld: MutableMap<GraphNode, String>) {
Expand Down
130 changes: 88 additions & 42 deletions src/main/java/at/hannibal2/skyhanni/test/graph/GraphEditorIO.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
import at.hannibal2.skyhanni.data.model.GraphNode
import at.hannibal2.skyhanni.data.model.GraphNodeTag
import at.hannibal2.skyhanni.skyhannimodule.SkyHanniModule
import at.hannibal2.skyhanni.test.command.ErrorManager
import at.hannibal2.skyhanni.utils.ChatUtils
import at.hannibal2.skyhanni.utils.NumberUtil.addSeparators
import at.hannibal2.skyhanni.utils.OSUtils
import at.hannibal2.skyhanni.utils.SimpleTimeMark.Companion.fromNow
import net.minecraft.client.Minecraft
import kotlin.time.Duration.Companion.seconds

@SkyHanniModule
Expand All @@ -22,9 +24,16 @@
private val nodes get() = state.nodes
private val edges get() = state.edges

fun compileGraph(): Graph {
val indexedTable = nodes.mapIndexed { index, node -> node.id to index }.toMap()
val compiledNodes = nodes.mapIndexed { index, node ->
fun compileGraph(nodeSubset: Set<GraphingNode>? = null): Graph {
val filteredNodes = nodeSubset ?: nodes
val filteredEdges = if (nodeSubset != null) {
edges.filter { it.node1 in nodeSubset && it.node2 in nodeSubset }
} else {
edges
}

val indexedTable = filteredNodes.mapIndexed { index, node -> node.id to index }.toMap()
val compiledNodes = filteredNodes.mapIndexed { index, node ->
GraphNode(
index,
node.position,
Expand All @@ -33,11 +42,11 @@
)
}

val edgesByNode = nodes.associateWith { node ->
edges.filter { it.isInEdge(node) && it.isValidDirectionFrom(node) }
val edgesByNode = filteredNodes.associateWith { node ->
filteredEdges.filter { it.isInEdge(node) && it.isValidDirectionFrom(node) }
}

val neighbours = nodes.map { node ->
val neighbours = filteredNodes.map { node ->
val nodeEdges = edgesByNode[node].orEmpty()

nodeEdges.map { edge ->
Expand All @@ -55,41 +64,9 @@

fun createStateFrom(graph: Graph): GraphEditorState {
val newState = GraphEditorState()

newState.nodes.addAll(
graph.map {
GraphingNode(
it.id,
it.position,
it.name,
it.tagNames.mapNotNull { tag -> GraphNodeTag.byId(tag) }.toMutableList(),
)
},
)
val translation = graph.zip(newState.nodes).toMap()

val neighbors = graph.flatMap { node ->
node.neighbours.mapNotNull { (neighbor, _) ->
val node1 = translation[node]
val node2 = translation[neighbor]
if (node1 == null || node2 == null) {
error("Invalid edge reference: node ${node.id} <-> neighbor ${neighbor.id}")
}
GraphingEdge(node1, node2, EdgeDirection.ONE_TO_TWO)
}
}

val reduced = neighbors.groupingBy { it }.reduce { _, accumulator, element ->
if (
(element.node1 == accumulator.node1 && accumulator.direction != element.direction) ||
(element.node1 == accumulator.node2 && accumulator.direction == element.direction)
) {
accumulator.direction = EdgeDirection.BOTH
}
accumulator
}

newState.edges.addAll(reduced.values)
val (nodes, edges) = convertToGraphingData(graph) { it.id }
newState.nodes.addAll(nodes)
newState.edges.addAll(edges)
newState.id = newState.nodes.lastOrNull()?.id?.plus(1) ?: 0
return newState
}
Expand All @@ -110,12 +87,14 @@
val json = compileGraph.toJson()
OSUtils.copyToClipboard(json)
ChatUtils.chat("Copied Graph to Clipboard.")
val networkCount = GraphEditorNetworks.recalculate()
if (config.showsStats) {
val length = edges.sumOf { it.node1.position.distance(it.node2.position) }.toInt().addSeparators()
val networkLine = if (networkCount > 1) "\n§eNetworks: ${networkCount.addSeparators()}" else ""
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",
}\n" + "§eNodes: ${nodes.size.addSeparators()}\n" + "§eEdges: ${edges.size.addSeparators()}\n" + "§eLength: $length" + networkLine,

Check warning on line 97 in src/main/java/at/hannibal2/skyhanni/test/graph/GraphEditorIO.kt

View workflow job for this annotation

GitHub Actions / Run detekt

detekt.style.MaxLineLength

Line detected, which is longer than the defined maximum line length in the code style.
)
}
}
Expand All @@ -141,7 +120,74 @@
GraphEditor.enable()
GraphEditorHistory.save("load island ${IslandGraphs.lastLoadedIslandType}")
GraphEditor.state = createStateFrom(graph)
GraphEditorNetworks.recalculate()
ChatUtils.chat("Graph Editor loaded this island!")
}

fun mergeFromClipboard() {
val json = OSUtils.readFromClipboard()
if (json == null) {
ChatUtils.userError("Clipboard is empty!")
return
}

SkyHanniMod.launchIOCoroutine("merge graph json") {
try {
val graph = Graph.fromJson(json)

Minecraft.getInstance().execute {
GraphEditorHistory.save("merge from clipboard")

var nextId = state.id
val (newNodes, newEdges) = convertToGraphingData(graph) { nextId++ }

Check warning on line 142 in src/main/java/at/hannibal2/skyhanni/test/graph/GraphEditorIO.kt

View workflow job for this annotation

GitHub Actions / Run detekt

detekt.formatting.NoMultipleSpaces

Unnecessary long whitespace
nodes.addAll(newNodes)
edges.addAll(newEdges)
state.id = nextId

GraphEditorNetworks.recalculate()
GraphEditor.updateCache()

val nodeCount = newNodes.size.addSeparators()
val edgeCount = newEdges.size.addSeparators()
ChatUtils.chat("Merged $nodeCount nodes and $edgeCount edges from clipboard.")
}
} catch (e: Exception) {
ErrorManager.logErrorWithData(e, "Merge failed", "json" to json, ignoreErrorCache = true)
}
}
}

private fun convertToGraphingData(graph: Graph, idProvider: (GraphNode) -> Int):

Check warning on line 160 in src/main/java/at/hannibal2/skyhanni/test/graph/GraphEditorIO.kt

View workflow job for this annotation

GitHub Actions / Run detekt

detekt.formatting.FunctionReturnTypeSpacing

Single space expected between colon and return type
Pair<List<GraphingNode>, List<GraphingEdge>> {
val importedNodes = graph.map { graphNode ->
GraphingNode(
idProvider(graphNode),
graphNode.position,
graphNode.name,
graphNode.tagNames.mapNotNull { tag -> GraphNodeTag.byId(tag) }.toMutableList(),
)
}
val translation = graph.zip(importedNodes).toMap()

val rawEdges = graph.flatMap { node ->
node.neighbours.mapNotNull { (neighbor, _) ->
val node1 = translation[node] ?: error("Invalid node in translation: ${node.id}")
val node2 = translation[neighbor] ?: error("Invalid neighbor in translation: ${neighbor.id}")
GraphingEdge(node1, node2, EdgeDirection.ONE_TO_TWO)
}
}

val reducedEdges = rawEdges.groupingBy { it }.reduce { _, accumulator, element ->
if (
(element.node1 == accumulator.node1 && accumulator.direction != element.direction) ||
(element.node1 == accumulator.node2 && accumulator.direction == element.direction)
) {
accumulator.direction = EdgeDirection.BOTH
}
accumulator
}

return importedNodes to reducedEdges.values.toList()
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ object GraphEditorInput {
Minecraft.getInstance().execute {
GraphEditorHistory.save("load from clipboard")
GraphEditor.state = newState
GraphEditorNetworks.recalculate()
ChatUtils.chat("Loaded Graph from clipboard.")
}
} catch (e: Exception) {
Expand Down
118 changes: 118 additions & 0 deletions src/main/java/at/hannibal2/skyhanni/test/graph/GraphEditorNetworks.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package at.hannibal2.skyhanni.test.graph

import at.hannibal2.skyhanni.utils.ChatUtils
import at.hannibal2.skyhanni.utils.LorenzColor
import at.hannibal2.skyhanni.utils.NumberUtil.addSeparators
import at.hannibal2.skyhanni.utils.OSUtils

object GraphEditorNetworks {

private val networkColors = listOf(
LorenzColor.GOLD,
LorenzColor.GREEN,
LorenzColor.AQUA,
LorenzColor.LIGHT_PURPLE,
LorenzColor.WHITE,
LorenzColor.DARK_GREEN,
)

fun recalculate(): Int {
val state = GraphEditor.state
val clusters = findClusters(state.nodes, state.edges)
val useNetworkColors = clusters.size > 1

if (!useNetworkColors) {
for (edge in state.edges) {
edge.networkColor = null
}
return clusters.size
}

val sortedClusters = clusters.sortedByDescending { it.size }
val nodeToColorIndex = mutableMapOf<GraphingNode, Int>()
for ((index, cluster) in sortedClusters.withIndex()) {
val colorIndex = index % networkColors.size
for (node in cluster) {
nodeToColorIndex[node] = colorIndex
}
}

for (edge in state.edges) {
val colorIndex = nodeToColorIndex[edge.node1] ?: 0
edge.networkColor = networkColors[colorIndex].addOpacity(150)
}

return clusters.size
}

fun copyClosestNetwork() {
val state = GraphEditor.state
val closestNode = state.closestNode
if (closestNode == null) {
ChatUtils.userError("No nearby node found!")
return
}

val adjacency = buildAdjacency(state.nodes, state.edges)
val cluster = bfs(closestNode, adjacency)

val clusterNodes = state.nodes.filter { it in cluster }.toSet()
val graph = GraphEditorIO.compileGraph(nodeSubset = clusterNodes)
val json = graph.toJson()
OSUtils.copyToClipboard(json)

val nodeCount = clusterNodes.size.addSeparators()
val edgeCount = state.edges.count { it.node1 in cluster && it.node2 in cluster }.addSeparators()
ChatUtils.chat("Copied network with $nodeCount nodes and $edgeCount edges to clipboard.")
}

private fun findClusters(
nodes: List<GraphingNode>,
edges: List<GraphingEdge>,
): List<Set<GraphingNode>> {
val adjacency = buildAdjacency(nodes, edges)
val visited = mutableSetOf<GraphingNode>()
val clusters = mutableListOf<Set<GraphingNode>>()

for (node in nodes) {
if (node in visited) continue
val cluster = bfs(node, adjacency)
visited.addAll(cluster)
clusters.add(cluster)
}

return clusters
}

private fun buildAdjacency(
nodes: List<GraphingNode>,
edges: List<GraphingEdge>,
): Map<GraphingNode, List<GraphingNode>> {
val adjacency = nodes.associateWith { mutableListOf<GraphingNode>() }
for (edge in edges) {
adjacency[edge.node1]?.add(edge.node2)
adjacency[edge.node2]?.add(edge.node1)
}
return adjacency
}

private fun bfs(
start: GraphingNode,
adjacency: Map<GraphingNode, List<GraphingNode>>,
): Set<GraphingNode> {
val visited = mutableSetOf(start)
val queue = ArrayDeque<GraphingNode>()
queue.add(start)

while (queue.isNotEmpty()) {
val current = queue.removeFirst()
for (neighbor in adjacency[current].orEmpty()) {
if (neighbor in visited) continue
visited.add(neighbor)
queue.add(neighbor)
}
}

return visited
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ object GraphEditorRenderer {
val color = when {
selectedEdge == edge -> edgeSelectedColor
edge in highlightedEdges -> edgeDijkstraColor
else -> edgeColor
else -> edge.networkColor ?: edgeColor
}

draw3DLine(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package at.hannibal2.skyhanni.test.graph

import java.awt.Color

class GraphingEdge(val node1: GraphingNode, val node2: GraphingNode, var direction: EdgeDirection = EdgeDirection.BOTH) {

var networkColor: Color? = null

fun isInEdge(node: GraphingNode?) = node1 == node || node2 == node

fun getOther(node: GraphingNode): GraphingNode = if (node == node1) node2 else node1
Expand Down
Loading