Skip to content

Search API

Search is the entry-point object for performing recipe searches in CustomCrafterAPI. It matches the input item arrangement against registered recipes and returns the matching recipes along with their coordinate-mapping information.


CraftView is the class that serves as the argument for recipe searches, representing the item arrangement in the crafting UI.

data class CraftView(
val materials: Map<CoordinateComponent, ItemStack> // arrangement of items placed by the player
)

The size of materials must be between 1 and 36 inclusive. Otherwise, Search.search / Search.asyncSearch will throw an IllegalArgumentException.

// Example: create a CraftView with stone placed at (0,0)
val view = CraftView(
materials = mapOf(CoordinateComponent(0, 0) to ItemStack.of(Material.STONE))
)

fun search(
crafterId: UUID,
view: CraftView,
searchQuery: SearchQuery = SearchQuery.DEFAULT,
sourceRecipes: List<CRecipe> = CustomCrafterAPI.getRecipes(),
explainer: Explainer? = null
): SearchResult

A synchronous search method that runs on the main thread. If there are many registered recipes or if predicates contain heavy processing, this may affect server TPS. In such cases, using asyncSearch (described below) is recommended.

Argument Description
crafterId The UUID of the player performing the craft
view The arrangement of input items
searchQuery Controls search behavior (search mode and vanilla search mode). Defaults to SearchQuery.DEFAULT
sourceRecipes The list of recipes to search (defaults to all registered recipes)
explainer A diagnostic instance that records how the search proceeded. Defaults to null, in which case nothing is recorded (available from 5.3.0 onwards)
val player: Player = /* ... */
val view = CraftView(
materials = mapOf(CoordinateComponent(0, 0) to ItemStack.of(Material.STONE)),
)
val result: Search.SearchResult = Search.search(player.uniqueId, view)

fun asyncSearch(
crafterId: UUID,
view: CraftView,
query: SearchQuery = SearchQuery.ASYNC_DEFAULT,
sourceRecipes: List<CRecipe> = CustomCrafterAPI.getRecipes(),
explainer: Explainer? = null
): CompletableFuture<SearchResult>

An asynchronous search method using virtual threads (available since 5.0.17). Each recipe search is executed in parallel on individual threads, making it especially effective when there are many recipes with heavy predicates that call databases or external APIs. This method is used internally by CustomCrafterAPI’s standard crafting screen and the all-candidates display feature.

val player: Player = /* ... */
val view = CraftView(
materials = mapOf(CoordinateComponent(0, 0) to ItemStack.of(Material.STONE)),
)
val future: CompletableFuture<Search.SearchResult> = Search.asyncSearch(player.uniqueId, view)
future.thenAccept { result ->
// Process results asynchronously
val customs = result.customs()
println("Matching custom recipe count: ${customs.size}")
}

SearchQuery is the class that controls the search behavior of asyncSearch.

class SearchQuery(
val searchMode: SearchMode,
val vanillaSearchMode: VanillaSearchMode,
val asyncContext: AsyncContext? = null
)
Value Description
ALL (default) Returns all matching custom recipes
ONLY_FIRST Returns only the first matching custom recipe. Cancels other search tasks as soon as one is found
Value Description
IF_CUSTOMS_NOT_FOUND (default) Searches vanilla only if no custom recipe is found
FORCE Always searches vanilla regardless of whether custom recipes were found
// Search in ONLY_FIRST mode
val query = Search.SearchQuery(
searchMode = Search.SearchQuery.SearchMode.ONLY_FIRST,
vanillaSearchMode = Search.SearchQuery.VanillaSearchMode.IF_CUSTOMS_NOT_FOUND,
asyncContext = AsyncContext.ofTurnOff()
)
val future = Search.asyncSearch(player.uniqueId, view, query)
Constant / Method Description
SearchQuery.DEFAULT Default query for search(): SearchMode.ALL, VanillaSearchMode.IF_CUSTOMS_NOT_FOUND, no async context
SearchQuery.ASYNC_DEFAULT Default query for asyncSearch(): same as DEFAULT but with an async context enabled
SearchQuery.defaultModeOf(asyncContext) Returns a query with the same search and vanilla modes as DEFAULT, but with the supplied asyncContext

SearchResult is the class that holds the search results.

SearchResult.EMPTY is a pre-built constant representing an empty result (no vanilla recipe, no custom recipes).

