Skip to content

Commit

Permalink
Merge pull request #758 from refinedmods/feat/NO-ISSUE/max-amount
Browse files Browse the repository at this point in the history
Max amount in autocrafting preview
  • Loading branch information
raoulvdberge authored Dec 27, 2024
2 parents 5eeea4c + c1457c6 commit 7ceb7e6
Show file tree
Hide file tree
Showing 43 changed files with 834 additions and 67 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,17 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### Added

- Autocrafting engine.
- The crafting preview now has the ability to fill out the maximum amount of a resource you can currently craft.

### Changed

- Autocrafting now handles multiple patterns with the same output correctly by trying to use the pattern with the
highest priority first. If there are missing resources, lower priority patterns are checked.

### Fixed

- Fixed amount in amount screens resetting when resizing the screen.

## [2.0.0-milestone.4.11] - 2024-12-08

### Added
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.refinedmods.refinedstorage.api.autocrafting.calculation;

class CalculationException extends RuntimeException {
protected CalculationException(final String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@

import org.apiguardian.api.API;

@FunctionalInterface
@API(status = API.Status.STABLE, since = "2.0.0-milestone.4.12")
public interface CraftingCalculator {
<T> void calculate(ResourceKey resource, long amount, CraftingCalculatorListener<T> listener);

long getMaxAmount(ResourceKey resource);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@

import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static com.refinedmods.refinedstorage.api.autocrafting.calculation.CraftingTree.root;

public class CraftingCalculatorImpl implements CraftingCalculator {
private static final Logger LOGGER = LoggerFactory.getLogger(CraftingCalculatorImpl.class);

private final PatternRepository patternRepository;
private final RootStorage rootStorage;

Expand All @@ -29,6 +34,9 @@ public <T> void calculate(final ResourceKey resource,
Amount lastPatternAmount = null;
for (final Pattern pattern : patterns) {
final Amount patternAmount = Amount.of(pattern, resource, amount);
if (patternAmount.getTotal() < 0) {
throw new NumberOverflowDuringCalculationException();
}
final CraftingCalculatorListener<T> childListener = listener.childCalculationStarted();
final CraftingTree<T> tree = root(pattern, rootStorage, patternAmount, patternRepository, childListener);
final CraftingTree.CalculationResult calculationResult = tree.calculate();
Expand All @@ -45,4 +53,47 @@ public <T> void calculate(final ResourceKey resource,
}
listener.childCalculationCompleted(resource, lastPatternAmount.getTotal(), lastChildListener);
}

private boolean isCraftable(final ResourceKey resource, final long amount) {
final MissingResourcesCraftingCalculatorListener listener = new MissingResourcesCraftingCalculatorListener();
calculate(resource, amount, listener);
return !listener.isMissingResources();
}

@Override
public long getMaxAmount(final ResourceKey resource) {
try {
LOGGER.debug("Finding max amount for {} starting from 1", resource);
long low = 1;
long high = 1;
int calculationCount = 1;
while (isCraftable(resource, high)) {
low = high;
high = high * 2;
LOGGER.debug("Finding low and high for the craftable amount, currently between {} and {}", low, high);
calculationCount++;
}
if (low == high) {
return 0;
}
LOGGER.debug("Our craftable amount is between {} and {}", low, high);
while (low < high) {
final long amount = low + (high - low + 1) / 2;
LOGGER.debug("Trying {} (between {} and {})", amount, low, high);
calculationCount++;
if (isCraftable(resource, amount)) {
LOGGER.debug("{} was craftable, increasing our low amount", amount);
low = amount;
} else {
LOGGER.debug("{} is not craftable, decreasing our high amount", amount);
high = amount - 1;
}
}
LOGGER.debug("Found the maximum amount of {} in {} tries", low, calculationCount);
return low;
} catch (final CalculationException e) {
LOGGER.debug("Failed to calculate the maximum amount", e);
return 0;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.refinedmods.refinedstorage.api.autocrafting.calculation;

import com.refinedmods.refinedstorage.api.autocrafting.Pattern;
import com.refinedmods.refinedstorage.api.resource.ResourceAmount;
import com.refinedmods.refinedstorage.api.resource.ResourceKey;
import com.refinedmods.refinedstorage.api.resource.list.MutableResourceList;
import com.refinedmods.refinedstorage.api.resource.list.MutableResourceListImpl;
Expand Down Expand Up @@ -28,10 +29,18 @@ void extractFromStorage(final ResourceKey resource, final long amount) {

void addOutputsToInternalStorage(final Pattern pattern, final Amount amount) {
pattern.getOutputs().forEach(
output -> internalStorage.add(output.resource(), output.amount() * amount.iterations())
output -> addOutputToInternalStorage(amount, output)
);
}

private void addOutputToInternalStorage(final Amount amount, final ResourceAmount output) {
final long totalAmount = output.amount() * amount.iterations();
if (totalAmount < 0) {
throw new NumberOverflowDuringCalculationException();
}
internalStorage.add(output.resource(), totalAmount);
}

CraftingState copy() {
return new CraftingState(storage.copy(), internalStorage.copy());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
import com.refinedmods.refinedstorage.api.autocrafting.PatternRepository;
import com.refinedmods.refinedstorage.api.storage.root.RootStorage;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;

import static java.util.Objects.requireNonNull;
Expand All @@ -15,18 +17,21 @@ class CraftingTree<T> {
private final Amount amount;
private final PatternRepository patternRepository;
private final CraftingCalculatorListener<T> listener;
private final Set<Pattern> activePatterns;
private CraftingState craftingState;

private CraftingTree(final Pattern pattern,
final CraftingState craftingState,
final Amount amount,
final PatternRepository patternRepository,
final CraftingCalculatorListener<T> listener) {
final CraftingCalculatorListener<T> listener,
final Set<Pattern> activePatterns) {
this.pattern = pattern;
this.craftingState = craftingState;
this.amount = amount;
this.patternRepository = patternRepository;
this.listener = listener;
this.activePatterns = activePatterns;
}

static <T> CraftingTree<T> root(final Pattern pattern,
Expand All @@ -35,19 +40,23 @@ static <T> CraftingTree<T> root(final Pattern pattern,
final PatternRepository patternRepository,
final CraftingCalculatorListener<T> listener) {
final CraftingState craftingState = CraftingState.of(rootStorage);
return new CraftingTree<>(pattern, craftingState, amount, patternRepository, listener);
return new CraftingTree<>(pattern, craftingState, amount, patternRepository, listener, new HashSet<>());
}

static <T> CraftingTree<T> child(final Pattern pattern,
final CraftingState parentState,
final Amount amount,
final PatternRepository patternRepository,
final CraftingCalculatorListener<T> listener) {
final CraftingCalculatorListener<T> listener,
final Set<Pattern> activePatterns) {
return new CraftingTree<>(pattern, parentState.copy(), amount, patternRepository,
listener.childCalculationStarted());
listener.childCalculationStarted(), activePatterns);
}

CalculationResult calculate() {
if (!activePatterns.add(pattern)) {
throw new PatternCycleDetectedException(pattern);
}
CalculationResult result = CalculationResult.SUCCESS;
for (final Ingredient ingredient : pattern.getIngredients()) {
if (ingredient.isEmpty()) {
Expand All @@ -60,12 +69,16 @@ CalculationResult calculate() {
}
}
craftingState.addOutputsToInternalStorage(pattern, amount);
activePatterns.remove(pattern);
return result;
}

private CalculationResult calculateIngredient(final IngredientState ingredientState) {
CraftingState.ResourceState resourceState = craftingState.getResource(ingredientState.get());
long remaining = ingredientState.amount() * amount.iterations();
if (remaining < 0) {
throw new NumberOverflowDuringCalculationException();
}
while (remaining > 0) {
if (resourceState.isInInternalStorage()) {
final long toTake = Math.min(remaining, resourceState.inInternalStorage());
Expand Down Expand Up @@ -147,7 +160,8 @@ private ChildCalculationResult<T> calculateChild(final long remaining,
craftingState,
childAmount,
patternRepository,
listener
listener,
activePatterns
);
final CalculationResult childResult = childTree.calculate();
if (childResult == CalculationResult.MISSING_RESOURCES) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.refinedmods.refinedstorage.api.autocrafting.calculation;

import com.refinedmods.refinedstorage.api.resource.ResourceKey;

class MissingResourcesCraftingCalculatorListener implements CraftingCalculatorListener<Boolean> {
private boolean missingResources;

MissingResourcesCraftingCalculatorListener() {
}

MissingResourcesCraftingCalculatorListener(final boolean missingResources) {
this.missingResources = missingResources;
}

boolean isMissingResources() {
return missingResources;
}

@Override
public CraftingCalculatorListener<Boolean> childCalculationStarted() {
return new MissingResourcesCraftingCalculatorListener(missingResources);
}

@Override
public void childCalculationCompleted(final ResourceKey resource,
final long amount,
final CraftingCalculatorListener<Boolean> childListener) {
missingResources = ((MissingResourcesCraftingCalculatorListener) childListener).missingResources;
}

@Override
public void ingredientsExhausted(final ResourceKey resource, final long amount) {
missingResources = true;
}

@Override
public void ingredientExtractedFromStorage(final ResourceKey resource, final long amount) {
// no op
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.refinedmods.refinedstorage.api.autocrafting.calculation;

public class NumberOverflowDuringCalculationException extends CalculationException {
NumberOverflowDuringCalculationException() {
super("Invalid amount");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.refinedmods.refinedstorage.api.autocrafting.calculation;

import com.refinedmods.refinedstorage.api.autocrafting.Pattern;

public class PatternCycleDetectedException extends CalculationException {
private final Pattern pattern;

PatternCycleDetectedException(final Pattern pattern) {
super("Pattern loop detected in pattern " + pattern);
this.pattern = pattern;
}

public Pattern getPattern() {
return this.pattern;
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.refinedmods.refinedstorage.api.autocrafting.preview;

import com.refinedmods.refinedstorage.api.resource.ResourceAmount;

import java.util.List;

import org.apiguardian.api.API;

@API(status = API.Status.STABLE, since = "2.0.0-milestone.4.9")
public record Preview(PreviewType type, List<PreviewItem> items) {
public record Preview(PreviewType type, List<PreviewItem> items, List<ResourceAmount> outputsOfPatternWithCycle) {
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
package com.refinedmods.refinedstorage.api.autocrafting.preview;

import com.refinedmods.refinedstorage.api.autocrafting.Pattern;
import com.refinedmods.refinedstorage.api.core.CoreValidations;
import com.refinedmods.refinedstorage.api.resource.ResourceAmount;
import com.refinedmods.refinedstorage.api.resource.ResourceKey;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class PreviewBuilder {
private final PreviewType type;
private final Map<ResourceKey, MutablePreviewItem> items = new LinkedHashMap<>();

private List<ResourceAmount> outputsOfPatternWithCycle = Collections.emptyList();

private PreviewBuilder(final PreviewType type) {
this.type = type;
}
Expand All @@ -22,6 +28,11 @@ private MutablePreviewItem get(final ResourceKey resource) {
return items.computeIfAbsent(resource, key -> new MutablePreviewItem());
}

public PreviewBuilder withPatternWithCycle(final Pattern pattern) {
this.outputsOfPatternWithCycle = pattern.getOutputs();
return this;
}

public PreviewBuilder addAvailable(final ResourceKey resource, final long amount) {
CoreValidations.validateLargerThanZero(amount, "Available amount must be larger than 0");
get(resource).available += amount;
Expand All @@ -44,7 +55,7 @@ public Preview build() {
return new Preview(type, items.entrySet()
.stream()
.map(entry -> entry.getValue().toPreviewItem(entry.getKey()))
.toList());
.toList(), outputsOfPatternWithCycle);
}

private static class MutablePreviewItem {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package com.refinedmods.refinedstorage.api.autocrafting.preview;

import com.refinedmods.refinedstorage.api.autocrafting.calculation.CraftingCalculator;
import com.refinedmods.refinedstorage.api.autocrafting.calculation.CraftingCalculatorListener;
import com.refinedmods.refinedstorage.api.autocrafting.calculation.NumberOverflowDuringCalculationException;
import com.refinedmods.refinedstorage.api.autocrafting.calculation.PatternCycleDetectedException;
import com.refinedmods.refinedstorage.api.resource.ResourceKey;
import com.refinedmods.refinedstorage.api.resource.list.MutableResourceList;
import com.refinedmods.refinedstorage.api.resource.list.MutableResourceListImpl;

import java.util.Collections;
import java.util.UUID;

import org.slf4j.Logger;
Expand All @@ -22,8 +26,18 @@ private PreviewCraftingCalculatorListener(final PreviewState previewState) {
this.previewState = previewState;
}

public static PreviewCraftingCalculatorListener ofRoot() {
return new PreviewCraftingCalculatorListener(new PreviewState());
public static Preview calculatePreview(final CraftingCalculator calculator,
final ResourceKey resource,
final long amount) {
final PreviewCraftingCalculatorListener listener = new PreviewCraftingCalculatorListener(new PreviewState());
try {
calculator.calculate(resource, amount, listener);
} catch (final PatternCycleDetectedException e) {
return new Preview(PreviewType.CYCLE_DETECTED, Collections.emptyList(), e.getPattern().getOutputs());
} catch (final NumberOverflowDuringCalculationException e) {
return new Preview(PreviewType.OVERFLOW, Collections.emptyList(), Collections.emptyList());
}
return listener.buildPreview();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,7 @@
public interface PreviewProvider {
Optional<Preview> getPreview(ResourceKey resource, long amount);

long getMaxAmount(ResourceKey resource);

boolean startTask(ResourceKey resource, long amount);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@
@API(status = API.Status.STABLE, since = "2.0.0-milestone.4.9")
public enum PreviewType {
SUCCESS,
MISSING_RESOURCES
MISSING_RESOURCES,
CYCLE_DETECTED,
OVERFLOW
}
Loading

0 comments on commit 7ceb7e6

Please sign in to comment.