Skip to content

Commit 22555fa

Browse files
Introduce CartographySearchService_v2
This new search service conforms to the AlidadeSearchEngine protocol, allowing it to inherit all the new features from it, including improved query parsing. This should also help address ALD-22, as AlidadeSearchEngine adds support for filtering by dimension.
1 parent f793855 commit 22555fa

File tree

6 files changed

+329
-21
lines changed

6 files changed

+329
-21
lines changed

Alidade.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

MCMaps.xcodeproj/xcshareddata/xcschemes/Alidade.xcscheme

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
buildArchitectures = "Automatic">
99
<BuildActionEntries>
1010
<BuildActionEntry
11-
buildForTesting = "YES"
11+
buildForTesting = "NO"
1212
buildForRunning = "YES"
1313
buildForProfiling = "YES"
1414
buildForArchiving = "YES"
@@ -27,25 +27,9 @@
2727
buildConfiguration = "Release"
2828
selectedDebuggerIdentifier = ""
2929
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
30-
shouldUseLaunchSchemeArgsEnv = "YES">
31-
<TestPlans>
32-
<TestPlanReference
33-
reference = "container:AllTests.xctestplan"
34-
default = "YES">
35-
</TestPlanReference>
36-
</TestPlans>
30+
shouldUseLaunchSchemeArgsEnv = "YES"
31+
shouldAutocreateTestPlan = "YES">
3732
<Testables>
38-
<TestableReference
39-
skipped = "NO"
40-
parallelizable = "YES">
41-
<BuildableReference
42-
BuildableIdentifier = "primary"
43-
BlueprintIdentifier = "950402DF2D4D522200F51903"
44-
BuildableName = "MCMapsTests.xctest"
45-
BlueprintName = "MCMapsTests"
46-
ReferencedContainer = "container:MCMaps.xcodeproj">
47-
</BuildableReference>
48-
</TestableReference>
4933
<TestableReference
5034
skipped = "NO"
5135
parallelizable = "YES">
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//
2+
// MinecraftWorldDimension+AlidadeSearchQuery.swift
3+
// MCMaps
4+
//
5+
// Created by Marquis Kurt on 19-10-2025.
6+
//
7+
8+
import AlidadeSearchEngine
9+
import CubiomesKit
10+
11+
extension MinecraftWorld.Dimension {
12+
init?(query: AlidadeSearchQuery) {
13+
guard let dimension = query.dimension else { return nil }
14+
switch dimension.localizedLowercase {
15+
case "overworld":
16+
self = .overworld
17+
case "nether":
18+
self = .nether
19+
case "end":
20+
self = .end
21+
default:
22+
return nil
23+
}
24+
}
25+
}

MCMaps/Shared/Services/CartographySearchService.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Foundation
1010
import MCMap
1111

