diff --git a/CHANGELOG.md b/CHANGELOG.md index 789f2e6..c68f772 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to **Applied Delight** are listed here. +## 2.0.0 + +The ME Cooking Pot now exposes every compatible Farmer's Delight cooking-pot recipe to AE2 as an autocrafting +pattern. Each pot accepts one job at a time, while multiple pots can process jobs in parallel. + +Serving containers are requested as pattern inputs, item and fluid ingredient substitutions are supported, and +finished meals are inserted directly into the crafting network. Active jobs and partial outputs survive chunk and +server reloads, and the pot retries delivery when the network is unavailable or full. + ## 1.1.0 You can now power a placed ME Cooking Pot from energy cables. Run any Forge Energy cable (Mekanism, Flux, and the like) into one of its sides and it charges the battery and keeps it topped, so a stationary pot never runs down; pick it up and it runs on the stored charge as before. The item still charges in an AE2 Charger or any FE charger too. diff --git a/README.md b/README.md index f1d5834..3a6cf12 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ cooks any Farmer's Delight recipe using items stored in your ME system. from the network's fluid storage. - **Serving on your terms**, meals wait in the pot until you supply their container, by hand or with a button that requests it from the network. +- **Native AE2 autocrafting**, every compatible cooking-pot recipe is exposed in ME terminals. A pot processes one + job at a time, requests the required serving containers and returns the finished meal directly to the network. ## Requirements diff --git a/gradle.properties b/gradle.properties index 9cdf068..0583cca 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,7 +21,7 @@ modrinth_id=GKLhL3bQ mod_id=applieddelight mod_name=Applied Delight mod_license=MIT -mod_version=1.1.0 +mod_version=2.0.0 mod_group_id=sebastrn mod_authors=ItsSebastrn mod_description=Cook Farmer's Delight meals straight from your Applied Energistics 2 network. diff --git a/src/main/java/sebastrn/applieddelight/blockentity/MECookingPotBlockEntity.java b/src/main/java/sebastrn/applieddelight/blockentity/MECookingPotBlockEntity.java index 6dafbf7..1431425 100644 --- a/src/main/java/sebastrn/applieddelight/blockentity/MECookingPotBlockEntity.java +++ b/src/main/java/sebastrn/applieddelight/blockentity/MECookingPotBlockEntity.java @@ -2,8 +2,10 @@ import appeng.api.config.Actionable; import appeng.api.config.PowerUnit; +import appeng.api.crafting.IPatternDetails; import appeng.api.implementations.blockentities.IWirelessAccessPoint; import appeng.api.networking.IGrid; +import appeng.api.networking.crafting.ICraftingProvider; import appeng.api.networking.security.IActionSource; import appeng.api.stacks.AEFluidKey; import appeng.api.stacks.AEItemKey; @@ -57,6 +59,7 @@ import sebastrn.applieddelight.AppliedDelight; import sebastrn.applieddelight.block.MECookingPotBlock; import sebastrn.applieddelight.config.ServerConfig; +import sebastrn.applieddelight.integration.ae2.AutoCookingPattern; import sebastrn.applieddelight.item.MECookingPotItem; import sebastrn.applieddelight.menu.MECookingPotMenu; import vectorwing.farmersdelight.common.block.entity.CookingPotBlockEntity; @@ -80,7 +83,7 @@ * ME network, paying for that network access out of the pot's battery. It does not extend Farmer's Delight; it only * reads FD's {@link CookingPotRecipe}s and implements FD's {@link HeatableBlockEntity} heat check. */ -public class MECookingPotBlockEntity extends BlockEntity implements MenuProvider, HeatableBlockEntity { +public class MECookingPotBlockEntity extends BlockEntity implements MenuProvider, HeatableBlockEntity, ICraftingProvider { public static final int INPUT_SLOTS = 6; public static final int MEAL_DISPLAY_SLOT = 6; @@ -187,8 +190,15 @@ public boolean canReceive() { private MEStorage meStorage; @Nullable private IGrid grid; + @Nullable + private IGrid craftingProviderGrid; + private List autoCookingPatterns = List.of(); + private int autoCookingPatternFingerprint = Integer.MIN_VALUE; private int ticksSinceNetworkRefresh = NETWORK_REFRESH_INTERVAL; + private boolean autoCrafting; + private ItemStack autoCraftingOutput = ItemStack.EMPTY; + /** Container-data slots synced to the menu: cookTime, cookTimeTotal, link state, energy%, containerRequestFailures. */ public static final int DATA_SLOTS = 5; /** Bumped whenever a container request finds nothing, so the screen can flash the button red. */ @@ -258,6 +268,85 @@ public boolean isConnected() { return grid != null && meStorage != null; } + public boolean isAutoCrafting() { + return autoCrafting; + } + + // ------------------------------------------------------------------------------------------------------------ + // AE2 autocrafting provider + // ------------------------------------------------------------------------------------------------------------ + + @Override + public List getAvailablePatterns() { + return autoCookingPatterns; + } + + @Override + public boolean pushPattern(IPatternDetails patternDetails, KeyCounter[] inputHolders) { + if (level == null || level.isClientSide || craftingProviderGrid == null || craftingProviderGrid != grid + || isBusy()) { + return false; + } + + AutoCookingPattern pattern = null; + for (IPatternDetails available : autoCookingPatterns) { + if (available.equals(patternDetails) && available instanceof AutoCookingPattern autoPattern) { + pattern = autoPattern; + break; + } + } + if (pattern == null) { + return false; + } + + AutoCookingPattern.AcceptedInputs accepted = pattern.acceptInputs(inputHolders); + if (accepted == null || accepted.ingredients().length > INPUT_SLOTS) { + return false; + } + + double cost = cfg().getDrainPerIngredient() * accepted.ingredients().length; + if (energy < cost) { + return false; + } + + // AE2 retains ownership until every input has been validated above. + Arrays.fill(fluidSourced, false); + for (int i = 0; i < accepted.ingredients().length; i++) { + inventory.setStackInSlot(i, accepted.ingredients()[i]); + fluidSourced[i] = accepted.fluidSourced()[i]; + } + if (!accepted.container().isEmpty()) { + inventory.setStackInSlot(CONTAINER_SLOT, accepted.container()); + } + + energy = Math.max(0, energy - cost); + cookTime = 0; + cookTimeTotal = 0; + mealContainerStack = ItemStack.EMPTY; + selectedRecipeId = pattern.recipeId(); + craftTarget = 1; + autoCraftingOutput = pattern.output(); + autoCrafting = true; + setChanged(); + return true; + } + + @Override + public boolean isBusy() { + if (autoCrafting || !isConnected()) { + return true; + } + if (selectedRecipeId != null || craftTarget > 0 || cookTime > 0) { + return true; + } + for (int i = 0; i < INVENTORY_SIZE; i++) { + if (!inventory.getStackInSlot(i).isEmpty()) { + return true; + } + } + return false; + } + /** * The pot's link state, for the status LED and HUD tooltips: 0 = no access point saved (Unlinked), 1 = linked but * not currently connected, no power, out of range, or the network is down (Offline), 2 = actively connected @@ -300,6 +389,7 @@ public ResourceLocation getSelectedRecipeId() { * false, so it would silently ignore ingredients loaded into it by hand. */ public void selectRecipe(@Nullable ResourceLocation id, int target) { + if (autoCrafting) return; boolean cleared = id == null || target <= 0; this.selectedRecipeId = cleared ? null : id; this.craftTarget = cleared ? 0 : target; @@ -319,6 +409,7 @@ public static void serverTick(Level level, BlockPos pos, BlockState state, MECoo if (++be.ticksSinceNetworkRefresh >= NETWORK_REFRESH_INTERVAL) { be.ticksSinceNetworkRefresh = 0; be.resolveAccessPoint(); + be.refreshAutoCookingPatterns(); } be.refreshConnection(); be.updateConnectedState(); @@ -566,7 +657,7 @@ private void cookingTick() { // the slots, so revert to plain cooking-pot behaviour (cook whatever is actually there) instead of latching on // the stale order. This never fires during normal cooking, nor while a full batch waits for meal-slot room, nor // while an unheated pot waits for heat: in all of those the loaded ingredients still match the selected recipe. - if (selectedRecipeId != null) { + if (selectedRecipeId != null && !autoCrafting) { Optional> holder = level.getRecipeManager().byKey(selectedRecipeId); boolean stillMatches = holder.isPresent() && holder.get().value() instanceof CookingPotRecipe cooking @@ -614,11 +705,45 @@ private void cookingTick() { } } + if (exportAutoCraftingOutput()) { + changed = true; + } + if (changed) { setChanged(); } } + /** Inserts the active job's output and keeps any rejected remainder for the next tick. */ + private boolean exportAutoCraftingOutput() { + if (!autoCrafting || autoCraftingOutput.isEmpty() || meStorage == null || accessPoint == null || !isConnected()) { + return false; + } + + ItemStack output = inventory.getStackInSlot(OUTPUT_SLOT); + if (output.isEmpty() || !ItemStack.isSameItemSameComponents(output, autoCraftingOutput)) { + return false; + } + + int offered = Math.min(output.getCount(), autoCraftingOutput.getCount()); + AEItemKey key = AEItemKey.of(autoCraftingOutput); + if (key == null || offered <= 0) { + return false; + } + long inserted = meStorage.insert(key, offered, Actionable.MODULATE, new MachineSource(accessPoint)); + if (inserted <= 0) { + return false; + } + + output.shrink((int) inserted); + autoCraftingOutput.shrink((int) inserted); + if (autoCraftingOutput.isEmpty()) { + autoCrafting = false; + autoCraftingOutput = ItemStack.EMPTY; + } + return true; + } + @Nullable private RecipeHolder resolveTargetRecipe() { if (level == null) return null; @@ -655,7 +780,7 @@ private boolean hasInput() { * @return the number of crafts actually loaded (may be fewer than requested), or 0 if nothing was taken. */ public int loadBatch(CookingPotRecipe recipe, int requested) { - if (level == null || requested <= 0 || !canAcceptBatch()) return 0; + if (level == null || autoCrafting || requested <= 0 || !canAcceptBatch()) return 0; List ingredients = recipe.getIngredients(); int count = ingredients.size(); @@ -837,7 +962,7 @@ public int loadBatch(CookingPotRecipe recipe, int requested) { * @return the number of sets actually added (0 if none could be). */ public int topUpBatch(CookingPotRecipe recipe, int additional) { - if (level == null || additional <= 0 || selectedRecipeId == null) return 0; + if (level == null || autoCrafting || additional <= 0 || selectedRecipeId == null) return 0; List ingredients = recipe.getIngredients(); int count = ingredients.size(); @@ -963,6 +1088,7 @@ public int topUpBatch(CookingPotRecipe recipe, int additional) { * @return true if any container arrived; false means nothing was available and the screen should say so. */ public boolean requestContainers() { + if (autoCrafting) return false; ItemStack required = mealContainerStack; ItemStack meal = inventory.getStackInSlot(MEAL_DISPLAY_SLOT); if (required.isEmpty() || meal.isEmpty() || meStorage == null || accessPoint == null || !isConnected()) { @@ -1004,6 +1130,7 @@ private boolean noContainersAvailable() { * a finished serving in the output does NOT block a new order; the batch simply sits until the meal is served out. */ public boolean canAcceptBatch() { + if (autoCrafting) return false; for (int i = 0; i < INPUT_SLOTS; i++) { if (!inventory.getStackInSlot(i).isEmpty()) return false; } @@ -1017,6 +1144,7 @@ public boolean canAcceptBatch() { * free servings. */ public void returnContents(@Nullable Player player) { + if (autoCrafting) return; for (int i = 0; i < INPUT_SLOTS; i++) { releaseSlot(i, player, false); } @@ -1036,6 +1164,7 @@ public void returnContents(@Nullable Player player) { * the floor is the last resort, so nothing is ever destroyed. */ public void returnInputsAndContainer(@Nullable Player player) { + if (autoCrafting) return; for (int i = 0; i < INPUT_SLOTS; i++) { releaseSlot(i, player, false); } @@ -1243,6 +1372,7 @@ private void useStoredContainersOnMeal() { * item isn't the right container or there is no meal waiting. Mirrors {@code CookingPotBlockEntity#useHeldItemOnMeal}. */ public ItemStack useHeldItemOnMeal(ItemStack container) { + if (autoCrafting) return ItemStack.EMPTY; ItemStack meal = inventory.getStackInSlot(MEAL_DISPLAY_SLOT); if (isContainerValid(container) && !meal.isEmpty()) { container.shrink(1); @@ -1268,34 +1398,87 @@ private void resolveAccessPoint() { } } + private void refreshAutoCookingPatterns() { + if (level == null || level.isClientSide) return; + + List> recipes = MECookingPotMenu.sortedRecipes(level); + int fingerprint = 1; + for (RecipeHolder holder : recipes) { + fingerprint = 31 * fingerprint + holder.id().hashCode(); + fingerprint = 31 * fingerprint + holder.value().hashCode(); + } + if (fingerprint == autoCookingPatternFingerprint) { + return; + } + + List rebuilt = new ArrayList<>(recipes.size()); + for (RecipeHolder holder : recipes) { + if (holder.value().getIngredients().size() > INPUT_SLOTS) { + continue; + } + AutoCookingPattern pattern = AutoCookingPattern.fromRecipe(holder, level); + if (pattern != null) { + rebuilt.add(pattern); + } + } + autoCookingPatterns = List.copyOf(rebuilt); + autoCookingPatternFingerprint = fingerprint; + if (craftingProviderGrid != null) { + craftingProviderGrid.getCraftingService().refreshGlobalCraftingProvider(this); + } + } + private void refreshConnection() { + IGrid liveGrid = null; + IWirelessAccessPoint liveAccessPoint = null; + MEStorage liveStorage = null; + grid = null; meStorage = null; accessPoint = null; - if (linkedAccessPoint == null || energy <= 0) return; - - // The link only identifies which NETWORK the pot belongs to, exactly how AE2's wireless terminal treats it. - // Reach is then judged against every access point on that grid, so building a nearer access point just works - // without re-linking the pot. - IGrid liveGrid = linkedAccessPoint.getGrid(); - if (liveGrid == null) return; - - accessPoint = selectReachableAccessPoint(liveGrid); - if (accessPoint == null) return; + if (linkedAccessPoint != null && energy > 0) { + // The link only identifies which NETWORK the pot belongs to, exactly how AE2's wireless terminal treats it. + // Reach is then judged against every access point on that grid, so building a nearer access point just works + // without re-linking the pot. + liveGrid = linkedAccessPoint.getGrid(); + if (liveGrid != null) { + liveAccessPoint = selectReachableAccessPoint(liveGrid); + } - // Keeping the link open is not free: pay the per-tick idle cost, or drop offline until recharged. - double idle = cfg().getIdleDrainPerTick(); - if (idle > 0) { - if (energy < idle) { - energy = 0; - return; + if (liveAccessPoint != null) { + // Keeping the link open is not free: pay the per-tick idle cost, or drop offline until recharged. + double idle = cfg().getIdleDrainPerTick(); + if (idle > 0 && energy < idle) { + energy = 0; + liveGrid = null; + liveAccessPoint = null; + } else { + energy -= idle; + liveStorage = liveGrid.getStorageService().getInventory(); + } + } else { + liveGrid = null; } - energy -= idle; } grid = liveGrid; - meStorage = liveGrid.getStorageService().getInventory(); + accessPoint = liveAccessPoint; + meStorage = liveStorage; + setCraftingProviderGrid(liveGrid); + } + + private void setCraftingProviderGrid(@Nullable IGrid newGrid) { + if (craftingProviderGrid == newGrid) { + return; + } + if (craftingProviderGrid != null) { + craftingProviderGrid.getCraftingService().removeGlobalCraftingProvider(this); + } + craftingProviderGrid = newGrid; + if (craftingProviderGrid != null) { + craftingProviderGrid.getCraftingService().addGlobalCraftingProvider(this); + } } /** @@ -1439,6 +1622,12 @@ public void dropContents(Level level, BlockPos pos) { } } + @Override + public void setRemoved() { + setCraftingProviderGrid(null); + super.setRemoved(); + } + // ------------------------------------------------------------------------------------------------------------ // Menu // ------------------------------------------------------------------------------------------------------------ @@ -1467,6 +1656,8 @@ protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) mealContainerStack = ItemStack.parseOptional(registries, tag.getCompound("Container")); energy = tag.getDouble("Energy"); craftTarget = tag.getInt("CraftTarget"); + autoCraftingOutput = ItemStack.parseOptional(registries, tag.getCompound("AutoCraftingOutput")); + autoCrafting = tag.getBoolean("AutoCrafting") && !autoCraftingOutput.isEmpty(); int fluidMask = tag.getInt("FluidSourced"); for (int i = 0; i < INPUT_SLOTS; i++) { fluidSourced[i] = (fluidMask & (1 << i)) != 0; @@ -1488,6 +1679,8 @@ protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) tag.put("Container", mealContainerStack.saveOptional(registries)); tag.putDouble("Energy", energy); tag.putInt("CraftTarget", craftTarget); + tag.putBoolean("AutoCrafting", autoCrafting); + tag.put("AutoCraftingOutput", autoCraftingOutput.saveOptional(registries)); int fluidMask = 0; for (int i = 0; i < INPUT_SLOTS; i++) { if (fluidSourced[i]) fluidMask |= (1 << i); diff --git a/src/main/java/sebastrn/applieddelight/integration/ae2/AutoCookingPattern.java b/src/main/java/sebastrn/applieddelight/integration/ae2/AutoCookingPattern.java new file mode 100644 index 0000000..6198297 --- /dev/null +++ b/src/main/java/sebastrn/applieddelight/integration/ae2/AutoCookingPattern.java @@ -0,0 +1,323 @@ +package sebastrn.applieddelight.integration.ae2; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.crafting.IPatternDetailsDecoder; +import appeng.api.crafting.PatternDetailsHelper; +import appeng.api.stacks.AEFluidKey; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import appeng.api.stacks.KeyCounter; +import net.minecraft.core.component.DataComponents; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.level.Level; +import net.neoforged.neoforge.fluids.FluidStack; +import net.neoforged.neoforge.fluids.FluidUtil; +import org.jetbrains.annotations.Nullable; +import sebastrn.applieddelight.ADItems; +import vectorwing.farmersdelight.common.crafting.CookingPotRecipe; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** An AE2 processing pattern backed by a Farmer's Delight cooking-pot recipe. */ +public final class AutoCookingPattern implements IPatternDetails { + + private static final String TAG_RECIPE = "AutoCookingRecipe"; + + private static final IPatternDetailsDecoder DECODER = new IPatternDetailsDecoder() { + @Override + public boolean isEncodedPattern(ItemStack stack) { + return stack.is(ADItems.ME_COOKING_POT.get()) && readRecipeId(stack.get(DataComponents.CUSTOM_DATA)) != null; + } + + @Nullable + @Override + public IPatternDetails decodePattern(AEItemKey what, Level level) { + if (what == null || level == null || !what.is(ADItems.ME_COOKING_POT.get())) { + return null; + } + ResourceLocation recipeId = readRecipeId(what.get(DataComponents.CUSTOM_DATA)); + if (recipeId == null) { + return null; + } + return fromRecipeId(recipeId, level); + } + }; + + private final ResourceLocation recipeId; + private final AEItemKey definition; + private final IngredientInput[] ingredientInputs; + @Nullable + private final ContainerInput containerInput; + private final IInput[] inputs; + private final List outputs; + private final ItemStack output; + + private AutoCookingPattern(ResourceLocation recipeId, CookingPotRecipe recipe, Level level) { + this.recipeId = recipeId; + + ItemStack definitionStack = new ItemStack(ADItems.ME_COOKING_POT.get()); + CompoundTag definitionTag = new CompoundTag(); + definitionTag.putString(TAG_RECIPE, recipeId.toString()); + definitionStack.set(DataComponents.CUSTOM_DATA, CustomData.of(definitionTag)); + this.definition = AEItemKey.of(definitionStack); + + List ingredients = recipe.getIngredients(); + this.ingredientInputs = new IngredientInput[ingredients.size()]; + List allInputs = new ArrayList<>(ingredients.size() + 1); + for (int i = 0; i < ingredients.size(); i++) { + IngredientInput input = new IngredientInput(ingredients.get(i)); + if (input.getPossibleInputs().length == 0) { + throw new IllegalArgumentException("Cooking recipe has an ingredient without AE2-compatible inputs: " + + recipeId); + } + ingredientInputs[i] = input; + allInputs.add(input); + } + + this.output = recipe.getResultItem(level.registryAccess()).copy(); + if (output.isEmpty()) { + throw new IllegalArgumentException("Cooking recipe has no output: " + recipeId); + } + this.outputs = List.of(new GenericStack(AEItemKey.of(output), output.getCount())); + + ItemStack container = recipe.getOutputContainer(); + if (container.isEmpty()) { + this.containerInput = null; + } else { + this.containerInput = new ContainerInput(container, output.getCount()); + allInputs.add(containerInput); + } + this.inputs = allInputs.toArray(IInput[]::new); + } + + public static void registerDecoder() { + PatternDetailsHelper.registerDecoder(DECODER); + } + + @Nullable + public static AutoCookingPattern fromRecipe(RecipeHolder holder, Level level) { + try { + return new AutoCookingPattern(holder.id(), holder.value(), level); + } catch (IllegalArgumentException ignored) { + return null; + } + } + + @Nullable + private static AutoCookingPattern fromRecipeId(ResourceLocation recipeId, Level level) { + var holder = level.getRecipeManager().byKey(recipeId); + if (holder.isEmpty() || !(holder.get().value() instanceof CookingPotRecipe)) { + return null; + } + @SuppressWarnings("unchecked") + RecipeHolder cookingHolder = (RecipeHolder) holder.get(); + return fromRecipe(cookingHolder, level); + } + + @Nullable + private static ResourceLocation readRecipeId(@Nullable CustomData customData) { + if (customData == null) { + return null; + } + CompoundTag tag = customData.copyTag(); + return tag.contains(TAG_RECIPE) ? ResourceLocation.tryParse(tag.getString(TAG_RECIPE)) : null; + } + + public ResourceLocation recipeId() { + return recipeId; + } + + public ItemStack output() { + return output.copy(); + } + + /** Validates AE2 inputs without mutating them and maps them to the pot's slots. */ + @Nullable + public AcceptedInputs acceptInputs(KeyCounter[] inputHolders) { + if (inputHolders == null || inputHolders.length != inputs.length) { + return null; + } + + ItemStack[] ingredients = new ItemStack[ingredientInputs.length]; + boolean[] fluidSourced = new boolean[ingredientInputs.length]; + for (int i = 0; i < ingredientInputs.length; i++) { + AcceptedIngredient accepted = ingredientInputs[i].accept(inputHolders[i]); + if (accepted == null) { + return null; + } + ingredients[i] = accepted.stack(); + fluidSourced[i] = accepted.fluidSourced(); + } + + ItemStack container = ItemStack.EMPTY; + if (containerInput != null) { + container = containerInput.accept(inputHolders[inputHolders.length - 1]); + if (container.isEmpty()) { + return null; + } + } + return new AcceptedInputs(ingredients, fluidSourced, container); + } + + @Override + public AEItemKey getDefinition() { + return definition; + } + + @Override + public IInput[] getInputs() { + return inputs; + } + + @Override + public List getOutputs() { + return outputs; + } + + @Override + public boolean supportsPushInputsToExternalInventory() { + return false; + } + + @Override + public boolean equals(Object other) { + return other instanceof AutoCookingPattern pattern && definition.equals(pattern.definition); + } + + @Override + public int hashCode() { + return definition.hashCode(); + } + + public record AcceptedInputs(ItemStack[] ingredients, boolean[] fluidSourced, ItemStack container) { + } + + private record AcceptedIngredient(ItemStack stack, boolean fluidSourced) { + } + + private static final class IngredientInput implements IInput { + private final Ingredient ingredient; + private final GenericStack[] possibleInputs; + private final Map fluidContainers; + + private IngredientInput(Ingredient ingredient) { + this.ingredient = ingredient; + Map possible = new LinkedHashMap<>(); + this.fluidContainers = new LinkedHashMap<>(); + + for (ItemStack candidate : ingredient.getItems()) { + AEItemKey itemKey = AEItemKey.of(candidate); + if (itemKey != null) { + possible.putIfAbsent(itemKey, new GenericStack(itemKey, 1)); + } + + FluidStack contained = FluidUtil.getFluidContained(candidate).orElse(FluidStack.EMPTY); + if (contained.getAmount() >= AEFluidKey.AMOUNT_BUCKET) { + AEFluidKey fluidKey = AEFluidKey.of(contained); + if (fluidKey != null) { + possible.putIfAbsent(fluidKey, new GenericStack(fluidKey, AEFluidKey.AMOUNT_BUCKET)); + fluidContainers.putIfAbsent(fluidKey, candidate.copyWithCount(1)); + } + } + } + this.possibleInputs = possible.values().toArray(GenericStack[]::new); + } + + @Nullable + private AcceptedIngredient accept(KeyCounter holder) { + if (holder == null || holder.size() != 1) { + return null; + } + var entry = holder.getFirstEntry(); + if (entry == null || !isValid(entry.getKey(), null)) { + return null; + } + if (entry.getKey() instanceof AEItemKey itemKey && entry.getLongValue() == 1) { + return new AcceptedIngredient(itemKey.toStack(1), false); + } + if (entry.getKey() instanceof AEFluidKey fluidKey + && entry.getLongValue() == AEFluidKey.AMOUNT_BUCKET) { + ItemStack container = fluidContainers.get(fluidKey); + return container == null ? null : new AcceptedIngredient(container.copy(), true); + } + return null; + } + + @Override + public GenericStack[] getPossibleInputs() { + return possibleInputs; + } + + @Override + public long getMultiplier() { + return 1; + } + + @Override + public boolean isValid(AEKey input, Level level) { + if (input instanceof AEItemKey itemKey) { + return itemKey.matches(ingredient); + } + return input instanceof AEFluidKey fluidKey && fluidContainers.containsKey(fluidKey); + } + + @Nullable + @Override + public AEKey getRemainingKey(AEKey template) { + return null; + } + } + + private static final class ContainerInput implements IInput { + private final AEItemKey key; + private final GenericStack[] possibleInputs; + private final int count; + + private ContainerInput(ItemStack container, int count) { + this.key = AEItemKey.of(container); + this.possibleInputs = new GenericStack[]{new GenericStack(key, 1)}; + this.count = count; + } + + private ItemStack accept(KeyCounter holder) { + if (holder == null || holder.size() != 1) { + return ItemStack.EMPTY; + } + var entry = holder.getFirstEntry(); + if (entry == null || !key.equals(entry.getKey()) || entry.getLongValue() != count) { + return ItemStack.EMPTY; + } + return key.toStack(count); + } + + @Override + public GenericStack[] getPossibleInputs() { + return possibleInputs; + } + + @Override + public long getMultiplier() { + return count; + } + + @Override + public boolean isValid(AEKey input, Level level) { + return key.equals(input); + } + + @Nullable + @Override + public AEKey getRemainingKey(AEKey template) { + return null; + } + } +} diff --git a/src/main/java/sebastrn/applieddelight/menu/MECookingPotMenu.java b/src/main/java/sebastrn/applieddelight/menu/MECookingPotMenu.java index ad6b545..98ed39e 100644 --- a/src/main/java/sebastrn/applieddelight/menu/MECookingPotMenu.java +++ b/src/main/java/sebastrn/applieddelight/menu/MECookingPotMenu.java @@ -95,7 +95,17 @@ public MECookingPotMenu(int id, Inventory playerInventory, MECookingPotBlockEnti int inputStartX = 30, inputStartY = 17, slot = 18; for (int row = 0; row < 2; row++) { for (int col = 0; col < 3; col++) { - addSlot(new SlotItemHandler(inventory, row * 3 + col, inputStartX + col * slot, inputStartY + row * slot)); + addSlot(new SlotItemHandler(inventory, row * 3 + col, inputStartX + col * slot, inputStartY + row * slot) { + @Override + public boolean mayPlace(ItemStack stack) { + return !blockEntity.isAutoCrafting() && super.mayPlace(stack); + } + + @Override + public boolean mayPickup(Player player) { + return !blockEntity.isAutoCrafting(); + } + }); } } // Meal display (result kept in the pot), serving-container input, finished-output. @@ -113,13 +123,28 @@ public boolean mayPickup(Player player) { } }); // Serving-container input (bowls/bottles): the player may place them here, or use the request-containers button; the pot never pulls containers on its own. - addSlot(new SlotItemHandler(inventory, MECookingPotBlockEntity.CONTAINER_SLOT, 92, 55)); + addSlot(new SlotItemHandler(inventory, MECookingPotBlockEntity.CONTAINER_SLOT, 92, 55) { + @Override + public boolean mayPlace(ItemStack stack) { + return !blockEntity.isAutoCrafting() && super.mayPlace(stack); + } + + @Override + public boolean mayPickup(Player player) { + return !blockEntity.isAutoCrafting(); + } + }); // Output: served meals land here (take them from here); cannot be placed into (FD's CookingPotResultSlot). addSlot(new SlotItemHandler(inventory, MECookingPotBlockEntity.OUTPUT_SLOT, 124, 55) { @Override public boolean mayPlace(ItemStack stack) { return false; } + + @Override + public boolean mayPickup(Player player) { + return !blockEntity.isAutoCrafting(); + } }); // Player inventory + hotbar. @@ -181,6 +206,9 @@ public static List> sortedRecipes(Level level) { @Override public boolean clickMenuButton(Player player, int id) { + if (blockEntity.isAutoCrafting() && id != TOGGLE_PANEL && id != TOGGLE_FILTER && id != TOGGLE_SORT) { + return false; + } if (id == CLEAR_SELECTION) { blockEntity.selectRecipe(null, 0); return true; @@ -321,6 +349,9 @@ public ItemStack quickMoveStack(Player player, int index) { if (index == MECookingPotBlockEntity.MEAL_DISPLAY_SLOT) { return ItemStack.EMPTY; } + if (blockEntity.isAutoCrafting() && index < INV_START) { + return ItemStack.EMPTY; + } ItemStack copy = ItemStack.EMPTY; Slot clicked = slots.get(index); if (clicked != null && clicked.hasItem()) { diff --git a/src/main/java/sebastrn/applieddelight/setup/CommonSetup.java b/src/main/java/sebastrn/applieddelight/setup/CommonSetup.java index ad79947..2395977 100644 --- a/src/main/java/sebastrn/applieddelight/setup/CommonSetup.java +++ b/src/main/java/sebastrn/applieddelight/setup/CommonSetup.java @@ -3,6 +3,7 @@ import appeng.api.features.GridLinkables; import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent; import sebastrn.applieddelight.ADItems; +import sebastrn.applieddelight.integration.ae2.AutoCookingPattern; import sebastrn.applieddelight.item.MECookingPotItem; public final class CommonSetup { @@ -14,7 +15,9 @@ public static void onCommonSetup(FMLCommonSetupEvent event) { // Register the ME Cooking Pot item as linkable at a Wireless Access Point's GUI, exactly like AE2's own // wireless terminals. Linking stores the access point's GlobalPos on the stack; placing the pot copies it // to the block entity. - event.enqueueWork(() -> - GridLinkables.register(ADItems.ME_COOKING_POT.get(), MECookingPotItem.LINKABLE_HANDLER)); + event.enqueueWork(() -> { + GridLinkables.register(ADItems.ME_COOKING_POT.get(), MECookingPotItem.LINKABLE_HANDLER); + AutoCookingPattern.registerDecoder(); + }); } }