diff --git a/src/main/java/org/cyclops/integratedcrafting/client/gui/ContainerScreenPartInterfaceCraftingSettings.java b/src/main/java/org/cyclops/integratedcrafting/client/gui/ContainerScreenPartInterfaceCraftingSettings.java index f0380fd59..2189c2dcc 100644 --- a/src/main/java/org/cyclops/integratedcrafting/client/gui/ContainerScreenPartInterfaceCraftingSettings.java +++ b/src/main/java/org/cyclops/integratedcrafting/client/gui/ContainerScreenPartInterfaceCraftingSettings.java @@ -12,14 +12,17 @@ import net.minecraft.world.entity.player.Inventory; import org.cyclops.commoncapabilities.api.ingredient.IngredientComponent; import org.cyclops.cyclopscore.client.gui.component.button.ButtonCheckbox; +import org.cyclops.cyclopscore.client.gui.component.button.ButtonImage; import org.cyclops.cyclopscore.client.gui.component.input.IInputListener; import org.cyclops.cyclopscore.client.gui.component.input.WidgetArrowedListField; import org.cyclops.cyclopscore.client.gui.component.input.WidgetNumberField; +import org.cyclops.cyclopscore.client.gui.image.IImage; import org.cyclops.cyclopscore.helper.Helpers; import org.cyclops.cyclopscore.helper.L10NHelpers; import org.cyclops.cyclopscore.helper.ValueNotifierHelpers; import org.cyclops.integratedcrafting.Reference; import org.cyclops.integratedcrafting.inventory.container.ContainerPartInterfaceCraftingSettings; +import org.cyclops.integrateddynamics.client.gui.image.Images; import org.cyclops.integrateddynamics.core.client.gui.WidgetTextFieldDropdown; import org.cyclops.integrateddynamics.core.client.gui.container.ContainerScreenPartSettings; import org.lwjgl.glfw.GLFW; @@ -95,6 +98,17 @@ protected void onSave() { public void init() { super.init(); + if (getMenu().getPartType().supportsOffsets()) { + addRenderableWidget(new ButtonImage(this.leftPos - 20, this.topPos + 10, 18, 18, + Component.translatable("gui.integrateddynamics.part_offsets"), + createServerPressable(ContainerPartInterfaceCraftingSettings.BUTTON_OFFSETS, (button) -> {}), + new IImage[]{ + Images.BUTTON_BACKGROUND_INACTIVE, + Images.BUTTON_MIDDLE_OFFSET + }, + false, 0, 0)); + } + ingredientComponentSideSelector = new WidgetArrowedListField>(font, leftPos + 106, topPos + 9, 68, 15, true, Component.translatable("gui.integratedcrafting.partsettings.ingredient"), @@ -202,6 +216,16 @@ protected void renderBg(GuiGraphics guiGraphics, float partialTicks, int mouseX, checkboxFieldBlockingMode.render(guiGraphics, mouseX, mouseY, partialTicks); } + @Override + protected void renderLabels(GuiGraphics guiGraphics, int mouseX, int mouseY) { + super.renderLabels(guiGraphics, mouseX, mouseY); + + if (getMenu().getPartType().supportsOffsets() && isHovering(-20, 0 + 10, 18, 18, mouseX, mouseY)) { + drawTooltip(Lists.newArrayList(Component.translatable("gui.integrateddynamics.part_offsets")), + guiGraphics.pose(), mouseX - leftPos, mouseY - topPos); + } + } + @Override protected int getBaseYSize() { return 256; diff --git a/src/main/java/org/cyclops/integratedcrafting/core/part/PartTypeCraftingBase.java b/src/main/java/org/cyclops/integratedcrafting/core/part/PartTypeCraftingBase.java index 9cec80dc1..860c5a664 100644 --- a/src/main/java/org/cyclops/integratedcrafting/core/part/PartTypeCraftingBase.java +++ b/src/main/java/org/cyclops/integratedcrafting/core/part/PartTypeCraftingBase.java @@ -1,8 +1,6 @@ package org.cyclops.integratedcrafting.core.part; -import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.chat.Component; -import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.MenuProvider; import net.minecraft.world.SimpleContainer; import net.minecraft.world.entity.player.Inventory; @@ -10,7 +8,6 @@ import net.minecraft.world.inventory.AbstractContainerMenu; import org.apache.commons.lang3.tuple.Triple; import org.cyclops.cyclopscore.init.ModBase; -import org.cyclops.cyclopscore.network.PacketCodec; import org.cyclops.integratedcrafting.IntegratedCrafting; import org.cyclops.integrateddynamics.api.part.IPartContainer; import org.cyclops.integrateddynamics.api.part.IPartState; @@ -21,15 +18,20 @@ import org.cyclops.integrateddynamics.core.helper.PartHelpers; import org.cyclops.integrateddynamics.core.inventory.container.ContainerPartSettings; import org.cyclops.integrateddynamics.core.part.PartTypeBase; +import org.cyclops.integrateddynamics.core.part.PartTypeConfigurable; import javax.annotation.Nullable; import java.util.Optional; /** * Base part for a crafting part. + * + * This extends {@link PartTypeConfigurable} so that crafting parts + * expose the part settings and part offsets guis. + * * @author rubensworks */ -public abstract class PartTypeCraftingBase

, S extends IPartState

> extends PartTypeBase { +public abstract class PartTypeCraftingBase

, S extends IPartState

> extends PartTypeConfigurable { public PartTypeCraftingBase(String name) { super(name, new PartRenderPosition(0.1875F, 0.1875F, 0.625F, 0.625F)); @@ -64,10 +66,4 @@ public boolean shouldTriggerClientSideContainerClosingOnOpen() { }); } - @Override - public void writeExtraGuiDataSettings(RegistryFriendlyByteBuf packetBuffer, PartPos pos, ServerPlayer player) { - PacketCodec.write(packetBuffer, pos); - packetBuffer.writeUtf(this.getUniqueName().toString()); - } - } diff --git a/src/main/java/org/cyclops/integratedcrafting/core/part/PartTypeInterfaceCraftingBase.java b/src/main/java/org/cyclops/integratedcrafting/core/part/PartTypeInterfaceCraftingBase.java index 0d38b5f36..cdad4ea3c 100644 --- a/src/main/java/org/cyclops/integratedcrafting/core/part/PartTypeInterfaceCraftingBase.java +++ b/src/main/java/org/cyclops/integratedcrafting/core/part/PartTypeInterfaceCraftingBase.java @@ -92,6 +92,36 @@ protected void addTargetToNetwork(INetwork network, PartTarget pos, S state, boo }); } + /** + * Update the target of the given part state, and make the crafting network aware of it. + * + * Contrary to {@link #removeTargetFromNetwork(INetwork, PartPos, S)} followed by + * {@link #addTargetToNetwork(INetwork, PartTarget, S, boolean)}, + * this retains the network and channel of the part, + * as only the targeted position changes. + * + * @param network The network. + * @param newTarget The new target. + * @param state The part state. + */ + protected void retarget(INetwork network, PartTarget newTarget, S state) { + ICraftingNetwork craftingNetwork = state.getCraftingNetwork(); + + // Unregister the recipes for the old target from the crafting network. + // This must happen before the recipes are reloaded, as the old recipes are needed for a proper removal. + if (craftingNetwork != null) { + craftingNetwork.removeCraftingInterface(state.getChannelCrafting(), state); + } + + // Update the target, and reload all recipes based on this new target. + state.setTarget(newTarget); + state.setNetworks(network, craftingNetwork, NetworkHelpers.getPartNetworkChecked(network), state.getChannel(), + ValueDeseralizationContext.of(newTarget.getCenter().getPos().getLevel(true)), false); + + // Re-register to the crafting network, so that the recipes for the new target are picked up. + state.setShouldAddToCraftingNetwork(true); + } + protected void removeTargetFromNetwork(INetwork network, PartPos pos, S state) { ICraftingNetwork craftingNetwork = state.getCraftingNetwork(); if (craftingNetwork != null) { @@ -139,6 +169,13 @@ public void update(INetwork network, IPartNetwork partNetwork, PartTarget target // This can occur when the part chunk is being reloaded. if (state.getCraftingNetwork() == null) { addTargetToNetwork(network, target, state, false); + } else { + // Detect changes to our target, which can occur when the target offset is changed. + // The target is recalculated here, as offset variables may have changed it during this update. + PartTarget currentTarget = getTarget(target.getCenter(), state); + if (!currentTarget.equals(state.getTarget())) { + retarget(network, currentTarget, state); + } } int channelCrafting = state.getChannelCrafting(); diff --git a/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestHelpersIntegratedCrafting.java b/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestHelpersIntegratedCrafting.java index dc2fc7e28..fbd605bcd 100644 --- a/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestHelpersIntegratedCrafting.java +++ b/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestHelpersIntegratedCrafting.java @@ -4,6 +4,7 @@ import com.google.common.collect.Maps; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; +import net.minecraft.core.Vec3i; import net.minecraft.gametest.framework.GameTestAssertException; import net.minecraft.gametest.framework.GameTestHelper; import net.minecraft.resources.ResourceLocation; @@ -34,6 +35,8 @@ import org.cyclops.integrateddynamics.RegistryEntries; import org.cyclops.integrateddynamics.api.evaluate.variable.IValue; import org.cyclops.integrateddynamics.api.evaluate.variable.IValueType; +import org.cyclops.integrateddynamics.api.part.IPartState; +import org.cyclops.integrateddynamics.api.part.IPartType; import org.cyclops.integrateddynamics.api.part.PartPos; import org.cyclops.integrateddynamics.api.part.PartTarget; import org.cyclops.integrateddynamics.api.part.aspect.IAspectWrite; @@ -241,6 +244,24 @@ public static , V extends IValue> void setCraftingInterf ((PartTypeInterfaceCrafting.State) partStateHolder.getState()).getCraftingJobHandler().setBlockingJobsMode(blocking); } + /** + * Make the part at the given position target another position via an offset. + * + * This also increases the max offset of the part, + * just like applying part offset enhancement items would do. + * + * @param partPos The (center) position of the part. + * @param offset The target offset. + */ + public static void setPartOffset(PartPos partPos, Vec3i offset) { + PartHelpers.PartStateHolder partStateHolder = PartHelpers.getPart(partPos); + IPartState partState = partStateHolder.getState(); + partState.setMaxOffset(Math.max(Math.abs(offset.getX()), Math.max(Math.abs(offset.getY()), Math.abs(offset.getZ())))); + if (!((IPartType) partStateHolder.getPart()).setTargetOffset(partState, partPos, offset)) { + throw new GameTestAssertException("Could not set target offset " + offset + " on the part at " + partPos); + } + } + public static , V extends IValue> void setCraftingInterfaceUpdateInterval(PartPos writerPos, int updateInterval) { PartHelpers.PartStateHolder partStateHolder = PartHelpers.getPart(writerPos); partStateHolder.getState().setUpdateInterval(updateInterval); diff --git a/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsPartOffsets.java b/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsPartOffsets.java new file mode 100644 index 000000000..282807687 --- /dev/null +++ b/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsPartOffsets.java @@ -0,0 +1,139 @@ +package org.cyclops.integratedcrafting.gametest; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Vec3i; +import net.minecraft.gametest.framework.GameTest; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.ChestBlockEntity; +import net.neoforged.neoforge.gametest.GameTestHolder; +import net.neoforged.neoforge.gametest.PrefixGameTestTemplate; +import org.apache.commons.lang3.tuple.Triple; +import org.cyclops.integratedcrafting.Reference; +import org.cyclops.integratedcrafting.part.PartTypeInterfaceCrafting; +import org.cyclops.integratedcrafting.part.PartTypeInterfaceCraftingAttuned; + +import static org.cyclops.integratedcrafting.gametest.GameTestHelpersIntegratedCrafting.createBasicNetwork; +import static org.cyclops.integratedcrafting.gametest.GameTestHelpersIntegratedCrafting.enableRecipeInWriter; +import static org.cyclops.integratedcrafting.gametest.GameTestHelpersIntegratedCrafting.setPartOffset; + +/** + * Game tests for crafting interfaces that target machines via a part offset. + * + * @author rubensworks + */ +@GameTestHolder(Reference.MOD_ID) +@PrefixGameTestTemplate(false) +public class GameTestsPartOffsets { + + public static final String TEMPLATE_EMPTY = "empty10"; + public static final int TIMEOUT = 2000; + public static final BlockPos POS = BlockPos.ZERO.offset(2, 0, 2); + + /** + * The crafting interface points at an empty block, and reaches the crafting table via an offset. + */ + @GameTest(template = TEMPLATE_EMPTY, timeoutTicks = TIMEOUT) + public void testItemsCraftChestOffset(GameTestHelper helper) { + GameTestHelpersIntegratedCrafting.INetworkPositions positions = createBasicNetwork(helper, POS); + + // Move the crafting table one block away from the crafting interface, + // and make the crafting interface target it via an offset. + helper.setBlock(POS.west(), Blocks.AIR); + helper.setBlock(POS.west().north(), Blocks.CRAFTING_TABLE); + setPartOffset(positions.interfaces().get(0), new Vec3i(0, 0, -1)); + + // Insert items in interface chest + ChestBlockEntity chestIn = helper.getBlockEntity(POS.east()); + chestIn.setItem(0, new ItemStack(Items.OAK_PLANKS, 64)); + + // Add chest recipe to crafting interface + positions.interfaceRecipeAdders().get(0).accept(Triple.of(0, RecipeType.CRAFTING, ResourceLocation.fromNamespaceAndPath("minecraft", "chest"))); + + // Enable crafting aspect in crafting writer + enableRecipeInWriter(helper, positions.writer(), new ItemStack(Items.CHEST)); + + helper.succeedWhen(() -> { + // Check crafting interface state + helper.assertTrue(positions.interfaceStates().get(0).isRecipeSlotValid(0), "Recipe in crafting interface is not valid"); + + // Check if items have been crafted + helper.assertValueEqual(chestIn.getItem(0).getItem(), Items.OAK_PLANKS, "Slot 0 item is incorrect"); + helper.assertValueEqual(chestIn.getItem(0).getCount(), 56, "Slot 0 amount is incorrect"); + helper.assertValueEqual(chestIn.getItem(1).getItem(), Items.CHEST, "Slot 1 item is incorrect"); + helper.assertValueEqual(chestIn.getItem(1).getCount(), 1, "Slot 1 amount is incorrect"); + }); + } + + /** + * The crafting interface points at a furnace, but must ignore it because an offset makes it target a crafting table. + */ + @GameTest(template = TEMPLATE_EMPTY, timeoutTicks = TIMEOUT) + public void testItemsCraftChestOffsetIgnoresAdjacentMachine(GameTestHelper helper) { + GameTestHelpersIntegratedCrafting.INetworkPositions positions = createBasicNetwork(helper, POS); + + // Place a furnace directly in front of the crafting interface, + // and make the crafting interface target a crafting table via an offset. + helper.setBlock(POS.west(), Blocks.FURNACE); + helper.setBlock(POS.west().north(), Blocks.CRAFTING_TABLE); + setPartOffset(positions.interfaces().get(0), new Vec3i(0, 0, -1)); + + // Insert items in interface chest + ChestBlockEntity chestIn = helper.getBlockEntity(POS.east()); + chestIn.setItem(0, new ItemStack(Items.OAK_PLANKS, 64)); + + // Add chest recipe to crafting interface + positions.interfaceRecipeAdders().get(0).accept(Triple.of(0, RecipeType.CRAFTING, ResourceLocation.fromNamespaceAndPath("minecraft", "chest"))); + + // Enable crafting aspect in crafting writer + enableRecipeInWriter(helper, positions.writer(), new ItemStack(Items.CHEST)); + + helper.succeedWhen(() -> { + // Check crafting interface state + helper.assertTrue(positions.interfaceStates().get(0).isRecipeSlotValid(0), "Recipe in crafting interface is not valid"); + + // Check if items have been crafted + helper.assertValueEqual(chestIn.getItem(0).getItem(), Items.OAK_PLANKS, "Slot 0 item is incorrect"); + helper.assertValueEqual(chestIn.getItem(0).getCount(), 56, "Slot 0 amount is incorrect"); + helper.assertValueEqual(chestIn.getItem(1).getItem(), Items.CHEST, "Slot 1 item is incorrect"); + helper.assertValueEqual(chestIn.getItem(1).getCount(), 1, "Slot 1 amount is incorrect"); + }); + } + + /** + * The attuned crafting interface must read the recipes of the machine it targets via an offset. + */ + @GameTest(template = TEMPLATE_EMPTY, timeoutTicks = TIMEOUT) + public void testItemsCraftAttunedPlanksOffset(GameTestHelper helper) { + GameTestHelpersIntegratedCrafting.INetworkPositions positions = createBasicNetwork(helper, POS, true); + + // Move the crafting table one block away from the crafting interface, + // and make the crafting interface target it via an offset. + helper.setBlock(POS.west(), Blocks.AIR); + helper.setBlock(POS.west().north(), Blocks.CRAFTING_TABLE); + setPartOffset(positions.interfaces().get(0), new Vec3i(0, 0, -1)); + + // Insert items in interface chest + ChestBlockEntity chestIn = helper.getBlockEntity(POS.east()); + chestIn.setItem(0, new ItemStack(Items.OAK_LOG, 64)); + + // Enable crafting aspect in crafting writer + enableRecipeInWriter(helper, positions.writer(), new ItemStack(Items.OAK_PLANKS)); + + helper.succeedWhen(() -> { + // Check crafting interface state + helper.assertTrue(positions.interfaceStates().get(0).hasValidTarget(), "Crafting interface has no valid target"); + + // Check if items have been crafted + helper.assertValueEqual(chestIn.getItem(0).getItem(), Items.OAK_LOG, "Slot 0 item is incorrect"); + helper.assertValueEqual(chestIn.getItem(0).getCount(), 63, "Slot 0 amount is incorrect"); + helper.assertValueEqual(chestIn.getItem(1).getItem(), Items.OAK_PLANKS, "Slot 1 item is incorrect"); + helper.assertValueEqual(chestIn.getItem(1).getCount(), 4, "Slot 1 amount is incorrect"); + }); + } + +} diff --git a/src/main/java/org/cyclops/integratedcrafting/inventory/container/ContainerPartInterfaceCraftingSettings.java b/src/main/java/org/cyclops/integratedcrafting/inventory/container/ContainerPartInterfaceCraftingSettings.java index 170f5b2a9..adcb10320 100644 --- a/src/main/java/org/cyclops/integratedcrafting/inventory/container/ContainerPartInterfaceCraftingSettings.java +++ b/src/main/java/org/cyclops/integratedcrafting/inventory/container/ContainerPartInterfaceCraftingSettings.java @@ -5,6 +5,7 @@ import net.minecraft.core.Direction; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.Container; import net.minecraft.world.SimpleContainer; import net.minecraft.world.entity.player.Inventory; @@ -28,6 +29,8 @@ */ public class ContainerPartInterfaceCraftingSettings extends ContainerPartSettings { + public static final String BUTTON_OFFSETS = "button_offsets"; + private final int lastChannelInterfaceCraftingValueId; private final Map, Integer> targetSideOverrideValueIds; private final int lastDisableCraftingCheckValueId; @@ -48,6 +51,14 @@ public ContainerPartInterfaceCraftingSettings(int id, Inventory playerInventory, } lastDisableCraftingCheckValueId = getNextValueId(); lastBlockingModeValueId = getNextValueId(); + + // Expose the offsets gui from within the settings gui, + // as some crafting interfaces (such as the attuned one) show the settings gui as their main gui. + putButtonAction(ContainerPartInterfaceCraftingSettings.BUTTON_OFFSETS, (s, containerExtended) -> { + if (!player.level().isClientSide()) { + PartHelpers.openContainerPartOffsets((ServerPlayer) player, getTarget().getCenter(), getPartType()); + } + }); } @Override