1212
/// A service that searches and filters content in Minecraft worlds and `.mcmap` files.
13+
@available(*, deprecated, message: "Use v2 of the search engine.")
1314
class CartographySearchService {
1415
/// The type of query used to initiate searches.
1516
typealias Query = String
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
//
2+
// CartographySearchService_v2.swift
3+
// MCMaps
4+
//
5+
// Created by Marquis Kurt on 19-10-2025.
6+
//
7+
8+
import AlidadeSearchEngine
9+
import CubiomesKit
10+
import MCMap
11+
import Foundation
12+
13+
class CartographySearchService_v2 {
14+
struct Context: Sendable {
15+
var world: MinecraftWorld
16+
var file: CartographyMapFile
17+
var position: MinecraftPoint = .zero
18+
var dimension: MinecraftWorld.Dimension = .overworld
19+
}
20+
21+
struct SearchResult: Sendable {
22+
var pins: [CartographyMapPin] = []
23+
var integratedData: [CartographyMapPin] = []
24+
var biomes: [CartographyMapPin] = []
25+
var structures: [CartographyMapPin] = []
26+
27+
var isEmpty: Bool {
28+
[pins, integratedData, biomes, structures].allSatisfy(\.isEmpty)
29+
}
30+
}
31+
32+
private enum Constants {
33+
static let defaultSearchRadius: Int32 = 20
34+
}
35+
36+
var searchRadius: Int32 = Constants.defaultSearchRadius
37+
38+
init() {
39+
self.searchRadius = Constants.defaultSearchRadius
40+
}
41+
}
42+
43+
extension CartographySearchService_v2: AlidadeSearchEngine {
44+
func search(query: AlidadeSearchQuery, in context: Context) async -> SearchResult {
45+
var results = SearchResult()
46+
var position = context.position
47+
var dimension = context.dimension
48+
var integratedMarkers = [CartographyMapPin]()
49+
50+
if let origin = query.origin {
51+
position = MinecraftPoint(cgPoint: origin)
52+
}
53+
54+
if let translatedDim = MinecraftWorld.Dimension(query: query) {
55+
dimension = translatedDim
56+
}
57+
58+
if context.file.supportedFeatures.contains(.integrations), context.file.integrations.bluemap.enabled {
59+
integratedMarkers.append(contentsOf: await getBluemapMarkers(in: context))
60+
}
61+
62+
let playerPins = filterPins(context.file.pins, by: query)
63+
let filteredMarkers = filterPins(integratedMarkers, by: query)
64+
results.pins = playerPins
65+
results.integratedData = filteredMarkers
66+
67+
if let structure = MinecraftStructure(string: query.request) {
68+
searchStructures(at: position, context, structure, dimension, &results)
69+
}
70+
71+
results.biomes = searchBiomes(
72+
query: query.request,
73+
mcVersion: context.file.manifest.worldSettings.version,
74+
world: context.world,
75+
pos: position,
76+
dimension: dimension
77+
)
78+
79+
return results
80+
}
81+
82+
private func getBluemapMarkers(in context: Context) async -> [CartographyMapPin] {
83+
let service = CartographyIntegrationService(
84+
serviceType: .bluemap,
85+
integrationSettings: context.file.integrations)
86+
do {
87+
let results: BluemapResults? = try await service.sync(dimension: context.dimension)
88+
guard let markers = results?.markers else {
89+
return []
90+
}
91+
let allMarkers = markers.map(\.value)
92+
return allMarkers.flatMap { group in
93+
group.markers.map { (id, marker) in
94+
CartographyMapPin(
95+
named: marker.label,
96+
at: CGPoint(x: marker.position.x, y: marker.position.z),
97+
color: .gray,
98+
alternateIDs: [id]
99+
)
100+
}
101+
}
102+
} catch {
103+
return []
104+
}
105+
}
106+
107+
private func filterPins(_ pins: [CartographyMapPin], by query: AlidadeSearchQuery) -> [CartographyMapPin] {
108+
var newPins = [CartographyMapPin]()
109+
for pin in pins {
110+
let nameMatches = pin.name.lowercased().contains(query.request.lowercased())
111+
if !query.tags.isEmpty, let tags = pin.tags {
112+
if tags.isDisjoint(with: query.tags) { continue }
113+
if !nameMatches, !query.request.isEmpty { continue }
114+
newPins.append(pin)
115+
continue
116+
}
117+
118+
if !nameMatches { continue }
119+
newPins.append(pin)
120+
}
121+
return newPins
122+
}
123+
124+
private func searchBiomes(
125+
query: String, mcVersion: String, world: MinecraftWorld, pos: Point3D<Int32>,
126+
dimension: MinecraftWorld.Dimension
127+
) -> [CartographyMapPin] {
128+
guard let biome = MinecraftBiome(localizedString: query, mcVersion: mcVersion) else {
129+
return []
130+
}
131+
let foundBiomes = world.findBiomes(
132+
ofType: biome,
133+
at: pos,
134+
inRadius: 8000,
135+
dimension: dimension
136+
)
137+
let name = biome.localizedString(for: world.version)
138+
var biomes = foundBiomes.map { foundBiome in
139+
CartographyMapPin(
140+
named: name,
141+
at: CGPoint(x: Double(foundBiome.x), y: Double(foundBiome.z)))
142+
}
143+
144+
let cgPointOrigin = CGPoint(x: Double(pos.x), y: Double(pos.z))
145+
biomes.sort { first, second in
146+
first.position
147+
.manhattanDistance(to: cgPointOrigin) < second.position.manhattanDistance(to: cgPointOrigin)
148+
}
149+
return biomes
150+
}
151+
152+
private func searchStructures(
153+
at position: MinecraftPoint,
154+
_ context: Context,
155+
_ structure: MinecraftStructure,
156+
_ dimension: MinecraftWorld.Dimension,
157+
_ results: inout SearchResult
158+
) {
159+
let foundStructures = context.world.findStructures(
160+
ofType: structure,
161+
at: position,
162+
inRadius: searchRadius,
163+
dimension: dimension
164+
)
165+
for foundStruct in foundStructures {
166+
results.structures
167+
.append(
168+
CartographyMapPin(
169+
named: structure.name,
170+
at: CGPoint(x: Double(foundStruct.x), y: Double(foundStruct.z)),
171+
color: structure.pinColor)
172+
)
173+
}
174+
175+
let cgPointOrigin = CGPoint(minecraftPoint: context.position)
176+
results.structures.sort { first, second in
177+
first.position
178+
.manhattanDistance(to: cgPointOrigin) < second.position.manhattanDistance(to: cgPointOrigin)
179+
}
180+
}
181+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
//
2+
// CartographySearchServiceV2Tests.swift
3+
// MCMaps
4+
//
5+
// Created by Marquis Kurt on 19-10-2025.
6+
//
7+
8+
import AlidadeSearchEngine
9+
import CubiomesKit
10+
import Foundation
11+
import MCMap
12+
import Testing
13+
14+
@testable import Alidade
15+
16+
struct CartographySearchServiceV2Tests {
17+
typealias SearchContext = CartographySearchService_v2.Context
18+
19+
@Test("Empty query")
20+
func searchReturnsNoResultsOnEmptyQuery() async throws {
21+
let world = try MinecraftWorld(version: "1.21.3", seed: 123)
22+
let file = CartographyMapFile(withManifest: .sampleFile)
23+
let service = CartographySearchService_v2()
24+
25+
let results = await service.search(query: "", in: SearchContext(world: world, file: file))
26+
#expect(results.isEmpty)
27+
}
28+
29+
@Test(arguments: ["Spawn", "spawn", "Spa", "awn"])
30+
func searchReturnsPinsByName(query: AlidadeSearchQuery) async throws {
31+
let world = try MinecraftWorld(version: "1.21.3", seed: 123)
32+
let file = CartographyMapFile(withManifest: .sampleFile)
33+
let service = CartographySearchService_v2()
34+
35+
let results = await service.search(query: query, in: SearchContext(world: world, file: file))
36+
#expect(results.pins.count == 1)
37+
#expect(results.pins.first?.name == "Spawn")
38+
#expect(results.pins.first?.position == .zero)
39+
}
40+
41+
@Test(arguments: ["#Tag test", "#Tag"])
42+
func searchReturnsPinsWithTag(query: AlidadeSearchQuery) async throws {
43+
let world = try MinecraftWorld(version: "1.21.3", seed: 123)
44+
var file = CartographyMapFile(withManifest: .sampleFile)
45+
let service = CartographySearchService_v2()
46+
47+
file.pins.append(contentsOf: [
48+
CartographyMapPin(named: "Testing", at: CGPoint(x: 12, y: 12), tags: ["Tag", "Forest"]),
49+
CartographyMapPin(named: "Testing Grounds", at: CGPoint(x: 10, y: 10), tags: ["Base"]),
50+
CartographyMapPin(named: "Test Test", at: CGPoint(x: 11, y: 11), tags: ["Tag"])
51+
])
52+
53+
let results = await service.search(query: query, in: SearchContext(world: world, file: file))
54+
#expect(results.pins.count == 2)
55+
#expect(results.pins.allSatisfy { $0.tags?.contains("Tag") != false })
56+
}
57+
58+
@Test func searchReturnsNearbyStructures() async throws {
59+
let world = try MinecraftWorld(version: "1.21.3", seed: 123)
60+
let file = CartographyMapFile(withManifest: .sampleFile)
61+
let service = CartographySearchService_v2()
62+
63+
let results = await service.search(
64+
query: "mineshaft",
65+
in: SearchContext(
66+
world: world,
67+
file: file,
68+
position: MinecraftPoint(x: 113, y: 15, z: 430)))
69+
#expect(results.structures.count == 11)
70+
#expect(results.structures.allSatisfy { $0.name == "Mineshaft" })
71+
}
72+
73+
@Test func searchReturnsNearbyBiomes() async throws {
74+
let world = try MinecraftWorld(version: "1.21.3", seed: 123)
75+
let file = CartographyMapFile(withManifest: .sampleFile)
76+
let service = CartographySearchService_v2()
77+
let results = await service.search(
78+
query: "Frozen River",
79+
in: SearchContext(world: world, file: file, position: .zero))
80+
#expect(!results.biomes.isEmpty)
81+
}
82+
83+
@Test func searchReturnsNearbyBiomesRelativeToFilteredOrigin() async throws {
84+
let world = try MinecraftWorld(version: "1.21.3", seed: 123)
85+
let file = CartographyMapFile(withManifest: .sampleFile)
86+
let service = CartographySearchService_v2()
87+
let context = SearchContext(world: world, file: file, position: .zero)
88+
89+
let originResults = await service.search(query: "Frozen River", in: context)
90+
91+
let results = await service.search(
92+
query: "Frozen River @{1000, 1000}",
93+
in: context)
94+
95+
#expect(originResults.biomes.map(\.position) != results.biomes.map(\.position))
96+
}
97+
98+
@Test func searchReturnsBiomeResultsRelativeToDimensionInQuery() async throws {
99+
let world = try MinecraftWorld(version: "1.21.3", seed: 123)
100+
let file = CartographyMapFile(withManifest: .sampleFile)
101+
let service = CartographySearchService_v2()
102+
let context = SearchContext(world: world, file: file, position: .zero, dimension: .nether)
103+
104+
let results = await service.search(query: "dimension: Overworld Frozen River", in: context)
105+
#expect(!results.isEmpty)
106+
}
107+
108+
@Test func searchReturnsStructureResultsRelativeToDimensionInQuery() async throws {
109+
let world = try MinecraftWorld(version: "1.21.3", seed: 123)
110+
let file = CartographyMapFile(withManifest: .sampleFile)
111+
let service = CartographySearchService_v2()
112+
let context = SearchContext(world: world, file: file, position: .zero, dimension: .overworld)
113+
114+
let results = await service.search(query: "dimension: Nether Bastion", in: context)
115+
#expect(!results.isEmpty)
116+
}
117+
}

0 commit comments

Comments
 (0)