Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@ Changes on `crafttweaker-integration` since `master-2.0-1.21.1-neo`.
overrides retain priority; shape-aware default loot remains supported.
- Add reproducible isolated client-smoke runs with CraftTweaker present and absent,
generated-content rendering captures, and a verification report for the parity work.
- Add a typed CraftTweaker manager for shared `mmdlib:crushing` recipes under a
separate crusher integration package. Reuse standard recipe actions, removals,
queries and reload handling; test script-added recipes through the crackhammer.
10 changes: 10 additions & 0 deletions docs/crafttweaker-api-inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -562,3 +562,13 @@ world and item-gallery captures were visually inspected. Release JAR inspection
confirmed no smoke content remained after restoring normal main outputs.
See [the verification report](crafttweaker-verification.md). The existing core
per-tool durability-scaling limitation remains deferred.

## Shared crushing recipe integration

The startup content inventory above is separate from the ordinary recipe API in
`compat/CraftTweaker/crusher`. `mods.mmdlib.crusher.CrushingRecipeManager` implements
CraftTweaker's IRecipeManager for `mmdlib:crushing` and adds the typed
`addRecipe(string name, IItemStack output, IIngredient input)` method.
`ExpandCrushingRecipe` registers `mods.mmdlib.crusher.CrushingRecipe` as the native
query-result type. Standard removals, queries and JSON additions are inherited.
See [shared crushing recipe scripting](crafttweaker-crusher.md).
81 changes: 81 additions & 0 deletions docs/crafttweaker-crusher.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# CraftTweaker crushing recipes

MMDLib's shared `mmdlib:crushing` recipe type accepts one item ingredient and one
output stack. Its consumers include the crackhammer and crushing machines in
mods that use this type. Changes apply to every consumer of the shared recipe
manager; the integration does not install a separate recipe pool or processor.

Use ordinary CraftTweaker recipe scripts, with its default loader or the explicit
loader below. These scripts run after registries are available and participate in
CraftTweaker's normal recipe reload lifecycle. Startup content-definition scripts
using the `mmdlib` loader run too early for recipe edits.

```zenscript
#loader crafttweaker
var crusher = <recipetype:mmdlib:crushing>;
crusher.addRecipe("clay_to_diamonds", <item:minecraft:diamond> * 2, <item:minecraft:clay_ball>);
crusher.addRecipe("planks_to_nugget", <item:minecraft:gold_nugget>, <tag:item:minecraft:planks>);
```

`addRecipe(name, output, input)` uses CraftTweaker's normal recipe ID rules:
`"clay_to_diamonds"` becomes `crafttweaker:clay_to_diamonds`. Outputs retain their
count and item components through the existing recipe's defensive stack copy.
Inputs use CraftTweaker's vanilla ingredient conversion, including tags. The recipe
consumes one input item; multiplied input stacks, empty input/output, and crafting
ingredient transformers are rejected rather than silently changing semantics.
Machine processing time and energy usage are consumer behavior, not fields of this
shared recipe type.

The manager inherits CraftTweaker's existing operations:

```zenscript
crusher.removeByName("crafttweaker:clay_to_diamonds");
crusher.removeByInput(<item:minecraft:clay_ball>);
crusher.remove(<item:minecraft:diamond>); // remove by output
var recipe = crusher.getRecipeByName("crafttweaker:planks_to_nugget"); // null if absent
var recipes = crusher.allRecipes;
```

`getRecipesByOutput`, `removeByModid`, `removeByRegex`, `removeMatching`, and
`removeAll` also use the standard IRecipeManager API. Remove an existing recipe by
its full ID before adding its replacement. Adding the same ID twice without a
removal retains CraftTweaker's normal duplicate handling.

The existing JSON route remains available:

```zenscript
crusher.addJsonRecipe("shard_to_emerald", {
ingredient: {item: "minecraft:amethyst_shard"},
result: {id: "minecraft:emerald", count: 2}
});
```

## Implementation and verification

The manager and native recipe query type live in
`compat/CraftTweaker/crusher`, separate from the startup definition builders.
CraftTweaker discovers them through its standard annotations; there is no additional
MMDLib startup loader or direct recipe-manager mutation. CraftTweaker remains
optional. The core recipe class, serializer, recipe ID and consumers are unchanged.

