Skip to content

About result items

In CustomCrafterAPI recipes, it is not strictly required to specify items to return for a given input. In other words, it is possible to provide a recipe that returns nothing.

However, since all vanilla recipes return an item, and in most situations you will want to create recipes that return items, this page describes ResultSupplier, which is used for exactly that purpose.

ResultSupplier is a functional interface with a single function, used as the mechanism by which recipes return items. Unless item creation is handled directly in your own plugin, ResultSupplier is executed asynchronously on a virtual thread.

Its definition in Kotlin is as follows:

fun interface ResultSupplier {
fun supply(ctx: Context): List<ItemStack>
}

Only supply is defined: it receives a context and returns a list of ItemStack items. The context holds the player’s item arrangement at the time supply is called, the click type, the number of invocations, and — during async calls — the interrupt state, among other values.

As noted above, the context provides a variety of state:

FieldTypeDescription
recipeCRecipeThe recipe that contains this ResultSupplier
relationMappedRelationThe mapping between the recipe’s arrangement and the actual item arrangement
mappedMap<CoordinateComponent, ItemStack>The actual item arrangement
shiftClickedBooleanWhether bulk crafting mode is active (i.e., whether the Shift key was held)
calledTimesIntThe number of crafts (if in bulk crafting mode, the maximum craftable quantity)
crafterIDUUIDThe UUID of the player who performed the craft
callModeCallModeWhether this call is a real craft (CRAFT) or an icon-generation call for display purposes (ICON, e.g. in AllCandidateUI)
asyncContextAsyncContext?Context for async execution; null during synchronous execution

You can use these values inside ResultSupplier to decide what items to produce.

calledTimes is calculated as the minimum value of “input count ÷ CMatter’s amount” across all items whose corresponding CMatter has anyAmount = false.

For example, suppose a recipe requires “2 Stones + 1 Gold Ingot” and the player inputs “64 Stones + 32 Gold Ingots” using Shift bulk crafting:

  • Stone: 64 ÷ 2 = 32
  • Gold Ingot: 32 ÷ 1 = 32
  • calledTimes = min(32, 32) = 32

Using ResultSupplier.timesSingle multiplies the result item count by calledTimes.

callMode is a CallMode enum value that indicates the purpose of the supply() invocation:

ValueDescription
CRAFTThe player performed an actual craft. Items should be delivered normally.
ICONThe result is used only as a display icon (e.g. in AllCandidateUI). Heavy computation may be skipped; returned items are not given to the player.

It is recommended to skip side effects (such as database writes) when callMode is ICON.

val result = ResultSupplier { ctx ->
if (ctx.callMode == ResultSupplier.Context.CallMode.ICON) {
// Do not trigger side effects for display-only calls
return@ResultSupplier listOf(ItemStack.of(Material.DIAMOND))
}
// Update the database only during actual crafting
MyDatabase.recordCraft(ctx.crafterID)
listOf(ItemStack.of(Material.DIAMOND))
}

When asyncContext is non-null, supply is running on a virtual thread. On an asynchronous thread, access to BukkitAPI worlds and entities is not permitted, so operations such as giving items to a player or retrieving coordinates are not possible.

Furthermore, because virtual threads work cooperatively, they cannot be forcibly interrupted. Periodically check asyncContext.isInterrupted() and abort processing if an interrupt request has been received.

val asyncAwareResult = ResultSupplier { ctx ->
val asyncCtx = ctx.asyncContext
if (asyncCtx != null) {
// Check for interrupts during async execution
if (asyncCtx.isInterrupted()) {
return@ResultSupplier emptyList()
}
// BukkitAPI world access is not permitted in async threads
// Use ctx.crafterID to fetch data instead
val data = MyDatabase.fetchData(ctx.crafterID)
return@ResultSupplier listOf(ItemStack.of(data.material))
}
// Normal processing during synchronous execution
listOf(ItemStack.of(Material.DIAMOND))
}

Because ResultSupplier has a very simple definition, any logic can be written inside it. However, for common cases such as simply returning a single item in response to the input, a significant amount of boilerplate may be needed to achieve the desired behaviour. To avoid this and make ResultSupplier easy to use, predefined objects are provided for these situations.

