mmb.content.stn.planner.STNPlanner Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of multimachinebuilder Show documentation
Show all versions of multimachinebuilder Show documentation
Dependency for the MultiMachineBuilder, a voxel game about building an industrial empire in a finite world.
THIS RELEASE IS NOT PLAYABLE. To play the game, donwload from >ITCH.IO LINK HERE< or >GH releases link here<
/**
*
*/
package mmb.content.stn.planner;
import java.util.ArrayDeque;
import java.util.Queue;
import java.util.Set;
import java.util.function.Function;
import it.unimi.dsi.fastutil.objects.Object2IntMap.Entry;
import mmb.NN;
import mmb.Nil;
import mmb.content.ditems.Stencil;
import mmb.content.stn.network.DataLayerSTN;
import mmb.content.stn.network.STNNetworkProcessing.STNRGroupTag.STNPRecipe;
import mmb.engine.craft.RecipeOutput;
import mmb.engine.item.ItemEntry;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
/**
* A class used to plan STN crafting jobs.
*
* Plans the processing sequence of the items before commencing the process.
*
* The item flow must form an Acyclic Directed Graph
*
* The planner uses a cyclic planner
*
*
*
*
Planning phases:
*
* - Calculate required withdrawals, procurements, crafts and processes.
* The method will fail with {@link ArithmeticException} if there are growing cycles in the recipe graph which involve planned items
* - Arrange the flow of process items. This step will fail if there are circular references in the item flow.
* - Assign the steps to the machines
*
* Run phases:
*
* - Take required items from the network
* (this will fail if any required items were extracted between phase 1 and now,
* or there is not enough space in the crafting helper)
* - Start the machines
* - Run until all processes are finished:
*
* - Deliver items from a recipe to a crafter or machine if it is a crafting or processing operation
* - Receive items from a recipe, factory, or crafter and count arrivals
*
*
* - Deliver crafted items to the network, or the user
*
*
* Planning overview:
*
* - Populate the process plan with head nodes for planned outputs
* - Look for valid sources in order of: exhaustion queue, inventory, procurement, crafting, processing.
* If no valid option is found, report inventory access
*
*
* @author oskar
*/
public class STNPlanner {
@NN public final DataLayerSTN stn;
public STNPlanner(DataLayerSTN stn) {
this.stn = stn;
}
//Phase 1
/**
* Phase 1 planning report
* @author oskar
*/
public static class Phase1{
/** Items which are going to be withdrawn */
@NN public final Object2IntOpenHashMap itemsWithdrawn;
/** Used processing recipes */
@NN public final Object2IntOpenHashMap<@NN STNPRecipe> processes;
/** Used crafts */
@NN public final Object2IntOpenHashMap<@NN Stencil> crafts;
/** Used procurements */
@NN public final Object2IntOpenHashMap procurements;
/** Missing items */
@NN public final Object2IntOpenHashMap missing;
/**
* Creates a finished Phase 1 plan
* @param itemsWithdrawn
* @param processes used processing recipes
* @param crafts used crafts
* @param procurements used procurements
* @param missing missing items
*/
public Phase1(Object2IntOpenHashMap itemsWithdrawn,
Object2IntOpenHashMap<@NN STNPRecipe> processes, Object2IntOpenHashMap<@NN Stencil> crafts,
Object2IntOpenHashMap procurements, Object2IntOpenHashMap missing) {
this.itemsWithdrawn = itemsWithdrawn;
this.processes = processes;
this.crafts = crafts;
this.procurements = procurements;
this.missing = missing;
}
}
/**
* 1st phase of planning: calculate items extracted, procured, crafted, processed and missing
*
* @param items
* @return a crafting plan
* @throws ArithmeticException if there are growing cycles which involve planned items
* @throws IllegalStateException if there are additional required items (if planning logic fails)
*/
public Phase1 plan1(RecipeOutput items) {
//Results
Object2IntOpenHashMap<@NN STNPRecipe> processRecipes = new Object2IntOpenHashMap<>();
Object2IntOpenHashMap<@NN Stencil> craftRecipes = new Object2IntOpenHashMap<>();
Object2IntOpenHashMap procurements = new Object2IntOpenHashMap<>();
//The planning queue (negative values mean that there are excess items
Queue queue = new ArrayDeque<>();
Object2IntOpenHashMap planMap = new Object2IntOpenHashMap<>();
Object2IntOpenHashMap invRemain = new Object2IntOpenHashMap<>();
Object2IntOpenHashMap missing = new Object2IntOpenHashMap<>();
//Populate the planning queue
planMap.putAll(items.getContents());
queue.addAll(items.items());
stn.inv.contents(invRemain);
//Run the planning
ItemEntry entry;
while((entry = queue.poll()) != null) {
int plannedAmount = planMap.getInt(entry);
if(plannedAmount <= 0) continue; //All items are planned for now
//Option A: Inventory
int itemsInInv = invRemain.getInt(entry);
if(itemsInInv >= plannedAmount) {
//Option A1: all planned items are stored
invRemain.addTo(entry, -plannedAmount);
continue;
}else if(itemsInInv > 0) {
//Option A2: some planned items are stored
plannedAmount -= itemsInInv;
invRemain.put(entry, 0);
}else if(itemsInInv < 0) {
//Error in planning logic
throw new IllegalStateException("Counted negative items in inventory: "+itemsInInv+" for: "+entry);
}
//TODO Option B: Procurement
//Option C: Crafting
Set stencils = stn.processor.stencil2OutIndex.multimap().get(entry);
Stencil stencil = findPlausibleRecipe(planMap, invRemain, stencils, s -> s.in().items());
//If no recipes are plausible, no recipe will be usable
if(stencil != null) {
//If a recipe is plausible, plan it
Set toPlan = planItems(stencil.in(), stencil.out(), entry, plannedAmount, planMap, stencil, craftRecipes, queue);
queue.addAll(toPlan);
continue;
}
//Option D: Processing
Set processables = stn.processor.processRecipe2OutIndex.multimap().get(entry);
STNPRecipe processRecipe = findPlausibleRecipe(planMap, invRemain, processables, pr -> pr.in.items());
//If no recipes are plausible, no recipe will be usable
if(processRecipe != null) {
//If a recipe is plausible, plan it
Set toPlan = planItems(processRecipe.in, processRecipe.out, entry, plannedAmount, planMap, processRecipe, processRecipes, queue);
queue.addAll(toPlan);
continue;
}
//Else: Missing items
missing.addTo(entry, plannedAmount);
}
Object2IntOpenHashMap putback = new Object2IntOpenHashMap<>();
//Check for stray items
for(Entry stack: planMap.object2IntEntrySet()) {
int amount = -stack.getIntValue();
if(amount < 0) throw new IllegalStateException("Unnacounted-for items in the plans: "+(-amount)+" x "+stack.getKey());
putback.put(stack.getKey(), amount);
}
//Calculate extractions
Object2IntOpenHashMap itemsInInv = new Object2IntOpenHashMap<>();
Object2IntOpenHashMap itemsToWithdraw = new Object2IntOpenHashMap<>();
stn.inv.contents(itemsInInv);
for(Entry itemCompare: itemsInInv.object2IntEntrySet()) {
ItemEntry item = itemCompare.getKey();
int remaining = invRemain.getInt(item);
int stored = itemCompare.getIntValue();
itemsToWithdraw.put(item, stored-remaining);
}
//FINISH
return new Phase1(itemsToWithdraw, processRecipes, craftRecipes, procurements, missing);
}
/**
* Internal helper method for Phase 1
* @param type of recipes
* @param planMap planning map
* @param invRemain
* @param possible all potential recipes
* @param transformer obtains input items for the recipe
* @return a iterator of plausible recipes, or null if not found
*/
private @Nil T findPlausibleRecipe(Object2IntOpenHashMap planMap,
Object2IntOpenHashMap invRemain, @Nil Set possible, Function super T, @NN Set> transformer) {
if(possible == null) return null;
for(T recipe: possible) {
Set inputs = transformer.apply(recipe);
if(stn.processor.isAllObtainable(inputs, invRemain, planMap))
return recipe;
}
return null;
}
private static Set planItems(RecipeOutput inputs, RecipeOutput outputs, ItemEntry plannedItem, int plannedAmount,
Object2IntOpenHashMap planMap, T recipe, Object2IntOpenHashMap<@NN T> recipesCounter, Queue queue) {
int unitOutputQuantity = outputs.get(plannedItem);
if(unitOutputQuantity <= 0) throw new InternalError("No such item: "+plannedItem);
double recipeQuantity0 = (double)plannedAmount / unitOutputQuantity;
int recipeQuantity = (int) Math.ceil(recipeQuantity0);
recipesCounter.addTo(recipe, recipeQuantity);
//Add the inputs to the plans and queue
Object2IntOpenHashMap totalInputs = inputs.mul2map(recipeQuantity, Object2IntOpenHashMap::new);
for(Entry input: totalInputs.object2IntEntrySet()) {
planMap.addTo(input.getKey(), Math.multiplyExact(input.getIntValue(), recipeQuantity));
queue.add(input.getKey());
}
//Add the outputs to the plans
Object2IntOpenHashMap totalOutputs = outputs.mul2map(recipeQuantity, Object2IntOpenHashMap::new);
for(Entry output: totalOutputs.object2IntEntrySet())
planMap.addTo(output.getKey(), Math.multiplyExact(-output.getIntValue(), recipeQuantity));
return totalInputs.keySet();
}
}