The executable [recipe fixture](../src/runtimeTest/scripts/crusher_recipes.zs)
checks addition, queries, removal by name/input/output, replacement, JSON fallback,
and tag ingredients. [CrusherRecipeScriptSmoke](../src/runtimeTest/java/zone/moddev/mc/lib/smoke/CrusherRecipeScriptSmoke.java)
checks the live recipes, invalid input rejection, and actual crackhammer consumption
with normal durability cost. The isolated client smoke reloads server resources and
checks that additions/removals persist without duplicate IDs.

The separately packaged companion demo currently has its own
`mmdlib_companion:crushing` generic machine recipes (with time and resource groups).
Those are used by its demo crusher and dual-crusher and are not aliases for the
shared type. This integration targets `mmdlib:crushing`; it does not migrate the
companion demo or change those recipes.

Verified on the current working tree: `./gradlew --offline check build` passed
(190 unit tests, 89 MMDLib GameTests, 52 companion GameTests, packaging and static
assets). The real integrated client passed a server-resource reload with additions,
removals and unique IDs intact (MMDLIB_CT_RECIPES_RELOAD_PASS). A separate client
launch without CraftTweaker passed. The release JAR includes the recipe manager
and native query type and excludes smoke classes/scripts. EA3/SA3 machines were
not launched in this verification; the live consumer test uses the shared type
through the crackhammer.
11 changes: 11 additions & 0 deletions docs/crafttweaker-verification.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,14 @@ static assets and packaging. Inspection of
`build/libs/mmdlib-2.0.0.121012-development.jar` confirmed that the integration
classes are included and smoke classes, fixture scripts and fixture resource IDs
are absent. No commit or remote publication was performed.

## Shared crushing recipe follow-up

The separate recipe integration is documented in [crushing recipe scripting](crafttweaker-crusher.md).
Normal `check build` passed with 190 unit tests, 89 MMDLib GameTests and 52
companion GameTests. The ordinary recipe fixture covers add/query/remove/replace,
JSON additions, tag ingredients, invalid arguments, and crackhammer consumption.
The client smoke now reloads server resources and verifies recipes are reapplied
without duplicate IDs; both installed and absent-mod client launches passed.
Release JAR inspection confirmed the two crusher integration classes are included
and smoke classes/scripts remain excluded.
3 changes: 3 additions & 0 deletions docs/crafttweaker.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# CraftTweaker definitions

For normal shared crushing-recipe edits, see [CraftTweaker crushing recipes](crafttweaker-crusher.md).
Those use the ordinary recipe loader, separately from these startup definitions.

When CraftTweaker is installed, scripts can define MMDLib materials, blocks, items,
and reusable block and item modules. Scripts use the `mmdlib` loader and run once at startup, after every mod has
constructed and before registry events. What they build is planned, checked, and
Expand Down
9 changes: 5 additions & 4 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,12 @@ loop, then machines defined entirely in JSON; also the plan for NeoForge 26.1 an
builds. Stage 1 is built (internals without GUI, and ingredients, groups, transactions and
matching).

### KubeJS and CraftTweaker recipe integrations
### KubeJS recipe integration

Optional polish over the generic support above, each loaded only when that mod is present: a
KubeJS recipe schema (`event.recipes.mmdlib.crushing(output, input)`) and a typed CraftTweaker
recipe manager (`addRecipe(name, output, input)`).
Possible optional polish over generic JSON support: a KubeJS recipe schema
(`event.recipes.mmdlib.crushing(output, input)`). The typed CraftTweaker manager
for shared `mmdlib:crushing` recipes is implemented; see
[crushing recipe scripting](crafttweaker-crusher.md).

### Global data pack loader

