Make sure during fuzzy crafting we can fulfill pattern (#1253)

Co-authored-by: Martin Robertz <dream-master@gmx.net>
Co-authored-by: Kogepan229 <kogepan25629@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-05-22 21:48:27 +02:00
1 parent fea2702293
commit 39829aa75f
5 files changed
+134 -25

No files matched your search

+8 -8
View File
@@ -2,7 +2,7 @@
dependencies {
implementation('com.github.GTNewHorizons:NotEnoughItems:2.8.98-GTNH:dev')
implementation("com.github.GTNewHorizons:GTNHLib:0.10.0:dev")
implementation("com.github.GTNewHorizons:GTNHLib:0.10.2:dev")
implementation('curse.maven:cofh-core-69162:2388751')
compileOnlyApi('com.github.GTNewHorizons:Angelica:2.1.25:dev')
@@ -12,7 +12,7 @@ dependencies {
compileOnly('com.github.GTNewHorizons:BuildCraft:7.1.60:dev') {transitive = false}
compileOnly('com.github.GTNewHorizons:ForestryMC:4.11.23:dev') {transitive = false} // required to compile CraftingV2Tests.java now
compileOnly('com.github.GTNewHorizons:ForgeMultipart:1.7.10:dev') {transitive = false}
compileOnly('com.github.GTNewHorizons:GT5-Unofficial:5.09.52.527:dev') {transitive = false}
compileOnly('com.github.GTNewHorizons:GT5-Unofficial:5.09.52.532:dev') {transitive = false}
compileOnly('com.github.GTNewHorizons:ModularUI2:2.3.65-1.7.10:dev') {transitive = false}
compileOnly('com.github.GTNewHorizons:Jabba:1.5.20:dev') {transitive = false}
compileOnly('com.github.GTNewHorizons:inventory-tweaks:1.7.1:api') {transitive = false}
@@ -27,11 +27,11 @@ dependencies {
compileOnly('curse.maven:better-storage-232919:2731636')
compileOnly('api:immibis:1')
compileOnly('curse.maven:computercraft-67504:2269339')
compileOnly('com.github.GTNewHorizons:EnderIO:5.2.62:dev') { transitive = false }
compileOnly('com.github.GTNewHorizons:EnderIO:2.10.27:dev') { transitive = false }
compileOnly(rfg.deobf('curse.maven:mekanism-268560:2475797')) // https://www.curseforge.com/minecraft/mc-mods/mekanism/files/2475797
compileOnly(rfg.deobf('maven.modrinth:immibis-microblocks:59.1.2')) // https://modrinth.com/mod/immibis-microblocks/version/59.1.2
compileOnly("curse.maven:endlessids-665730:7585931") {transitive = false }
compileOnly('com.github.GTNewHorizons:AE2FluidCraft-Rework:1.5.84-gtnh:dev') { transitive = false }
compileOnly('com.github.GTNewHorizons:AE2FluidCraft-Rework:1.5.85-gtnh:dev') { transitive = false }
compileOnly('com.github.GTNewHorizons:ae2stuff:0.10.19-GTNH:dev') {
exclude module: "Applied-Energistics-2-Unofficial"
}
@@ -48,7 +48,7 @@ dependencies {
//runtimeOnlyNonPublishable('curse.maven:thermal-expansion-69163:2388759')
//Uncomment to test with GT5 machines
//runtimeOnlyNonPublishable('com.github.GTNewHorizons:GT5-Unofficial:5.09.52.527:dev') {transitive = false}
//runtimeOnlyNonPublishable('com.github.GTNewHorizons:GT5-Unofficial:5.09.52.532:dev') {transitive = false}
//runtimeOnlyNonPublishable('com.github.GTNewHorizons:StructureLib:1.4.34:dev') {transitive = false}
//runtimeOnlyNonPublishable('com.github.GTNewHorizons:ModularUI:1.3.4:dev') {transitive = false}
//runtimeOnlyNonPublishable('com.github.GTNewHorizons:ModularUI2:2.3.65-1.7.10:dev') {transitive = false}
@@ -68,7 +68,7 @@ dependencies {
functionalTestImplementation('org.junit.platform:junit-platform-engine')
functionalTestImplementation('org.junit.platform:junit-platform-launcher')
functionalTestImplementation('org.junit.platform:junit-platform-reporting')
functionalTestImplementation('com.github.GTNewHorizons:GT5-Unofficial:5.09.52.527:dev') {
functionalTestImplementation('com.github.GTNewHorizons:GT5-Unofficial:5.09.52.532:dev') {
exclude module: "Applied-Energistics-2-Unofficial"
exclude module: "Hodgepodge"
exclude module: "waila"
@@ -81,8 +81,8 @@ dependencies {
runtimeOnlyNonPublishable('thaumcraft:Thaumcraft:1.7.10-4.2.3.5:dev') {
exclude module: "Baubles"
}
runtimeOnlyNonPublishable('com.github.GTNewHorizons:ThaumicEnergistics:1.7.50-GTNH:dev') { transitive = false }
runtimeOnlyNonPublishable('com.github.GTNewHorizons:AE2FluidCraft-Rework:1.5.84-gtnh:dev') { transitive = false }
runtimeOnlyNonPublishable('com.github.GTNewHorizons:ThaumicEnergistics:1.7.51-GTNH:dev') { transitive = false }
runtimeOnlyNonPublishable('com.github.GTNewHorizons:AE2FluidCraft-Rework:1.5.85-gtnh:dev') { transitive = false }
runtimeOnlyNonPublishable('com.github.GTNewHorizons:NotEnoughEnergistics:1.7.29:dev') {
exclude module: "Applied-Energistics-2-Unofficial"
exclude module: "waila"
@@ -221,6 +221,23 @@ public class CraftingV2Tests {
.addOutput(new ItemStack(Blocks.chest, 1)).buildAndAdd();
}
private void addFuzzyProcessingChestPattern(MockAESystem aeSystem) {
aeSystem.newProcessingPattern().allowUsingSubstitutes().addInput(new ItemStack(Blocks.planks, 8, 0))
.addOutput(new ItemStack(Blocks.chest, 1)).buildAndAdd();
}
private void addFuzzyBedPattern(MockAESystem aeSystem) {
aeSystem.newCraftingPattern().allowUsingSubstitutes()
// row 1
.addInput(new ItemStack(Blocks.wool, 1, 0)).addInput(new ItemStack(Blocks.wool, 1, 0))
.addInput(new ItemStack(Blocks.wool, 1, 0))
// row 2
.addInput(new ItemStack(Blocks.planks, 1, 0)).addInput(new ItemStack(Blocks.planks, 1, 0))
.addInput(new ItemStack(Blocks.planks, 1, 0))
// end
.addOutput(new ItemStack(Items.bed, 1)).buildAndAdd();
}
@ParameterizedTest
@ValueSource(ints = { 0, 1 })
void craftChestFromLogs(int woodMetadata) {
@@ -265,6 +282,70 @@ public class CraftingV2Tests {
AEItemStack.create(new ItemStack(Blocks.chest, 0)).setCountRequestable(1));
}
@Test
void craftingPatternFuzzyCanUseMixedSubstitutes() {
MockAESystem aeSystem = new MockAESystem(dummyWorld);
aeSystem.addStoredItem(new ItemStack(Blocks.wool, 1, 0));
aeSystem.addStoredItem(new ItemStack(Blocks.wool, 2, 1));
aeSystem.addStoredItem(new ItemStack(Blocks.planks, 3, 0));
aeSystem.addStoredItem(new ItemStack(Items.gold_ingot, 64));
addFuzzyBedPattern(aeSystem);
// Another pattern that shouldn't match
addDummyGappleRecipe(aeSystem);
final CraftingJobV2 job = aeSystem.makeCraftingJob(new ItemStack(Items.bed, 1));
simulateJobAndCheck(job, SIMPLE_SIMULATION_TIMEOUT_MS);
assertFalse(job.isSimulation());
assertEquals(job.getOutput(), AEItemStack.create(new ItemStack(Items.bed, 1)));
assertJobPlanEquals(
job,
AEItemStack.create(new ItemStack(Blocks.wool, 1, 0)),
AEItemStack.create(new ItemStack(Blocks.wool, 2, 1)),
AEItemStack.create(new ItemStack(Blocks.planks, 3, 0)),
AEItemStack.create(new ItemStack(Items.bed, 0)).setCountRequestable(1));
}
@Test
void processingPatternFuzzySubstitutesCompleteInputGroupsOnly() {
MockAESystem aeSystem = new MockAESystem(dummyWorld);
aeSystem.addStoredItem(new ItemStack(Blocks.planks, 12, 0));
aeSystem.addStoredItem(new ItemStack(Blocks.planks, 20, 1));
aeSystem.addStoredItem(new ItemStack(Blocks.planks, 8, 2));
aeSystem.addStoredItem(new ItemStack(Items.gold_ingot, 64));
addFuzzyProcessingChestPattern(aeSystem);
// Another pattern that shouldn't match
addDummyGappleRecipe(aeSystem);
final CraftingJobV2 job = aeSystem.makeCraftingJob(new ItemStack(Blocks.chest, 4));
simulateJobAndCheck(job, SIMPLE_SIMULATION_TIMEOUT_MS);
assertFalse(job.isSimulation());
assertEquals(job.getOutput(), AEItemStack.create(new ItemStack(Blocks.chest, 4)));
assertJobPlanEquals(
job,
AEItemStack.create(new ItemStack(Blocks.planks, 8, 0)),
AEItemStack.create(new ItemStack(Blocks.planks, 16, 1)),
AEItemStack.create(new ItemStack(Blocks.planks, 8, 2)),
AEItemStack.create(new ItemStack(Blocks.chest, 0)).setCountRequestable(4));
}
@Test
void processingPatternFuzzyDoesNotTreatPartialGroupsAsCraftable() {
MockAESystem aeSystem = new MockAESystem(dummyWorld);
aeSystem.addStoredItem(new ItemStack(Blocks.planks, 12, 0));
aeSystem.addStoredItem(new ItemStack(Blocks.planks, 20, 1));
aeSystem.addStoredItem(new ItemStack(Items.gold_ingot, 64));
addFuzzyProcessingChestPattern(aeSystem);
// Another pattern that shouldn't match
addDummyGappleRecipe(aeSystem);
final CraftingJobV2 job = aeSystem.makeCraftingJob(new ItemStack(Blocks.chest, 4));
simulateJobAndCheck(job, SIMPLE_SIMULATION_TIMEOUT_MS);
assertTrue(job.isSimulation());
assertEquals(job.getOutput(), AEItemStack.create(new ItemStack(Blocks.chest, 4)));
assertJobPlanEquals(
job,
AEItemStack.create(new ItemStack(Blocks.planks, 16, 0)),
AEItemStack.create(new ItemStack(Blocks.planks, 16, 1)),
AEItemStack.create(new ItemStack(Blocks.chest, 0)).setCountRequestable(4));
}
@Test
void canHandleCyclicalPatterns() {
MockAESystem aeSystem = new MockAESystem(dummyWorld);
@@ -84,6 +84,7 @@ public class CraftingRequest implements ITreeSerializable {
public final SubstitutionMode substitutionMode;
public final Predicate<IAEStack<?>> acceptableSubstituteFn;
public final long substitutionGroupSize;
// (task, amount fulfilled by task)
public final CraftingMode craftingMode;
@@ -124,6 +125,7 @@ public class CraftingRequest implements ITreeSerializable {
buffer.writeBoolean(wasSimulated);
buffer.writeBoolean(incomplete);
buffer.writeInt(craftingMode.ordinal());
buffer.writeLong(substitutionGroupSize);
return usedResolvers;
}
@@ -151,6 +153,7 @@ public class CraftingRequest implements ITreeSerializable {
if (index < 0 || index >= CraftingMode.values().length || CraftingMode.values()[index] == CraftingMode.STANDARD)
craftingMode = CraftingMode.STANDARD;
else craftingMode = CraftingMode.IGNORE_MISSING;
substitutionGroupSize = buffer.readLong();
acceptableSubstituteFn = x -> true;
}
@@ -162,6 +165,19 @@ public class CraftingRequest implements ITreeSerializable {
*/
public CraftingRequest(CraftingRequest parentRequest, @Nonnull IAEStack<?> stack, SubstitutionMode substitutionMode,
boolean allowSimulation, CraftingMode craftingMode, Predicate<IAEStack<?>> acceptableSubstituteFn) {
this(parentRequest, stack, substitutionMode, allowSimulation, craftingMode, acceptableSubstituteFn, 1);
}
/**
* @param parentRequest From which this crafting request is initiated. null if it's the root
* @param stack The item/fluid and stack to request
* @param substitutionMode Whether and how to allow substitutions when resolving this request
* @param acceptableSubstituteFn A predicate testing if a given item (in fuzzy mode) can fulfill the request
* @param substitutionGroupSize The amount that must be fulfilled by a single substitute stack in fuzzy mode
*/
public CraftingRequest(CraftingRequest parentRequest, @Nonnull IAEStack<?> stack, SubstitutionMode substitutionMode,
boolean allowSimulation, CraftingMode craftingMode, Predicate<IAEStack<?>> acceptableSubstituteFn,
long substitutionGroupSize) {
this.parentRequest = parentRequest;
if (parentRequest == null) {
this.parentRequests = Collections.emptySet();
@@ -174,6 +190,7 @@ public class CraftingRequest implements ITreeSerializable {
this.stack = stack;
this.substitutionMode = substitutionMode;
this.acceptableSubstituteFn = acceptableSubstituteFn;
this.substitutionGroupSize = Math.max(1, substitutionGroupSize);
this.remainingToProcess = stack.getStackSize();
this.allowSimulation = allowSimulation;
this.craftingMode = craftingMode;
@@ -446,7 +446,8 @@ public class CraftableItemResolver implements CraftingRequestResolver {
childMode,
allowSimulation,
request.craftingMode,
stack -> this.isValidSubstitute(input, stack, context.world, finalSlot));
stack -> this.isValidSubstitute(input, stack, context.world, finalSlot),
1);
complexRequestPerSlot.add(req);
newChildren.add(req);
childRequests.add(new RequestAndPerCraftAmount(req, input.getStackSize()));
@@ -472,7 +473,8 @@ public class CraftableItemResolver implements CraftingRequestResolver {
childMode,
allowSimulation,
request.craftingMode,
stack -> this.isValidSubstitute(recInput, stack, context.world));
stack -> this.isValidSubstitute(recInput, stack, context.world),
pattern.isCraftable() ? 1 : recInput.getStackSize());
newChildren.add(req);
childRecursionRequests.put(recInput, req);
}
@@ -500,7 +502,8 @@ public class CraftableItemResolver implements CraftingRequestResolver {
childMode,
allowSimulation,
request.craftingMode,
stack -> this.isValidSubstitute(input, stack, context.world));
stack -> this.isValidSubstitute(input, stack, context.world),
pattern.isCraftable() ? 1 : input.getStackSize());
newChildren.add(req);
childRequests.add(new RequestAndPerCraftAmount(req, input.getStackSize()));
}
@@ -54,16 +54,16 @@ public class ExtractItemResolver implements CraftingRequestResolver {
@Override
public StepOutput calculateOneStep(CraftingContext context) {
final boolean isFuzzy = request.substitutionMode == CraftingRequest.SubstitutionMode.ACCEPT_FUZZY;
state = State.SUCCESS;
if (request.remainingToProcess <= 0) {
return new StepOutput(Collections.emptyList());
}
extractExact(context, context.byproductsInventory, removedFromByproducts);
extractExact(context, context.byproductsInventory, removedFromByproducts, isFuzzy);
if (request.remainingToProcess > 0) {
extractExact(context, context.itemModel, removedFromSystem);
extractExact(context, context.itemModel, removedFromSystem, isFuzzy);
}
if (request.remainingToProcess > 0
&& request.substitutionMode == CraftingRequest.SubstitutionMode.ACCEPT_FUZZY) {
if (request.remainingToProcess > 0 && isFuzzy) {
extractFuzzy(context, context.byproductsInventory, removedFromByproducts);
if (request.remainingToProcess > 0) {
extractFuzzy(context, context.itemModel, removedFromSystem);
@@ -74,10 +74,12 @@ public class ExtractItemResolver implements CraftingRequestResolver {
return new StepOutput(Collections.emptyList());
}
private void extractExact(CraftingContext context, MECraftingInventory source, List<IAEStack<?>> removedList) {
private void extractExact(CraftingContext context, MECraftingInventory source, List<IAEStack<?>> removedList,
boolean isFuzzy) {
StackType exactMatching = source.extractItems((StackType) request.stack, Actionable.SIMULATE);
if (exactMatching != null) {
final long requestSize = Math.min(request.remainingToProcess, exactMatching.getStackSize());
if (exactMatching != null && exactMatching.getStackSize() > 0) {
final long requestSize = this.getRequestSize(exactMatching.getStackSize(), isFuzzy);
if (requestSize <= 0) return;
final StackType extracted = source
.extractItems(exactMatching.copy().setStackSize(requestSize), Actionable.MODULATE);
if (extracted != null && extracted.getStackSize() > 0) {
@@ -91,16 +93,13 @@ public class ExtractItemResolver implements CraftingRequestResolver {
private void extractFuzzy(CraftingContext context, MECraftingInventory source, List<IAEStack<?>> removedList) {
Collection<StackType> fuzzyMatching = source.findFuzzy((StackType) request.stack, FuzzyMode.IGNORE_ALL);
for (final StackType candidate : fuzzyMatching) {
if (candidate == null) {
continue;
}
if (candidate == null || candidate.getStackSize() <= 0) continue;
if (request.acceptableSubstituteFn.test(candidate)) {
final long requestSize = Math.min(request.remainingToProcess, candidate.getStackSize());
final long requestSize = this.getRequestSize(candidate.getStackSize(), true);
if (requestSize <= 0) continue;
final StackType extracted = source
.extractItems(candidate.copy().setStackSize(requestSize), Actionable.MODULATE);
if (extracted == null || extracted.getStackSize() <= 0) {
continue;
}
if (extracted == null || extracted.getStackSize() <= 0) continue;
extracted.setCraftable(false);
request.fulfill(this, extracted, context);
removedList.add(extracted.copy());
@@ -108,6 +107,15 @@ public class ExtractItemResolver implements CraftingRequestResolver {
}
}
private long getRequestSize(final long available, final boolean isFuzzy) {
final long upperBound = Math.min(request.remainingToProcess, available);
if (!isFuzzy) return upperBound;
// Fuzzy substitutions must not mix candidates inside one pattern input group. Extract as many complete
// groups as this candidate can cover, but ignore any remainder smaller than the group.
final long groupSize = request.substitutionGroupSize;
return upperBound / groupSize * groupSize;
}
@Override
public long partialRefund(CraftingContext context, long amount) {
final long originalAmount = amount;