Merge remote-tracking branch 'origin/master' into fabric/master

# Conflicts:
#	gradle.properties
#	src/main/java/appeng/core/Registration.java
#	src/main/java/appeng/core/sync/packets/JEIRecipePacket.java
#	src/main/java/appeng/entity/TinyTNTPrimedEntity.java
#	src/main/java/appeng/fluids/client/gui/widgets/FluidSlotWidget.java
#	src/main/java/appeng/fluids/container/FluidConfigurableContainer.java
#	src/main/java/appeng/integration/modules/jei/FacadeRegistryPlugin.java
#	src/main/java/appeng/integration/modules/jei/JEIPlugin.java
#	src/main/java/appeng/integration/modules/jei/RecipeTransferHandler.java
#	src/main/java/appeng/mixins/structure/BiomeAccessor.java
#	src/main/java/appeng/parts/automation/AnnihilationPlanePart.java
#	src/main/java/appeng/parts/automation/FormationPlanePart.java
This commit is contained in:
Sebastian Hartte
2020-09-10 19:20:58 +02:00
9 changed files with 553 additions and 232 deletions
@@ -18,17 +18,28 @@
package appeng.core.sync.packets;
import java.util.Arrays;
import javax.annotation.Nullable;
import com.google.common.base.Preconditions;
import com.mojang.datafixers.util.Pair;
import io.netty.buffer.Unpooled;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.entity.player.ServerPlayerEntity;
import net.minecraft.inventory.container.Container;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.network.PacketByteBuf;
import net.minecraft.screen.ScreenHandler;
import net.minecraft.server.network.ServerPlayerEntity;
import alexiil.mc.lib.attributes.item.FixedItemInv;
import net.minecraft.item.crafting.IRecipe;
import net.minecraft.item.crafting.IRecipeSerializer;
import net.minecraft.item.crafting.Ingredient;
import net.minecraft.item.crafting.ShapedRecipe;
import net.minecraft.network.PacketBuffer;
import net.minecraft.util.NonNullList;
import net.minecraft.util.ResourceLocation;
import net.minecraftforge.common.crafting.IShapedRecipe;
import net.minecraftforge.items.IItemHandler;
import appeng.api.config.Actionable;
import appeng.api.config.SecurityPermissions;
@@ -41,165 +52,305 @@ import appeng.api.networking.storage.IStorageGrid;
import appeng.api.storage.IMEMonitor;
import appeng.api.storage.channels.IItemStorageChannel;
import appeng.api.storage.data.IAEItemStack;
import appeng.container.implementations.PatternTermContainer;
import appeng.core.Api;
import appeng.core.sync.BasePacket;
import appeng.core.sync.BasePacketHandler;
import appeng.core.sync.network.INetworkInfo;
import appeng.helpers.IContainerCraftingPacket;
import appeng.items.storage.ViewCellItem;
import appeng.util.Platform;
import appeng.util.helpers.ItemHandlerUtil;
import appeng.util.inv.AdaptorFixedInv;
import appeng.util.inv.AdaptorItemHandler;
import appeng.util.inv.WrapperInvItemHandler;
import appeng.util.item.AEItemStack;
import appeng.util.prioritylist.IPartitionList;
public class JEIRecipePacket extends BasePacket {
private ItemStack[][] recipe;
/**
* Transmit only a recipe ID.
*/
private static final int INLINE_RECIPE_NONE = 1;
public JEIRecipePacket(final PacketByteBuf stream) {
final CompoundTag comp = stream.readCompoundTag();
if (comp != null) {
this.recipe = new ItemStack[9][];
for (int x = 0; x < this.recipe.length; x++) {
final ListTag list = comp.getList("#" + x, 10);
if (list.size() > 0) {
this.recipe[x] = new ItemStack[list.size()];
for (int y = 0; y < list.size(); y++) {
this.recipe[x][y] = ItemStack.fromTag(list.getCompound(y));
}
}
}
}
}
/**
* Transmit the information about the recipe we actually need. This is
* explicitly limited since this is untrusted client->server info.
*/
private static final int INLINE_RECIPE_SHAPED = 2;
// api
public JEIRecipePacket(final CompoundTag recipe) {
final PacketByteBuf data = new PacketByteBuf(Unpooled.buffer());
private ResourceLocation recipeId;
/**
* This is optional, in case the client already knows it could not resolve the
* recipe id.
*/
@Nullable
private IRecipe<?> recipe;
private boolean crafting;
data.writeInt(this.getPacketID());
public JEIRecipePacket(final PacketBuffer stream) {
this.crafting = stream.readBoolean();
final String id = stream.readString(Short.MAX_VALUE);
this.recipeId = new ResourceLocation(id);
data.writeCompoundTag(recipe);
this.configureWrite(data);
}
@Override
public void serverPacketData(final INetworkInfo manager, final PlayerEntity player) {
final ServerPlayerEntity pmp = (ServerPlayerEntity) player;
final ScreenHandler con = pmp.currentScreenHandler;
if (!(con instanceof IContainerCraftingPacket)) {
return;
}
final IContainerCraftingPacket cct = (IContainerCraftingPacket) con;
final IGridNode node = cct.getNetworkNode();
if (node == null) {
return;
}
final IGrid grid = node.getGrid();
if (grid == null) {
return;
}
final IStorageGrid inv = grid.getCache(IStorageGrid.class);
final IEnergyGrid energy = grid.getCache(IEnergyGrid.class);
final ISecurityGrid security = grid.getCache(ISecurityGrid.class);
final ICraftingGrid crafting = grid.getCache(ICraftingGrid.class);
final FixedItemInv craftMatrix = cct.getInventoryByName("crafting");
final FixedItemInv playerInventory = cct.getInventoryByName("player");
if (inv != null && this.recipe != null && security != null) {
final IMEMonitor<IAEItemStack> storage = inv
.getInventory(Api.instance().storage().getStorageChannel(IItemStorageChannel.class));
final IPartitionList<IAEItemStack> filter = ViewCellItem.createFilter(cct.getViewCells());
for (int x = 0; x < craftMatrix.getSlotCount(); x++) {
ItemStack currentItem = craftMatrix.getInvStack(x);
// prepare slots
if (!currentItem.isEmpty()) {
// already the correct item?
ItemStack newItem = this.canUseInSlot(x, currentItem);
// put away old item
if (newItem != currentItem && security.hasPermission(player, SecurityPermissions.INJECT)) {
final IAEItemStack in = AEItemStack.fromItemStack(currentItem);
final IAEItemStack out = cct.useRealItems()
? Platform.poweredInsert(energy, storage, in, cct.getActionSource())
: null;
if (out != null) {
currentItem = out.createItemStack();
} else {
currentItem = ItemStack.EMPTY;
}
}
}
if (currentItem.isEmpty() && this.recipe[x] != null) {
// for each variant
for (int y = 0; y < this.recipe[x].length && currentItem.isEmpty(); y++) {
final IAEItemStack request = AEItemStack.fromItemStack(this.recipe[x][y]);
if (request != null) {
// try ae
if ((filter == null || filter.isListed(request))
&& security.hasPermission(player, SecurityPermissions.EXTRACT)) {
request.setStackSize(1);
IAEItemStack out;
if (cct.useRealItems()) {
out = Platform.poweredExtraction(energy, storage, request, cct.getActionSource());
} else {
// Query the crafting grid if there is a pattern providing the item
if (!crafting.getCraftingFor(request, null, 0, null).isEmpty()) {
out = request;
} else {
// Fall back using an existing item
out = storage.extractItems(request, Actionable.SIMULATE, cct.getActionSource());
}
}
if (out != null) {
currentItem = out.createItemStack();
}
}
// try inventory
if (currentItem.isEmpty()) {
AdaptorFixedInv ad = new AdaptorFixedInv(playerInventory);
if (cct.useRealItems()) {
currentItem = ad.removeItems(1, this.recipe[x][y], null);
} else {
currentItem = ad.simulateRemove(1, this.recipe[x][y], null);
}
}
}
}
}
ItemHandlerUtil.setStackInSlot(craftMatrix, x, currentItem);
}
con.onContentChanged(new WrapperInvItemHandler(craftMatrix));
int inlineRecipeType = stream.readVarInt();
switch (inlineRecipeType) {
case INLINE_RECIPE_NONE:
break;
case INLINE_RECIPE_SHAPED:
recipe = IRecipeSerializer.CRAFTING_SHAPED.read(this.recipeId, stream);
break;
default:
throw new IllegalArgumentException("Invalid inline recipe type.");
}
}
/**
* @param slot
* @param is itemstack
* @return is if it can be used, else EMPTY
* Sends a recipe identified by the given recipe ID to the server for either
* filling a crafting grid or a pattern.
*/
private ItemStack canUseInSlot(int slot, ItemStack is) {
if (this.recipe[slot] != null) {
for (ItemStack option : this.recipe[slot]) {
if (is.isItemEqual(option)) {
return is;
public JEIRecipePacket(final ResourceLocation recipeId, final boolean crafting) {
PacketBuffer data = createCommonHeader(recipeId, crafting, INLINE_RECIPE_NONE);
this.configureWrite(data);
}
/**
* Sends a recipe to the server for either filling a crafting grid or a pattern.
* <p>
* Prefer the id-based constructor above whereever possible.
*/
public JEIRecipePacket(final ShapedRecipe recipe, final boolean crafting) {
PacketBuffer data = createCommonHeader(recipe.getId(), crafting, INLINE_RECIPE_SHAPED);
IRecipeSerializer.CRAFTING_SHAPED.write(data, recipe);
this.configureWrite(data);
}
private PacketBuffer createCommonHeader(ResourceLocation recipeId, boolean crafting, int inlineRecipeType) {
final PacketBuffer data = new PacketBuffer(Unpooled.buffer());
data.writeInt(this.getPacketID());
data.writeBoolean(crafting);
data.writeResourceLocation(recipeId);
data.writeVarInt(inlineRecipeType);
return data;
}
/**
* Servside handler for this packet.
* <p>
* Makes use of {@link Preconditions#checkArgument(boolean)} as the
* {@link BasePacketHandler} is catching them and in general these cases should
* never happen except in an error case and should be logged then.
*/
@Override
public void serverPacketData(final INetworkInfo manager, final PlayerEntity player) {
// Setup and verification
final ServerPlayerEntity pmp = (ServerPlayerEntity) player;
final Container con = pmp.openContainer;
Preconditions.checkArgument(con instanceof IContainerCraftingPacket);
IRecipe<?> recipe = player.getEntityWorld().getRecipeManager().getRecipe(this.recipeId).orElse(null);
if (recipe == null && this.recipe != null) {
// Certain recipes (i.e. AE2 facades) are represented in JEI as ShapedRecipe's,
// while in reality they
// are special recipes. Those recipes are sent across the wire...
recipe = this.recipe;
}
Preconditions.checkArgument(recipe != null);
final IContainerCraftingPacket cct = (IContainerCraftingPacket) con;
final IGridNode node = cct.getNetworkNode();
Preconditions.checkArgument(node != null);
final IGrid grid = node.getGrid();
Preconditions.checkArgument(grid != null);
final IStorageGrid inv = grid.getCache(IStorageGrid.class);
Preconditions.checkArgument(inv != null);
final ISecurityGrid security = grid.getCache(ISecurityGrid.class);
Preconditions.checkArgument(security != null);
final IEnergyGrid energy = grid.getCache(IEnergyGrid.class);
final ICraftingGrid crafting = grid.getCache(ICraftingGrid.class);
final IItemHandler craftMatrix = cct.getInventoryByName("crafting");
final IItemHandler playerInventory = cct.getInventoryByName("player");
final IMEMonitor<IAEItemStack> storage = inv
.getInventory(Api.instance().storage().getStorageChannel(IItemStorageChannel.class));
final IPartitionList<IAEItemStack> filter = ViewCellItem.createFilter(cct.getViewCells());
final NonNullList<Ingredient> ingredients = this.ensure3by3CraftingMatrix(recipe);
// Handle each slot
for (int x = 0; x < craftMatrix.getSlots(); x++) {
ItemStack currentItem = craftMatrix.getStackInSlot(x);
Ingredient ingredient = ingredients.get(x);
// prepare slots
if (!currentItem.isEmpty()) {
// already the correct item? True, skip everything else
ItemStack newItem = this.canUseInSlot(ingredient, currentItem);
// put away old item, if not correct
if (newItem != currentItem && security.hasPermission(player, SecurityPermissions.INJECT)) {
final IAEItemStack in = AEItemStack.fromItemStack(currentItem);
final IAEItemStack out = cct.useRealItems()
? Platform.poweredInsert(energy, storage, in, cct.getActionSource())
: null;
if (out != null) {
currentItem = out.createItemStack();
} else {
currentItem = ItemStack.EMPTY;
}
}
}
// Find item or pattern from the network
if (currentItem.isEmpty() && security.hasPermission(player, SecurityPermissions.EXTRACT)) {
IAEItemStack out;
if (cct.useRealItems()) {
IAEItemStack request = findBestMatchingItemStack(ingredient, filter, storage, cct);
out = request != null
? Platform.poweredExtraction(energy, storage, request.setStackSize(1),
cct.getActionSource())
: null;
} else {
out = findBestMatchingPattern(ingredient, filter, crafting, storage, cct);
if (out == null) {
out = findBestMatchingItemStack(ingredient, filter, storage, cct);
}
if (out == null && ingredient.getMatchingStacks().length > 0) {
out = AEItemStack.fromItemStack(ingredient.getMatchingStacks()[0]);
}
}
if (out != null) {
currentItem = out.createItemStack();
}
}
// If still nothing, search the player inventory.
if (currentItem.isEmpty()) {
ItemStack[] matchingStacks = ingredient.getMatchingStacks();
for (ItemStack matchingStack : matchingStacks) {
if (currentItem.isEmpty()) {
AdaptorItemHandler ad = new AdaptorItemHandler(playerInventory);
if (cct.useRealItems()) {
currentItem = ad.removeItems(1, matchingStack, null);
} else {
currentItem = ad.simulateRemove(1, matchingStack, null);
}
}
}
}
ItemHandlerUtil.setStackInSlot(craftMatrix, x, currentItem);
}
if (!this.crafting) {
this.handleProcessing(con, cct, recipe);
}
con.onCraftMatrixChanged(new WrapperInvItemHandler(craftMatrix));
}
/**
* Expand any recipe to a 3x3 matrix.
* <p>
* Will throw an {@link IllegalArgumentException} in case it has more than 9 or
* a shaped recipe is either wider or higher than 3. ingredients.
*/
private NonNullList<Ingredient> ensure3by3CraftingMatrix(IRecipe<?> recipe) {
NonNullList<Ingredient> ingredients = recipe.getIngredients();
NonNullList<Ingredient> expandedIngredients = NonNullList.withSize(9, Ingredient.EMPTY);
Preconditions.checkArgument(ingredients.size() <= 9);
// shaped recipes can be smaller than 3x3, expand to 3x3 to match the crafting
// matrix
if (recipe instanceof IShapedRecipe) {
IShapedRecipe<?> shapedRecipe = (IShapedRecipe<?>) recipe;
int width = shapedRecipe.getRecipeWidth();
int height = shapedRecipe.getRecipeHeight();
Preconditions.checkArgument(width <= 3 && height <= 3);
for (int h = 0; h < height; h++) {
for (int w = 0; w < width; w++) {
int source = w + h * width;
int target = w + h * 3;
Ingredient i = ingredients.get(source);
expandedIngredients.set(target, i);
}
}
}
return ItemStack.EMPTY;
// Anything else should be a flat list
else {
for (int i = 0; i < ingredients.size(); i++) {
expandedIngredients.set(i, ingredients.get(i));
}
}
return expandedIngredients;
}
/**
* @param is itemstack
* @return is if it can be used, else EMPTY
*/
private ItemStack canUseInSlot(Ingredient ingredient, ItemStack is) {
return Arrays.stream(ingredient.getMatchingStacks()).filter(p -> p.isItemEqual(is)).findFirst()
.orElse(ItemStack.EMPTY);
}
/**
* Finds the first matching itemstack with the highest stored amount.
*/
private IAEItemStack findBestMatchingItemStack(Ingredient ingredients, IPartitionList<IAEItemStack> filter,
IMEMonitor<IAEItemStack> storage, IContainerCraftingPacket cct) {
return Arrays.stream(ingredients.getMatchingStacks()).map(AEItemStack::fromItemStack) //
.filter(r -> r != null && (filter == null || filter.isListed(r))) //
.map(s -> {
// Determine the stored count
IAEItemStack stored = storage.extractItems(s.copy().setStackSize(Long.MAX_VALUE),
Actionable.SIMULATE, cct.getActionSource());
return Pair.of(s, stored != null ? stored.getStackSize() : 0);
}).min((left, right) -> Long.compare(right.getSecond(), left.getSecond()))//
.map(Pair::getFirst).orElse(null);
}
/**
* This tries to find the first pattern matching the list of ingredients.
* <p>
* As additional condition, it sorts by the stored amount to return the one with
* the highest stored amount.
*/
private IAEItemStack findBestMatchingPattern(Ingredient ingredients, IPartitionList<IAEItemStack> filter,
ICraftingGrid crafting, IMEMonitor<IAEItemStack> storage, IContainerCraftingPacket cct) {
return Arrays.stream(ingredients.getMatchingStacks()).map(AEItemStack::fromItemStack)
.filter(r -> r != null && (filter == null || filter.isListed(r)))
.map(s -> s.setCraftable(!crafting.getCraftingFor(s, null, 0, null).isEmpty()))
.filter(IAEItemStack::isCraftable).map(s -> {
final IAEItemStack stored = storage.extractItems(s, Actionable.SIMULATE, cct.getActionSource());
return s.setStackSize(stored != null ? stored.getStackSize() : 0);
}).min((left, right) -> {
final int craftable = Boolean.compare(left.isCraftable(), right.isCraftable());
return craftable != 0 ? craftable : Long.compare(right.getStackSize(), left.getStackSize());
}).orElse(null);
}
private void handleProcessing(Container con, IContainerCraftingPacket cct, IRecipe<?> recipe) {
if (con instanceof PatternTermContainer) {
PatternTermContainer patternTerm = (PatternTermContainer) con;
if (!patternTerm.craftingMode) {
final IItemHandler output = cct.getInventoryByName("output");
ItemHandlerUtil.setStackInSlot(output, 0, recipe.getRecipeOutput());
ItemHandlerUtil.setStackInSlot(output, 1, ItemStack.EMPTY);
ItemHandlerUtil.setStackInSlot(output, 2, ItemStack.EMPTY);
}
}
}
}
@@ -19,11 +19,9 @@
package appeng.core.sync.packets;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.BufferOverflowException;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.GZIPOutputStream;
import javax.annotation.Nullable;
@@ -18,6 +18,8 @@
package appeng.integration.abstraction;
import mezz.jei.api.runtime.IJeiRuntime;
import appeng.integration.IIntegrationModule;
/**
@@ -25,6 +27,10 @@ import appeng.integration.IIntegrationModule;
*/
public interface IRei extends IIntegrationModule {
default IJeiRuntime getRuntime() {
return null;
}
default String getSearchText() {
return "";
}
@@ -0,0 +1,47 @@
/*
* This file is part of Applied Energistics 2.
* Copyright (c) 2020 Team Appliedenergistics, All rights reserved.
*
* Applied Energistics 2 is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Applied Energistics 2 is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Applied Energistics 2. If not, see <http://www.gnu.org/licenses/lgpl>.
*/
package appeng.integration.modules.jei;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.crafting.IRecipe;
import mezz.jei.api.gui.IRecipeLayout;
import mezz.jei.api.recipe.transfer.IRecipeTransferError;
import mezz.jei.api.recipe.transfer.IRecipeTransferHandlerHelper;
import appeng.container.implementations.CraftingTermContainer;
public class CraftingRecipeTransferHandler extends RecipeTransferHandler<CraftingTermContainer> {
CraftingRecipeTransferHandler(Class<CraftingTermContainer> containerClass, IRecipeTransferHandlerHelper helper) {
super(containerClass, helper);
}
@Override
protected IRecipeTransferError doTransferRecipe(CraftingTermContainer container, IRecipe<?> recipe,
IRecipeLayout recipeLayout, PlayerEntity player, boolean maxTransfer) {
return null;
}
@Override
protected boolean isCrafting() {
return true;
}
}
@@ -0,0 +1,58 @@
/*
* This file is part of Applied Energistics 2.
* Copyright (c) 2020 Team Appliedenergistics, All rights reserved.
*
* Applied Energistics 2 is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Applied Energistics 2 is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Applied Energistics 2. If not, see <http://www.gnu.org/licenses/lgpl>.
*/
package appeng.integration.modules.jei;
import net.minecraft.client.resources.I18n;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.crafting.IRecipe;
import mezz.jei.api.constants.VanillaRecipeCategoryUid;
import mezz.jei.api.gui.IRecipeLayout;
import mezz.jei.api.recipe.transfer.IRecipeTransferError;
import mezz.jei.api.recipe.transfer.IRecipeTransferHandlerHelper;
import appeng.container.implementations.PatternTermContainer;
public class PatternRecipeTransferHandler extends RecipeTransferHandler<PatternTermContainer> {
PatternRecipeTransferHandler(Class<PatternTermContainer> containerClass, IRecipeTransferHandlerHelper helper) {
super(containerClass, helper);
}
protected IRecipeTransferError doTransferRecipe(PatternTermContainer container, IRecipe<?> recipe,
IRecipeLayout recipeLayout, PlayerEntity player, boolean maxTransfer) {
if (container.isCraftingMode()
&& recipeLayout.getRecipeCategory().getUid() != VanillaRecipeCategoryUid.CRAFTING) {
return this.helper
.createUserErrorWithTooltip(I18n.format("jei.appliedenergistics2.requires_processing_mode"));
}
if (recipe.getRecipeOutput().isEmpty()) {
return this.helper.createUserErrorWithTooltip(I18n.format("jei.appliedenergistics2.no_output"));
}
return null;
}
@Override
protected boolean isCrafting() {
return false;
}
}
@@ -18,96 +18,128 @@
package appeng.integration.modules.jei;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import net.minecraft.client.resources.I18n;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.inventory.container.Container;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.screen.ScreenHandler;
import net.minecraft.screen.slot.Slot;
import net.minecraft.item.crafting.IRecipe;
import net.minecraft.item.crafting.Ingredient;
import net.minecraft.item.crafting.ShapedRecipe;
import net.minecraft.item.crafting.ShapelessRecipe;
import net.minecraft.util.NonNullList;
import net.minecraft.util.ResourceLocation;
import me.shedaniel.rei.api.AutoTransferHandler;
import me.shedaniel.rei.api.EntryStack;
import me.shedaniel.rei.api.TransferRecipeDisplay;
import mezz.jei.api.gui.IRecipeLayout;
import mezz.jei.api.gui.ingredient.IGuiIngredient;
import mezz.jei.api.recipe.transfer.IRecipeTransferError;
import mezz.jei.api.recipe.transfer.IRecipeTransferHandler;
import mezz.jei.api.recipe.transfer.IRecipeTransferHandlerHelper;
import appeng.container.slot.CraftingMatrixSlot;
import appeng.container.slot.FakeCraftingMatrixSlot;
import appeng.core.sync.network.NetworkHandler;
import appeng.core.sync.packets.JEIRecipePacket;
import appeng.mixins.SlotMixin;
import appeng.util.Platform;
import appeng.helpers.IContainerCraftingPacket;
class RecipeTransferHandler<T extends ScreenHandler> implements AutoTransferHandler {
abstract class RecipeTransferHandler<T extends Container & IContainerCraftingPacket>
implements IRecipeTransferHandler<T> {
private final Class<T> containerClass;
protected final IRecipeTransferHandlerHelper helper;
RecipeTransferHandler(Class<T> containerClass) {
RecipeTransferHandler(Class<T> containerClass, IRecipeTransferHandlerHelper helper) {
this.containerClass = containerClass;
this.helper = helper;
}
@Override
public Result handle(Context context) {
if (!(context.getRecipe() instanceof TransferRecipeDisplay)) {
return Result.createNotApplicable();
public final Class<T> getContainerClass() {
return this.containerClass;
}
@Override
public final IRecipeTransferError transferRecipe(T container, Object recipe, IRecipeLayout recipeLayout,
PlayerEntity player, boolean maxTransfer, boolean doTransfer) {
if (!(recipe instanceof IRecipe)) {
return this.helper.createInternalError();
}
final IRecipe<?> irecipe = (IRecipe<?>) recipe;
final ResourceLocation recipeId = irecipe.getId();
if (recipeId == null) {
return this.helper.createUserErrorWithTooltip(I18n.format("jei.appliedenergistics2.missing_id"));
}
ScreenHandler container = context.getContainerScreen().getScreenHandler();
if (!containerClass.isInstance(container)) {
return Result.createNotApplicable();
// Check that the recipe can actually be looked up via the manager, i.e. our
// facade recipes
// have an ID, but are never registered with the recipe manager.
boolean canSendReference = true;
if (!player.getEntityWorld().getRecipeManager().getRecipe(recipeId).isPresent()) {
// Validate that the recipe is a shapeless or shapedrecipe, since we can
// serialize those
if (!(recipe instanceof ShapedRecipe) && !(recipe instanceof ShapelessRecipe)) {
return this.helper.createUserErrorWithTooltip(I18n.format("jei.appliedenergistics2.missing_id"));
}
canSendReference = false;
}
if (!context.isActuallyCrafting()) {
// This is just to check whether the button is enabled
return Result.createSuccessful();
if (!irecipe.canFit(3, 3)) {
return this.helper.createUserErrorWithTooltip(I18n.format("jei.appliedenergistics2.recipe_too_large"));
}
TransferRecipeDisplay transferRecipe = (TransferRecipeDisplay) context.getRecipe();
final IRecipeTransferError error = doTransferRecipe(container, irecipe, recipeLayout, player, maxTransfer);
final CompoundTag recipe = new CompoundTag();
if (error != null) {
return error;
}
List<List<EntryStack>> ingredients = transferRecipe.getInputEntries();
for (int i = 0; i < ingredients.size(); i++) {
List<EntryStack> ingredientEntry = ingredients.get(i);
if (doTransfer) {
if (canSendReference) {
NetworkHandler.instance().sendToServer(new JEIRecipePacket(recipeId, isCrafting()));
} else {
// To avoid earlier problems of too large packets being sent that crashed the
// client,
// as a fallback when the recipe ID could not be resolved, we'll just send the
// displayed
// items.
NonNullList<Ingredient> flatIngredients = NonNullList.withSize(9, Ingredient.EMPTY);
ItemStack output = ItemStack.EMPTY;
// Vanilla will pack ingredients if the recipe is less than 3x3
int x = i % transferRecipe.getWidth();
int y = (i - x) / transferRecipe.getWidth();
int slotIndex = 3 * y + x;
// Determine the first JEI slot that has an actual input, we'll use this to
// offset the
// crafting grid target slot
int firstInputSlot = recipeLayout.getItemStacks().getGuiIngredients().entrySet().stream()
.filter(e -> e.getValue().isInput()).mapToInt(Map.Entry::getKey).min().orElse(0);
for (final Slot slot : container.slots) {
if (slot instanceof CraftingMatrixSlot || slot instanceof FakeCraftingMatrixSlot) {
int containerSlotInvIdx = ((SlotMixin) slot).getIndex();
if (containerSlotInvIdx == slotIndex) {
final ListTag tags = new ListTag();
final List<ItemStack> list = new ArrayList<>();
// Now map the actual ingredients into the output/input
for (Map.Entry<Integer, ? extends IGuiIngredient<ItemStack>> entry : recipeLayout.getItemStacks()
.getGuiIngredients().entrySet()) {
IGuiIngredient<ItemStack> item = entry.getValue();
if (item.getDisplayedIngredient() == null) {
continue;
}
// prefer pure crystals.
for (EntryStack stack : ingredientEntry) {
if (Platform.isRecipePrioritized(stack.getItemStack())) {
list.add(0, stack.getItemStack());
} else {
list.add(stack.getItemStack());
}
int inputIndex = entry.getKey() - firstInputSlot;
if (item.isInput() && inputIndex < flatIngredients.size()) {
ItemStack displayedIngredient = item.getDisplayedIngredient();
if (displayedIngredient != null) {
flatIngredients.set(inputIndex, Ingredient.fromStacks(displayedIngredient));
}
for (final ItemStack is : list) {
final CompoundTag tag = new CompoundTag();
is.toTag(tag);
tags.add(tag);
}
recipe.put("#" + containerSlotInvIdx, tags);
break;
} else if (!item.isInput() && output.isEmpty()) {
output = item.getDisplayedIngredient();
}
}
ShapedRecipe fallbackRecipe = new ShapedRecipe(recipeId, "", 3, 3, flatIngredients, output);
NetworkHandler.instance().sendToServer(new JEIRecipePacket(fallbackRecipe, isCrafting()));
}
}
NetworkHandler.instance().sendToServer(new JEIRecipePacket(recipe));
return Result.createFailed(""); // this will return to the screen
return null;
}
protected abstract IRecipeTransferError doTransferRecipe(T container, IRecipe<?> recipe, IRecipeLayout recipeLayout,
PlayerEntity player, boolean maxTransfer);
protected abstract boolean isCrafting();
}
@@ -39,6 +39,10 @@ class ReiRuntimeAdapter implements IRei {
return true;
}
public IJeiRuntime getRuntime() {
return runtime;
}
@Override
public String getSearchText() {
TextFieldWidget searchField = this.runtime.getSearchTextField();
@@ -25,7 +25,6 @@ import java.util.Random;
import net.minecraft.block.entity.BlockEntity;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityType;
import net.minecraft.entity.ItemEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.AutomaticItemPlacementContext;
@@ -281,50 +280,72 @@ public class FormationPlanePart extends AbstractFormationPlanePart<IAEItemStack>
}
private static boolean spawnItemEntity(ServerWorld w, BlockEntity te, AEPartLocation side, ItemStack is) {
// the item offset based on the entity height plus some offset
final double itemOffset = .55 + EntityType.ITEM.getHeight();
// The center of the block the plane is located in
final double centerX = te.getPos().getX() + .5;
final double centerY = te.getPos().getY() + .5;
final double centerY = te.getPos().getY();
final double centerZ = te.getPos().getZ() + .5;
// When spawning downwards, we have to take the item height of 0.25 into account
// Otherwise it will get stuck and be spit out in a random direction as
// minecraft spawns it at its feet position and not center
final double additionalYOffset = side.yOffset == -1 ? -.3 : 0;
// Create an ItemEntity already at the position of the plane.
// We don't know the final position, but need it for its size.
Entity entity = new ItemEntity(w, centerX, centerY, centerZ, is.copy());
// Replace it if there is a custom entity
if (is.getItem() instanceof AECustomEntityItem) {
Entity result = ((AECustomEntityItem) is.getItem()).replaceItemEntity(w, entity, is);
// Destroy the old one, in case it's spawned somehow and replace with the new
// one.
if (result != entity) {
entity.remove();
entity = result;
}
}
if (is.getItem().hasCustomEntity(is)) {
Entity result = is.getItem().createEntity(w, entity, is);
// Destroy the old one, in case it's spawned somehow and replace with the new
// one.
if (result != null) {
entity.remove();
entity = result;
}
}
// When spawning downwards, we have to take into account that it spawns it at
// their "feet" and not center like x or z. So we move it up to be flush with
// the plane
final double additionalYOffset = side.yOffset == -1 ? 1 - entity.getHeight() : 0;
// Calculate the maximum spawn area so an entity hitbox will always be inside
// the block.
final double spawnAreaHeight = Math.max(0, 1 - entity.getHeight());
final double spawnAreaWidth = Math.max(0, 1 - entity.getWidth());
// Calculate the offsets to spawn it into the adjacent block, taking the sign
// into account.
// Spawn it 0.8 blocks away from the center pos when facing in this direction
// Every other direction will select a position in a .5 block area around the
// block center.
final double offsetX = (side.xOffset == 0) ? ((RANDOM_OFFSET.nextFloat() / 2) - .25)
: (side.xOffset * itemOffset);
final double offsetY = (side.yOffset == 0) ? ((RANDOM_OFFSET.nextFloat() / 2) - .25)
: ((side.yOffset * itemOffset) + additionalYOffset);
final double offsetZ = (side.zOffset == 0) ? ((RANDOM_OFFSET.nextFloat() / 2) - .25)
: (side.zOffset * itemOffset);
final double offsetX = (side.xOffset == 0) //
? ((RANDOM_OFFSET.nextFloat() * spawnAreaWidth) - spawnAreaWidth / 2)
: (side.xOffset * (.525 + entity.getWidth() / 2));
final double offsetY = (side.yOffset == 0) //
? (RANDOM_OFFSET.nextFloat() * spawnAreaHeight)
: ((side.yOffset) + additionalYOffset);
final double offsetZ = (side.zOffset == 0) //
? ((RANDOM_OFFSET.nextFloat() * spawnAreaWidth) - spawnAreaWidth / 2)
: (side.zOffset * (.525 + entity.getWidth() / 2));
final double absoluteX = centerX + offsetX;
final double absoluteY = centerY + offsetY;
final double absoluteZ = centerZ + offsetZ;
final ItemEntity ei = new ItemEntity(w, absoluteX, absoluteY, absoluteZ, is.copy());
ei.setVelocity(side.xOffset * .1, side.yOffset * 0.1, side.zOffset * 0.1);
// Set to correct position and slow the motion down a bit
entity.setPosition(absoluteX, absoluteY, absoluteZ);
entity.setVelocity(side.xOffset * .1, side.yOffset * 0.1, side.zOffset * 0.1);
Entity result;
if (is.getItem() instanceof AECustomEntityItem) {
result = ((AECustomEntityItem) is.getItem()).replaceItemEntity(w, ei, is);
if (result != ei) {
ei.remove();
}
} else {
result = ei;
}
if (!w.spawnEntity(result)) {
result.remove();
// Try to spawn it and destroy it in case it's not possible
if (!w.spawnEntity(entity)) {
entity.remove();
return false;
}
return true;