@adeficior/data-modifier-recipes 2.0.0-rc.20260814115618

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/.turbo/turbo-build.log +1 -0
  2. package/.turbo/turbo-check.log +1 -0
  3. package/.turbo/turbo-lint.log +1 -0
  4. package/@types/modules.d.ts +15 -0
  5. package/@types/registry.d.ts +20 -0
  6. package/CHANGELOG.md +14 -0
  7. package/dist/emitter.d.ts +48 -0
  8. package/dist/emitter.js +116 -0
  9. package/dist/emitter.js.map +1 -0
  10. package/dist/hooks.d.ts +6 -0
  11. package/dist/hooks.js +4 -0
  12. package/dist/hooks.js.map +1 -0
  13. package/dist/index.d.ts +18 -0
  14. package/dist/index.js +14 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/loader.d.ts +27 -0
  17. package/dist/loader.js +141 -0
  18. package/dist/loader.js.map +1 -0
  19. package/dist/module.d.ts +15 -0
  20. package/dist/module.js +38 -0
  21. package/dist/module.js.map +1 -0
  22. package/dist/rule.d.ts +15 -0
  23. package/dist/rule.js +31 -0
  24. package/dist/rule.js.map +1 -0
  25. package/dist/schema.d.ts +7 -0
  26. package/dist/schema.js +14 -0
  27. package/dist/schema.js.map +1 -0
  28. package/dist/serializer/abstract.d.ts +17 -0
  29. package/dist/serializer/abstract.js +19 -0
  30. package/dist/serializer/abstract.js.map +1 -0
  31. package/dist/serializer/context.d.ts +12 -0
  32. package/dist/serializer/context.js +4 -0
  33. package/dist/serializer/context.js.map +1 -0
  34. package/dist/serializer/forge/conditional.d.ts +25 -0
  35. package/dist/serializer/forge/conditional.js +45 -0
  36. package/dist/serializer/forge/conditional.js.map +1 -0
  37. package/dist/serializer/holder.d.ts +25 -0
  38. package/dist/serializer/holder.js +52 -0
  39. package/dist/serializer/holder.js.map +1 -0
  40. package/dist/serializer/manyToMany.d.ts +21 -0
  41. package/dist/serializer/manyToMany.js +37 -0
  42. package/dist/serializer/manyToMany.js.map +1 -0
  43. package/dist/serializer/manyToOne.d.ts +21 -0
  44. package/dist/serializer/manyToOne.js +37 -0
  45. package/dist/serializer/manyToOne.js.map +1 -0
  46. package/dist/serializer/modifier.d.ts +6 -0
  47. package/dist/serializer/modifier.js +3 -0
  48. package/dist/serializer/modifier.js.map +1 -0
  49. package/dist/serializer/oneToOne.d.ts +21 -0
  50. package/dist/serializer/oneToOne.js +37 -0
  51. package/dist/serializer/oneToOne.js.map +1 -0
  52. package/dist/serializer/vanilla/shaped.d.ts +22 -0
  53. package/dist/serializer/vanilla/shaped.js +37 -0
  54. package/dist/serializer/vanilla/shaped.js.map +1 -0
  55. package/dist/serializer/vanilla/shapeless.d.ts +4 -0
  56. package/dist/serializer/vanilla/shapeless.js +4 -0
  57. package/dist/serializer/vanilla/shapeless.js.map +1 -0
  58. package/dist/serializer/vanilla/smelting.d.ts +22 -0
  59. package/dist/serializer/vanilla/smelting.js +37 -0
  60. package/dist/serializer/vanilla/smelting.js.map +1 -0
  61. package/dist/serializer/vanilla/smithing.d.ts +25 -0
  62. package/dist/serializer/vanilla/smithing.js +46 -0
  63. package/dist/serializer/vanilla/smithing.js.map +1 -0
  64. package/dist/serializer/vanilla/stonecutting.d.ts +22 -0
  65. package/dist/serializer/vanilla/stonecutting.js +38 -0
  66. package/dist/serializer/vanilla/stonecutting.js.map +1 -0
  67. package/eslint.config.js +8 -0
  68. package/package.json +42 -0
  69. package/src/emitter.ts +239 -0
  70. package/src/hooks.ts +10 -0
  71. package/src/index.ts +18 -0
  72. package/src/loader.ts +219 -0
  73. package/src/module.ts +70 -0
  74. package/src/rule.ts +45 -0
  75. package/src/schema.ts +30 -0
  76. package/src/serializer/abstract.ts +41 -0
  77. package/src/serializer/context.ts +17 -0
  78. package/src/serializer/forge/conditional.ts +72 -0
  79. package/src/serializer/holder.ts +63 -0
  80. package/src/serializer/manyToMany.ts +62 -0
  81. package/src/serializer/manyToOne.ts +62 -0
  82. package/src/serializer/modifier.ts +10 -0
  83. package/src/serializer/oneToOne.ts +60 -0
  84. package/src/serializer/vanilla/shaped.ts +65 -0
  85. package/src/serializer/vanilla/shapeless.ts +8 -0
  86. package/src/serializer/vanilla/smelting.ts +62 -0
  87. package/src/serializer/vanilla/smithing.ts +74 -0
  88. package/src/serializer/vanilla/stonecutting.ts +63 -0
  89. package/src/testing/index.ts +1 -0
  90. package/src/testing/setup.ts +85 -0
  91. package/test/1.20.1/__snapshots__/emitter.test.ts.snap +122 -0
  92. package/test/1.20.1/__snapshots__/loader.test.ts.snap +215 -0
  93. package/test/1.20.1/emitter.test.ts +222 -0
  94. package/test/1.20.1/loader.test.ts +17 -0
  95. package/test/1.21.1/loader.test.ts +17 -0
  96. package/test/1.21.1/packFormat.test.ts +52 -0
  97. package/test/resources/1.20.1/default/custom/data/custom/recipes/conditional.json +25 -0
  98. package/tsconfig.build.json +14 -0
  99. package/tsconfig.json +34 -0