Expand Down
5 changes: 4 additions & 1 deletion scripts/crafttweaker-smoke.init.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ allprojects {
smokeDir.mkdirs()
new File(smokeDir, 'options.txt').text = 'guiScale:2\nmaxFps:30\nonboardAccessibility:false\npauseOnLostFocus:false\nrenderDistance:5\n'
project.delete(new File(smokeDir, 'scripts'))
if (!absent) project.copy { from 'src/clientSmoke/scripts'; into new File(smokeDir, 'scripts') }
if (!absent) {
project.copy { from 'src/clientSmoke/scripts'; into new File(smokeDir, 'scripts') }
project.copy { from 'src/runtimeTest/scripts/crusher_recipes.zs'; into new File(smokeDir, 'scripts') }
}
}
doLast {
def report = new File(smokeDir, 'logs/latest.log')
Expand Down
4 changes: 3 additions & 1 deletion src/clientSmoke/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ The present-mod run copies only this directory's scripts to an isolated build
folder. It checks startup registrations, English labels, baked model/texture
availability, and front-face model rotation for four horizontal facings. It opens
an integrated world and captures actual world rendering and an item/name gallery.
The absent-mod run copies no scripts and checks the script-only item is absent,
The installed-mod run also copies the ordinary crushing-recipe fixture, reloads
server resources, and verifies recipe additions/removals survive without duplicate
IDs (MMDLIB_CT_RECIPES_RELOAD_PASS). The absent-mod run copies no scripts and checks the script-only item is absent,
then creates an integrated world and exits successfully. Both runs use a bounded
timeout and explicit pass/fail log markers.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,17 @@ public static void tick(ClientTickEvent.Post event) {
}
player.teleportTo(.5, 103, 7.5);
player.setYRot(180); player.setXRot(22);
ready = true;
if (present()) {
checkRecipes(mc);
var server = mc.getSingleplayerServer();
server.reloadResources(server.getPackRepository().getSelectedIds()).thenRunAsync(() -> {
try {
checkRecipes(mc);
MMDLib.LOGGER.info("MMDLIB_CT_RECIPES_RELOAD_PASS");
ready = true;
} catch (Throwable error) { fail(error); }
}, server).exceptionally(error -> { fail(error); return null; });
} else ready = true;
} catch (Throwable error) { fail(error); }
});
}
Expand Down Expand Up @@ -103,6 +113,18 @@ public static void tick(ClientTickEvent.Post event) {
} catch (Throwable error) { fail(error); }
}

private static void checkRecipes(Minecraft mc) {
var server = mc.getSingleplayerServer();
var manager = server.getRecipeManager();
var recipe = manager.byKey(ResourceLocation.parse("crafttweaker:ct_crusher_added")).orElseThrow().value();
var output = recipe.getResultItem(server.registryAccess());
require(output.is(net.minecraft.world.item.Items.DIAMOND) && output.getCount() == 2, "recipe survives reload");
require(manager.byKey(ResourceLocation.parse("crafttweaker:ct_crusher_removed")).isEmpty(), "removal survives reload");
long count = manager.getAllRecipesFor(zone.moddev.mc.lib.common.recipes.CrushingRecipe.TYPE.get())
.stream().filter(holder -> holder.id().toString().equals("crafttweaker:ct_crusher_added")).count();
require(count == 1, "recipe reload does not duplicate IDs");
}