ResultSupplier.single(vararg items: ItemStack) creates a ResultSupplier that simply gives the player the specified items upon a crafting call from a recipe. Even when multiple sets of materials are placed and crafted in bulk crafting mode, the items given to the player are exactly those specified in items — the count is not multiplied.

ResultSupplier.timesSingle(vararg items: ItemStack) behaves the same as single except that it adjusts the number of items provided. When crafted in bulk crafting mode, the count of each item in items is multiplied by the “number of crafts.” For example, if materials for 2 crafts are placed and the player crafts in bulk mode, the items specified in items are provided with their counts doubled. You can think of this object’s behaviour as essentially the same as vanilla item crafting.

// Example of when to use single vs timesSingle
val stone = CMatterImpl(
name = "stone",
candidate = setOf(Material.STONE),
amount = 2, // requires 2 stones
anyAmount = false,
predicates = null
)
// single: always returns exactly 1 Diamond regardless of how many are crafted
val singleResult = ResultSupplier.single(ItemStack.of(Material.DIAMOND))
// timesSingle: Shift bulk crafting with 64 Stones returns 32 Diamonds
val timesResult = ResultSupplier.timesSingle(ItemStack.of(Material.DIAMOND))
val recipe = CRecipeImpl(
name = "stone-to-diamond",
items = mapOf(CoordinateComponent(0, 0) to stone),
results = listOf(timesResult),
type = CRecipe.Type.SHAPED
)

An example that changes the returned item based on the player’s UUID.

val customResult = ResultSupplier { ctx ->
// Return a special item to a specific player
val specialPlayer = UUID.fromString("069a79f4-44e9-4726-a5be-fca90e38aaf5")
if (ctx.crafterID == specialPlayer) {
return@ResultSupplier listOf(ItemStack.of(Material.ENCHANTED_GOLDEN_APPLE))
}
listOf(ItemStack.of(Material.GOLDEN_APPLE))
}

ReplaceableResultSupplier is a subinterface of ResultSupplier that writes items back into crafting UI slots after a craft completes, instead of (or in addition to) giving them directly to the player.

Typical use cases:

  • Ingredient transformation — returning a modified or depleted tool to the grid after use
  • Byproduct placement — returning an empty bucket after a filled bucket is consumed

supply() is already implemented and always returns an empty list; item delivery is handled inside replaceResultHandler.

MethodDescription
replaceQueries(ctx)Returns a map of grid coordinates to items that should be written back. Called on an executor thread.
replaceResultHandler(results, usedQueries, usedContext)Called after all write-back attempts complete. Use this to inspect per-slot outcomes or deliver items to the player.

Each slot’s outcome is reported as a ReplaceState value:

ValueDescription
SUCCESSThe item was successfully placed into the slot
ITEM_ALREADY_PLACEDThe slot already contained an item; write-back was skipped
UI_CLOSEDThe CraftUI was closed before write-back could complete
PLAYER_OFFLINEThe player was offline when supply() was called
TIMEOUTThe entity scheduler did not execute within timeoutMilli() milliseconds
UNKNOWNThe operation timed out before replaceQueries() returned

Returns the maximum number of milliseconds to wait for the scheduler. Defaults to 1000 ms. Override to change this threshold.

class EmptyBucketReplacer : ReplaceableResultSupplier {
override fun replaceQueries(ctx: ReplaceableResultSupplier.Context): Map<CoordinateComponent, ItemStack> {
// Replace each input bucket slot with an empty bucket
return ctx.relation.components.associate { (_, inputCoord) ->
inputCoord to ItemStack.of(Material.BUCKET)
}
}
override fun replaceResultHandler(
results: Map<CoordinateComponent, ReplaceableResultSupplier.ReplaceState>,
usedQueries: Map<CoordinateComponent, ItemStack>?,
usedContext: ReplaceableResultSupplier.Context
) {
val failed = results.filter { (_, state) -> !state.isSuccess() }
if (failed.isNotEmpty()) {
// Fall back: give the player the items that could not be placed
val player = Bukkit.getPlayer(usedContext.crafterID) ?: return
failed.keys.forEach { coord ->
usedQueries?.get(coord)?.let { player.inventory.addItem(it) }
}
}
}
}