Method Return type Description
vanilla() Recipe? The vanilla recipe. null if not found or not searched
customs() List<Pair<CRecipe, MappedRelation>> The list of matching custom recipes and their coordinate mappings
size() Int The total number of matches across vanilla and custom recipes
getMergedResults() List<Pair<CRecipe, MappedRelation?>> A combined list of vanilla and custom results. The vanilla entry has null for MappedRelation
getMergedResults(view) List<Pair<CRecipe, MappedRelation>> A combined list of vanilla and custom results, with the vanilla recipe’s MappedRelation computed against view. All entries carry a concrete relation
val result: Search.SearchResult = Search.search(player.uniqueId, view)
// Get vanilla recipe
val vanilla: Recipe? = result.vanilla()
vanilla?.let { println("Vanilla recipe: ${it.result.type}") }
// Get custom recipes
val customs: List<Pair<CRecipe, MappedRelation>> = result.customs()
customs.forEach { (recipe, relation) ->
println("Custom recipe: ${recipe.name}")
}
// Get all results combined
result.getMergedResults().forEach { (recipe, relation) ->
println("Recipe: ${recipe.name}, has coordinate mapping: ${relation != null}")
}

VanillaSearch is an object for searching only vanilla recipes without going through the CustomCrafterAPI search flow.

// Example: search for a vanilla recipe to craft a furnace from cobblestone
val view = CraftView(
materials = CoordinateComponent.squareFill(3)
.filter { it.x < 3 && it.y < 3 }
.associate { it to ItemStack.of(Material.COBBLESTONE) },
)
val vanillaRecipe: Recipe? = VanillaSearch.search(Bukkit.getWorlds().first(), view)
vanillaRecipe?.let { println("Result item: ${it.result.type}") }

MappedRelation and MappedRelationComponent

Section titled “MappedRelation and MappedRelationComponent”

MappedRelation is a class that holds the correspondence between coordinates in the recipe and the actual input slot coordinates. MappedRelationComponent represents a single correspondence pair (recipe coordinate → input coordinate).

data class MappedRelation(
val components: Set<MappedRelationComponent>
)
data class MappedRelationComponent(
val recipe: CoordinateComponent, // coordinate in the recipe
val input: CoordinateComponent // coordinate of the actual input slot
)

For example, if a shaped recipe requires stone at (0,0), there are cases where the recipe still matches even if the player places the item at (2,2). In that case the MappedRelationComponent would be recipe = (0,0), input = (2,2). This information is passed as ResultSupplier.Context.relation and CRecipePredicate.Context.relation, and is used to track which item was placed in which slot.


PartialSearch provides asynchronous partial recipe match searches. A partial match occurs when the player’s current crafting grid satisfies some — but not necessarily all — of a recipe’s required slots. This is useful for crafting hints, recipe guides, and autocomplete suggestions.

Recipes implementing UnPartialSearchableRecipe are excluded from all partial searches.

fun asyncPartialSearch(
crafterId: UUID,
view: CraftView,
searchQuery: SearchQuery = SearchQuery.ASYNC_DEFAULT,
sourceRecipes: List<CRecipe> = CustomCrafterAPI.getRecipes(),
explainer: Explainer? = null
): CompletableFuture<List<PartialSearchResult>>

Each entry in the returned list implements PartialSearchResult:

Method Return type Description
recipe CRecipe The candidate recipe that was evaluated
matched() Set<CoordinateComponent> Recipe slot coordinates covered by at least one input item
notEnough() Set<CoordinateComponent> Recipe slot coordinates that have no matching input item
state() MatchState ALL if all slots are satisfied; PARTIAL_NOT_ENOUGH otherwise

PartialShapedResult is returned for shaped recipes and includes a relation: MappedRelation. PartialShapelessResult is returned for shapeless recipes and exposes weakRelations() — a matter-keyed map of compatible input slots.

Value Description
ALL Every required recipe slot is covered by a corresponding input item
PARTIAL_NOT_ENOUGH One or more required recipe slots have no matching input item
Method Return type Description
isPartialMatch() Boolean Returns true when this state is not ALL
val player: Player = /* ... */
val view = CraftView(
materials = mapOf(CoordinateComponent(0, 0) to ItemStack.of(Material.STONE)),
)
PartialSearch.asyncPartialSearch(player.uniqueId, view).thenAccept { results ->
results.forEach { result ->
println("Recipe: ${result.recipe.name}, state: ${result.state()}")
if (result.notEnough().isNotEmpty()) {
println(" Missing slots: ${result.notEnough()}")
}
}
}

When a recipe does not match as expected, pass an Explainer to record which check rejected it.

val explainer = Explainer(Explainer.Loglevel.DEBUG, "debug")
Search.search(player.uniqueId, view, explainer = explainer)
explainer.getStringList().forEach { println(it) }

See Advanced Debugging for Recipe Search for details.