Crafting calculator v2 (#221)

* Update NEI

* Update buildscript

* Crafting Calculator V2 [WIP]

* Implement refunds

* Add a config option to switch crafting calculators, default to v2

* Initial AE2 glue code

* Fix scheduling as a Future

* The simplest of crafting now works

* Fix crafting cpu picking up the wrong pattern fuzzy matches

* Revert "use as substitute" changes in the old crafting calculator.
This commit is contained in:
Raven Szewczyk authored and GitHub committed 2023-01-12 19:25:42 +01:00
1 parent 2eaef34300
commit 406c256832
27 files changed
+1514 -96

No files matched your search

+11 -6
View File
@@ -1,4 +1,4 @@
//version: 1671313514
//version: 1673027205
/*
DO NOT CHANGE THIS FILE!
Also, you may replace this file at any time if there is an update available.
@@ -46,7 +46,7 @@ buildscript {
}
dependencies {
//Overwrite the current ASM version to fix shading newer than java 8 applicatations.
classpath 'org.ow2.asm:asm-debug-all-custom:5.0.3'
classpath 'org.ow2.asm:asm-debug-all-custom:5.0.3'
classpath 'net.minecraftforge.gradle:ForgeGradle:1.2.13'
}
}
@@ -319,9 +319,13 @@ if (file('addon.gradle').exists()) {
apply from: 'repositories.gradle'
configurations {
implementation.extendsFrom(shadowImplementation) // TODO: remove after all uses are refactored
implementation.extendsFrom(shadowCompile)
implementation.extendsFrom(shadeCompile)
// TODO: remove Compile after all uses are refactored to Implementation
for (config in [shadowImplementation, shadowCompile, shadeCompile]) {
compileClasspath.extendsFrom(config)
runtimeClasspath.extendsFrom(config)
testCompileClasspath.extendsFrom(config)
testRuntimeClasspath.extendsFrom(config)
}
}
repositories {
@@ -350,7 +354,8 @@ dependencies {
annotationProcessor('com.google.code.gson:gson:2.8.6')
annotationProcessor('com.gtnewhorizon:gtnhmixins:2.1.3:processor')
if (usesMixinDebug.toBoolean()) {
runtimeOnly('org.jetbrains:intellij-fernflower:1.2.1.16')
runtimeClasspath('org.jetbrains:intellij-fernflower:1.2.1.16')
testRuntimeClasspath('org.jetbrains:intellij-fernflower:1.2.1.16')
}
}
if (usesMixins.toBoolean() || forceEnableMixins.toBoolean()) {
+3 -3
View File
@@ -1,11 +1,11 @@
// Add your dependencies here
dependencies {
compile('com.github.GTNewHorizons:CodeChickenLib:1.1.5.5:dev')
compile('com.github.GTNewHorizons:NotEnoughItems:2.2.33-GTNH:dev')
compile('com.github.GTNewHorizons:CodeChickenLib:1.1.5.6:dev')
compile('com.github.GTNewHorizons:NotEnoughItems:2.3.20-GTNH:dev')
compile('curse.maven:cofh-core-69162:2388751')
compile('com.github.GTNewHorizons:BuildCraft:7.1.27:dev') {transitive = false}
compile('com.github.GTNewHorizons:BuildCraft:7.1.28:dev') {transitive = false}
compileOnly('com.github.GTNewHorizons:ForgeMultipart:1.2.8:dev') {transitive = false}
compileOnly('com.github.GTNewHorizons:GT5-Unofficial:5.09.41.18:dev') {transitive = false}
compileOnly('com.github.GTNewHorizons:Jabba:1.2.21:dev') {transitive = false}
@@ -28,6 +28,8 @@ import appeng.api.networking.IGridCache;
import appeng.api.networking.security.BaseActionSource;
import appeng.api.storage.data.IAEItemStack;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import java.util.concurrent.Future;
import net.minecraft.world.World;
@@ -44,6 +46,11 @@ public interface ICraftingGrid extends IGridCache {
ImmutableCollection<ICraftingPatternDetails> getCraftingFor(
IAEItemStack whatToCraft, ICraftingPatternDetails details, int slot, World world);
/**
* @return a collection of all the crafting patterns in the system
*/
ImmutableMap<IAEItemStack, ImmutableList<ICraftingPatternDetails>> getCraftingPatterns();
/**
* Begin calculating a crafting job.
*
@@ -23,8 +23,11 @@
package appeng.api.networking.crafting;
import appeng.api.networking.security.BaseActionSource;
import appeng.api.storage.data.IAEItemStack;
import appeng.api.storage.data.IItemList;
import appeng.crafting.MECraftingInventory;
import java.util.concurrent.Future;
public interface ICraftingJob {
@@ -51,4 +54,27 @@ public interface ICraftingJob {
* @return the final output of the job.
*/
IAEItemStack getOutput();
/**
* returns true if this needs more simulation.
*
* @param milli milliseconds of simulation
* @return true if this needs more simulation
*/
boolean simulateFor(final int milli);
Future<ICraftingJob> schedule();
/**
* @return whether this job can run on the given cluster
*/
default boolean supportsCPUCluster(final ICraftingCPU cluster) {
return false;
}
/**
* Begins crafting on a CPU cluster
*/
default void startCrafting(
final MECraftingInventory storage, final ICraftingCPU craftingCPUCluster, final BaseActionSource src) {}
}
@@ -23,6 +23,7 @@
package appeng.api.storage.data;
import java.util.ArrayList;
import java.util.Iterator;
/**
@@ -77,4 +78,12 @@ public interface IItemList<StackType extends IAEStack> extends IItemContainer<St
* resets stack sizes to 0.
*/
void resetStatus();
default StackType[] toArray(StackType[] zeroSizedArray) {
ArrayList<StackType> output = new ArrayList<>(size());
for (StackType stack : this) {
output.add(stack);
}
return output.toArray(zeroSizedArray);
}
}
+4
View File
@@ -124,6 +124,7 @@ public final class AEConfig extends Configuration implements IConfigurableObject
private double WirelessBoosterRangeMultiplier = 1;
private double WirelessBoosterExp = 1.5;
public int levelEmitterDelay = 40;
public int craftingCalculatorVersion = 2;
public AEConfig(final File configFile) {
super(configFile);
@@ -235,6 +236,9 @@ public final class AEConfig extends Configuration implements IConfigurableObject
this.quantumBridgeBackboneTransfer = this.get(
"debug", "EnableQuantumBridgeBackboneTransfer", this.quantumBridgeBackboneTransfer)
.getBoolean(this.quantumBridgeBackboneTransfer);
this.craftingCalculatorVersion = this.get("debug", "CraftingCalculatorVersion", this.craftingCalculatorVersion)
.getInt(this.craftingCalculatorVersion);
this.craftingCalculatorVersion = Math.max(1, Math.min(this.craftingCalculatorVersion, 2));
this.clientSync();
for (final AEFeature feature : AEFeature.values()) {
@@ -20,7 +20,7 @@ package appeng.crafting;
import appeng.api.storage.data.IAEItemStack;
public class CraftBranchFailure extends Exception {
public class CraftBranchFailure extends RuntimeException {
private static final long serialVersionUID = 654603652836724823L;
+21 -11
View File
@@ -23,10 +23,7 @@ import appeng.api.config.Actionable;
import appeng.api.networking.IGrid;
import appeng.api.networking.IGridHost;
import appeng.api.networking.IGridNode;
import appeng.api.networking.crafting.ICraftingCallback;
import appeng.api.networking.crafting.ICraftingGrid;
import appeng.api.networking.crafting.ICraftingJob;
import appeng.api.networking.crafting.ICraftingPatternDetails;
import appeng.api.networking.crafting.*;
import appeng.api.networking.security.BaseActionSource;
import appeng.api.networking.security.IActionHost;
import appeng.api.networking.security.MachineSource;
@@ -37,14 +34,17 @@ import appeng.api.storage.data.IItemList;
import appeng.api.util.DimensionalCoord;
import appeng.core.AELog;
import appeng.hooks.TickHandler;
import appeng.me.cache.CraftingGridCache;
import appeng.me.cluster.implementations.CraftingCPUCluster;
import com.google.common.base.Stopwatch;
import java.util.HashMap;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.world.World;
public class CraftingJob implements Runnable, ICraftingJob {
public class CraftingJob implements ICraftingJob, Runnable {
private static final String LOG_CRAFTING_JOB =
"CraftingJob (%s) issued by %s requesting [%s] using %s bytes took %s ms";
private static final String LOG_MACHINE_SOURCE_DETAILS = "Machine[object=%s, %s]";
@@ -118,6 +118,11 @@ public class CraftingJob implements Runnable, ICraftingJob {
this.missing.add(what);
}
@Override
public Future<ICraftingJob> schedule() {
return CraftingGridCache.getCraftingPool().submit(this, this);
}
@Override
public void run() {
try {
@@ -262,12 +267,6 @@ public class CraftingJob implements Runnable, ICraftingJob {
return this.world;
}
/**
* returns true if this needs more simulation.
*
* @param milli milliseconds of simulation
* @return true if this needs more simulation
*/
public boolean simulateFor(final int milli) {
this.time = milli;
@@ -335,6 +334,17 @@ public class CraftingJob implements Runnable, ICraftingJob {
}
}
@Override
public boolean supportsCPUCluster(ICraftingCPU cluster) {
return cluster instanceof CraftingCPUCluster;
}
@Override
public void startCrafting(MECraftingInventory storage, ICraftingCPU craftingCPUCluster, BaseActionSource src) {
CraftingCPUCluster cluster = (CraftingCPUCluster) craftingCPUCluster;
this.tree.setJob(storage, cluster, src);
}
private static class TwoIntegers {
private final long perOp = 0;
private final long times = 0;
@@ -309,8 +309,9 @@ public class CraftingTreeNode {
}
public void setJob(
final MECraftingInventory storage, final CraftingCPUCluster craftingCPUCluster, final BaseActionSource src)
throws CraftBranchFailure {
final MECraftingInventory storage,
final CraftingCPUCluster craftingCPUCluster,
final BaseActionSource src) {
for (final IAEItemStack i : this.used) {
final IAEItemStack ex = storage.extractItems(i, Actionable.MODULATE, src);
@@ -237,13 +237,8 @@ public class CraftingTreeProcess {
// more fuzzy!
for (final IAEItemStack is : this.details.getCondensedOutputs()) {
// there are now two ways this craft is triggered via fuzzy
// perfectMatch: done due to fuzziness in damage value
// oredict: done through substitutes
// catch both of them here
final boolean perfectMatch = is.getItem() == what2.getItem()
&& (is.getItem().isDamageable() || is.getItemDamage() == what2.getItemDamage());
if (perfectMatch || this.details.canBeSubstitute() && is.sameOre(what2)) {
if (is.getItem() == what2.getItem()
&& (is.getItem().isDamageable() || is.getItemDamage() == what2.getItemDamage())) {
what2 = is.copy();
what2.setStackSize(is.getStackSize());
return what2;
@@ -285,7 +285,7 @@ public class MECraftingInventory implements IMEInventory<IAEItemStack> {
this.missingCache.add(extra);
}
void ignore(final IAEItemStack what) {
public void ignore(final IAEItemStack what) {
final IAEItemStack list = this.localCache.findPrecise(what);
if (list != null) {
list.setStackSize(0);
@@ -0,0 +1,59 @@
package appeng.crafting.v2;
import appeng.api.storage.data.IAEFluidStack;
import appeng.api.storage.data.IAEItemStack;
import appeng.api.storage.data.IAEStack;
import appeng.core.AELog;
import appeng.crafting.v2.resolvers.*;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimaps;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.logging.log4j.Level;
/**
* You can register additional crafting handlers here
*/
public class CraftingCalculations {
private static final ListMultimap<Class<? extends IAEStack<?>>, CraftingRequestResolver<?>> providers =
ArrayListMultimap.create(2, 8);
public static <StackType extends IAEStack<StackType>> void registerProvider(
CraftingRequestResolver<StackType> provider, Class<StackType> stackTypeClass) {
providers.put(stackTypeClass, provider);
}
@SuppressWarnings("unchecked")
public static <StackType extends IAEStack<StackType>> List<CraftingTask> tryResolveCraftingRequest(
CraftingRequest<StackType> request, CraftingContext context) {
final ArrayList<CraftingTask> allTasks = new ArrayList<>(4);
for (final CraftingRequestResolver<?> unsafeProvider : Multimaps.filterKeys(
providers, key -> key.isAssignableFrom(request.stackTypeClass))
.values()) {
try {
// Safety: Filtered by type using Multimaps.filterKeys
final CraftingRequestResolver<StackType> provider = (CraftingRequestResolver<StackType>) unsafeProvider;
final List<CraftingTask> tasks = provider.provideCraftingRequestResolvers(request, context);
allTasks.addAll(tasks);
} catch (Exception t) {
AELog.log(
Level.DEBUG,
t,
"Error encountered when trying to generate the list of CraftingTasks for crafting {}",
request.toString());
}
}
allTasks.sort(CraftingTask.PRIORITY_COMPARATOR);
return Collections.unmodifiableList(allTasks);
}
static {
registerProvider(new ExtractItemResolver(), IAEItemStack.class);
registerProvider(new SimulateMissingItemResolver<>(), IAEItemStack.class);
registerProvider(new SimulateMissingItemResolver<>(), IAEFluidStack.class);
registerProvider(new EmitableItemResolver(), IAEItemStack.class);
registerProvider(new CraftableItemResolver(), IAEItemStack.class);
}
}
@@ -0,0 +1,251 @@
package appeng.crafting.v2;
import appeng.api.networking.IGrid;
import appeng.api.networking.crafting.ICraftingGrid;
import appeng.api.networking.crafting.ICraftingPatternDetails;
import appeng.api.networking.security.BaseActionSource;
import appeng.api.networking.storage.IStorageGrid;
import appeng.api.storage.data.IAEItemStack;
import appeng.api.storage.data.IAEStack;
import appeng.api.storage.data.IItemList;
import appeng.crafting.MECraftingInventory;
import appeng.crafting.v2.resolvers.CraftingTask;
import appeng.me.cluster.implementations.CraftingCPUCluster;
import appeng.util.item.OreListMultiMap;
import com.google.common.collect.ClassToInstanceMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.MutableClassToInstanceMap;
import java.util.*;
import java.util.function.Supplier;
import javax.annotation.Nonnull;
import net.minecraft.world.World;
/**
* A bundle of state for the crafting operation like the ME grid, who requested crafting, etc.
*/
public final class CraftingContext {
public final World world;
public final IGrid meGrid;
public final ICraftingGrid craftingGrid;
public BaseActionSource actionSource;
/**
* A working copy of the AE system's item list used for modelling what happens as crafting requests get resolved
*/
public final MECraftingInventory itemModel;
/**
* A cache of how many items were present at the beginning of the crafting request, do not modify
*/
public final MECraftingInventory availableCache;
public boolean wasSimulated = false;
public static final class RequestInProcessing<StackType extends IAEStack<StackType>> {
public final CraftingRequest<StackType> request;
/**
* Ordered by priority
*/
public final Deque<CraftingTask> resolvers = new ArrayDeque<>(4);
public RequestInProcessing(CraftingRequest<StackType> request) {
this.request = request;
}
}
private final List<RequestInProcessing<?>> liveRequests = new ArrayList<>(32);
private final List<CraftingTask> resolvedTasks = new ArrayList<>();
private final ArrayDeque<CraftingTask> tasksToProcess = new ArrayDeque<>(64);
private boolean doingWork = false;
// State at the point when the last task executed.
private CraftingTask.State finishedState = CraftingTask.State.FAILURE;
private final ImmutableMap<IAEItemStack, ImmutableList<ICraftingPatternDetails>> availablePatterns;
private final Map<IAEItemStack, List<ICraftingPatternDetails>> precisePatternCache = new HashMap<>();
private final OreListMultiMap<ICraftingPatternDetails> fuzzyPatternCache = new OreListMultiMap<>();
private final ClassToInstanceMap<Object> userCaches = MutableClassToInstanceMap.create();
public CraftingContext(@Nonnull World world, @Nonnull IGrid meGrid, @Nonnull BaseActionSource actionSource) {
this.world = world;
this.meGrid = meGrid;
this.craftingGrid = meGrid.getCache(ICraftingGrid.class);
this.actionSource = actionSource;
final IStorageGrid sg = meGrid.getCache(IStorageGrid.class);
this.itemModel = new MECraftingInventory(sg.getItemInventory(), this.actionSource, true, false, true);
this.availableCache = new MECraftingInventory(sg.getItemInventory(), this.actionSource, false, false, false);
this.availablePatterns = craftingGrid.getCraftingPatterns();
}
/**
* Can be used for custom caching in plugins
*/
public <T> T getUserCache(Class<T> cacheType, Supplier<T> constructor) {
// Can't use compute with generic types safely here
T instance = userCaches.getInstance(cacheType);
if (instance == null) {
instance = constructor.get();
userCaches.putInstance(cacheType, instance);
}
return instance;
}
public void addRequest(@Nonnull CraftingRequest<?> request) {
if (doingWork) {
throw new IllegalStateException(
"Trying to add requests while inside a CraftingTask handler, return requests in the StepOutput instead");
}
final RequestInProcessing<?> processing = new RequestInProcessing<>(request);
processing.resolvers.addAll(CraftingCalculations.tryResolveCraftingRequest(request, this));
liveRequests.add(processing);
if (processing.resolvers.isEmpty()) {
throw new IllegalStateException("No resolvers available for request " + request.toString());
}
queueNextTaskOf(processing, true);
}
public List<ICraftingPatternDetails> getPrecisePatternsFor(@Nonnull IAEItemStack stack) {
return precisePatternCache.compute(stack, (key, value) -> {
if (value == null) {
return availablePatterns.getOrDefault(stack, ImmutableList.of());
} else {
return value;
}
});
}
public List<ICraftingPatternDetails> getFuzzyPatternsFor(@Nonnull IAEItemStack stack) {
if (!fuzzyPatternCache.isPopulated()) {
for (final ImmutableList<ICraftingPatternDetails> patternSet : availablePatterns.values()) {
for (final ICraftingPatternDetails pattern : patternSet) {
if (pattern.canBeSubstitute()) {
for (final IAEItemStack output : pattern.getOutputs()) {
fuzzyPatternCache.put(output.copy(), pattern);
}
}
}
}
fuzzyPatternCache.freeze();
}
return fuzzyPatternCache.get(stack);
}
/**
* Does one unit of work towards solving the crafting problem.
*
* @return Is more work needed?
*/
public CraftingTask.State doWork() {
if (tasksToProcess.isEmpty()) {
return finishedState;
}
final CraftingTask frontTask = tasksToProcess.getFirst();
if (frontTask.getState() == CraftingTask.State.SUCCESS || frontTask.getState() == CraftingTask.State.FAILURE) {
resolvedTasks.add(frontTask);
tasksToProcess.removeFirst();
return CraftingTask.State.NEEDS_MORE_WORK;
}
doingWork = true;
CraftingTask.StepOutput out = frontTask.calculateOneStep(this);
CraftingTask.State newState = frontTask.getState();
doingWork = false;
if (out.extraInputsRequired.size() > 0) {
out.extraInputsRequired.forEach(this::addRequest);
} else if (newState == CraftingTask.State.SUCCESS) {
if (tasksToProcess.getFirst() != frontTask) {
throw new IllegalStateException("A crafting task got added to the queue without requesting more work.");
}
resolvedTasks.add(frontTask);
tasksToProcess.removeFirst();
finishedState = CraftingTask.State.SUCCESS;
} else if (newState == CraftingTask.State.FAILURE) {
tasksToProcess.clear();
finishedState = CraftingTask.State.FAILURE;
return CraftingTask.State.FAILURE;
}
return tasksToProcess.isEmpty() ? CraftingTask.State.SUCCESS : CraftingTask.State.NEEDS_MORE_WORK;
}
/**
* Gets the list of tasks that have finished executing, sorted topologically (dependencies before the tasks that require them)
* @return An unmodifiable list of resolved tasks.
*/
public List<CraftingTask> getResolvedTasks() {
return Collections.unmodifiableList(resolvedTasks);
}
/**
* Gets all requests that have been added to the context.
* @return An unmodifiable list of the requests.
*/
public List<RequestInProcessing<?>> getLiveRequests() {
return Collections.unmodifiableList(liveRequests);
}
/**
* @return If a task was added
*/
private boolean queueNextTaskOf(RequestInProcessing<?> request, boolean addResolverTask) {
if (request.request.remainingToProcess <= 0 || request.resolvers.isEmpty()) {
return false;
}
CraftingTask nextResolver = request.resolvers.removeFirst();
if (addResolverTask && !request.resolvers.isEmpty()) {
tasksToProcess.addFirst(new CheckOtherResolversTask(request));
}
tasksToProcess.addFirst(nextResolver);
return true;
}
/**
* A task to call queueNextTaskOf after a resolver gets computed to check if more resolving is needed for the same request-in-processing.
*/
private final class CheckOtherResolversTask extends CraftingTask {
private final RequestInProcessing<?> myRequest;
public CheckOtherResolversTask(RequestInProcessing<?> myRequest) {
super(0); // priority doesn't matter as this task is never a resolver output
this.myRequest = myRequest;
}
@Override
public StepOutput calculateOneStep(CraftingContext context) {
final boolean needsMoreWork = queueNextTaskOf(myRequest, false);
if (needsMoreWork) {
this.state = State.NEEDS_MORE_WORK;
} else if (myRequest.request.remainingToProcess <= 0) {
this.state = State.SUCCESS;
} else {
this.state = State.FAILURE;
}
return new StepOutput();
}
@Override
public void partialRefund(CraftingContext context, long amount) {
// no-op
}
@Override
public void fullRefund(CraftingContext context) {
// no-op
}
@Override
public void populatePlan(IItemList<IAEItemStack> targetPlan) {
// no-op
}
@Override
public void startOnCpu(
CraftingContext context, CraftingCPUCluster cpuCluster, MECraftingInventory craftingInv) {
// no-op
}
@Override
public String toString() {
return "CheckOtherResolversTask{" + "myRequest="
+ myRequest + ", priority="
+ priority + ", state="
+ state + '}';
}
}
}
@@ -0,0 +1,181 @@
package appeng.crafting.v2;
import appeng.api.networking.IGrid;
import appeng.api.networking.crafting.ICraftingCPU;
import appeng.api.networking.crafting.ICraftingCallback;
import appeng.api.networking.crafting.ICraftingJob;
import appeng.api.networking.security.BaseActionSource;
import appeng.api.storage.data.IAEItemStack;
import appeng.api.storage.data.IItemList;
import appeng.crafting.MECraftingInventory;
import appeng.crafting.v2.CraftingContext.RequestInProcessing;
import appeng.crafting.v2.CraftingRequest.SubstitutionMode;
import appeng.crafting.v2.resolvers.CraftingTask;
import appeng.crafting.v2.resolvers.CraftingTask.State;
import appeng.hooks.TickHandler;
import appeng.me.cluster.implementations.CraftingCPUCluster;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import net.minecraft.world.World;
/**
* A new, self-contained implementation of the crafting calculator.
* Does an iterative search on the crafting recipe tree.
*/
public class CraftingJobV2 implements ICraftingJob, Future<ICraftingJob> {
protected volatile long totalByteCost = -1; // -1 means it needs to be recalculated
protected CraftingContext context;
protected final CraftingRequest<IAEItemStack> originalRequest;
protected ICraftingCallback callback;
protected enum State {
RUNNING,
FINISHED,
CANCELLED
}
protected State state = State.RUNNING;
public CraftingJobV2(
final World world,
final IGrid meGrid,
final BaseActionSource actionSource,
final IAEItemStack what,
final ICraftingCallback callback) {
this.context = new CraftingContext(world, meGrid, actionSource);
this.callback = callback;
this.originalRequest = new CraftingRequest<>(what, SubstitutionMode.PRECISE_FRESH, IAEItemStack.class, true);
this.context.addRequest(this.originalRequest);
this.context.itemModel.ignore(what);
}
@Override
public boolean isSimulation() {
return context.wasSimulated;
}
@Override
public long getByteTotal() {
long byteCost = totalByteCost;
if (byteCost < 0) {
byteCost = 0;
for (RequestInProcessing<?> request : context.getLiveRequests()) {
byteCost += request.request.byteCost;
}
totalByteCost = byteCost;
}
return byteCost;
}
@Override
public void populatePlan(IItemList<IAEItemStack> plan) {
for (CraftingTask task : context.getResolvedTasks()) {
task.populatePlan(plan);
}
}
@Override
public IAEItemStack getOutput() {
return originalRequest.stack;
}
@Override
public boolean simulateFor(int milli) {
if (this.state != State.RUNNING) {
return false;
}
final long startTime = System.currentTimeMillis();
final long finishTime = startTime + milli;
CraftingTask.State taskState = CraftingTask.State.NEEDS_MORE_WORK;
do {
taskState = context.doWork();
totalByteCost = -1;
} while (taskState.needsMoreWork && System.currentTimeMillis() < finishTime && (state == State.RUNNING));
if (!taskState.needsMoreWork) {
getByteTotal();
this.state = State.FINISHED;
if (callback != null) {
callback.calculationComplete(this);
}
}
return taskState.needsMoreWork;
}
@Override
public Future<ICraftingJob> schedule() {
TickHandler.INSTANCE.registerCraftingSimulation(this.context.world, this);
return this;
}
@Override
public boolean supportsCPUCluster(ICraftingCPU cluster) {
return cluster instanceof CraftingCPUCluster;
}
@Override
public void startCrafting(MECraftingInventory storage, ICraftingCPU rawCluster, BaseActionSource src) {
if (this.state == State.RUNNING) {
throw new IllegalStateException(
"Trying to start crafting a not fully calculated job for " + originalRequest.toString());
}
CraftingCPUCluster cluster = (CraftingCPUCluster) rawCluster;
context.actionSource = src;
List<CraftingTask> resolvedTasks = context.getResolvedTasks();
for (CraftingTask task : resolvedTasks) {
task.startOnCpu(context, cluster, storage);
}
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
if (this.state != State.RUNNING) {
return false;
} else {
this.state = State.CANCELLED;
return true;
}
}
@Override
public boolean isCancelled() {
return state == State.CANCELLED;
}
@Override
public boolean isDone() {
return state != State.RUNNING;
}
@Override
public CraftingJobV2 get() throws InterruptedException, ExecutionException {
this.simulateFor(Integer.MAX_VALUE);
return this;
}
@Override
public CraftingJobV2 get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
try {
this.simulateFor((int) unit.convert(timeout, TimeUnit.MILLISECONDS));
} catch (Exception e) {
throw new ExecutionException(e);
}
switch (this.state) {
case RUNNING:
throw new TimeoutException();
case CANCELLED:
throw new InterruptedException();
case FINISHED:
return this;
default:
throw new IllegalStateException();
}
}
}
@@ -0,0 +1,149 @@
package appeng.crafting.v2;
import appeng.api.config.Actionable;
import appeng.api.storage.data.IAEFluidStack;
import appeng.api.storage.data.IAEItemStack;
import appeng.api.storage.data.IAEStack;
import appeng.api.storage.data.IItemList;
import appeng.crafting.v2.resolvers.CraftingTask;
import appeng.util.item.FluidList;
import appeng.util.item.ItemList;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
/**
* A single requested stack (item or fluid) to craft, e.g. 32x Torches
*
* @param <StackType> Should be {@link IAEItemStack} or {@link appeng.api.storage.data.IAEFluidStack}
*/
public class CraftingRequest<StackType extends IAEStack<StackType>> {
public enum SubstitutionMode {
/**
* No substitution, do not use items from the AE system - used for user-started requests
*/
PRECISE_FRESH,
/**
* Use precisely the item requested
*/
PRECISE,
/**
* Allow fuzzy matching of ingredients, the request will have a {@link CraftingRequest#acceptableSubstituteFn} predicate to determine if the given fuzzy match item is valid
*/
ACCEPT_FUZZY
}
public final Class<StackType> stackTypeClass;
/**
* An item/fluid + count representing how many need to be crafted
*/
public final StackType stack;
public final SubstitutionMode substitutionMode;
public final Predicate<StackType> acceptableSubstituteFn;
public final IItemList<StackType> resolvedInputs;
public final List<CraftingTask> usedResolvers = new ArrayList<>();
/**
* Whether this request and its children can be fulfilled by simulations
*/
public final boolean allowSimulation;
/**
* The number of yet-unresolved elements from the stack (items/mB) that need to be crafted. Can go into the negatives if more are crafted than needed.
*/
public volatile long remainingToProcess;
/**
* The cost in bytes to process this task so far
*/
public volatile long byteCost = 0;
/**
* If the item had to be simulated (there was not enough ingredients in the system to fulfill this request in any way)
*/
public volatile boolean wasSimulated = false;
/**
* @param stack The item/fluid and stack to request
* @param substitutionMode Whether and how to allow substitutions when resolving this request
* @param stackTypeClass Pass in {@code StackType.class}, needed for resolving types at runtime
* @param acceptableSubstituteFn A predicate testing if a given item (in fuzzy mode) can fulfill the request
*/
public CraftingRequest(
StackType stack,
SubstitutionMode substitutionMode,
Class<StackType> stackTypeClass,
boolean allowSimulation,
Predicate<StackType> acceptableSubstituteFn) {
this.stackTypeClass = stackTypeClass;
this.stack = stack;
this.substitutionMode = substitutionMode;
this.acceptableSubstituteFn = acceptableSubstituteFn;
this.remainingToProcess = stack.getStackSize();
this.allowSimulation = allowSimulation;
if (stackTypeClass == IAEItemStack.class) {
this.resolvedInputs = (IItemList<StackType>) new ItemList();
} else if (stackTypeClass == IAEFluidStack.class) {
this.resolvedInputs = (IItemList<StackType>) new FluidList();
} else {
throw new IllegalArgumentException(
"Invalid stack type for a crafting request: " + stackTypeClass.getName());
}
}
/**
* @param request The item/fluid and stack to request
* @param substitutionMode Whether and how to allow substitutions when resolving this request
* @param stackTypeClass Pass in {@code StackType.class}, needed for resolving types at runtime
*/
public CraftingRequest(
StackType request,
SubstitutionMode substitutionMode,
Class<StackType> stackTypeClass,
boolean allowSimulation) {
this(request, substitutionMode, stackTypeClass, allowSimulation, stack -> true);
if (substitutionMode == SubstitutionMode.ACCEPT_FUZZY) {
throw new IllegalArgumentException("Fuzzy requests must have a substitution-valid predicate");
}
}
@Override
public String toString() {
return "CraftingRequest{request=" + stack + ", substitutionMode=" + substitutionMode + ", remainingToProcess="
+ remainingToProcess + ", byteCost=" + byteCost + ", wasSimulated=" + wasSimulated + '}';
}
/**
* Reduces the items needed to fulfill this request, and adds any leftovers into the item cache of the context.
*/
public void fulfill(CraftingTask origin, StackType input, CraftingContext context) {
if (input.getStackSize() < 0) {
throw new IllegalArgumentException("Can't fulfill crafting request with a negative amount of " + input);
}
final long consumed = Math.max(0L, this.remainingToProcess);
final long remaining = input.getStackSize() - consumed;
if (remaining > 0 && input instanceof IAEItemStack) {
context.itemModel.injectItems((IAEItemStack) input, Actionable.MODULATE, context.actionSource);
}
this.byteCost += input.getStackSize();
this.remainingToProcess -= input.getStackSize();
this.resolvedInputs.add(input);
}
/**
* Reduces the amount of items needed by {@code amount}, propagating any necessary refunds via the resolver crafting tasks.
*/
public void partialRefund(CraftingContext context, long amount) {
for (CraftingTask task : usedResolvers) {
task.partialRefund(context, amount);
}
final long processed = this.stack.getStackSize() - this.remainingToProcess;
this.stack.setStackSize(this.stack.getStackSize() - amount);
this.remainingToProcess = this.stack.getStackSize() - processed;
}
public void fullRefund(CraftingContext context) {
for (CraftingTask task : usedResolvers) {
task.fullRefund(context);
}
this.remainingToProcess = 0;
this.stack.setStackSize(0);
}
}
@@ -0,0 +1,309 @@
package appeng.crafting.v2.resolvers;
import appeng.api.config.Actionable;
import appeng.api.config.FuzzyMode;
import appeng.api.networking.crafting.ICraftingPatternDetails;
import appeng.api.storage.data.IAEItemStack;
import appeng.api.storage.data.IItemList;
import appeng.crafting.MECraftingInventory;
import appeng.crafting.v2.CraftingContext;
import appeng.crafting.v2.CraftingRequest;
import appeng.crafting.v2.CraftingRequest.SubstitutionMode;
import appeng.me.cluster.implementations.CraftingCPUCluster;
import appeng.util.Platform;
import appeng.util.item.HashBasedItemList;
import com.google.common.collect.ImmutableList;
import java.util.*;
import java.util.Map.Entry;
import javax.annotation.Nonnull;
import net.minecraft.world.World;
public class CraftableItemResolver implements CraftingRequestResolver<IAEItemStack> {
public static class RequestAndPerCraftAmount {
public final CraftingRequest<IAEItemStack> request;
public final long perCraftAmount;
public RequestAndPerCraftAmount(CraftingRequest<IAEItemStack> request, long perCraftAmount) {
this.request = request;
this.perCraftAmount = perCraftAmount;
}
}
public static class CraftFromPatternTask extends CraftingTask {
public final CraftingRequest<IAEItemStack> request;
public final ICraftingPatternDetails pattern;
public final boolean allowSimulation;
// Inputs needed to kickstart recursive crafting
protected final IAEItemStack[] patternRecursionInputs;
// With the recursive part subtracted
protected final IAEItemStack[] patternInputs;
// With the recursive part subtracted
protected final IAEItemStack[] patternOutputs;
protected final IAEItemStack matchingOutput;
protected final Map<IAEItemStack, RequestAndPerCraftAmount> childRequests = new HashMap<>();
protected final Map<IAEItemStack, CraftingRequest<IAEItemStack>> childRecursionRequests = new HashMap<>();
// byproduct injected -> amount per craft
protected final IdentityHashMap<IAEItemStack, Long> byproducts = new IdentityHashMap<>();
protected boolean requestedInputs = false;
protected long totalCraftsDone = 0;
public CraftFromPatternTask(
CraftingRequest<IAEItemStack> request,
ICraftingPatternDetails pattern,
int priority,
boolean allowSimulation) {
super(priority);
this.request = request;
this.pattern = pattern;
this.allowSimulation = allowSimulation;
HashBasedItemList pInputs = new HashBasedItemList();
HashBasedItemList pOutputs = new HashBasedItemList();
HashBasedItemList pRecInputs = new HashBasedItemList();
Arrays.stream(pattern.getInputs()).filter(Objects::nonNull).forEach(pInputs::add);
Arrays.stream(pattern.getOutputs()).filter(Objects::nonNull).forEach(pOutputs::add);
for (IAEItemStack output : pOutputs) {
IAEItemStack input = pInputs.findPrecise(output);
if (input != null) {
final long netProduced = output.getStackSize() - input.getStackSize();
if (netProduced > 0) {
pRecInputs.add(input);
input.setStackSize(0);
output.setStackSize(netProduced);
} else {
// Ensure recInput.stackSize + input.stackSize == original input.stackSize
pRecInputs.add(input.copy().setStackSize(input.getStackSize() + netProduced));
input.setStackSize(-netProduced);
output.setStackSize(0);
}
}
}
this.patternInputs = pInputs.toArray(new IAEItemStack[0]);
this.patternOutputs = pOutputs.toArray(new IAEItemStack[0]);
this.patternRecursionInputs = pRecInputs.toArray(new IAEItemStack[0]);
IAEItemStack foundMatchingOutput = null;
for (final IAEItemStack patternOutput : patternOutputs) {
if (isOutputSameAs(patternOutput)) {
foundMatchingOutput = patternOutput;
break;
}
}
if (foundMatchingOutput == null) {
state = State.FAILURE;
throw new IllegalStateException("Invalid pattern crafting step for " + request);
}
this.matchingOutput = foundMatchingOutput;
}
public boolean isOutputSameAs(IAEItemStack otherStack) {
if (request.substitutionMode == SubstitutionMode.ACCEPT_FUZZY) {
return this.request.stack.fuzzyComparison(otherStack, FuzzyMode.IGNORE_ALL);
} else {
return this.request.stack.isSameType(otherStack);
}
}
public boolean isValidSubstitute(IAEItemStack reference, IAEItemStack stack, World world) {
if (!pattern.isCraftable()) {
return true;
}
IAEItemStack[] rawInputs = pattern.getInputs();
for (int slot = 0; slot < rawInputs.length; slot++) {
if (rawInputs[slot] != null && rawInputs[slot].isSameType(reference)) {
return pattern.isValidItemForSlot(slot, stack.getItemStack(), world);
}
}
return true;
}
@Override
public StepOutput calculateOneStep(CraftingContext context) {
if (request.remainingToProcess <= 0) {
state = State.SUCCESS;
return new StepOutput(Collections.emptyList());
}
final boolean canUseSubstitutes = pattern.canSubstitute();
final SubstitutionMode childMode =
canUseSubstitutes ? SubstitutionMode.ACCEPT_FUZZY : SubstitutionMode.PRECISE;
final long toCraft = Platform.ceilDiv(request.remainingToProcess, matchingOutput.getStackSize());
if (requestedInputs) {
// Calculate how many full recipes we could fulfill
long maxCraftable = toCraft;
for (CraftingRequest<IAEItemStack> recInputChild : childRecursionRequests.values()) {
if (recInputChild.remainingToProcess > 0) {
// If we can't resolve an input to the recursive process, we can't craft anything at all
maxCraftable = 0;
}
}
for (RequestAndPerCraftAmount inputChildPair : childRequests.values()) {
final CraftingRequest<IAEItemStack> inputChild = inputChildPair.request;
final long costPerRecipe = inputChild.stack.getStackSize() / toCraft;
final long available = inputChild.stack.getStackSize() - inputChild.remainingToProcess;
final long fullRecipes = available / costPerRecipe;
maxCraftable = Math.min(maxCraftable, fullRecipes);
}
// Fulfill those recipes
request.fulfill(
this,
matchingOutput
.copy()
.setStackSize(Math.multiplyExact(maxCraftable, matchingOutput.getStackSize())),
context);
for (IAEItemStack output : patternOutputs) {
// add byproducts to the system
if (output != matchingOutput) {
final IAEItemStack injected =
output.copy().setStackSize(Math.multiplyExact(maxCraftable, output.getStackSize()));
context.itemModel.injectItems(injected, Actionable.MODULATE, context.actionSource);
this.byproducts.put(injected.copy(), output.getStackSize());
}
}
this.totalCraftsDone = maxCraftable;
if (maxCraftable != toCraft) {
// Need to refund some items as not everything could be crafted.
for (RequestAndPerCraftAmount inputChildPair : childRequests.values()) {
final CraftingRequest<IAEItemStack> inputChild = inputChildPair.request;
final long actuallyNeeded = Math.multiplyExact(inputChild.stack.getStackSize(), maxCraftable);
final long produced =
inputChild.stack.getStackSize() - Math.max(inputChild.remainingToProcess, 0);
if (produced > actuallyNeeded) {
if (maxCraftable == 0) {
inputChild.fullRefund(context);
} else {
inputChild.partialRefund(context, produced - actuallyNeeded);
}
}
}
// If we couldn't craft even a single recipe, refund recursive inputs too
if (maxCraftable == 0) {
for (CraftingRequest<IAEItemStack> recChild : childRecursionRequests.values()) {
recChild.fullRefund(context);
}
}
}
state = State.SUCCESS;
return new StepOutput(Collections.emptyList());
} else {
ArrayList<CraftingRequest<IAEItemStack>> newChildren = new ArrayList<>(patternInputs.length);
if (patternRecursionInputs.length > 0) {
for (IAEItemStack recInput : patternRecursionInputs) {
CraftingRequest<IAEItemStack> req = new CraftingRequest<>(
recInput.copy(),
childMode,
IAEItemStack.class,
allowSimulation,
stack -> this.isValidSubstitute(recInput, stack, context.world));
newChildren.add(req);
childRecursionRequests.put(recInput, req);
}
state = State.NEEDS_MORE_WORK;
}
for (IAEItemStack input : patternInputs) {
final long amount = Math.multiplyExact(input.getStackSize(), toCraft);
CraftingRequest<IAEItemStack> req = new CraftingRequest<>(
input.copy().setStackSize(amount),
childMode,
IAEItemStack.class,
allowSimulation,
stack -> this.isValidSubstitute(input, stack, context.world));
newChildren.add(req);
childRequests.put(input, new RequestAndPerCraftAmount(req, input.getStackSize()));
}
requestedInputs = true;
state = State.NEEDS_MORE_WORK;
return new StepOutput(Collections.unmodifiableList(newChildren));
}
}
@Override
public void partialRefund(CraftingContext context, long amount) {
if (amount >= totalCraftsDone) {
fullRefund(context);
return;
}
totalCraftsDone -= amount;
amount = amount / matchingOutput.getStackSize();
if (amount == 0) {
return;
}
for (RequestAndPerCraftAmount subrequest : childRequests.values()) {
subrequest.request.partialRefund(context, subrequest.perCraftAmount * amount);
}
for (Entry<IAEItemStack, Long> entry : byproducts.entrySet()) {
final IAEItemStack byproductStack = entry.getKey();
final long perCraft = entry.getValue();
context.itemModel.extractItems(
byproductStack.copy().setStackSize(perCraft * amount),
Actionable.MODULATE,
context.actionSource);
entry.getKey().setStackSize(byproductStack.getStackSize() - amount * perCraft);
}
}
@Override
public void fullRefund(CraftingContext context) {
totalCraftsDone = 0;
childRequests.values().forEach(req -> req.request.fullRefund(context));
childRequests.clear();
childRecursionRequests.values().forEach(req -> req.fullRefund(context));
childRecursionRequests.clear();
}
@Override
public void populatePlan(IItemList<IAEItemStack> targetPlan) {
if (totalCraftsDone == 0) {
return;
}
for (IAEItemStack output : patternOutputs) {
targetPlan.addRequestable(output.copy().setCountRequestable(output.getStackSize() * totalCraftsDone));
}
}
@Override
public void startOnCpu(
CraftingContext context, CraftingCPUCluster cpuCluster, MECraftingInventory craftingInv) {
cpuCluster.addCrafting(pattern, totalCraftsDone);
}
@Override
public String toString() {
return "CraftFromPatternTask{" + "request="
+ request + ", pattern="
+ pattern + ", allowSimulation="
+ allowSimulation + ", matchingOutput="
+ matchingOutput + ", requestedInputs="
+ requestedInputs + ", totalCraftsDone="
+ totalCraftsDone + ", priority="
+ priority + ", state="
+ state + '}';
}
}
@Nonnull
@Override
public List<CraftingTask> provideCraftingRequestResolvers(
@Nonnull CraftingRequest<IAEItemStack> request, @Nonnull CraftingContext context) {
ImmutableList.Builder<CraftingTask> tasks = new ImmutableList.Builder<>();
final List<ICraftingPatternDetails> patterns = new ArrayList<>(context.getPrecisePatternsFor(request.stack));
patterns.sort(Comparator.comparing(ICraftingPatternDetails::getPriority).reversed());
// If fuzzy patterns are allowed,
if (request.substitutionMode == SubstitutionMode.ACCEPT_FUZZY) {
final List<ICraftingPatternDetails> fuzzyPatterns =
new ArrayList<>(context.getFuzzyPatternsFor(request.stack));
fuzzyPatterns.sort(
Comparator.comparing(ICraftingPatternDetails::getPriority).reversed());
patterns.addAll(fuzzyPatterns);
}
int priority = CraftingTask.PRIORITY_CRAFT_OFFSET + patterns.size() - 1;
for (ICraftingPatternDetails pattern : patterns) {
tasks.add(new CraftFromPatternTask(request, pattern, priority, false));
priority--;
}
// Fallback: use highest priority pattern to simulate if nothing else works
if (!patterns.isEmpty()) {
tasks.add(new CraftFromPatternTask(request, patterns.get(0), CraftingTask.PRIORITY_SIMULATE_CRAFT, true));
}
return tasks.build();
}
}
@@ -0,0 +1,28 @@
package appeng.crafting.v2.resolvers;
import appeng.api.storage.data.IAEStack;
import appeng.crafting.v2.CraftingContext;
import appeng.crafting.v2.CraftingRequest;
import java.util.List;
import javax.annotation.Nonnull;
/**
* See {@link CraftingRequestResolver#provideCraftingRequestResolvers(CraftingRequest, CraftingContext)}
*/
@FunctionalInterface
public interface CraftingRequestResolver<StackType extends IAEStack<StackType>> {
/**
* Provides a list of potential solutions for doing one crafting step, the calculator will try the solutions ordered by priority in turn.
* For example, given a plank item, it can give tasks for log->plank crafting
* Higher priority = will be used first, Integer.MAX_VALUE priority is used for items already in the system. <p>
* <p>
* It should reduce the {@link CraftingRequest#remainingToProcess} value by the number of items immediately available.
*
* @param request The request being resolved
* @param context The ME system, job queue and pattern caches used to calculate potential resolutions
* @return The list of potential solutions - return an empty list if none are available
*/
@Nonnull
List<CraftingTask> provideCraftingRequestResolvers(
@Nonnull CraftingRequest<StackType> request, @Nonnull CraftingContext context);
}
@@ -0,0 +1,87 @@
package appeng.crafting.v2.resolvers;
import appeng.api.storage.data.IAEItemStack;
import appeng.api.storage.data.IItemList;
import appeng.crafting.MECraftingInventory;
import appeng.crafting.v2.CraftingContext;
import appeng.crafting.v2.CraftingRequest;
import appeng.me.cluster.implementations.CraftingCPUCluster;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import javax.annotation.Nonnull;
/**
* A single action that can be performed to solve a {@link CraftingRequest}.
* Can have multiple inputs and outputs, resolved at runtime during crafting resolution (e.g. for handling substitutions).
*/
public abstract class CraftingTask {
public enum State {
NEEDS_MORE_WORK(true),
SUCCESS(false),
/**
* This aborts the entire crafting operation, use only if absolutely necessary
*/
FAILURE(false);
public final boolean needsMoreWork;
State(boolean needsMoreWork) {
this.needsMoreWork = needsMoreWork;
}
}
public static final class StepOutput {
@Nonnull
public final List<CraftingRequest<?>> extraInputsRequired;
public StepOutput() {
this(Collections.emptyList());
}
public StepOutput(@Nonnull List<CraftingRequest<?>> extraInputsRequired) {
this.extraInputsRequired = extraInputsRequired;
}
}
public static final int PRIORITY_EXTRACT = Integer.MAX_VALUE - 100;
public static final int PRIORITY_CRAFTING_EMITTER = PRIORITY_EXTRACT - 200;
/** Gets added to a priority determined by the priority of the crafting pattern */
public static final int PRIORITY_CRAFT_OFFSET = 0;
public static final int PRIORITY_SIMULATE_CRAFT = Integer.MIN_VALUE + 200;
public static final int PRIORITY_SIMULATE = Integer.MIN_VALUE + 100;
public final int priority;
protected State state;
/**
* Called when it's this task's turn for computation.
*
* @return A {@link StepOutput} instance describing progress made by the task in this call.
* If success or failure, the task should have cleaned up after itself - cancel won't be called.
*/
public abstract StepOutput calculateOneStep(CraftingContext context);
public abstract void partialRefund(CraftingContext context, long amount);
public abstract void fullRefund(CraftingContext context);
public abstract void populatePlan(IItemList<IAEItemStack> targetPlan);
public abstract void startOnCpu(
CraftingContext context, CraftingCPUCluster cpuCluster, MECraftingInventory craftingInv);
protected CraftingTask(int priority) {
this.priority = priority;
this.state = State.NEEDS_MORE_WORK;
}
public State getState() {
return state;
}
/**
* Compares priorities - highest priority first
*/
public static final Comparator<CraftingTask> PRIORITY_COMPARATOR = Comparator.comparing(ct -> -ct.priority);
}
@@ -0,0 +1,72 @@
package appeng.crafting.v2.resolvers;
import appeng.api.storage.data.IAEItemStack;
import appeng.api.storage.data.IItemList;
import appeng.crafting.MECraftingInventory;
import appeng.crafting.v2.CraftingContext;
import appeng.crafting.v2.CraftingRequest;
import appeng.me.cluster.implementations.CraftingCPUCluster;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nonnull;
public class EmitableItemResolver implements CraftingRequestResolver<IAEItemStack> {
public static class EmitItemTask extends CraftingTask {
public final CraftingRequest<IAEItemStack> request;
public EmitItemTask(CraftingRequest<IAEItemStack> request) {
super(CraftingTask.PRIORITY_CRAFTING_EMITTER); // conjure items for calculations out of thin air as a last
// resort
this.request = request;
}
@Override
public StepOutput calculateOneStep(CraftingContext context) {
if (request.remainingToProcess <= 0) {
state = State.SUCCESS;
return new StepOutput(Collections.emptyList());
}
// Assume items will be generated, triggered by the emitter
request.fulfill(this, request.stack.copy().setStackSize(request.remainingToProcess), context);
state = State.SUCCESS;
return new StepOutput(Collections.emptyList());
}
@Override
public void partialRefund(CraftingContext context, long amount) {
// no-op: items were simulated to be emitted, so there's nothing to refund
}
@Override
public void fullRefund(CraftingContext context) {
// no-op: items were simulated to be emitted, so there's nothing to refund
}
@Override
public void populatePlan(IItemList<IAEItemStack> targetPlan) {
targetPlan.addRequestable(request.stack.copy());
}
@Override
public void startOnCpu(
CraftingContext context, CraftingCPUCluster cpuCluster, MECraftingInventory craftingInv) {
cpuCluster.addEmitable(this.request.stack.copy());
}
@Override
public String toString() {
return "EmitItemTask{" + "request=" + request + ", priority=" + priority + ", state=" + state + '}';
}
}
@Nonnull
@Override
public List<CraftingTask> provideCraftingRequestResolvers(
@Nonnull CraftingRequest<IAEItemStack> request, @Nonnull CraftingContext context) {
if (context.craftingGrid.canEmitFor(request.stack)) {
return Collections.singletonList(new EmitItemTask(request));
} else {
return Collections.emptyList();
}
}
}
@@ -0,0 +1,132 @@
package appeng.crafting.v2.resolvers;
import appeng.api.config.Actionable;
import appeng.api.config.FuzzyMode;
import appeng.api.storage.data.IAEItemStack;
import appeng.api.storage.data.IItemList;
import appeng.crafting.CraftBranchFailure;
import appeng.crafting.MECraftingInventory;
import appeng.crafting.v2.CraftingContext;
import appeng.crafting.v2.CraftingRequest;
import appeng.me.cluster.implementations.CraftingCPUCluster;
import java.util.*;
import javax.annotation.Nonnull;
public class ExtractItemResolver implements CraftingRequestResolver<IAEItemStack> {
public static class ExtractItemTask extends CraftingTask {
public final CraftingRequest<IAEItemStack> request;
public final List<IAEItemStack> removedFromSystem = new ArrayList<>();
public ExtractItemTask(CraftingRequest<IAEItemStack> request) {
super(CraftingTask.PRIORITY_EXTRACT); // always try to extract items first
this.request = request;
}
@Override
public StepOutput calculateOneStep(CraftingContext context) {
state = State.SUCCESS;
if (request.remainingToProcess <= 0) {
return new StepOutput(Collections.emptyList());
}
// Extract exact
IAEItemStack exactMatching = context.itemModel.getItemList().findPrecise(request.stack);
if (exactMatching != null) {
final long requestSize = Math.min(request.remainingToProcess, exactMatching.getStackSize());
final IAEItemStack extracted = context.itemModel.extractItems(
exactMatching.copy().setStackSize(requestSize), Actionable.MODULATE, context.actionSource);
request.fulfill(this, extracted, context);
removedFromSystem.add(extracted.copy());
}
// Extract fuzzy
if (request.remainingToProcess > 0
&& request.substitutionMode == CraftingRequest.SubstitutionMode.ACCEPT_FUZZY) {
Collection<IAEItemStack> fuzzyMatching =
context.itemModel.getItemList().findFuzzy(request.stack, FuzzyMode.IGNORE_ALL);
for (final IAEItemStack candidate : fuzzyMatching) {
if (candidate == null) {
continue;
}
if (request.acceptableSubstituteFn.test(candidate)) {
final long requestSize = Math.min(request.remainingToProcess, candidate.getStackSize());
final IAEItemStack extracted = context.itemModel.extractItems(
candidate.copy().setStackSize(requestSize), Actionable.MODULATE, context.actionSource);
request.fulfill(this, extracted, context);
removedFromSystem.add(extracted.copy());
}
}
}
return new StepOutput(Collections.emptyList());
}
@Override
public void partialRefund(CraftingContext context, long amount) {
// Remove fuzzy things first
Collections.reverse(removedFromSystem);
final Iterator<IAEItemStack> removedIt = removedFromSystem.iterator();
while (removedIt.hasNext() && amount > 0) {
final IAEItemStack available = removedIt.next();
final long availAmount = available.getStackSize();
if (availAmount > amount) {
context.itemModel.injectItems(
available.copy().setStackSize(amount), Actionable.MODULATE, context.actionSource);
available.setStackSize(availAmount - amount);
amount = 0;
} else {
context.itemModel.injectItems(available, Actionable.MODULATE, context.actionSource);
amount -= availAmount;
removedIt.remove();
}
}
Collections.reverse(removedFromSystem);
}
@Override
public void fullRefund(CraftingContext context) {
for (IAEItemStack removed : removedFromSystem) {
context.itemModel.injectItems(removed, Actionable.MODULATE, context.actionSource);
}
removedFromSystem.clear();
}
@Override
public void populatePlan(IItemList<IAEItemStack> targetPlan) {
for (IAEItemStack removed : removedFromSystem) {
targetPlan.add(removed.copy());
}
}
@Override
public void startOnCpu(
CraftingContext context, CraftingCPUCluster cpuCluster, MECraftingInventory craftingInv) {
for (IAEItemStack stack : removedFromSystem) {
if (stack.getStackSize() > 0) {
IAEItemStack extracted = craftingInv.extractItems(stack, Actionable.MODULATE, context.actionSource);
if (extracted == null || extracted.getStackSize() != stack.getStackSize()) {
throw new CraftBranchFailure(stack, stack.getStackSize());
}
cpuCluster.addStorage(extracted);
}
}
}
@Override
public String toString() {
return "ExtractItemTask{" + "request="
+ request + ", removedFromSystem="
+ removedFromSystem + ", priority="
+ priority + ", state="
+ state + '}';
}
}
@Nonnull
@Override
public List<CraftingTask> provideCraftingRequestResolvers(
@Nonnull CraftingRequest<IAEItemStack> request, @Nonnull CraftingContext context) {
if (request.substitutionMode == CraftingRequest.SubstitutionMode.PRECISE_FRESH) {
return Collections.emptyList();
} else {
return Collections.singletonList(new ExtractItemTask(request));
}
}
}
@@ -0,0 +1,76 @@
package appeng.crafting.v2.resolvers;
import appeng.api.storage.data.IAEItemStack;
import appeng.api.storage.data.IAEStack;
import appeng.api.storage.data.IItemList;
import appeng.crafting.MECraftingInventory;
import appeng.crafting.v2.CraftingContext;
import appeng.crafting.v2.CraftingRequest;
import appeng.me.cluster.implementations.CraftingCPUCluster;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nonnull;
public class SimulateMissingItemResolver<StackType extends IAEStack<StackType>>
implements CraftingRequestResolver<StackType> {
public static class ConjureItemTask<StackType extends IAEStack<StackType>> extends CraftingTask {
public final CraftingRequest<StackType> request;
public ConjureItemTask(CraftingRequest<StackType> request) {
super(CraftingTask.PRIORITY_SIMULATE); // conjure items for calculations out of thin air as a last resort
this.request = request;
}
@Override
public StepOutput calculateOneStep(CraftingContext context) {
state = State.SUCCESS;
if (request.remainingToProcess <= 0) {
return new StepOutput(Collections.emptyList());
}
// Simulate items existing
request.wasSimulated = true;
context.wasSimulated = true;
request.fulfill(this, request.stack.copy().setStackSize(request.remainingToProcess), context);
return new StepOutput(Collections.emptyList());
}
@Override
public void partialRefund(CraftingContext context, long amount) {
// no-op: items were simulated, so there's nothing to refund
}
@Override
public void fullRefund(CraftingContext context) {
// no-op: items were simulated, so there's nothing to refund
}
@Override
public void populatePlan(IItemList<IAEItemStack> targetPlan) {
if (request.stack instanceof IAEItemStack) {
targetPlan.add((IAEItemStack) request.stack.copy());
}
}
@Override
public void startOnCpu(
CraftingContext context, CraftingCPUCluster cpuCluster, MECraftingInventory craftingInv) {
throw new IllegalStateException("Trying to start crafting a schedule with simulated items");
}
@Override
public String toString() {
return "ConjureItemTask{" + "request=" + request + ", priority=" + priority + ", state=" + state + '}';
}
}
@Nonnull
@Override
public List<CraftingTask> provideCraftingRequestResolvers(
@Nonnull CraftingRequest<StackType> request, @Nonnull CraftingContext context) {
if (request.allowSimulation) {
return Collections.singletonList(new ConjureItemTask<StackType>(request));
} else {
return Collections.emptyList();
}
}
}
+6 -6
View File
@@ -20,13 +20,13 @@ package appeng.hooks;
import appeng.api.AEApi;
import appeng.api.networking.IGridNode;
import appeng.api.networking.crafting.ICraftingJob;
import appeng.api.parts.CableRenderMode;
import appeng.api.util.AEColor;
import appeng.core.AEConfig;
import appeng.core.AELog;
import appeng.core.CommonHelper;
import appeng.core.sync.packets.PacketPaintedEntity;
import appeng.crafting.CraftingJob;
import appeng.entity.EntityFloatingItem;
import appeng.me.Grid;
import appeng.me.NetworkList;
@@ -51,7 +51,7 @@ public class TickHandler {
public static final TickHandler INSTANCE = new TickHandler();
private final Queue<IWorldCallable<?>> serverQueue = new LinkedList<IWorldCallable<?>>();
private final Multimap<World, CraftingJob> craftingJobs = LinkedListMultimap.create();
private final Multimap<World, ICraftingJob> craftingJobs = LinkedListMultimap.create();
private final WeakHashMap<World, Queue<IWorldCallable<?>>> callQueue =
new WeakHashMap<World, Queue<IWorldCallable<?>>>();
private final HandlerRep server = new HandlerRep();
@@ -163,12 +163,12 @@ public class TickHandler {
if (ev.type == Type.WORLD && ev.phase == Phase.END) {
final WorldTickEvent wte = (WorldTickEvent) ev;
synchronized (this.craftingJobs) {
final Collection<CraftingJob> jobSet = this.craftingJobs.get(wte.world);
final Collection<ICraftingJob> jobSet = this.craftingJobs.get(wte.world);
if (!jobSet.isEmpty()) {
final int simTime = Math.max(1, AEConfig.instance.craftingCalculationTimePerTick / jobSet.size());
final Iterator<CraftingJob> i = jobSet.iterator();
final Iterator<ICraftingJob> i = jobSet.iterator();
while (i.hasNext()) {
final CraftingJob cj = i.next();
final ICraftingJob cj = i.next();
if (!cj.simulateFor(simTime)) {
i.remove();
}
@@ -242,7 +242,7 @@ public class TickHandler {
// AELog.info( "processQueue Time: " + time + "ms" );
}
public void registerCraftingSimulation(final World world, final CraftingJob craftingJob) {
public void registerCraftingSimulation(final World world, final ICraftingJob craftingJob) {
synchronized (this.craftingJobs) {
this.craftingJobs.put(world, craftingJob);
}
@@ -43,7 +43,7 @@ public class NEIGuiHandler extends INEIGuiAdapter {
return false;
}
private String formattingText(final String displayName) {
protected String formattingText(final String displayName) {
return SPECIAL_REGEX_CHARS
.matcher(EnumChatFormatting.getTextWithoutFormattingCodes(displayName))
.replaceAll("\\\\$0");
+30 -42
View File
@@ -20,7 +20,6 @@ package appeng.me.cache;
import appeng.api.config.AccessRestriction;
import appeng.api.config.Actionable;
import appeng.api.config.FuzzyMode;
import appeng.api.networking.IGrid;
import appeng.api.networking.IGridHost;
import appeng.api.networking.IGridNode;
@@ -39,10 +38,12 @@ import appeng.api.storage.StorageChannel;
import appeng.api.storage.data.IAEItemStack;
import appeng.api.storage.data.IAEStack;
import appeng.api.storage.data.IItemList;
import appeng.core.AEConfig;
import appeng.crafting.CraftingJob;
import appeng.crafting.CraftingLink;
import appeng.crafting.CraftingLinkNexus;
import appeng.crafting.CraftingWatcher;
import appeng.crafting.v2.CraftingJobV2;
import appeng.me.cluster.implementations.CraftingCPUCluster;
import appeng.me.helpers.GenericInterestManager;
import appeng.tile.crafting.TileCraftingStorageTile;
@@ -410,60 +411,30 @@ public class CraftingGridCache
return StorageChannel.ITEMS;
}
@Override
public ImmutableMap<IAEItemStack, ImmutableList<ICraftingPatternDetails>> getCraftingPatterns() {
return ImmutableMap.copyOf(this.craftableItems);
}
@Override
public ImmutableCollection<ICraftingPatternDetails> getCraftingFor(
final IAEItemStack whatToCraft,
final ICraftingPatternDetails details,
final int slotIndex,
final World world) {
ImmutableList<ICraftingPatternDetails> res;
boolean normalMode = false;
if (details != null && details.canSubstitute()) {
final ImmutableList<ICraftingPatternDetails> substitutions = this.craftableItemSubstitutes.get(whatToCraft);
if (substitutions.isEmpty()) {
res = this.craftableItems.get(whatToCraft);
normalMode = true;
} else {
res = substitutions;
}
} else if (details == null) {
final ImmutableList<ICraftingPatternDetails> substitutions =
this.craftableItemSubstitutes.getBeSubstitutePattern(whatToCraft);
if (substitutions.isEmpty()) {
res = this.craftableItems.get(whatToCraft);
normalMode = true;
} else {
res = this.craftableItems.get(whatToCraft);
}
} else {
res = this.craftableItems.get(whatToCraft);
}
final ImmutableList<ICraftingPatternDetails> res = this.craftableItems.get(whatToCraft);
if (res == null) {
if (details != null && details.isCraftable()) {
for (final IAEItemStack ais : this.craftableItems.keySet()) {
final boolean perfectMatch = ais.getItem() == whatToCraft.getItem()
&& (!ais.getItem().getHasSubtypes() || ais.getItemDamage() == whatToCraft.getItemDamage());
if (perfectMatch) {
if (ais.getItem() == whatToCraft.getItem()
&& (!ais.getItem().getHasSubtypes()
|| ais.getItemDamage() == whatToCraft.getItemDamage())) {
if (details.isValidItemForSlot(slotIndex, ais.getItemStack(), world)) {
return this.craftableItems.get(ais);
}
}
}
// no perfect match found, look for substitutions
if (details.canSubstitute() && !normalMode) {
for (final Map.Entry<IAEItemStack, ImmutableList<ICraftingPatternDetails>> entry :
this.craftableItems.entrySet()) {
final boolean canBeASubstitute =
entry.getValue().stream().anyMatch(cp -> (cp != null) && cp.canBeSubstitute());
if (canBeASubstitute && entry.getKey().fuzzyComparison(whatToCraft, FuzzyMode.IGNORE_ALL)) {
if (details.isValidItemForSlot(
slotIndex, entry.getKey().getItemStack(), world)) {
return entry.getValue();
}
}
}
}
}
return ImmutableSet.of();
@@ -472,6 +443,13 @@ public class CraftingGridCache
return res;
}
/**
* @return The task pool for executing crafting calculations.
*/
public static ExecutorService getCraftingPool() {
return CRAFTING_POOL;
}
@Override
public Future<ICraftingJob> beginCraftingJob(
final World world,
@@ -483,9 +461,19 @@ public class CraftingGridCache
throw new IllegalArgumentException("Invalid Crafting Job Request");
}
final CraftingJob job = new CraftingJob(world, grid, actionSrc, slotItem, cb);
final ICraftingJob job;
switch (AEConfig.instance.craftingCalculatorVersion) {
case 1:
job = new CraftingJob(world, grid, actionSrc, slotItem, cb);
break;
case 2:
job = new CraftingJobV2(world, grid, actionSrc, slotItem, cb);
break;
default:
throw new IllegalStateException("Invalid crafting calculator version");
}
return CRAFTING_POOL.submit(job, (ICraftingJob) job);
return job.schedule();
}
@Override
@@ -54,6 +54,7 @@ import com.google.common.collect.ImmutableSet;
import cpw.mods.fml.common.FMLCommonHandler;
import java.util.*;
import java.util.Map.Entry;
import java.util.stream.IntStream;
import net.minecraft.inventory.InventoryCrafting;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
@@ -407,29 +408,46 @@ public final class CraftingCPUCluster implements IAECluster, ICraftingCPU {
return null;
}
private ArrayList<IAEItemStack> getExtractItems(IAEItemStack g, ICraftingPatternDetails details) {
ArrayList<IAEItemStack> list = new ArrayList<IAEItemStack>();
if (details.canBeSubstitute()) {
for (IAEItemStack fuzz : this.inventory.getItemList().findFuzzy(g, FuzzyMode.IGNORE_ALL)) {
if (!details.isCraftable() && fuzz.getStackSize() <= 0) continue;
private ArrayList<IAEItemStack> getExtractItems(IAEItemStack ingredient, ICraftingPatternDetails patternDetails) {
ArrayList<IAEItemStack> list = new ArrayList<>();
if (patternDetails.canSubstitute()) {
for (IAEItemStack fuzz : this.inventory.getItemList().findFuzzy(ingredient, FuzzyMode.IGNORE_ALL)) {
if (!patternDetails.isCraftable() && fuzz.getStackSize() <= 0) continue;
if (patternDetails.isCraftable()) {
final IAEItemStack[] inputSlots = patternDetails.getInputs();
final IAEItemStack finalIngredient =
ingredient; // have to copy because of Java lambda capture rules here
final int matchingSlot = IntStream.range(0, inputSlots.length)
.filter(idx -> inputSlots[idx] != null && inputSlots[idx].equals(finalIngredient))
.findFirst()
.orElse(-1);
if (matchingSlot < 0) {
continue;
}
if (!patternDetails.isValidItemForSlot(matchingSlot, fuzz.getItemStack(), getWorld())) {
// Skip invalid fuzzy matches
continue;
}
}
fuzz = fuzz.copy();
fuzz.setStackSize(g.getStackSize());
fuzz.setStackSize(ingredient.getStackSize());
final IAEItemStack ais = this.inventory.extractItems(fuzz, Actionable.SIMULATE, this.machineSrc);
final ItemStack is = ais == null ? null : ais.getItemStack();
if (is != null && is.stackSize == g.getStackSize()) {
if (is != null && is.stackSize == ingredient.getStackSize()) {
list.add(ais);
return list;
} else if (is != null && details.isCraftable()) {
g = g.copy();
g.decStackSize(is.stackSize);
} else if (is != null && patternDetails.isCraftable()) {
ingredient = ingredient.copy();
ingredient.decStackSize(is.stackSize);
list.add(ais);
}
}
} else {
final IAEItemStack extractItems = this.inventory.extractItems(g, Actionable.SIMULATE, this.machineSrc);
final IAEItemStack extractItems =
this.inventory.extractItems(ingredient, Actionable.SIMULATE, this.machineSrc);
final ItemStack is = extractItems == null ? null : extractItems.getItemStack();
if (is != null && is.stackSize == g.getStackSize()) {
if (is != null && is.stackSize == ingredient.getStackSize()) {
list.add(extractItems);
return list;
}
@@ -704,11 +722,11 @@ public final class CraftingCPUCluster implements IAECluster, ICraftingCPU {
return null;
}
if (!(job instanceof CraftingJob)) {
if (this.isBusy() || !this.isActive() || this.availableStorage < job.getByteTotal()) {
return null;
}
if (this.isBusy() || !this.isActive() || this.availableStorage < job.getByteTotal()) {
if (!job.supportsCPUCluster(this)) {
return null;
}
@@ -718,7 +736,7 @@ public final class CraftingCPUCluster implements IAECluster, ICraftingCPU {
try {
this.waitingFor.resetStatus();
((CraftingJob) job).getTree().setJob(ci, this, src);
job.startCrafting(ci, this, src);
if (ci.commit(src)) {
this.finalOutput = job.getOutput();
this.waiting = false;
+7
View File
@@ -1829,4 +1829,11 @@ public class Platform {
return AEItemStack.create(slot.getStack());
}
/**
* @return (a divided by b) rounded up
*/
public static long ceilDiv(long a, long b) {
return Math.addExact(Math.addExact(a, b), -1) / b;
}
}
@@ -12,7 +12,7 @@ public class OreListMultiMap<T> {
private ImmutableListMultimap<Integer, T> map;
private final Map<Integer, Collection<ICraftingPatternDetails>> patternHashMap = new HashMap<>();
private ImmutableListMultimap<Integer, T> patternMap;
private ImmutableListMultimap.Builder<Integer, T> builder;
private ImmutableListMultimap.Builder<Integer, T> builder = new Builder<>();
private static Collection<Integer> getAEEquivalents(IAEItemStack stack) {
AEItemStack s;
@@ -28,6 +28,10 @@ public class OreListMultiMap<T> {
return ids;
}
public boolean isPopulated() {
return patternMap != null;
}
public void put(IAEItemStack key, ICraftingPatternDetails val) {
if (((AEItemStack) key).getDefinition() != null) {
Collection<ICraftingPatternDetails> tmp = patternHashMap.getOrDefault(