diff --git a/World Manager for Minecraft/Services/AppSupport/Scanning/WorldScanner.swift b/World Manager for Minecraft/Services/AppSupport/Scanning/WorldScanner.swift index 893e71f..cc1b487 100644 --- a/World Manager for Minecraft/Services/AppSupport/Scanning/WorldScanner.swift +++ b/World Manager for Minecraft/Services/AppSupport/Scanning/WorldScanner.swift @@ -684,6 +684,7 @@ enum JavaContentScanner { var candidatesByID: [String: SourceCandidate] = [:] for root in roots { let candidateFolders = boundedCandidateFolders(from: root, maxDepth: 4, maxFolderCount: 600, fileManager: fileManager) + var candidatesForRoot: [SourceCandidate] = [] for folderURL in candidateFolders { guard let probe = probeLocalFolder(folderURL, providerID: providerID) else { continue @@ -699,6 +700,14 @@ enum JavaContentScanner { detectedKinds: probe.detectedKinds ) + candidatesForRoot.append(candidate) + } + + for candidate in collapsedCandidates( + candidatesForRoot, + under: root, + providerID: providerID + ) { if let existingCandidate = candidatesByID[candidate.id], existingCandidate.confidence >= candidate.confidence { continue @@ -938,6 +947,40 @@ enum JavaContentScanner { return candidates } + nonisolated private static func collapsedCandidates( + _ candidates: [SourceCandidate], + under root: URL, + providerID: PlatformProviderID + ) -> [SourceCandidate] { + let uniqueCandidates = Dictionary(grouping: candidates, by: \.sourceRootURL).compactMap { _, groupedCandidates in + groupedCandidates.max { lhs, rhs in + lhs.confidence < rhs.confidence + } + } + + guard uniqueCandidates.count > 1 else { + return uniqueCandidates + } + + let detectedKinds = uniqueCandidates.reduce(into: Set()) { result, candidate in + result.formUnion(candidate.detectedKinds) + } + let confidence = uniqueCandidates.map(\.confidence).max() ?? .medium + let standardizedRoot = root.standardizedFileURL + + return [ + SourceCandidate( + providerID: providerID, + edition: .java, + sourceRootURL: standardizedRoot, + displayName: standardizedRoot.lastPathComponent, + confidence: confidence, + reason: "Found multiple Java sources under \(standardizedRoot.lastPathComponent)", + detectedKinds: detectedKinds + ) + ] + } + nonisolated private static func javaProbeScore(for url: URL, fileManager: FileManager) -> (value: Int, kinds: Set) { var score = 0 var kinds = Set() diff --git a/World Manager for MinecraftTests/World_Manager_for_MinecraftTests.swift b/World Manager for MinecraftTests/World_Manager_for_MinecraftTests.swift index 5223476..44bec8e 100644 --- a/World Manager for MinecraftTests/World_Manager_for_MinecraftTests.swift +++ b/World Manager for MinecraftTests/World_Manager_for_MinecraftTests.swift @@ -359,6 +359,32 @@ struct World_Manager_for_MinecraftTests { }) } + @Test func javaProviderCollapsesNestedSourceCandidatesToSearchRoot() async throws { + let fileManager = FileManager.default + let workingURL = fileManager.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) + let firstInstanceURL = workingURL.appendingPathComponent("a/b/c", isDirectory: true) + let secondInstanceURL = workingURL.appendingPathComponent("a/e/f", isDirectory: true) + defer { try? fileManager.removeItem(at: workingURL) } + + for instanceURL in [firstInstanceURL, secondInstanceURL] { + try fileManager.createDirectory( + at: instanceURL.appendingPathComponent("mods", isDirectory: true), + withIntermediateDirectories: true + ) + try Data("jar".utf8).write(to: instanceURL.appendingPathComponent("mods/ExampleMod.jar")) + } + + let candidates = JavaContentScanner.discoverSourceCandidates( + providerID: JavaLocalFolderSourceAccess().accessorIdentifier, + searchRoots: [workingURL] + ) + + #expect(candidates.count == 1) + #expect(candidates.first?.sourceRootURL == workingURL.standardizedFileURL) + #expect(candidates.first?.displayName == workingURL.lastPathComponent) + #expect(candidates.first?.detectedKinds.contains(.mod) == true) + } + @Test func sourceLibraryAddSourceResolvesJavaWrapperFolder() async throws { let fileManager = FileManager.default let rootURL = fileManager.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)