AstralRecipes Overview
AstralRecipes registers custom crafting, cooking, and stonecutting recipes from plain YAML files. It is a standalone Paper plugin split out of AstralEssentials' older RecipeService, and it now runs its own matching engine rather than handing recipes to Bukkit: ingredients are identified by item id (so items from CraftEngine and other ItemStackSupplier providers can be crafted), and a mirrored backing vanilla recipe is registered alongside each one purely so the recipe book, crafters, and furnaces light up.
It depends only on AstralCore (depend: [AstralCore] in plugin.yml, load: STARTUP, api-version: 26.2) and has no commands, placeholders, events, or messages of its own.
Recipe files — the YAML shape of every recipe type.
Crafting behavior — how matching and consumption actually work at each station.
Loading lifecycle
AstralRecipes#onEnable:
Registers the seven recipe readers (see Recipe types) in the static
AstralRecipes.RECIPE_SERIALIZERS, aNamedRegistry<RecipeSerializer<? extends Recipe>>. The three crafting/stonecutter types are registered explicitly; the four cooking types are registered by looping overCookingType.values().Registers two Configurate type serializers on the plugin's configuration manager —
RecipeTypeSerializerforRecipe(dispatches on the file'stypekey) andIngredientTypeSerializerforIngredient.Constructs
RecipeServiceand registersCraftingListener.Schedules
loadConfiguration()10 ticks later (Bukkit.getScheduler().runTaskLater(this, this::loadConfiguration, 10L)) rather than loading synchronously, so item providers such as CraftEngine have finished registering their items before ingredient ids are resolved.
loadConfiguration() calls RecipeService#load(), which:
Unloads the previous pass — see Reload.
Reads
plugins/AstralRecipes/recipes/throughconfigurationManager().loadFolder("recipes", Recipe.class). The folder is created if missing, walked recursively, and every.yml/.yamlfile is deserialized into aRecipe. A file that fails to parse is logged asFailed to load configuration file: <path>and skipped — the rest still load.Registers backing recipes — each recipe is mirrored as a Bukkit recipe and added with
Bukkit.addRecipe(recipe, false), plus (for crafter-compatible recipes that use a custom item) a hidden material-based crafter trigger. See Backing recipes.Unlocks every backing recipe in the recipe book of everyone already online (the join handler covers players who connect later).
Logs a summary line that is the quickest health check after an edit:
Loaded <n> recipe(s) (<n> in recipe book, <n> crafter-compatible, <n> custom-item crafter trigger(s)).
There is no first-run seeding. The plugin jar carries three annotated example files (ruby_sword.yml, ruby_block.yml, smelt_ruby.yml) as reference, but nothing copies them to disk — recipes/ is created empty and every recipe is author-provided.
Reload
AstralRecipes has no reload command of its own. Reloading goes through AstralCore's generic /core reload AstralRecipes (permission core.commands), which calls loadConfiguration() on any AstralPaperPlugin. A recipe file added, edited, or removed on disk takes effect immediately, with no restart.
RecipeService#unload() — run at the start of every load and again on onDisable — removes every backing key and every crafter-trigger key it is tracking with Bukkit.removeRecipe(key, false) and clears both maps. The false matters: each add/removeRecipe otherwise re-finalizes the server's recipe manager and resends advancement data to every online player, which froze the main thread for tens of seconds on a large recipe set. Registration and removal are batched instead, with a single Bukkit.updateRecipes() afterwards.
Recipe types
| Recipe class | Station | Backing vanilla recipe |
|---|---|---|---|
|
| Crafting table, 2×2 grid, crafter |
|
|
| Crafting table, 2×2 grid, crafter |
|
|
| Stonecutter |
|
|
| Furnace |
|
|
| Blast furnace |
|
|
| Smoker |
|
|
| Campfire and soul campfire |
|
The type key is matched case-sensitively (NamedRegistry#findByName is a plain map lookup). A missing type throws Missing recipe 'type'; an unrecognized one throws Unknown recipe type: <value>. Either way loadFolder catches it per file, logs it, and skips just that file.
See Recipe files for the field reference and YAML shape of every type.
Developer access
RecipeService is reachable off the plugin instance via the Lombok fluent accessor plugin.recipes() (this project's lombok.config sets lombok.accessors.fluent = true, so there is no getRecipes()). It is not registered with AstralPaperAPI's service registry, so a consumer must resolve the plugin instance directly:
Method | Returns | Use |
|---|---|---|
|
| Re-read |
|
| Snapshot of every loaded recipe. |
|
| First recipe matching a |
|
| Same, restricted to one cooking station. |
|
| Resolve the custom recipe behind a registered Bukkit key — covers both backing recipes and crafter triggers. |
|
| Whether a crafter may run it (false when any ingredient sets |
|
| Unlock every backing recipe in a player's recipe book. |
Recipes themselves implement Recipe (key(), matches(RecipeInput), takeInput(RecipeInput[, reserve]), assemble(RecipeInput), ingredients()). Custom recipe types can be added at runtime by registering a RecipeSerializer under a new name in AstralRecipes.RECIPE_SERIALIZERS before the 10-tick load fires.
Where to go next
Recipe files — the YAML shape of every recipe type.
Crafting behavior — matching, consumption, and per-station limitations.