@SubscribeEvent
public static void frame(RenderFrameEvent.Post event) {
if (!Boolean.getBoolean("mmdlib.craftTweakerSmoke") || finished || stage == 0) return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ final class DrawerMemoryClientSmoke {
private static final BlockPos FIRST = new BlockPos(4, 100, 0), SECOND = FIRST.east(2);
private static int step, waitTicks, reported = -1;
private static boolean requested;
private static volatile BlockPos reopenAfterClose;
private static volatile String serverMenu = "not sampled";

static boolean run() {
if (!BackpackFixture.installed() || step > 13) return true;
Expand All @@ -21,7 +23,25 @@ static boolean run() {
reported = step;
zone.moddev.mc.lib.MMDLib.LOGGER.info("DRAWER_MEMORY_STEP {}", step);
}
if (++waitTicks > 160) throw new AssertionError("Drawer memory timed out at step " + step);
if (++waitTicks > 160)
throw new AssertionError("Drawer memory timed out at step " + step
+ "; reopening=" + reopenAfterClose + "; client=" + describeClient(mc)
+ "; server=" + serverMenu);
if (waitTicks == 1 || waitTicks % 20 == 0)
server(player -> serverMenu = player.containerMenu.getClass().getSimpleName()
+ "#" + player.containerMenu.containerId);
if (reopenAfterClose != null) {
var target = reopenAfterClose;
server(player -> {
// The client close packet and server.execute use different queues. Never open a
// replacement menu until the server has actually handled the vanilla close.
if (reopenAfterClose == target && player.containerMenu == player.inventoryMenu) {
reopenAfterClose = null;
ScreenRuntime.open(player, player.serverLevel().getBlockEntity(target));
}
});
return false;
}
if (step == 0) {
server(
player -> {
Expand Down Expand Up @@ -161,7 +181,7 @@ static boolean run() {
require(
mc.screen != screen, "Escape closes screen immediately during dropped item request");
MachineScreen.PendingActions.logout(null);
open(SECOND);
reopenAfterClose = SECOND;
advance();
}
case 11 -> {
Expand All @@ -177,7 +197,7 @@ static boolean run() {
screen.keyPressed(256, 0, 0);
require(mc.screen != screen, "Escape immediately closes a pending redstone request");
} else mc.player.closeContainer();
open(FIRST);
reopenAfterClose = FIRST;
advance();
}
case 12 -> {
Expand All @@ -191,7 +211,7 @@ static boolean run() {
screen.keyPressed(256, 0, 0);
require(
mc.screen != screen, "Escape closes immediately during a pending automation request");
open(SECOND);
reopenAfterClose = SECOND;
advance();
}
case 13 -> {
Expand All @@ -217,6 +237,16 @@ static boolean run() {
return false;
}

private static String describeClient(Minecraft mc) {
if (!(mc.screen instanceof MachineScreen screen)) return String.valueOf(mc.screen);
var menu = screen.getMenu();
return "MachineMenu#" + menu.containerId + " at " + menu.blockEntity.getBlockPos()
+ ", backpackPending=" + menu.containerDrawer.pending()
+ ", curiosPending=" + menu.curiosDrawer.pending()
+ ", automation=" + (menu.automation == null ? "absent"
: "open:" + menu.automation.open() + ",pending:" + menu.automation.pending());
}

private static void advance() {
step++;
waitTicks = 0;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package zone.moddev.mc.lib.compat.CraftTweaker.crusher;

import com.blamejared.crafttweaker.api.CraftTweakerAPI;
import com.blamejared.crafttweaker.api.action.recipe.ActionAddRecipe;
import com.blamejared.crafttweaker.api.annotation.ZenRegister;
import com.blamejared.crafttweaker.api.ingredient.IIngredient;
import com.blamejared.crafttweaker.api.item.IItemStack;
import com.blamejared.crafttweaker.api.recipe.manager.base.IRecipeManager;
import net.minecraft.world.item.crafting.RecipeType;
import org.openzen.zencode.java.ZenCodeType;
import zone.moddev.mc.lib.common.recipes.CrushingRecipe;

/** Normal recipe-loader integration, independent of the mmdlib startup content loader. */
@ZenRegister
@ZenCodeType.Name("mods.mmdlib.crusher.CrushingRecipeManager")
public final class CrushingRecipeManager implements IRecipeManager<CrushingRecipe> {
/**
* Add a one-input crushing recipe, using standard CraftTweaker recipe IDs and actions.
* Ingredient amounts greater than one are not supported by this recipe type.
*/
@ZenCodeType.Method
public void addRecipe(String name, IItemStack output, IIngredient input) {
if (input.asIIngredientWithAmount().amount() != 1)
throw new IllegalArgumentException("Crushing recipes consume exactly one input item");
if (!input.transformers().isEmpty())
throw new IllegalArgumentException("Crushing recipes do not support crafting ingredient transformers");
var ingredient = input.asVanillaIngredient();
if (ingredient.isEmpty()) throw new IllegalArgumentException("Crushing input must not be empty");
var recipe = new CrushingRecipe(ingredient, output.getInternal());
CraftTweakerAPI.apply(new ActionAddRecipe<>(this, createHolder(fixRecipeId(name), recipe)));
}

@Override
public RecipeType<CrushingRecipe> getRecipeType() {
return CrushingRecipe.TYPE.get();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package zone.moddev.mc.lib.compat.CraftTweaker.crusher;

import com.blamejared.crafttweaker.api.annotation.ZenRegister;
import com.blamejared.crafttweaker_annotations.annotations.NativeTypeRegistration;
import zone.moddev.mc.lib.common.recipes.CrushingRecipe;

/** Expose the native recipe type for inherited recipe-manager query results. */
@ZenRegister
@NativeTypeRegistration(value = CrushingRecipe.class, zenCodeName = "mods.mmdlib.crusher.CrushingRecipe")
public final class ExpandCrushingRecipe {}
Loading
Loading