@@ -0,0 +1,63 @@
1
+ import {
2
+ type Ingredient,
3
+ type Result,
4
+ } from "@adeficior/data-modifier-ingredients";
5
+ import { type RecipeDefinition } from "../../schema";
6
+ import { Recipe, RecipeParser } from "../abstract";
7
+ import { type RecipeParseContext } from "../context";
8
+ import { type RecipeModifier } from "../modifier";
9
+
10
+ export type StonecuttingRecipeDefinition = RecipeDefinition &
11
+ Readonly<{
12
+ ingredient: unknown;
13
+ result: unknown;
14
+ count?: number;
15
+ }>;
16
+
17
+ // TODO could also be SmeltingRecipe?
18
+ export class StonecuttingRecipe extends Recipe {
19
+ constructor(
20
+ private readonly ingredient: Ingredient,
21
+ private readonly result: Result,
22
+ ) {
23
+ super();
24
+ }
25
+
26
+ getIngredients() {
27
+ return [this.ingredient];
28
+ }
29
+
30
+ getResults() {
31
+ return [this.result];
32
+ }
33
+
34
+ override modify(modifier: RecipeModifier) {
35
+ return new StonecuttingRecipe(
36
+ modifier.ingredient(this.ingredient),
37
+ modifier.result(this.result),
38
+ );
39
+ }
40
+
41
+ override serialize(
42
+ context: RecipeParseContext,
43
+ ): Partial<StonecuttingRecipeDefinition> {
44
+ return {
45
+ ingredient: context.ingredients.serialize(this.ingredient),
46
+ result: context.results.serialize(this.result),
47
+ };
48
+ }
49
+ }
50
+
51
+ export class StonecuttingParser extends RecipeParser<
52
+ StonecuttingRecipeDefinition,
53
+ StonecuttingRecipe
54
+ > {
55
+ deserialize(
56
+ definition: StonecuttingRecipeDefinition,
57
+ context: RecipeParseContext,
58
+ ): StonecuttingRecipe {
59
+ const ingredient = context.ingredients.deserialize(definition.ingredient);
60
+ const result = context.results.deserialize(definition.result);
61
+ return new StonecuttingRecipe(ingredient, result);
62
+ }
63
+ }
@@ -0,0 +1 @@
1
+ export * from "./setup";
@@ -0,0 +1,85 @@
1
+ import { packFormatOf, type EventHandler } from "@adeficior/data-modifier-core";
2
+ import {
3
+ setupIngredientSerializer,
4
+ setupPredicates,
5
+ setupResultSerializer,
6
+ } from "@adeficior/data-modifier-ingredients/testing";
7
+ import { setupTagRegistry } from "@adeficior/data-modifier-tags/testing";
8
+ import { createTestLogger } from "@adeficior/pack-resolver/testing";
9
+ import { createTestDataResolver, setupLookup } from "@adeficior/testing";
10
+ import { afterAll, afterEach, beforeAll } from "bun:test";
11
+ import { RecipeEmitter } from "../emitter";
12
+ import { type RegisterRecipeParser } from "../hooks";
13
+ import { RecipeLoader } from "../loader";
14
+ import { recipePattern } from "../schema";
15
+
16
+ // TODO move to testing package?
17
+ export function setupRecipeLoader(
18
+ version: string,
19
+ parsers?: EventHandler<RegisterRecipeParser>,
20
+ mods: string[] = [],
21
+ ) {
22
+ const lookup = setupLookup(version);
23
+
24
+ const results = setupResultSerializer(version, lookup);
25
+ const ingredients = setupIngredientSerializer(version, lookup);
26
+
27
+ const loader = new RecipeLoader(results, ingredients, {
28
+ mods: ["minecraft", ...mods],
29
+ });
30
+ const logger = createTestLogger();
31
+
32
+ beforeAll(async () => {
33
+ await parsers?.({
34
+ register: (...args) => loader.registerParser(...args),
35
+ });
36
+
37
+ const resolver = await createTestDataResolver(version, {
38
+ include: recipePattern(packFormatOf(version)),
39
+ logger,
40
+ });
41
+ // TODO use distribute for this somehow
42
+ await resolver.extract(loader);
43
+ });
44
+
45
+ afterAll(() => {
46
+ logger.reset();
47
+ });
48
+
49
+ return { loader, logger };
50
+ }
51
+
52
+ export function setupRecipeEmitter(
53
+ version: string,
54
+ parsers?: EventHandler<RegisterRecipeParser>,
55
+ mods: string[] = ["minecraft"],
56
+ ) {
57
+ const lookup = setupLookup(version);
58
+
59
+ const results = setupResultSerializer(version, lookup);
60
+ const ingredients = setupIngredientSerializer(version, lookup);
61
+ const tags = setupTagRegistry(version);
62
+ const predicates = setupPredicates(lookup, tags, ingredients);
63
+
64
+ const logger = createTestLogger();
65
+ const { loader } = setupRecipeLoader(version, parsers, mods);
66
+
67
+ const emitter = new RecipeEmitter(
68
+ logger,
69
+ packFormatOf(version),
70
+ loader,
71
+ results,
72
+ ingredients,
73
+ predicates,
74
+ loader,
75
+ );
76
+
77
+ const resolver = emitter.resolver({ logger });
78
+
79
+ afterEach(() => {
80
+ emitter.clear();
81
+ logger.reset();
82
+ });
83
+
84
+ return { loader, emitter, resolver, logger };
85
+ }
@@ -0,0 +1,122 @@
1
+ // Bun Snapshot v1, https://bun.sh/docs/test/snapshots
2
+
3
+ exports[`recipe ingredient replacement replaces ingredients: modified piston recipe 1`] = `
4
+ {
5
+ "key": {
6
+ "#": {
7
+ "tag": "forge:cobblestone/normal",
8
+ },
9
+ "R": {
10
+ "item": "minecraft:emerald",
11
+ },
12
+ "T": {
13
+ "tag": "minecraft:planks",
14
+ },
15
+ "X": {
16
+ "tag": "forge:ingots/iron",
17
+ },
18
+ },
19
+ "pattern": [
20
+ "TTT",
21
+ "#X#",
22
+ "#R#",
23
+ ],
24
+ "result": {
25
+ "item": "minecraft:piston",
26
+ },
27
+ "type": "minecraft:crafting_shaped",
28
+ }
29
+ `;
30
+
31
+ exports[`recipe ingredient replacement replaces ingredients: modified compass recipe 1`] = `
32
+ {
33
+ "key": {
34
+ "#": {
35
+ "tag": "forge:ingots/iron",
36
+ },
37
+ "X": {
38
+ "item": "minecraft:emerald",
39
+ },
40
+ },
41
+ "pattern": [
42
+ " # ",
43
+ "#X#",
44
+ " # ",
45
+ ],
46
+ "result": {
47
+ "item": "minecraft:compass",
48
+ },
49
+ "type": "minecraft:crafting_shaped",
50
+ }
51
+ `;
52
+
53
+ exports[`recipe ingredient replacement replaces ingredients: recipes including redstone as an ingredient 1`] = `
54
+ [
55
+ "data/minecraft/recipes/clock.json",
56
+ "data/minecraft/recipes/compass.json",
57
+ "data/minecraft/recipes/detector_rail.json",
58
+ "data/minecraft/recipes/dispenser.json",
59
+ "data/minecraft/recipes/dropper.json",
60
+ "data/minecraft/recipes/note_block.json",
61
+ "data/minecraft/recipes/observer.json",
62
+ "data/minecraft/recipes/piston.json",
63
+ "data/minecraft/recipes/powered_rail.json",
64
+ "data/minecraft/recipes/redstone_block.json",
65
+ "data/minecraft/recipes/redstone_lamp.json",
66
+ "data/minecraft/recipes/redstone_torch.json",
67
+ "data/minecraft/recipes/repeater.json",
68
+ "data/minecraft/recipes/target.json",
69
+ ]
70
+ `;
71
+
72
+ exports[`recipe ingredient replacement replaces ingredients with additional input filter: modified piston recipe 1`] = `
73
+ {
74
+ "key": {
75
+ "#": {
76
+ "tag": "forge:cobblestone/normal",
77
+ },
78
+ "R": {
79
+ "item": "minecraft:emerald",
80
+ },
81
+ "T": {
82
+ "tag": "minecraft:planks",
83
+ },
84
+ "X": {
85
+ "tag": "forge:ingots/iron",
86
+ },
87
+ },
88
+ "pattern": [
89
+ "TTT",
90
+ "#X#",
91
+ "#R#",
92
+ ],
93
+ "result": {
94
+ "item": "minecraft:piston",
95
+ },
96
+ "type": "minecraft:crafting_shaped",
97
+ }
98
+ `;
99
+
100
+ exports[`recipe ingredient replacement replaces ingredients with additional input filter: modified note_block recipe 1`] = `
101
+ {
102
+ "category": "redstone",
103
+ "key": {
104
+ "#": {
105
+ "tag": "minecraft:planks",
106
+ },
107
+ "X": {
108
+ "item": "minecraft:emerald",
109
+ },
110
+ },
111
+ "pattern": [
112
+ "###",
113
+ "#X#",
114
+ "###",
115
+ ],
116
+ "result": {
117
+ "item": "minecraft:note_block",
118
+ },
119
+ "show_notification": true,
120
+ "type": "minecraft:crafting_shaped",
121
+ }
122
+ `;
@@ -0,0 +1,215 @@
1
+ // Bun Snapshot v1, https://bun.sh/docs/test/snapshots
2
+
3
+ exports[`recipe ingredient replacement replaces ingredients: modified piston recipe 1`] = `
4
+ {
5
+ "key": {
6
+ "#": {
7
+ "tag": "forge:cobblestone/normal",
8
+ },
9
+ "R": {
10
+ "item": "minecraft:emerald",
11
+ },
12
+ "T": {
13
+ "tag": "minecraft:planks",
14
+ },
15
+ "X": {
16
+ "tag": "forge:ingots/iron",
17
+ },
18
+ },
19
+ "pattern": [
20
+ "TTT",
21
+ "#X#",
22
+ "#R#",
23
+ ],
24
+ "result": {
25
+ "item": "minecraft:piston",
26
+ },
27
+ "type": "minecraft:crafting_shaped",
28
+ }
29
+ `;
30
+
31
+ exports[`recipe ingredient replacement replaces ingredients: modified compass recipe 1`] = `
32
+ {
33
+ "key": {
34
+ "#": {
35
+ "tag": "forge:ingots/iron",
36
+ },
37
+ "X": {
38
+ "item": "minecraft:emerald",
39
+ },
40
+ },
41
+ "pattern": [
42
+ " # ",
43
+ "#X#",
44
+ " # ",
45
+ ],
46
+ "result": {
47
+ "item": "minecraft:compass",
48
+ },
49
+ "type": "minecraft:crafting_shaped",
50
+ }
51
+ `;
52
+
53
+ exports[`recipe ingredient replacement replaces ingredients: recipes including redstone as an ingredient 1`] = `
54
+ [
55
+ "data/botania/recipes/brew/speed.json",
56
+ "data/botania/recipes/brew/weak_regeneration.json",
57
+ "data/botania/recipes/detector_light_relay.json",
58
+ "data/botania/recipes/ender_eye_block.json",
59
+ "data/botania/recipes/hourglass.json",
60
+ "data/botania/recipes/lens_mine.json",
61
+ "data/botania/recipes/mana_detector.json",
62
+ "data/botania/recipes/mana_infusion/mana_powder_dust.json",
63
+ "data/botania/recipes/mana_infusion/redstone_dupe.json",
64
+ "data/botania/recipes/mana_infusion/redstone_to_glowstone_dust.json",
65
+ "data/botania/recipes/pattern_1_1.json",
66
+ "data/botania/recipes/pattern_1_2.json",
67
+ "data/botania/recipes/pattern_1_3.json",
68
+ "data/botania/recipes/pattern_2_1.json",
69
+ "data/botania/recipes/pattern_2_2.json",
70
+ "data/botania/recipes/pattern_2_3.json",
71
+ "data/botania/recipes/pattern_3_1.json",
72
+ "data/botania/recipes/pattern_3_2.json",
73
+ "data/botania/recipes/pattern_donut.json",
74
+ "data/botania/recipes/quartz_red.json",
75
+ "data/botania/recipes/red_pavement.json",
76
+ "data/botania/recipes/redstone_root.json",
77
+ "data/botania/recipes/redstone_spreader.json",
78
+ "data/botania/recipes/spark_changer.json",
79
+ "data/botania/recipes/unholy_cloak.json",
80
+ "data/minecraft/recipes/clock.json",
81
+ "data/minecraft/recipes/compass.json",
82
+ "data/minecraft/recipes/detector_rail.json",
83
+ "data/minecraft/recipes/dispenser.json",
84
+ "data/minecraft/recipes/dropper.json",
85
+ "data/minecraft/recipes/note_block.json",
86
+ "data/minecraft/recipes/observer.json",
87
+ "data/minecraft/recipes/piston.json",
88
+ "data/minecraft/recipes/powered_rail.json",
89
+ "data/minecraft/recipes/redstone_block.json",
90
+ "data/minecraft/recipes/redstone_lamp.json",
91
+ "data/minecraft/recipes/redstone_torch.json",
92
+ "data/minecraft/recipes/repeater.json",
93
+ "data/minecraft/recipes/target.json",
94
+ ]
95
+ `;
96
+
97
+ exports[`recipe ingredient replacement replaces ingredients with additional input filter: modified piston recipe 1`] = `
98
+ {
99
+ "key": {
100
+ "#": {
101
+ "tag": "forge:cobblestone/normal",
102
+ },
103
+ "R": {
104
+ "item": "minecraft:emerald",
105
+ },
106
+ "T": {
107
+ "tag": "minecraft:planks",
108
+ },
109
+ "X": {
110
+ "tag": "forge:ingots/iron",
111
+ },
112
+ },
113
+ "pattern": [
114
+ "TTT",
115
+ "#X#",
116
+ "#R#",
117
+ ],
118
+ "result": {
119
+ "item": "minecraft:piston",
120
+ },
121
+ "type": "minecraft:crafting_shaped",
122
+ }
123
+ `;
124
+
125
+ exports[`recipe ingredient replacement replaces ingredients with additional input filter: modified note_block recipe 1`] = `
126
+ {
127
+ "category": "redstone",
128
+ "key": {
129
+ "#": {
130
+ "tag": "minecraft:planks",
131
+ },
132
+ "X": {
133
+ "item": "minecraft:emerald",
134
+ },
135
+ },
136
+ "pattern": [
137
+ "###",
138
+ "#X#",
139
+ "###",
140
+ ],
141
+ "result": {
142
+ "item": "minecraft:note_block",
143
+ },
144
+ "show_notification": true,
145
+ "type": "minecraft:crafting_shaped",
146
+ }
147
+ `;
148
+
149
+ exports[`recipe ingredient replacement replaces ingredients in create recipes: modified create:raw_zinc_block recipe 1`] = `
150
+ {
151
+ "key": {
152
+ "C": {
153
+ "tag": "forge:raw_materials/iron",
154
+ },
155
+ },
156
+ "pattern": [
157
+ "CCC",
158
+ "CCC",
159
+ "CCC",
160
+ ],
161
+ "result": {
162
+ "item": "create:raw_zinc_block",
163
+ },
164
+ "type": "minecraft:crafting_shaped",
165
+ }
166
+ `;
167
+
168
+ exports[`recipe ingredient replacement replaces ingredients in create recipes: modified create:raw_zinc recipe 1`] = `
169
+ {
170
+ "ingredients": [
171
+ {
172
+ "tag": "forge:raw_materials/iron",
173
+ },
174
+ ],
175
+ "processingTime": 400,
176
+ "results": [
177
+ {
178
+ "item": "create:crushed_raw_zinc",
179
+ },
180
+ {
181
+ "chance": 0.75,
182
+ "item": "create:experience_nugget",
183
+ },
184
+ ],
185
+ "type": "create:crushing",
186
+ }
187
+ `;
188
+
189
+ exports[`recipe ingredient replacement replaces ingredients in create recipes: modified create:zinc_ingot_from_raw_ore recipe 1`] = `
190
+ {
191
+ "cookingtime": 100,
192
+ "experience": 0.7,
193
+ "ingredient": {
194
+ "tag": "forge:raw_materials/iron",
195
+ },
196
+ "result": {
197
+ "item": "create:zinc_ingot",
198
+ },
199
+ "type": "minecraft:blasting",
200
+ }
201
+ `;
202
+
203
+ exports[`recipe ingredient replacement replaces ingredients in create recipes: modified create:zinc_ingot_from_raw_ore recipe 2`] = `
204
+ {
205
+ "cookingtime": 200,
206
+ "experience": 0.7,
207
+ "ingredient": {
208
+ "tag": "forge:raw_materials/iron",
209
+ },
210
+ "result": {
211
+ "item": "create:zinc_ingot",
212
+ },
213
+ "type": "minecraft:smelting",
214
+ }
215
+ `;
@@ -0,0 +1,222 @@
1
+ import { type NormalizedId } from "@adeficior/data-modifier-core";
2
+ import {
3
+ ItemIngredient,
4
+ ItemResult,
5
+ } from "@adeficior/data-modifier-ingredients";
6
+ import { createTestAcceptor } from "@adeficior/pack-resolver/testing";
7
+ import { describe, expect, it } from "bun:test";
8
+ import { basename } from "node:path";
9
+ import {
10
+ type RecipeTest,
11
+ type ShapedRecipeDefinition,
12
+ EMPTY_RECIPE,
13
+ } from "../../src";
14
+ import { setupRecipeEmitter } from "../../src/testing";
15
+
16
+ const version = basename(import.meta.dir);
17
+ const { emitter, resolver, logger } = setupRecipeEmitter(version);
18
+
19
+ describe("recipe ingredient replacement", () => {
20
+ it("replaces ingredients", async () => {
21
+ const acceptor = createTestAcceptor();
22
+
23
+ emitter.replaceIngredient(
24
+ "minecraft:redstone",
25
+ new ItemIngredient("minecraft:emerald"),
26
+ );
27
+
28
+ await resolver.extract(acceptor);
29
+
30
+ expect(
31
+ acceptor.jsonAt("data/minecraft/recipes/piston.json"),
32
+ ).toMatchSnapshot("modified piston recipe");
33
+ expect(
34
+ acceptor.jsonAt("data/minecraft/recipes/compass.json"),
35
+ ).toMatchSnapshot("modified compass recipe");
36
+
37
+ expect(acceptor.paths()).toMatchSnapshot(
38
+ "recipes including redstone as an ingredient",
39
+ );
40
+ });
41
+
42
+ it("replaces ingredients with additional input filter", async () => {
43
+ const acceptor = createTestAcceptor();
44
+
45
+ emitter.replaceIngredient(
46
+ "minecraft:redstone",
47
+ new ItemIngredient("minecraft:emerald"),
48
+ {
49
+ input: "#minecraft:planks",
50
+ },
51
+ );
52
+
53
+ await resolver.extract(acceptor);
54
+
55
+ expect(
56
+ acceptor.jsonAt("data/minecraft/recipes/piston.json"),
57
+ ).toMatchSnapshot("modified piston recipe");
58
+ expect(
59
+ acceptor.jsonAt("data/minecraft/recipes/note_block.json"),
60
+ ).toMatchSnapshot("modified note_block recipe");
61
+ expect(acceptor.jsonAt("data/minecraft/recipes/compass.json")).toBeNull();
62
+ });
63
+
64
+ it("matches recipes wrapped in forge:conditional", async () => {
65
+ const acceptor = createTestAcceptor();
66
+
67
+ emitter.remove({
68
+ input: new ItemIngredient("minecraft:poppy"),
69
+ type: "minecraft:crafting_shapeless",
70
+ });
71
+
72
+ await resolver.extract(acceptor);
73
+
74
+ expect(
75
+ acceptor.jsonAt("data/custom/recipes/conditional.json"),
76
+ ).toMatchObject(EMPTY_RECIPE);
77
+ });
78
+ });
79
+
80
+ describe("recipe removal", () => {
81
+ it("removes recipes with id filter", async () => {
82
+ const acceptor = createTestAcceptor();
83
+
84
+ emitter.remove({
85
+ id: /minecraft:.*piston/,
86
+ });
87
+
88
+ await resolver.extract(acceptor);
89
+
90
+ expect(acceptor.paths()).toHaveLength(2);
91
+
92
+ expect(acceptor.jsonAt("data/minecraft/recipes/piston.json")).toMatchObject(
93
+ EMPTY_RECIPE,
94
+ );
95
+ expect(
96
+ acceptor.jsonAt("data/minecraft/recipes/sticky_piston.json"),
97
+ ).toMatchObject(EMPTY_RECIPE);
98
+ });
99
+
100
+ it("removes recipes with type filter", async () => {
101
+ const acceptor = createTestAcceptor();
102
+
103
+ emitter.remove({
104
+ type: "minecraft:smelting",
105
+ });
106
+
107
+ await resolver.extract(acceptor);
108
+
109
+ expect(acceptor.paths()).toHaveLength(70);
110
+ });
111
+
112
+ it("removes recipes with result filter", async () => {
113
+ const acceptor = createTestAcceptor();
114
+
115
+ emitter.remove({
116
+ output: "minecraft:cooked_beef",
117
+ });
118
+
119
+ await resolver.extract(acceptor);
120
+
121
+ expect(acceptor.paths()).toHaveLength(3);
122
+
123
+ expect(
124
+ acceptor.jsonAt("data/minecraft/recipes/cooked_beef.json"),
125
+ ).toMatchObject(EMPTY_RECIPE);
126
+ expect(
127
+ acceptor.jsonAt("data/minecraft/recipes/cooked_beef_from_smoking.json"),
128
+ ).toMatchObject(EMPTY_RECIPE);
129
+ expect(
130
+ acceptor.jsonAt(
131
+ "data/minecraft/recipes/cooked_beef_from_campfire_cooking.json",
132
+ ),
133
+ ).toMatchObject(EMPTY_RECIPE);
134
+ });
135
+ });
136
+
137
+ it("creates custom recipes", async () => {
138
+ const acceptor = createTestAcceptor();
139
+
140
+ const recipe: ShapedRecipeDefinition = {
141
+ type: "minecraft:shaped",
142
+ key: {
143
+ A: {
144
+ item: "minecraft:diamond",
145
+ },
146
+ B: {
147
+ tag: "minecraft:iron_ores",
148
+ },
149
+ },
150
+ result: {
151
+ item: "minecraft:command_block",
152
+ },
153
+ pattern: ["A ", " B"],
154
+ };
155
+
156
+ emitter.add("example:custom", recipe);
157
+
158
+ await resolver.extract(acceptor);
159
+
160
+ expect(acceptor.jsonAt("data/example/recipes/custom.json")).toMatchObject(
161
+ recipe,
162
+ );
163
+ });
164
+
165
+ it("warns about duplicate custom recipe IDs", () => {
166
+ const id: NormalizedId = "example:recipe";
167
+
168
+ emitter.add(id, { type: "example:something" });
169
+ emitter.add(id, { type: "example:something_else" });
170
+
171
+ expect(logger.error).toHaveBeenCalledWith(
172
+ `Overwriting custom recipe with ID ${id}`,
173
+ );
174
+ });
175
+
176
+ it("warns about missing recipe removal matches", async () => {
177
+ const test: RecipeTest = {
178
+ type: "example:not_existing",
179
+ };
180
+
181
+ emitter.remove(test);
182
+
183
+ const acceptor = createTestAcceptor();
184
+ await resolver.extract(acceptor);
185
+
186
+ expect(logger.trace).toHaveBeenCalledWith(
187
+ "could not find any recipes matching",
188
+ expect.objectContaining({
189
+ operation: "remove",
190
+ test,
191
+ }),
192
+ );
193
+ });
194
+
195
+ it("warns about missing recipe replacement matches", async () => {
196
+ const from = "minecraft:bedrock";
197
+ const to = new ItemResult("minecraft:dirt");
198
+ emitter.replaceResult(from, to);
199
+
200
+ const acceptor = createTestAcceptor();
201
+ await resolver.extract(acceptor);
202
+
203
+ expect(logger.trace).toHaveBeenCalledWith(
204
+ "could not find any recipes matching",
205
+ expect.objectContaining({
206
+ operation: "replace result",
207
+ from,
208
+ to,
209
+ }),
210
+ );
211
+ });
212
+
213
+ it("does not warn about optional missing recipe matches", async () => {
214
+ const from = "minecraft:bedrock";
215
+ const to = new ItemResult("minecraft:dirt");
216
+ emitter.replaceResult(from, to, { optional: true });
217
+
218
+ const acceptor = createTestAcceptor();
219
+ await resolver.extract(acceptor);
220
+
221
+ expect(logger.error).not.toHaveBeenCalled();
222
+ });