Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package dev.slne.surf.surfapi.core.server.hook

import com.github.benmanes.caffeine.cache.Caffeine
import com.sksamuel.aedile.core.asLoadingCache
import dev.slne.surf.surfapi.core.api.util.mutableObject2IntMapOf
import dev.slne.surf.surfapi.core.api.util.mutableObject2ObjectMapOf
import dev.slne.surf.surfapi.core.api.util.mutableObjectListOf
import dev.slne.surf.surfapi.core.api.util.mutableObjectSetOf
import dev.slne.surf.surfapi.core.api.util.requiredService
import dev.slne.surf.surfapi.shared.api.hook.Hook
Expand All @@ -13,6 +15,7 @@ import dev.slne.surf.surfapi.shared.internal.hook.PluginHookMeta
import kotlinx.serialization.SerializationException
import net.kyori.adventure.text.logger.slf4j.ComponentLogger
import java.io.InputStream
import java.util.PriorityQueue

abstract class HookService {

Expand Down Expand Up @@ -179,54 +182,89 @@ abstract class HookService {
return emptyList()
}

val sorted = mutableListOf<Hook>()
val visited = mutableSetOf<String>()
val visiting = mutableSetOf<String>()

fun visit(className: String) {
if (className in visited) return
// Build dependency graph: className -> list of dependents (successors)
// Note: In Kahn's algorithm, edges go from dependency to dependent
val graph = mutableObject2ObjectMapOf<String, MutableList<String>>()
for ((meta, _) in validHooks) {
// Ensure all nodes exist in the graph
if (meta.className !in graph) {
graph[meta.className] = mutableListOf()
}
// Add edges from dependencies to this hook
for (depClassName in meta.hookDependencies) {
if (depClassName in hooksByClassName) {
graph.computeIfAbsent(depClassName) { mutableListOf() }.add(meta.className)
}
}
Comment on lines +194 to +198
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition if (depClassName in hooksByClassName) checks against all hooks, but should check against validHooks only. If a dependency exists in hooksByClassName but was filtered out of validHooks (due to missing dependencies), it will create a graph node that doesn't correspond to a valid hook. This causes the algorithm to fail because the graph will have nodes that don't represent actual processable hooks. The check should ensure the dependency is in the validHooks set.

Copilot uses AI. Check for mistakes.
}

if (className in visiting) {
val chain = buildCircularDependencyChain(className, metaByClassName, visiting)
throw IllegalStateException(
"Circular hook dependency detected: ${chain.joinToString(" -> ")}"
)
// Kahn's algorithm with priority queue for tie-breaking
val incomingEdges = mutableObject2IntMapOf<String>()
for ((vertex, successors) in graph) {
if (vertex !in incomingEdges) {
incomingEdges[vertex] = 0
}
for (successor in successors) {
incomingEdges.mergeInt(successor, 1, Int::plus)
}
}

visiting.add(className)
// Use a priority queue ordered by hook priority (lower priority value = higher priority)
val queue = PriorityQueue<String>(compareBy { className ->
hooksByClassName[className]?.priority ?: Short.MAX_VALUE
})

incomingEdges.object2IntEntrySet().fastForEach { entry ->
val vertex = entry.key
val edges = entry.intValue
if (edges == 0) queue += vertex
}

val dependencies = metaByClassName[className]?.hookDependencies ?: emptyList()
for (depClassName in dependencies) {
if (depClassName in hooksByClassName) {
visit(depClassName)
val result = mutableObjectListOf<Hook>()

while (queue.isNotEmpty()) {
val vertex = queue.poll()
hooksByClassName[vertex]?.let { result += it }

for (successor in graph[vertex].orEmpty()) {
incomingEdges.mergeInt(successor, -1, Int::minus)
if (incomingEdges.getInt(successor) == 0) {
queue += successor
}
}
}

visiting.remove(className)
visited.add(className)

hooksByClassName[className]?.let { sorted.add(it) }
if (result.size != incomingEdges.size) {
val chain = findCyclicDependency(graph, incomingEdges)
throw IllegalStateException(
"Circular hook dependency detected: ${chain.joinToString(" -> ")}"
)
}

validHooks.forEach { (meta, _) -> visit(meta.className) }
return sorted.sortedBy { it.priority }
return result
}

private fun buildCircularDependencyChain(
startClassName: String,
metaByClassName: Map<String, PluginHookMeta.Hook>,
visiting: Set<String>
private fun findCyclicDependency(
graph: Map<String, List<String>>,
incomingEdges: Map<String, Int>
): List<String> {
// Find a node that still has incoming edges (part of cycle)
val cycleNode = incomingEdges.entries.firstOrNull { it.value > 0 }?.key
?: return emptyList()

// Trace back through dependencies to find the cycle
val visited = mutableSetOf<String>()
val chain = mutableListOf<String>()
var current = startClassName
var current = cycleNode

while (current !in chain) {
while (current !in visited) {
visited.add(current)
chain.add(current)
val deps = metaByClassName[current]?.hookDependencies ?: break
current = deps.firstOrNull { it in visiting } ?: break
// Find a successor that still has incoming edges (part of cycle)
current = graph[current]?.firstOrNull { incomingEdges[it] ?: 0 > 0 } ?: break
}
Comment on lines +255 to 265
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cycle detection logic is incorrect. The graph represents edges from dependencies to dependents (A -> B means B depends on A). To trace a cycle, we need to follow backwards through the dependency chain (from dependent to dependency), but this code follows successors (dependents) instead. This means the function won't correctly identify the actual cycle path. The algorithm needs to either build a reverse graph or use a different approach (like DFS) to correctly trace dependencies and find the cycle.

Copilot uses AI. Check for mistakes.

chain.add(startClassName)
chain.add(current)
return chain
}

Expand Down