From c85482f10f8bd285212bd42245666bb677f1fd41 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 14:05:38 +0000 Subject: [PATCH 01/13] Allow part configurations to be copied and pasted with the Wrench Adds a new CONFIG mode to the Wrench, in which shift+right-clicking a part copies its configuration into the Wrench, and right-clicking another part of the same type pastes it. The configuration consists of three sections: the general part settings (update interval, priority, channel, target side override and target offset), the statically configured aspect properties, and all variable cards (the active variable inventory, the aspect setting variables and the offset variables). Pasted variable cards get a new variable id, and each of them consumes one blank Variable Card from the player's inventory, unless they are in creative mode. If there are not enough blank cards, the settings are still applied and the player is told how many more cards are needed. Cards that were already present in the target part are given back to the player. The Settings and Aspect Settings screens also get copy and paste buttons, which only copy and paste what those screens show. In the Aspect Settings screen, settings are matched by property type instead of by aspect, so settings can be copied between different aspects that share a property. Closes #859 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01LBAgcFp6jWcYRWz9N8h7AF --- CHANGELOG-1.21.1.md | 1 + .../gametest/GameTestsWrenchConfig.java | 446 +++++++++++++++ .../cyclops/integrateddynamics/Configs.java | 1 + .../integrateddynamics/RegistryEntries.java | 1 + .../api/part/IPartType.java | 41 ++ .../api/part/PartTypeAdapter.java | 3 +- ...xelShapeComponentsFactoryHandlerParts.java | 10 +- .../DataComponentWrenchPartConfigConfig.java | 19 + .../ContainerScreenAspectSettings.java | 8 + .../ContainerScreenPartSettings.java | 8 + .../core/helper/PartConfigHelpers.java | 514 ++++++++++++++++++ .../container/ContainerAspectSettings.java | 143 +++++ .../container/ContainerPartOffset.java | 5 +- .../container/ContainerPartSettings.java | 67 +++ .../core/part/PartConfigApplyResult.java | 78 +++ .../core/part/PartConfigSection.java | 37 ++ .../core/part/PartConfigSnapshot.java | 152 ++++++ .../core/part/PartStateOffsetHandler.java | 7 +- .../integrateddynamics/item/ItemWrench.java | 98 +++- .../assets/integrateddynamics/lang/en_us.json | 19 + .../info/on_the_dynamics_of_integration.xml | 1 + .../core/part/TestPartConfigSnapshot.java | 91 ++++ 22 files changed, 1744 insertions(+), 6 deletions(-) create mode 100644 src/integrationtest/java/org/cyclops/integrateddynamics/gametest/GameTestsWrenchConfig.java create mode 100644 src/main/java/org/cyclops/integrateddynamics/component/DataComponentWrenchPartConfigConfig.java create mode 100644 src/main/java/org/cyclops/integrateddynamics/core/helper/PartConfigHelpers.java create mode 100644 src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigApplyResult.java create mode 100644 src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigSection.java create mode 100644 src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigSnapshot.java create mode 100644 src/test/java/org/cyclops/integrateddynamics/core/part/TestPartConfigSnapshot.java diff --git a/CHANGELOG-1.21.1.md b/CHANGELOG-1.21.1.md index 9782c636a37..e5db9f2aad1 100644 --- a/CHANGELOG-1.21.1.md +++ b/CHANGELOG-1.21.1.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Added +* Allow part configurations to be copied and pasted with the Wrench, Closes #859 * Allow aspect settings to be determined by variables (#1707), Closes CyclopsMC/IntegratedTunnels#278 * Show modified aspect property values in tooltip (#1706), Closes #1704 diff --git a/src/integrationtest/java/org/cyclops/integrateddynamics/gametest/GameTestsWrenchConfig.java b/src/integrationtest/java/org/cyclops/integrateddynamics/gametest/GameTestsWrenchConfig.java new file mode 100644 index 00000000000..487091676a4 --- /dev/null +++ b/src/integrationtest/java/org/cyclops/integrateddynamics/gametest/GameTestsWrenchConfig.java @@ -0,0 +1,446 @@ +package org.cyclops.integrateddynamics.gametest; + +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.GameTest; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.Container; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.GameType; +import net.minecraft.world.phys.BlockHitResult; +import net.neoforged.neoforge.gametest.GameTestHolder; +import net.neoforged.neoforge.gametest.PrefixGameTestTemplate; +import org.apache.commons.lang3.tuple.Triple; +import org.cyclops.cyclopscore.inventory.SimpleInventory; +import org.cyclops.integrateddynamics.Reference; +import org.cyclops.integrateddynamics.RegistryEntries; +import org.cyclops.integrateddynamics.api.evaluate.variable.ValueDeseralizationContext; +import org.cyclops.integrateddynamics.api.network.INetwork; +import org.cyclops.integrateddynamics.api.part.IPartContainer; +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.IAspect; +import org.cyclops.integrateddynamics.core.helper.NetworkHelpers; +import org.cyclops.integrateddynamics.core.helper.PartConfigHelpers; +import org.cyclops.integrateddynamics.core.helper.PartHelpers; +import org.cyclops.integrateddynamics.core.inventory.container.ContainerAspectSettings; +import org.cyclops.integrateddynamics.core.part.PartConfigApplyResult; +import org.cyclops.integrateddynamics.core.part.PartConfigSection; +import org.cyclops.integrateddynamics.core.part.PartConfigSnapshot; +import org.cyclops.integrateddynamics.core.part.PartStateActiveVariableBase; +import org.cyclops.integrateddynamics.core.part.PartTypeBase; +import org.cyclops.integrateddynamics.core.part.PartTypes; +import org.cyclops.integrateddynamics.item.ItemWrench; +import org.cyclops.integrateddynamics.part.aspect.Aspects; +import org.cyclops.integrateddynamics.part.aspect.write.AspectWriteBuilders; +import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypeBoolean; +import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypeInteger; +import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypes; + +import javax.annotation.Nullable; +import java.util.Map; +import java.util.Optional; + +import static org.cyclops.integrateddynamics.gametest.GameTestHelpersIntegratedDynamics.createVariableForValue; +import static org.cyclops.integrateddynamics.gametest.GameTestHelpersIntegratedDynamics.getEffectiveAspectProperty; +import static org.cyclops.integrateddynamics.gametest.GameTestHelpersIntegratedDynamics.placeVariableInWriter; +import static org.cyclops.integrateddynamics.gametest.GameTestHelpersIntegratedDynamics.setAspectProperty; + +/** + * Tests for copying and pasting part configurations with the Wrench. + * @author rubensworks + */ +@GameTestHolder(Reference.MOD_ID) +@PrefixGameTestTemplate(false) +public class GameTestsWrenchConfig { + + public static final String TEMPLATE_EMPTY = "empty10"; + public static final BlockPos POS_SOURCE = BlockPos.ZERO.offset(2, 0, 2); + public static final BlockPos POS_TARGET = BlockPos.ZERO.offset(2, 0, 4); + + protected static PartPos placePart(GameTestHelper helper, BlockPos pos, IPartType partType) { + helper.setBlock(pos, RegistryEntries.BLOCK_CABLE.value()); + PartHelpers.addPart(helper.getLevel(), helper.absolutePos(pos), Direction.WEST, partType, + new ItemStack(partType.getItem())); + return PartPos.of(helper.getLevel(), helper.absolutePos(pos), Direction.WEST); + } + + protected static ItemWrench wrenchItem() { + return (ItemWrench) RegistryEntries.ITEM_WRENCH.value(); + } + + protected static ItemStack createWrench(ItemWrench.Mode mode) { + ItemStack wrench = new ItemStack(wrenchItem()); + wrenchItem().setMode(wrench, mode); + return wrench; + } + + /** + * Click on the given part with the given item, in the same way as a player would. + */ + protected static void clickPart(GameTestHelper helper, Player player, ItemStack itemStack, PartPos partPos, + boolean sneaking) { + player.setShiftKeyDown(sneaking); + player.setItemInHand(InteractionHand.MAIN_HAND, itemStack); + GameTestsOffsets.facePlayerToPart(player, partPos); + BlockPos blockPos = partPos.getPos().getBlockPos(); + helper.getLevel().getBlockState(blockPos).useItemOn(itemStack, helper.getLevel(), player, + InteractionHand.MAIN_HAND, + new BlockHitResult(blockPos.getCenter(), partPos.getSide(), blockPos, false)); + } + + protected static IPartType partType(PartPos partPos) { + return PartHelpers.getPart(partPos).getPart(); + } + + protected static IPartState partState(PartPos partPos) { + return PartHelpers.getPart(partPos).getState(); + } + + /** + * Give the given part a non-default configuration, so that copying it is observable. + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + protected static void configurePart(GameTestHelper helper, PartPos partPos, Vec3i offset) { + IPartType partType = partType(partPos); + IPartState state = partState(partPos); + partType.setUpdateInterval(state, 40); + state.setPriority(3); + state.setChannel(2); + partType.setTargetSideOverride(state, Direction.SOUTH); + if (offset.compareTo(Vec3i.ZERO) != 0) { + GameTestsOffsets.increaseMaxOffset(helper, partPos, 4); + partType.setTargetOffset(state, partPos, offset); + } + setAspectProperty(partPos, Aspects.Write.Redstone.BOOLEAN, + AspectWriteBuilders.Redstone.PROP_STRONG_POWER, ValueTypeBoolean.ValueBoolean.of(true)); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + protected static PartConfigApplyResult applyConfig(GameTestHelper helper, PartPos partPos, + PartConfigSnapshot snapshot, Player player) { + IPartType partType = partType(partPos); + IPartState state = partState(partPos); + INetwork network = NetworkHelpers.getNetwork(partPos).orElse(null); + PartTarget target = partType.getTarget(partPos, state); + return partType.applyConfig(ValueDeseralizationContext.of(helper.getLevel()), network, + NetworkHelpers.getPartNetwork(network).orElse(null), target, state, snapshot, + PartConfigSection.ALL, player); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + protected static PartConfigSnapshot snapshotConfig(GameTestHelper helper, PartPos partPos) { + IPartType partType = partType(partPos); + return partType.snapshotConfig(ValueDeseralizationContext.of(helper.getLevel()), + partState(partPos), PartConfigSection.ALL); + } + + @Nullable + protected static ItemStack getActiveVariable(PartPos partPos) { + SimpleInventory inventory = ((PartStateActiveVariableBase) partState(partPos)).getInventory(); + for (int slot = 0; slot < inventory.getContainerSize(); slot++) { + if (!inventory.getItem(slot).isEmpty()) { + return inventory.getItem(slot); + } + } + return null; + } + + protected static int getVariableId(GameTestHelper helper, ItemStack itemStack) { + return RegistryEntries.ITEM_VARIABLE.get() + .getVariableFacade(ValueDeseralizationContext.of(helper.getLevel()), itemStack).getId(); + } + + protected static int countBlankVariables(Player player) { + return PartConfigHelpers.countBlankVariables(player); + } + + protected static void giveBlankVariables(Player player, int count) { + player.getInventory().add(new ItemStack(RegistryEntries.ITEM_VARIABLE.value(), count)); + } + + @GameTest(template = TEMPLATE_EMPTY) + public void testWrenchConfigCopyPasteSamePartType(GameTestHelper helper) { + PartPos source = placePart(helper, POS_SOURCE, PartTypes.REDSTONE_WRITER); + PartPos target = placePart(helper, POS_TARGET, PartTypes.REDSTONE_WRITER); + configurePart(helper, source, new Vec3i(1, 0, 0)); + GameTestsOffsets.increaseMaxOffset(helper, target, 4); + placeVariableInWriter(helper, source, Aspects.Write.Redstone.BOOLEAN, + createVariableForValue(helper.getLevel(), ValueTypes.BOOLEAN, ValueTypeBoolean.ValueBoolean.of(true))); + + Player player = helper.makeMockPlayer(GameType.SURVIVAL); + ItemStack wrench = createWrench(ItemWrench.Mode.CONFIG); + // The wrench occupies the selected hotbar slot, so it must be given before the blank variable cards + player.setItemInHand(InteractionHand.MAIN_HAND, wrench); + giveBlankVariables(player, 1); + clickPart(helper, player, wrench, source, true); + clickPart(helper, player, wrench, target, false); + + helper.succeedWhen(() -> { + IPartType partType = partType(target); + IPartState state = partState(target); + helper.assertValueEqual(partType.getUpdateInterval(state), 40, "Update interval was not pasted"); + helper.assertValueEqual(partType.getPriority(state), 3, "Priority was not pasted"); + helper.assertValueEqual(partType.getChannel(state), 2, "Channel was not pasted"); + helper.assertValueEqual(partType.getTargetSideOverride(state), Direction.SOUTH, "Target side was not pasted"); + helper.assertValueEqual(partType.getTargetOffset(state), new Vec3i(1, 0, 0), "Target offset was not pasted"); + helper.assertValueEqual( + getEffectiveAspectProperty(target, Aspects.Write.Redstone.BOOLEAN, + AspectWriteBuilders.Redstone.PROP_STRONG_POWER), + ValueTypeBoolean.ValueBoolean.of(true), "Aspect property was not pasted"); + helper.assertTrue(getActiveVariable(target) != null, "Variable card was not pasted"); + helper.assertValueEqual(countBlankVariables(player), 0, "Blank variable card was not consumed"); + }); + } + + @GameTest(template = TEMPLATE_EMPTY) + public void testWrenchConfigPasteOtherPartType(GameTestHelper helper) { + PartPos source = placePart(helper, POS_SOURCE, PartTypes.REDSTONE_WRITER); + PartPos target = placePart(helper, POS_TARGET, PartTypes.REDSTONE_READER); + configurePart(helper, source, Vec3i.ZERO); + + int updateInterval = ((IPartType) partType(target)).getUpdateInterval(partState(target)); + + Player player = helper.makeMockPlayer(GameType.SURVIVAL); + ItemStack wrench = createWrench(ItemWrench.Mode.CONFIG); + clickPart(helper, player, wrench, source, true); + clickPart(helper, player, wrench, target, false); + + helper.succeedWhen(() -> { + IPartType partType = partType(target); + IPartState state = partState(target); + helper.assertValueEqual(partType.getUpdateInterval(state), updateInterval, "Update interval was pasted"); + helper.assertValueEqual(partType.getPriority(state), 0, "Priority was pasted"); + helper.assertValueEqual(partType.getChannel(state), 0, "Channel was pasted"); + helper.assertTrue(partType.getTargetSideOverride(state) == null, "Target side was pasted"); + }); + } + + @GameTest(template = TEMPLATE_EMPTY) + public void testWrenchConfigPasteCardsGetNewIdAndEjectExisting(GameTestHelper helper) { + PartPos source = placePart(helper, POS_SOURCE, PartTypes.REDSTONE_WRITER); + PartPos target = placePart(helper, POS_TARGET, PartTypes.REDSTONE_WRITER); + ItemStack sourceVariable = createVariableForValue(helper.getLevel(), ValueTypes.BOOLEAN, + ValueTypeBoolean.ValueBoolean.of(true)); + ItemStack targetVariable = createVariableForValue(helper.getLevel(), ValueTypes.BOOLEAN, + ValueTypeBoolean.ValueBoolean.of(false)); + placeVariableInWriter(helper, source, Aspects.Write.Redstone.BOOLEAN, sourceVariable); + placeVariableInWriter(helper, target, Aspects.Write.Redstone.BOOLEAN, targetVariable); + int sourceId = getVariableId(helper, sourceVariable); + int ejectedId = getVariableId(helper, targetVariable); + + Player player = helper.makeMockPlayer(GameType.SURVIVAL); + ItemStack wrench = createWrench(ItemWrench.Mode.CONFIG); + // The wrench occupies the selected hotbar slot, so it must be given before the blank variable cards + player.setItemInHand(InteractionHand.MAIN_HAND, wrench); + giveBlankVariables(player, 3); + clickPart(helper, player, wrench, source, true); + clickPart(helper, player, wrench, target, false); + + helper.succeedWhen(() -> { + ItemStack pasted = getActiveVariable(target); + helper.assertTrue(pasted != null, "Variable card was not pasted"); + helper.assertTrue(getVariableId(helper, pasted) != sourceId, + "Pasted variable card has the same id as the copied one"); + helper.assertValueEqual(countBlankVariables(player), 2, "Wrong number of blank variable cards consumed"); + helper.assertTrue(hasVariableWithId(helper, player, ejectedId), + "The variable card that was in the target part was not given back to the player"); + }); + } + + protected static boolean hasVariableWithId(GameTestHelper helper, Player player, int id) { + for (int slot = 0; slot < player.getInventory().getContainerSize(); slot++) { + ItemStack itemStack = player.getInventory().getItem(slot); + if (itemStack.is(RegistryEntries.ITEM_VARIABLE.value()) + && itemStack.has(RegistryEntries.DATACOMPONENT_VARIABLE_FACADE.get()) + && getVariableId(helper, itemStack) == id) { + return true; + } + } + return false; + } + + @GameTest(template = TEMPLATE_EMPTY) + public void testWrenchConfigPasteWithoutBlanks(GameTestHelper helper) { + PartPos source = placePart(helper, POS_SOURCE, PartTypes.REDSTONE_WRITER); + PartPos target = placePart(helper, POS_TARGET, PartTypes.REDSTONE_WRITER); + configurePart(helper, source, Vec3i.ZERO); + ItemStack sourceVariable = createVariableForValue(helper.getLevel(), ValueTypes.BOOLEAN, + ValueTypeBoolean.ValueBoolean.of(true)); + placeVariableInWriter(helper, source, Aspects.Write.Redstone.BOOLEAN, sourceVariable); + + Player player = helper.makeMockPlayer(GameType.SURVIVAL); + PartConfigApplyResult result = applyConfig(helper, target, snapshotConfig(helper, source), player); + + helper.succeedWhen(() -> { + helper.assertValueEqual(result.getMissingBlanks(), 1, "Wrong number of missing blank variable cards"); + helper.assertValueEqual(result.getCardsPasted(), 0, "Variable cards were pasted"); + helper.assertTrue(getActiveVariable(target) == null, "The target part received a variable card"); + helper.assertTrue(result.isPartSettingsApplied(), "The part settings were not applied"); + helper.assertValueEqual(((IPartType) partType(target)).getUpdateInterval(partState(target)), 40, + "Update interval was not pasted"); + }); + } + + @GameTest(template = TEMPLATE_EMPTY) + public void testWrenchConfigPasteCreativeWithoutBlanks(GameTestHelper helper) { + PartPos source = placePart(helper, POS_SOURCE, PartTypes.REDSTONE_WRITER); + PartPos target = placePart(helper, POS_TARGET, PartTypes.REDSTONE_WRITER); + placeVariableInWriter(helper, source, Aspects.Write.Redstone.BOOLEAN, + createVariableForValue(helper.getLevel(), ValueTypes.BOOLEAN, ValueTypeBoolean.ValueBoolean.of(true))); + + Player player = helper.makeMockPlayer(GameType.CREATIVE); + PartConfigApplyResult result = applyConfig(helper, target, snapshotConfig(helper, source), player); + + helper.succeedWhen(() -> { + helper.assertValueEqual(result.getCardsPasted(), 1, "The variable card was not pasted"); + helper.assertValueEqual(result.getMissingBlanks(), 0, "Blank variable cards were reported as missing"); + helper.assertValueEqual(countBlankVariables(player), 0, "Blank variable cards were consumed"); + helper.assertTrue(getActiveVariable(target) != null, "The target part did not receive a variable card"); + }); + } + + @GameTest(template = TEMPLATE_EMPTY) + public void testWrenchConfigPasteOffsetOutOfRange(GameTestHelper helper) { + PartPos source = placePart(helper, POS_SOURCE, PartTypes.REDSTONE_WRITER); + PartPos target = placePart(helper, POS_TARGET, PartTypes.REDSTONE_WRITER); + // The target part has no offset enhancements, so the copied offset can not be applied + configurePart(helper, source, new Vec3i(3, 0, 0)); + + Player player = helper.makeMockPlayer(GameType.SURVIVAL); + PartConfigApplyResult result = applyConfig(helper, target, snapshotConfig(helper, source), player); + + helper.succeedWhen(() -> { + helper.assertTrue(result.isOffsetFailed(), "The offset failure was not reported"); + helper.assertValueEqual(((IPartType) partType(target)).getTargetOffset(partState(target)), Vec3i.ZERO, + "The offset was changed"); + helper.assertValueEqual(((IPartType) partType(target)).getUpdateInterval(partState(target)), 40, + "Update interval was not pasted"); + helper.assertValueEqual(((IPartType) partType(target)).getTargetSideOverride(partState(target)), + Direction.SOUTH, "Target side was not pasted"); + }); + } + + /** + * The aspect settings container, with all value syncing recorded in-memory, + * as there is no client to sync to in game tests. + */ + public static class RecordingContainerAspectSettings extends ContainerAspectSettings { + + private final Map recordedValues = Maps.newHashMap(); + + public RecordingContainerAspectSettings(int id, Inventory playerInventory, Container inventory, + Optional target, Optional partContainer, + Optional partType, IAspect aspect) { + super(id, playerInventory, inventory, target, partContainer, partType, aspect); + } + + @Override + public void setValue(int id, CompoundTag value) { + this.recordedValues.put(id, value); + } + + @Override + public CompoundTag getValue(int id) { + return this.recordedValues.get(id); + } + } + + /** + * Construct the aspect settings container for the given part and aspect, + * in the same way as it is constructed when the given player opens the aspect settings gui. + */ + protected static ContainerAspectSettings openAspectSettings(Player player, PartPos partPos, IAspect aspect) { + Triple data = PartHelpers.getContainerPartConstructionData(partPos); + return new RecordingContainerAspectSettings(1, player.getInventory(), new SimpleContainer(0), + Optional.of(data.getRight()), Optional.of(data.getLeft()), Optional.of(data.getMiddle()), aspect); + } + + @GameTest(template = TEMPLATE_EMPTY) + public void testAspectSettingsConfigCopyPasteAcrossAspects(GameTestHelper helper) { + PartPos partPos = placePart(helper, POS_SOURCE, PartTypes.REDSTONE_WRITER); + // The pulse aspect has a strong power setting that the plain aspect also has, + // and a pulse length setting that it does not have. + setAspectProperty(partPos, Aspects.Write.Redstone.BOOLEAN_PULSE, + AspectWriteBuilders.Redstone.PROP_STRONG_POWER, ValueTypeBoolean.ValueBoolean.of(true)); + setAspectProperty(partPos, Aspects.Write.Redstone.BOOLEAN_PULSE, + AspectWriteBuilders.Redstone.PROP_PULSE_LENGTH, ValueTypeInteger.ValueInteger.of(5)); + + Player player = helper.makeMockPlayer(GameType.SURVIVAL); + player.setItemInHand(InteractionHand.MAIN_HAND, createWrench(ItemWrench.Mode.CONFIG)); + openAspectSettings(player, partPos, Aspects.Write.Redstone.BOOLEAN_PULSE) + .onButtonClick(ContainerAspectSettings.BUTTON_CONFIG_COPY); + openAspectSettings(player, partPos, Aspects.Write.Redstone.BOOLEAN) + .onButtonClick(ContainerAspectSettings.BUTTON_CONFIG_PASTE); + + helper.succeedWhen(() -> { + helper.assertValueEqual( + getEffectiveAspectProperty(partPos, Aspects.Write.Redstone.BOOLEAN, + AspectWriteBuilders.Redstone.PROP_STRONG_POWER), + ValueTypeBoolean.ValueBoolean.of(true), "The shared setting was not pasted"); + helper.assertValueEqual( + getEffectiveAspectProperty(partPos, Aspects.Write.Redstone.BOOLEAN_PULSE, + AspectWriteBuilders.Redstone.PROP_PULSE_LENGTH), + ValueTypeInteger.ValueInteger.of(5), "The copied aspect was changed"); + }); + } + + @GameTest(template = TEMPLATE_EMPTY) + public void testWrenchConfigSneakDoesNotRemovePart(GameTestHelper helper) { + PartPos partPos = placePart(helper, POS_SOURCE, PartTypes.REDSTONE_WRITER); + + Player player = helper.makeMockPlayer(GameType.SURVIVAL); + clickPart(helper, player, createWrench(ItemWrench.Mode.CONFIG), partPos, true); + + helper.succeedWhen(() -> { + helper.assertTrue(PartHelpers.getPart(partPos) != null, "The part was removed"); + helper.assertItemEntityNotPresent(PartTypes.REDSTONE_WRITER.getItem()); + }); + } + + @GameTest(template = TEMPLATE_EMPTY) + public void testWrenchConfigSurvivesModeCycling(GameTestHelper helper) { + PartPos partPos = placePart(helper, POS_SOURCE, PartTypes.REDSTONE_WRITER); + + Player player = helper.makeMockPlayer(GameType.SURVIVAL); + ItemStack wrench = createWrench(ItemWrench.Mode.CONFIG); + clickPart(helper, player, wrench, partPos, true); + for (int i = 0; i < ItemWrench.Mode.values().length; i++) { + wrenchItem().incrementMode(wrench); + } + + helper.succeedWhen(() -> { + helper.assertValueEqual(wrenchItem().getMode(wrench), ItemWrench.Mode.CONFIG, "The mode did not cycle back"); + helper.assertTrue( + PartConfigHelpers.getSnapshot(helper.getLevel().registryAccess(), wrench) + .map(snapshot -> snapshot.hasSection(PartConfigSection.PART_SETTINGS)) + .orElse(false), + "The copied configuration was lost when cycling the wrench mode"); + }); + } + + @GameTest(template = TEMPLATE_EMPTY) + public void testWrenchConfigPasteWithoutCopy(GameTestHelper helper) { + PartPos partPos = placePart(helper, POS_SOURCE, PartTypes.REDSTONE_WRITER); + IPartType partType = partType(partPos); + int updateInterval = partType.getUpdateInterval(partState(partPos)); + + Player player = helper.makeMockPlayer(GameType.SURVIVAL); + clickPart(helper, player, createWrench(ItemWrench.Mode.CONFIG), partPos, false); + + helper.succeedWhen(() -> { + helper.assertValueEqual(partType.getUpdateInterval(partState(partPos)), updateInterval, + "The part was changed"); + }); + } + +} diff --git a/src/main/java/org/cyclops/integrateddynamics/Configs.java b/src/main/java/org/cyclops/integrateddynamics/Configs.java index bc40b7fe375..267907db992 100644 --- a/src/main/java/org/cyclops/integrateddynamics/Configs.java +++ b/src/main/java/org/cyclops/integrateddynamics/Configs.java @@ -205,6 +205,7 @@ public static void registerBlocks(ConfigHandler configHandler) { configHandler.addConfigurable(new DataComponentWrenchTargetBlockPosConfig()); configHandler.addConfigurable(new DataComponentWrenchTargetDirectionConfig()); configHandler.addConfigurable(new DataComponentWrenchModeConfig()); + configHandler.addConfigurable(new DataComponentWrenchPartConfigConfig()); } } diff --git a/src/main/java/org/cyclops/integrateddynamics/RegistryEntries.java b/src/main/java/org/cyclops/integrateddynamics/RegistryEntries.java index b19de376d90..16d11d3349e 100644 --- a/src/main/java/org/cyclops/integrateddynamics/RegistryEntries.java +++ b/src/main/java/org/cyclops/integrateddynamics/RegistryEntries.java @@ -170,6 +170,7 @@ public class RegistryEntries { public static final DeferredHolder, DataComponentType> DATACOMPONENT_WRENCH_TARGET_BLOCKPOS = DeferredHolder.create(Registries.DATA_COMPONENT_TYPE, ResourceLocation.parse("integrateddynamics:wrench_target_blockpos")); public static final DeferredHolder, DataComponentType> DATACOMPONENT_WRENCH_TARGET_DIRECTION = DeferredHolder.create(Registries.DATA_COMPONENT_TYPE, ResourceLocation.parse("integrateddynamics:wrench_target_direction")); public static final DeferredHolder, DataComponentType> DATACOMPONENT_WRENCH_MODE = DeferredHolder.create(Registries.DATA_COMPONENT_TYPE, ResourceLocation.parse("integrateddynamics:wrench_mode")); + public static final DeferredHolder, DataComponentType> DATACOMPONENT_WRENCH_PART_CONFIG = DeferredHolder.create(Registries.DATA_COMPONENT_TYPE, ResourceLocation.parse("integrateddynamics:wrench_part_config")); } diff --git a/src/main/java/org/cyclops/integrateddynamics/api/part/IPartType.java b/src/main/java/org/cyclops/integrateddynamics/api/part/IPartType.java index d61fca4aa1b..cc1dd382c5f 100644 --- a/src/main/java/org/cyclops/integrateddynamics/api/part/IPartType.java +++ b/src/main/java/org/cyclops/integrateddynamics/api/part/IPartType.java @@ -29,10 +29,15 @@ import org.cyclops.integrateddynamics.api.network.INetworkEventListener; import org.cyclops.integrateddynamics.api.network.IPartNetwork; import org.cyclops.integrateddynamics.api.network.IPartNetworkElement; +import org.cyclops.integrateddynamics.core.helper.PartConfigHelpers; +import org.cyclops.integrateddynamics.core.part.PartConfigApplyResult; +import org.cyclops.integrateddynamics.core.part.PartConfigSection; +import org.cyclops.integrateddynamics.core.part.PartConfigSnapshot; import javax.annotation.Nullable; import java.util.List; import java.util.Optional; +import java.util.Set; /** * A type of part that can be inserted into a {@link IPartContainer}. @@ -205,6 +210,42 @@ public default void onAspectVariablesChanged(PartTarget target, S state) { state.markAspectVariablesChanged(); } + /** + * Take a snapshot of the configuration of this part, so that it can be pasted onto another part. + * @param valueDeseralizationContext A value deserialization context. + * @param state The state. + * @param sections The configuration sections to include. + * @return The snapshot. + */ + // TODO: make non-default in nextmajor + public default PartConfigSnapshot snapshotConfig(ValueDeseralizationContext valueDeseralizationContext, S state, + Set sections) { + return PartConfigHelpers.snapshot(valueDeseralizationContext, this, state, sections); + } + + /** + * Paste a configuration snapshot onto this part. + * + * This is only called server-side. + * + * @param valueDeseralizationContext A value deserialization context. + * @param network The network of this part, or null if it is not in a network. + * @param partNetwork The part network of this part, or null if it is not in a network. + * @param target The target block. + * @param state The state. + * @param snapshot The snapshot to paste. + * @param sections The configuration sections to paste. + * @param player The player that is pasting, whose inventory is used for the variable cards. + * @return The outcome. + */ + // TODO: make non-default in nextmajor + public default PartConfigApplyResult applyConfig(ValueDeseralizationContext valueDeseralizationContext, + @Nullable INetwork network, @Nullable IPartNetwork partNetwork, + PartTarget target, S state, PartConfigSnapshot snapshot, + Set sections, Player player) { + return PartConfigHelpers.apply(valueDeseralizationContext, network, target, this, state, snapshot, sections, player); + } + /** * @param state The state * @return If this element should be updated. This method is only called once during network initialization. diff --git a/src/main/java/org/cyclops/integrateddynamics/api/part/PartTypeAdapter.java b/src/main/java/org/cyclops/integrateddynamics/api/part/PartTypeAdapter.java index 456e15223b8..fbe52e1fed5 100644 --- a/src/main/java/org/cyclops/integrateddynamics/api/part/PartTypeAdapter.java +++ b/src/main/java/org/cyclops/integrateddynamics/api/part/PartTypeAdapter.java @@ -23,6 +23,7 @@ import org.cyclops.integrateddynamics.api.network.IPartNetworkElement; import org.cyclops.integrateddynamics.api.network.event.INetworkEvent; import org.cyclops.integrateddynamics.core.part.PartStateAspectVariablesHandler; +import org.cyclops.integrateddynamics.core.part.PartStateOffsetHandler; import javax.annotation.Nullable; import java.util.Collections; @@ -137,7 +138,7 @@ public PartTarget getTarget(PartPos pos, S state) { } protected boolean hasOffsetVariables(S state) { - NonNullList inventory = state.getInventoryNamed("offsetVariablesInventory"); + NonNullList inventory = state.getInventoryNamed(PartStateOffsetHandler.INVENTORY_NAME); return inventory != null && inventory.stream().anyMatch(item -> !item.isEmpty()); } diff --git a/src/main/java/org/cyclops/integrateddynamics/block/shapes/VoxelShapeComponentsFactoryHandlerParts.java b/src/main/java/org/cyclops/integrateddynamics/block/shapes/VoxelShapeComponentsFactoryHandlerParts.java index fb5237194c3..25fd9a448b0 100644 --- a/src/main/java/org/cyclops/integrateddynamics/block/shapes/VoxelShapeComponentsFactoryHandlerParts.java +++ b/src/main/java/org/cyclops/integrateddynamics/block/shapes/VoxelShapeComponentsFactoryHandlerParts.java @@ -20,6 +20,7 @@ import org.cyclops.cyclopscore.helper.RenderHelpers; import org.cyclops.integrateddynamics.api.part.IPartContainer; import org.cyclops.integrateddynamics.api.part.IPartType; +import org.cyclops.integrateddynamics.api.part.PartPos; import org.cyclops.integrateddynamics.core.block.BlockRayTraceResultComponent; import org.cyclops.integrateddynamics.core.block.VoxelShapeComponents; import org.cyclops.integrateddynamics.core.block.VoxelShapeComponentsFactory; @@ -27,6 +28,7 @@ import org.cyclops.integrateddynamics.core.helper.PartHelpers; import org.cyclops.integrateddynamics.core.helper.WrenchHelpers; import org.cyclops.integrateddynamics.item.ItemBlockCable; +import org.cyclops.integrateddynamics.item.ItemWrench; import javax.annotation.Nullable; import java.util.Collection; @@ -107,7 +109,13 @@ public BakedModel getBreakingBaseModel(Level world, BlockPos pos) { @Override public InteractionResult onBlockActivated(BlockState state, Level world, BlockPos blockPos, Player player, InteractionHand hand, BlockRayTraceResultComponent hit) { ItemStack heldItem = player.getItemInHand(hand); - if(WrenchHelpers.isWrench(player, heldItem, world, blockPos, hit.getDirection()) && player.isSecondaryUseActive()) { + if(heldItem.getItem() instanceof ItemWrench itemWrench + && itemWrench.getMode(heldItem) == ItemWrench.Mode.CONFIG + && player.isSecondaryUseActive()) { + // Copy the configuration of this part into the wrench, instead of removing the part + itemWrench.copyPartConfig(heldItem, player, PartPos.of(world, blockPos, direction)); + return InteractionResult.SUCCESS; + } else if(WrenchHelpers.isWrench(player, heldItem, world, blockPos, hit.getDirection()) && player.isSecondaryUseActive()) { // Remove part from cable if (!world.isClientSide()) { destroy(world, blockPos, player, true); diff --git a/src/main/java/org/cyclops/integrateddynamics/component/DataComponentWrenchPartConfigConfig.java b/src/main/java/org/cyclops/integrateddynamics/component/DataComponentWrenchPartConfigConfig.java new file mode 100644 index 00000000000..8819ee5851e --- /dev/null +++ b/src/main/java/org/cyclops/integrateddynamics/component/DataComponentWrenchPartConfigConfig.java @@ -0,0 +1,19 @@ +package org.cyclops.integrateddynamics.component; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.codec.ByteBufCodecs; +import org.cyclops.cyclopscore.config.extendedconfig.DataComponentConfig; +import org.cyclops.integrateddynamics.IntegratedDynamics; +import org.cyclops.integrateddynamics.core.helper.Codecs; + +/** + * @author rubensworks + */ +public class DataComponentWrenchPartConfigConfig extends DataComponentConfig { + + public DataComponentWrenchPartConfigConfig() { + super(IntegratedDynamics._instance, "wrench_part_config", builder -> builder + .persistent(Codecs.COMPOUND_TAG) + .networkSynchronized(ByteBufCodecs.COMPOUND_TAG)); + } +} diff --git a/src/main/java/org/cyclops/integrateddynamics/core/client/gui/container/ContainerScreenAspectSettings.java b/src/main/java/org/cyclops/integrateddynamics/core/client/gui/container/ContainerScreenAspectSettings.java index acc98f3e825..71860be36c5 100644 --- a/src/main/java/org/cyclops/integrateddynamics/core/client/gui/container/ContainerScreenAspectSettings.java +++ b/src/main/java/org/cyclops/integrateddynamics/core/client/gui/container/ContainerScreenAspectSettings.java @@ -117,6 +117,14 @@ public void init() { refreshButtonEnabled(); } }, true)); + addRenderableWidget(new ButtonText(leftPos + 141, topPos + 109, 12, 12, + Component.translatable("gui.integrateddynamics.aspectsettings.config.copy"), Component.literal("C"), + createServerPressable(ContainerAspectSettings.BUTTON_CONFIG_COPY, (button) -> { + saveSetting(); + }), true)); + addRenderableWidget(new ButtonText(leftPos + 155, topPos + 109, 12, 12, + Component.translatable("gui.integrateddynamics.aspectsettings.config.paste"), Component.literal("P"), + createServerPressable(ContainerAspectSettings.BUTTON_CONFIG_PASTE, (button) -> {}), true)); refreshButtonEnabled(); setActiveProperty(activePropertyIndex); diff --git a/src/main/java/org/cyclops/integrateddynamics/core/client/gui/container/ContainerScreenPartSettings.java b/src/main/java/org/cyclops/integrateddynamics/core/client/gui/container/ContainerScreenPartSettings.java index 03504a68a65..496a0bf0335 100644 --- a/src/main/java/org/cyclops/integrateddynamics/core/client/gui/container/ContainerScreenPartSettings.java +++ b/src/main/java/org/cyclops/integrateddynamics/core/client/gui/container/ContainerScreenPartSettings.java @@ -138,6 +138,14 @@ public void init() { addRenderableWidget(buttonSave = new ButtonText(this.leftPos + 178, this.topPos + 8, font.width(save.getVisualOrderText()) + 6, 16, save, save, createServerPressable(ContainerPartSettings.BUTTON_SAVE, b -> onSave()), true)); + addRenderableWidget(new ButtonText(this.leftPos + 178, this.topPos + 30, 14, 14, + Component.translatable("gui.integrateddynamics.partsettings.config.copy"), Component.literal("C"), + // Persist any pending edits first, so that they end up in the wrench as well + createServerPressable(ContainerPartSettings.BUTTON_CONFIG_COPY, b -> onSave()), true)); + addRenderableWidget(new ButtonText(this.leftPos + 194, this.topPos + 30, 14, 14, + Component.translatable("gui.integrateddynamics.partsettings.config.paste"), Component.literal("P"), + createServerPressable(ContainerPartSettings.BUTTON_CONFIG_PASTE, b -> {}), true)); + this.refreshValues(); } diff --git a/src/main/java/org/cyclops/integrateddynamics/core/helper/PartConfigHelpers.java b/src/main/java/org/cyclops/integrateddynamics/core/helper/PartConfigHelpers.java new file mode 100644 index 00000000000..d1c84236737 --- /dev/null +++ b/src/main/java/org/cyclops/integrateddynamics/core/helper/PartConfigHelpers.java @@ -0,0 +1,514 @@ +package org.cyclops.integrateddynamics.core.helper; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.NonNullList; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.Containers; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import org.cyclops.cyclopscore.inventory.SimpleInventory; +import org.cyclops.integrateddynamics.IntegratedDynamics; +import org.cyclops.integrateddynamics.RegistryEntries; +import org.cyclops.integrateddynamics.api.evaluate.variable.ValueDeseralizationContext; +import org.cyclops.integrateddynamics.api.item.IVariableFacade; +import org.cyclops.integrateddynamics.api.item.IVariableFacadeHandlerRegistry; +import org.cyclops.integrateddynamics.api.network.INetwork; +import org.cyclops.integrateddynamics.api.part.IPartState; +import org.cyclops.integrateddynamics.api.part.IPartType; +import org.cyclops.integrateddynamics.api.part.PartTarget; +import org.cyclops.integrateddynamics.api.part.aspect.IAspect; +import org.cyclops.integrateddynamics.api.part.aspect.property.IAspectProperties; +import org.cyclops.integrateddynamics.api.part.aspect.property.IAspectPropertyTypeInstance; +import org.cyclops.integrateddynamics.api.part.write.IPartStateWriter; +import org.cyclops.integrateddynamics.api.part.write.IPartTypeWriter; +import org.cyclops.integrateddynamics.core.network.PartNetworkElement; +import org.cyclops.integrateddynamics.core.part.PartConfigApplyResult; +import org.cyclops.integrateddynamics.core.part.PartConfigSection; +import org.cyclops.integrateddynamics.core.part.PartConfigSnapshot; +import org.cyclops.integrateddynamics.core.part.PartStateActiveVariableBase; +import org.cyclops.integrateddynamics.core.part.PartStateAspectVariablesHandler; +import org.cyclops.integrateddynamics.core.part.PartStateOffsetHandler; +import org.cyclops.integrateddynamics.core.part.PartTypeAspects; +import org.cyclops.integrateddynamics.core.part.aspect.property.AspectProperties; +import org.cyclops.integrateddynamics.core.persist.world.LabelsWorldStorage; +import org.cyclops.integrateddynamics.item.ItemWrench; +import org.cyclops.integrateddynamics.part.aspect.Aspects; + +import javax.annotation.Nullable; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** + * Helpers for copying and pasting part configurations. + * + * All methods in here are meant to be called server-side only. + * + * @author rubensworks + */ +public final class PartConfigHelpers { + + private PartConfigHelpers() { + } + + /** + * @param partType A part type. + * @return All aspects that the given part type can hold. + */ + public static Set getAspects(IPartType partType) { + return partType instanceof PartTypeAspects partTypeAspects + ? partTypeAspects.getAspects() : Collections.emptySet(); + } + + /** + * Take a snapshot of the configuration of the given part. + * @param valueDeseralizationContext A value deserialization context. + * @param partType The part type. + * @param state The part state. + * @param sections The sections to include. + * @return The snapshot. + */ + @SuppressWarnings("unchecked") + public static PartConfigSnapshot snapshot(ValueDeseralizationContext valueDeseralizationContext, + IPartType partType, IPartState state, Set sections) { + Optional partSettings = Optional.empty(); + if (sections.contains(PartConfigSection.PART_SETTINGS)) { + partSettings = Optional.of(new PartConfigSnapshot.PartSettings( + partType.getUpdateInterval(state), + partType.getPriority(state), + partType.getChannel(state), + Optional.ofNullable(partType.getTargetSideOverride(state)), + partType.getTargetOffset(state))); + } + + Map aspectProperties = Maps.newLinkedHashMap(); + if (sections.contains(PartConfigSection.ASPECT_PROPERTIES)) { + for (IAspect aspect : getAspects(partType)) { + if (aspect.hasProperties()) { + IAspectProperties properties = state.getAspectProperties(aspect); + if (properties != null) { + aspectProperties.put(aspect.getUniqueName(), properties.toNBT(valueDeseralizationContext)); + } + } + } + } + + List variableCards = Lists.newArrayList(); + if (sections.contains(PartConfigSection.VARIABLE_CARDS)) { + for (Map.Entry> entry : state.getInventoriesNamed().entrySet()) { + NonNullList inventory = entry.getValue(); + for (int slot = 0; slot < inventory.size(); slot++) { + if (!inventory.get(slot).isEmpty()) { + variableCards.add(new PartConfigSnapshot.VariableCard(entry.getKey(), slot, + inventory.get(slot).copy())); + } + } + } + if (state instanceof PartStateActiveVariableBase activeState) { + SimpleInventory inventory = activeState.getInventory(); + for (int slot = 0; slot < inventory.getContainerSize(); slot++) { + if (!inventory.getItem(slot).isEmpty()) { + variableCards.add(new PartConfigSnapshot.VariableCard( + PartConfigSnapshot.INVENTORY_NAME_ACTIVE, slot, inventory.getItem(slot).copy())); + } + } + } + } + + return new PartConfigSnapshot(PartConfigSnapshot.VERSION, partType.getUniqueName(), + partSettings, aspectProperties, variableCards); + } + + /** + * Paste the given snapshot onto the given part. + * @param valueDeseralizationContext A value deserialization context. + * @param network The network of the part, or null if it is not in a network. + * @param target The part target. + * @param partType The part type. + * @param state The part state. + * @param snapshot The snapshot to paste. + * @param sections The sections to paste. + * @param player The player that is pasting, used to consume and eject variable cards. + * @return The outcome. + */ + public static PartConfigApplyResult apply(ValueDeseralizationContext valueDeseralizationContext, + @Nullable INetwork network, PartTarget target, + IPartType partType, IPartState state, PartConfigSnapshot snapshot, + Set sections, Player player) { + PartConfigApplyResult result = new PartConfigApplyResult(); + + if (sections.contains(PartConfigSection.PART_SETTINGS) && snapshot.partSettings().isPresent()) { + applyPartSettings(network, target, partType, state, snapshot.partSettings().get(), result); + } + if (sections.contains(PartConfigSection.ASPECT_PROPERTIES)) { + applyAspectProperties(valueDeseralizationContext, target, partType, state, snapshot, result); + } + if (sections.contains(PartConfigSection.VARIABLE_CARDS)) { + applyVariableCards(valueDeseralizationContext, target, partType, state, + snapshot.variableCards(), player, result); + } + + return result; + } + + @SuppressWarnings("unchecked") + protected static void applyPartSettings(@Nullable INetwork network, PartTarget target, IPartType partType, + IPartState state, PartConfigSnapshot.PartSettings settings, + PartConfigApplyResult result) { + partType.setUpdateInterval(state, Math.max(partType.getMinimumUpdateInterval(state), settings.updateInterval())); + partType.setTargetSideOverride(state, settings.targetSide().orElse(null)); + if (!partType.setTargetOffset(state, target.getCenter(), settings.targetOffset())) { + result.setOffsetFailed(true); + } + if (network != null) { + network.setPriorityAndChannel(new PartNetworkElement(partType, target.getCenter()), + settings.priority(), settings.channel()); + } else { + state.setPriority(settings.priority()); + state.setChannel(settings.channel()); + } + result.setPartSettingsApplied(true); + state.markDirty(); + state.sendUpdate(); + } + + @SuppressWarnings("unchecked") + protected static void applyAspectProperties(ValueDeseralizationContext valueDeseralizationContext, PartTarget target, + IPartType partType, IPartState state, PartConfigSnapshot snapshot, + PartConfigApplyResult result) { + Set aspects = getAspects(partType); + for (Map.Entry entry : snapshot.aspectProperties().entrySet()) { + IAspect aspect = Aspects.REGISTRY.getAspect(entry.getKey()); + if (aspect == null || !aspects.contains(aspect)) { + // The target part does not have this aspect + continue; + } + IAspectProperties source = readProperties(valueDeseralizationContext, entry.getValue()); + IAspectProperties properties = aspect.getStaticProperties(partType, target, state).clone(); + int applied = applyPropertiesByType(source, properties, aspect); + if (applied > 0) { + aspect.setProperties(partType, target, state, properties); + } + result.addAppliedProperties(applied); + result.addSkippedProperties(countPropertyTypes(source) - applied); + } + } + + /** + * @param valueDeseralizationContext A value deserialization context. + * @param tag Serialized aspect properties. + * @return The deserialized aspect properties. + */ + public static IAspectProperties readProperties(ValueDeseralizationContext valueDeseralizationContext, CompoundTag tag) { + IAspectProperties properties = new AspectProperties(); + properties.fromNBT(valueDeseralizationContext, tag); + return properties; + } + + /** + * Copy all property values from the source into the target properties, + * for all property types that the given aspect declares. + * + * Property types are matched by their value type and translation key, + * so properties can also be copied between different aspects. + * + * @param source The properties to copy from. + * @param properties The properties to copy into. + * @param aspect The aspect that the target properties belong to. + * @return The number of copied properties. + */ + @SuppressWarnings({"unchecked", "deprecation"}) + public static int applyPropertiesByType(IAspectProperties source, IAspectProperties properties, IAspect aspect) { + Collection sourceTypes = source.getTypes(); + int applied = 0; + for (IAspectPropertyTypeInstance propertyType : aspect.getPropertyTypes()) { + if (sourceTypes.contains(propertyType)) { + properties.setValue(propertyType, source.getValue(propertyType)); + applied++; + } + } + return applied; + } + + @SuppressWarnings("deprecation") + protected static int countPropertyTypes(IAspectProperties properties) { + return properties.getTypes().size(); + } + + /** + * Paste the given variable cards into the given part. + * + * Only the inventories that occur in the given cards are touched: + * their current contents are given back to the player, and replaced by copies of the given cards. + * Each pasted card consumes one blank Variable Card from the player's inventory, + * unless the player is in creative mode. + * + * @param valueDeseralizationContext A value deserialization context. + * @param target The part target. + * @param partType The part type. + * @param state The part state. + * @param cards The cards to paste. + * @param player The player that is pasting. + * @param result The outcome to report into. + */ + public static void applyVariableCards(ValueDeseralizationContext valueDeseralizationContext, PartTarget target, + IPartType partType, IPartState state, + List cards, Player player, + PartConfigApplyResult result) { + // Group the cards by inventory, and resolve the inventories that the target part actually has + Map> cardsByInventory = Maps.newLinkedHashMap(); + for (PartConfigSnapshot.VariableCard card : cards) { + cardsByInventory.computeIfAbsent(card.inventoryName(), name -> Lists.newArrayList()).add(card); + } + Map inventories = Maps.newLinkedHashMap(); + int required = 0; + for (Map.Entry> entry : cardsByInventory.entrySet()) { + SimpleInventory inventory = resolveInventory(partType, state, entry.getKey()); + if (inventory == null) { + result.addCardsSkipped(entry.getValue().size()); + continue; + } + inventories.put(entry.getKey(), inventory); + for (PartConfigSnapshot.VariableCard card : entry.getValue()) { + if (card.slot() < inventory.getContainerSize()) { + required++; + } else { + result.addCardsSkipped(1); + } + } + } + if (required == 0) { + return; + } + + // Consume the required blank variable cards + if (!player.isCreative()) { + int available = countBlankVariables(player); + if (available < required) { + result.setMissingBlanks(required - available); + result.addCardsSkipped(required); + return; + } + consumeBlankVariables(player, required); + } + + for (Map.Entry entry : inventories.entrySet()) { + SimpleInventory inventory = entry.getValue(); + + // Give the cards that are currently present back to the player + for (int slot = 0; slot < inventory.getContainerSize(); slot++) { + ItemStack current = inventory.getItem(slot); + if (!current.isEmpty()) { + giveOrDrop(player, current); + inventory.setItem(slot, ItemStack.EMPTY); + } + } + + for (PartConfigSnapshot.VariableCard card : cardsByInventory.get(entry.getKey())) { + if (card.slot() < inventory.getContainerSize()) { + inventory.setItem(card.slot(), copyVariable(valueDeseralizationContext, card.itemStack())); + result.addCardsPasted(1); + } + } + + saveInventory(partType, state, target, entry.getKey(), inventory, player); + } + } + + /** + * @param partType A part type. + * @param state A part state. + * @param inventoryName An inventory name from a snapshot. + * @return The matching inventory inside the given part, or null if the part does not have it. + */ + @Nullable + public static SimpleInventory resolveInventory(IPartType partType, IPartState state, String inventoryName) { + if (PartConfigSnapshot.INVENTORY_NAME_ACTIVE.equals(inventoryName)) { + return state instanceof PartStateActiveVariableBase activeState ? activeState.getInventory() : null; + } + if (PartStateOffsetHandler.INVENTORY_NAME.equals(inventoryName)) { + if (!partType.supportsOffsets()) { + return null; + } + SimpleInventory inventory = new SimpleInventory(3, 1); + state.loadInventoryNamed(inventoryName, inventory); + return inventory; + } + IAspect aspect = PartStateAspectVariablesHandler.getAspectByInventoryName(inventoryName); + if (aspect != null) { + return getAspects(partType).contains(aspect) + ? PartStateAspectVariablesHandler.getVariablesInventory(state, aspect) : null; + } + // Unknown named inventories are only pasted if the target part already has them + NonNullList existing = state.getInventoryNamed(inventoryName); + if (existing == null) { + return null; + } + SimpleInventory inventory = new SimpleInventory(existing.size(), 1); + state.loadInventoryNamed(inventoryName, inventory); + return inventory; + } + + @SuppressWarnings("unchecked") + protected static void saveInventory(IPartType partType, IPartState state, PartTarget target, String inventoryName, + SimpleInventory inventory, Player player) { + if (PartConfigSnapshot.INVENTORY_NAME_ACTIVE.equals(inventoryName)) { + // The active inventory is the live inventory of the part state, so it does not have to be saved + if (partType instanceof IPartTypeWriter partTypeWriter) { + partTypeWriter.updateActivation(target, (IPartStateWriter) state, player); + } else if (state instanceof PartStateActiveVariableBase activeState) { + activeState.onVariableContentsUpdated(partType, target); + } + return; + } + state.saveInventoryNamed(inventoryName, inventory); + if (PartStateOffsetHandler.INVENTORY_NAME.equals(inventoryName)) { + partType.onOffsetVariablesChanged(target, state); + } else if (PartStateAspectVariablesHandler.getAspectByInventoryName(inventoryName) != null) { + partType.onAspectVariablesChanged(target, state); + } + } + + /** + * @param itemStack An item stack. + * @return If the given stack is a Variable Card without any variable in it. + */ + public static boolean isBlankVariable(ItemStack itemStack) { + return itemStack.is(RegistryEntries.ITEM_VARIABLE.get()) + && !itemStack.has(RegistryEntries.DATACOMPONENT_VARIABLE_FACADE.get()); + } + + /** + * @param player A player. + * @return The number of blank Variable Cards in the inventory of the given player. + */ + public static int countBlankVariables(Player player) { + int count = 0; + for (int slot = 0; slot < player.getInventory().getContainerSize(); slot++) { + ItemStack itemStack = player.getInventory().getItem(slot); + if (isBlankVariable(itemStack)) { + count += itemStack.getCount(); + } + } + return count; + } + + /** + * Remove the given number of blank Variable Cards from the inventory of the given player. + * @param player A player. + * @param count The number of cards to remove. + */ + public static void consumeBlankVariables(Player player, int count) { + int remaining = count; + for (int slot = 0; slot < player.getInventory().getContainerSize() && remaining > 0; slot++) { + ItemStack itemStack = player.getInventory().getItem(slot); + if (isBlankVariable(itemStack)) { + int consumed = Math.min(remaining, itemStack.getCount()); + itemStack.shrink(consumed); + if (itemStack.isEmpty()) { + player.getInventory().setItem(slot, ItemStack.EMPTY); + } + remaining -= consumed; + } + } + } + + /** + * Copy the given variable card, so that the copy refers to a new variable. + * @param valueDeseralizationContext A value deserialization context. + * @param itemStack A variable card. + * @return The copy. + */ + public static ItemStack copyVariable(ValueDeseralizationContext valueDeseralizationContext, ItemStack itemStack) { + if (!itemStack.is(RegistryEntries.ITEM_VARIABLE.get()) + || !itemStack.has(RegistryEntries.DATACOMPONENT_VARIABLE_FACADE.get())) { + return itemStack.copy(); + } + + IVariableFacade facade = RegistryEntries.ITEM_VARIABLE.get() + .getVariableFacade(valueDeseralizationContext, itemStack); + ItemStack copy = IntegratedDynamics._instance.getRegistryManager() + .getRegistry(IVariableFacadeHandlerRegistry.class).copy(true, itemStack); + + // If the original had a label, also copy the label + if (facade.isValid()) { + LabelsWorldStorage labels = LabelsWorldStorage.getInstance(IntegratedDynamics._instance); + String label = labels.getLabel(facade.getId()); + if (label != null) { + IVariableFacade facadeCopy = RegistryEntries.ITEM_VARIABLE.get() + .getVariableFacade(valueDeseralizationContext, copy); + if (facadeCopy != null && facadeCopy.isValid()) { + labels.put(facadeCopy.getId(), label); + } + } + } + + return copy; + } + + /** + * Give the given item stack to the player, or drop it on the ground if their inventory is full. + * @param player A player. + * @param itemStack An item stack. + */ + public static void giveOrDrop(Player player, ItemStack itemStack) { + if (!player.getInventory().add(itemStack) && !itemStack.isEmpty()) { + Containers.dropItemStack(player.level(), player.getX(), player.getY(), player.getZ(), itemStack); + } + } + + /** + * Find the Wrench that the given player is holding or carrying. + * @param player A player. + * @return The Wrench, or empty if the player has none. + */ + public static Optional findWrench(Player player) { + for (ItemStack itemStack : List.of(player.getMainHandItem(), player.getOffhandItem())) { + if (itemStack.getItem() instanceof ItemWrench) { + return Optional.of(itemStack); + } + } + for (int slot = 0; slot < player.getInventory().getContainerSize(); slot++) { + ItemStack itemStack = player.getInventory().getItem(slot); + if (itemStack.getItem() instanceof ItemWrench) { + return Optional.of(itemStack); + } + } + return Optional.empty(); + } + + /** + * @param provider A holder lookup provider. + * @param wrench A Wrench. + * @return The configuration snapshot inside the given Wrench, if any. + */ + public static Optional getSnapshot(HolderLookup.Provider provider, ItemStack wrench) { + CompoundTag tag = wrench.get(RegistryEntries.DATACOMPONENT_WRENCH_PART_CONFIG.get()); + return tag == null ? Optional.empty() : PartConfigSnapshot.fromNBT(provider, tag); + } + + /** + * Store the given configuration snapshot inside the given Wrench. + * @param provider A holder lookup provider. + * @param wrench A Wrench. + * @param snapshot A configuration snapshot. + */ + public static void setSnapshot(HolderLookup.Provider provider, ItemStack wrench, PartConfigSnapshot snapshot) { + wrench.set(RegistryEntries.DATACOMPONENT_WRENCH_PART_CONFIG.get(), snapshot.toNBT(provider)); + } + + /** + * @return A message telling the player that they need a Wrench to copy or paste a configuration. + */ + public static Component getNoWrenchMessage() { + return Component.translatable("gui.integrateddynamics.config.nowrench"); + } + +} diff --git a/src/main/java/org/cyclops/integrateddynamics/core/inventory/container/ContainerAspectSettings.java b/src/main/java/org/cyclops/integrateddynamics/core/inventory/container/ContainerAspectSettings.java index 1bdf5a47066..1893ce0f6a2 100644 --- a/src/main/java/org/cyclops/integrateddynamics/core/inventory/container/ContainerAspectSettings.java +++ b/src/main/java/org/cyclops/integrateddynamics/core/inventory/container/ContainerAspectSettings.java @@ -33,14 +33,19 @@ import org.cyclops.integrateddynamics.api.part.aspect.property.IAspectPropertyTypeInstance; import org.cyclops.integrateddynamics.core.evaluate.variable.ValueHelpers; import org.cyclops.integrateddynamics.core.helper.NetworkHelpers; +import org.cyclops.integrateddynamics.core.helper.PartConfigHelpers; import org.cyclops.integrateddynamics.core.helper.PartHelpers; import org.cyclops.integrateddynamics.core.inventory.container.slot.SlotVariable; import org.cyclops.integrateddynamics.core.network.event.VariableContentsUpdatedEvent; +import org.cyclops.integrateddynamics.core.part.PartConfigApplyResult; +import org.cyclops.integrateddynamics.core.part.PartConfigSection; +import org.cyclops.integrateddynamics.core.part.PartConfigSnapshot; import org.cyclops.integrateddynamics.core.part.PartStateAspectVariablesHandler; import org.cyclops.integrateddynamics.core.part.aspect.AspectRegistry; import javax.annotation.Nullable; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -51,6 +56,8 @@ public class ContainerAspectSettings extends InventoryContainer { public static final String BUTTON_EXIT = "button_exit"; + public static final String BUTTON_CONFIG_COPY = "button_config_copy"; + public static final String BUTTON_CONFIG_PASTE = "button_config_paste"; public static final int BUTTON_SETTINGS = 1; private static final int PAGE_SIZE = 3; @@ -125,6 +132,142 @@ public ContainerAspectSettings(int id, Inventory playerInventory, Container inve PartHelpers.openContainerPart((ServerPlayer) playerInventory.player, getTarget().get().getCenter(), getPartType().get()); } }); + putButtonAction(ContainerAspectSettings.BUTTON_CONFIG_COPY, (s, containerExtended) -> { + if (!world.isClientSide()) { + copyConfig(); + } + }); + putButtonAction(ContainerAspectSettings.BUTTON_CONFIG_PASTE, (s, containerExtended) -> { + if (!world.isClientSide()) { + pasteConfig(); + } + }); + } + + /** + * Copy the settings of this aspect into the Wrench of the player. + */ + protected void copyConfig() { + ItemStack wrench = PartConfigHelpers.findWrench(player).orElse(ItemStack.EMPTY); + if (wrench.isEmpty()) { + player.displayClientMessage(PartConfigHelpers.getNoWrenchMessage(), true); + return; + } + PartConfigHelpers.setSnapshot(world.registryAccess(), wrench, snapshotAspect()); + player.displayClientMessage(Component.translatable("gui.integrateddynamics.config.copied"), true); + } + + /** + * @return A snapshot that only holds the settings and setting variables of this aspect. + */ + protected PartConfigSnapshot snapshotAspect() { + IPartType partType = getPartType().get(); + IPartState partState = getPartState().get(); + ValueDeseralizationContext valueDeseralizationContext = ValueDeseralizationContext.of(world); + + saveVariablesInventory(partState); + String inventoryName = PartStateAspectVariablesHandler.getInventoryName(aspect); + List variableCards = Lists.newArrayList(); + for (int slot = 0; slot < this.variablesInventory.getContainerSize(); slot++) { + ItemStack itemStack = this.variablesInventory.getItem(slot); + if (!itemStack.isEmpty()) { + variableCards.add(new PartConfigSnapshot.VariableCard(inventoryName, slot, itemStack.copy())); + } + } + + return new PartConfigSnapshot(PartConfigSnapshot.VERSION, partType.getUniqueName(), Optional.empty(), + Map.of(aspect.getUniqueName(), + aspect.getStaticProperties(partType, getTarget().get(), partState).toNBT(valueDeseralizationContext)), + variableCards); + } + + /** + * Paste the aspect settings inside the Wrench of the player onto this aspect. + * + * Settings are matched by property type instead of by aspect, + * so settings can also be copied between different aspects. + */ + protected void pasteConfig() { + ItemStack wrench = PartConfigHelpers.findWrench(player).orElse(ItemStack.EMPTY); + if (wrench.isEmpty()) { + player.displayClientMessage(PartConfigHelpers.getNoWrenchMessage(), true); + return; + } + PartConfigSnapshot snapshot = PartConfigHelpers.getSnapshot(world.registryAccess(), wrench).orElse(null); + if (snapshot == null || (!snapshot.hasSection(PartConfigSection.ASPECT_PROPERTIES) + && !snapshot.hasSection(PartConfigSection.VARIABLE_CARDS))) { + player.displayClientMessage(Component.translatable("item.integrateddynamics.wrench.mode.config.empty"), true); + return; + } + + IPartType partType = getPartType().get(); + PartTarget target = getTarget().get(); + IPartState partState = getPartState().get(); + ValueDeseralizationContext valueDeseralizationContext = ValueDeseralizationContext.of(world); + PartConfigApplyResult result = new PartConfigApplyResult(); + + // Persist any pending slot changes, as the variable slots are rewritten outside of this container below + saveVariablesInventory(partState); + + // Apply all settings of all copied aspects that this aspect also declares + IAspectProperties properties = aspect.getStaticProperties(partType, target, partState).clone(); + int applied = 0; + for (CompoundTag propertiesTag : snapshot.aspectProperties().values()) { + applied += PartConfigHelpers.applyPropertiesByType( + PartConfigHelpers.readProperties(valueDeseralizationContext, propertiesTag), properties, aspect); + } + if (applied > 0) { + aspect.setProperties(partType, target, partState, properties); + } + result.addAppliedProperties(applied); + + // Move the copied setting variables to the slots of the matching properties of this aspect + PartConfigHelpers.applyVariableCards(valueDeseralizationContext, target, partType, partState, + remapVariableCards(snapshot, result), player, result); + + // Reload the variable slots, as they were changed outside of this container + this.variablesInventory.clearContent(); + partState.loadInventoryNamed(PartStateAspectVariablesHandler.getInventoryName(aspect), this.variablesInventory); + this.dirtyInv = false; + + player.displayClientMessage(result.getMessage(), true); + + // Show the pasted values in the gui + initializeValues(); + + // Changing the settings might cause some erroring variables to become valid again, so trigger an update. + NetworkHelpers.getNetwork(target.getCenter()) + .ifPresent(network -> network.getEventBus().post(new VariableContentsUpdatedEvent(network))); + } + + /** + * Rewrite the copied setting variables so that they end up in the slot of the matching property of this aspect. + * @param snapshot A configuration snapshot. + * @param result The outcome to report skipped cards into. + * @return The rewritten cards. + */ + protected List remapVariableCards(PartConfigSnapshot snapshot, + PartConfigApplyResult result) { + String inventoryName = PartStateAspectVariablesHandler.getInventoryName(aspect); + List cards = Lists.newArrayList(); + for (PartConfigSnapshot.VariableCard card : snapshot.variableCards()) { + IAspect sourceAspect = PartStateAspectVariablesHandler.getAspectByInventoryName(card.inventoryName()); + if (sourceAspect == null) { + continue; + } + List sourcePropertyTypes = PartStateAspectVariablesHandler.getPropertyTypes(sourceAspect); + if (card.slot() >= sourcePropertyTypes.size()) { + continue; + } + int slot = this.propertyTypes.indexOf(sourcePropertyTypes.get(card.slot())); + if (slot < 0) { + // This aspect does not have the property that the card was configuring + result.addCardsSkipped(1); + continue; + } + cards.add(new PartConfigSnapshot.VariableCard(inventoryName, slot, card.itemStack())); + } + return cards; } public BiMap getPropertyIds() { diff --git a/src/main/java/org/cyclops/integrateddynamics/core/inventory/container/ContainerPartOffset.java b/src/main/java/org/cyclops/integrateddynamics/core/inventory/container/ContainerPartOffset.java index 97c8062df5b..1b3333f6833 100644 --- a/src/main/java/org/cyclops/integrateddynamics/core/inventory/container/ContainerPartOffset.java +++ b/src/main/java/org/cyclops/integrateddynamics/core/inventory/container/ContainerPartOffset.java @@ -22,6 +22,7 @@ import org.cyclops.integrateddynamics.api.part.IPartType; import org.cyclops.integrateddynamics.api.part.PartTarget; import org.cyclops.integrateddynamics.core.helper.PartHelpers; +import org.cyclops.integrateddynamics.core.part.PartStateOffsetHandler; import org.cyclops.integrateddynamics.core.inventory.container.slot.SlotVariable; import javax.annotation.Nullable; @@ -89,7 +90,7 @@ public ContainerPartOffset(@Nullable MenuType type, int id, Inventory playerI offsetVariablesInventory = new SimpleInventory(3, 1); offsetVariablesInventory.addDirtyMarkListener(() -> dirtyInv = true); if (!player.level().isClientSide) { - getPartState().loadInventoryNamed("offsetVariablesInventory", offsetVariablesInventory); + getPartState().loadInventoryNamed(PartStateOffsetHandler.INVENTORY_NAME, offsetVariablesInventory); } addSlot(new SlotVariable(offsetVariablesInventory, 0, 45, 51)); addSlot(new SlotVariable(offsetVariablesInventory, 1, 99, 51)); @@ -186,7 +187,7 @@ public void broadcastChanges() { if (this.dirtyInv) { this.dirtyInv = false; - partState.saveInventoryNamed("offsetVariablesInventory", offsetVariablesInventory); + partState.saveInventoryNamed(PartStateOffsetHandler.INVENTORY_NAME, offsetVariablesInventory); getPartType().onOffsetVariablesChanged(getTarget(), partState); } diff --git a/src/main/java/org/cyclops/integrateddynamics/core/inventory/container/ContainerPartSettings.java b/src/main/java/org/cyclops/integrateddynamics/core/inventory/container/ContainerPartSettings.java index 57cb5556dbd..a65b327e4f2 100644 --- a/src/main/java/org/cyclops/integrateddynamics/core/inventory/container/ContainerPartSettings.java +++ b/src/main/java/org/cyclops/integrateddynamics/core/inventory/container/ContainerPartSettings.java @@ -1,31 +1,41 @@ package org.cyclops.integrateddynamics.core.inventory.container; +import com.google.common.collect.Sets; import net.minecraft.core.Direction; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.chat.Component; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.Container; import net.minecraft.world.SimpleContainer; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Player; import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; import org.cyclops.cyclopscore.datastructure.DimPos; import org.cyclops.cyclopscore.helper.ValueNotifierHelpers; import org.cyclops.cyclopscore.inventory.container.InventoryContainer; import org.cyclops.integrateddynamics.RegistryEntries; import org.cyclops.integrateddynamics.api.PartStateException; +import org.cyclops.integrateddynamics.api.evaluate.variable.ValueDeseralizationContext; import org.cyclops.integrateddynamics.api.network.INetwork; +import org.cyclops.integrateddynamics.api.network.IPartNetwork; import org.cyclops.integrateddynamics.api.part.IPartContainer; import org.cyclops.integrateddynamics.api.part.IPartState; import org.cyclops.integrateddynamics.api.part.IPartType; import org.cyclops.integrateddynamics.api.part.PartTarget; import org.cyclops.integrateddynamics.core.helper.NetworkHelpers; +import org.cyclops.integrateddynamics.core.helper.PartConfigHelpers; import org.cyclops.integrateddynamics.core.helper.PartHelpers; import org.cyclops.integrateddynamics.core.network.PartNetworkElement; +import org.cyclops.integrateddynamics.core.part.PartConfigApplyResult; +import org.cyclops.integrateddynamics.core.part.PartConfigSection; +import org.cyclops.integrateddynamics.core.part.PartConfigSnapshot; import javax.annotation.Nullable; import java.util.Optional; +import java.util.Set; /** * Container for part settings. @@ -35,8 +45,15 @@ public class ContainerPartSettings extends InventoryContainer { public static final String BUTTON_SAVE = "button_save"; public static final String BUTTON_SETTINGS = "button_settings"; + public static final String BUTTON_CONFIG_COPY = "button_config_copy"; + public static final String BUTTON_CONFIG_PASTE = "button_config_paste"; private static final int PAGE_SIZE = 3; + /** + * The Wrench copies and pastes only the general part settings from this gui. + */ + private static final Set SECTIONS = Sets.immutableEnumSet(PartConfigSection.PART_SETTINGS); + private final PartTarget target; private final Optional partContainer; private final IPartType partType; @@ -79,6 +96,56 @@ public ContainerPartSettings(@Nullable MenuType type, int id, Inventory playe PartHelpers.openContainerPart((ServerPlayer) player, target.getCenter(), getPartType()); } }); + putButtonAction(ContainerPartSettings.BUTTON_CONFIG_COPY, (s, containerExtended) -> { + if(!world.isClientSide()) { + copyConfig(); + } + }); + putButtonAction(ContainerPartSettings.BUTTON_CONFIG_PASTE, (s, containerExtended) -> { + if(!world.isClientSide()) { + pasteConfig(); + } + }); + } + + /** + * Copy the settings of this part into the Wrench of the player. + */ + protected void copyConfig() { + ItemStack wrench = PartConfigHelpers.findWrench(player).orElse(ItemStack.EMPTY); + if (wrench.isEmpty()) { + player.displayClientMessage(PartConfigHelpers.getNoWrenchMessage(), true); + return; + } + PartConfigSnapshot snapshot = getPartType() + .snapshotConfig(ValueDeseralizationContext.of(world), getPartState(), SECTIONS); + PartConfigHelpers.setSnapshot(world.registryAccess(), wrench, snapshot); + player.displayClientMessage(Component.translatable("gui.integrateddynamics.config.copied"), true); + } + + /** + * Paste the settings inside the Wrench of the player onto this part. + */ + protected void pasteConfig() { + ItemStack wrench = PartConfigHelpers.findWrench(player).orElse(ItemStack.EMPTY); + if (wrench.isEmpty()) { + player.displayClientMessage(PartConfigHelpers.getNoWrenchMessage(), true); + return; + } + PartConfigSnapshot snapshot = PartConfigHelpers.getSnapshot(world.registryAccess(), wrench).orElse(null); + if (snapshot == null || !snapshot.hasSection(PartConfigSection.PART_SETTINGS)) { + player.displayClientMessage(Component.translatable("item.integrateddynamics.wrench.mode.config.empty"), true); + return; + } + + INetwork network = NetworkHelpers.getNetwork(getTarget().getCenter()).orElse(null); + IPartNetwork partNetwork = NetworkHelpers.getPartNetwork(network).orElse(null); + PartConfigApplyResult result = getPartType().applyConfig(ValueDeseralizationContext.of(world), network, + partNetwork, getTarget(), getPartState(), snapshot, SECTIONS, player); + player.displayClientMessage(result.getMessage(), true); + + // Show the pasted values in the gui + initializeValues(); } public IPartType getPartType() { diff --git a/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigApplyResult.java b/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigApplyResult.java new file mode 100644 index 00000000000..dc7dafb4b2e --- /dev/null +++ b/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigApplyResult.java @@ -0,0 +1,78 @@ +package org.cyclops.integrateddynamics.core.part; + +import com.google.common.collect.Lists; +import lombok.Getter; +import lombok.Setter; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; + +import java.util.List; + +/** + * The outcome of pasting a {@link PartConfigSnapshot} onto a part. + * @author rubensworks + */ +@Getter +public class PartConfigApplyResult { + + @Setter + private boolean partSettingsApplied = false; + @Setter + private boolean offsetFailed = false; + private int appliedProperties = 0; + private int skippedProperties = 0; + private int cardsPasted = 0; + private int cardsSkipped = 0; + @Setter + private int missingBlanks = 0; + + public void addAppliedProperties(int amount) { + this.appliedProperties += amount; + } + + public void addSkippedProperties(int amount) { + this.skippedProperties += amount; + } + + public void addCardsPasted(int amount) { + this.cardsPasted += amount; + } + + public void addCardsSkipped(int amount) { + this.cardsSkipped += amount; + } + + /** + * @return All messages describing this outcome, which can be shown to the player. + */ + public List getMessages() { + List messages = Lists.newArrayList(); + messages.add(Component.translatable("item.integrateddynamics.wrench.mode.config.pasted", + this.appliedProperties, this.cardsPasted)); + if (this.offsetFailed) { + messages.add(Component.translatable("item.integrateddynamics.wrench.mode.offset.fail")); + } + if (this.cardsSkipped > 0) { + messages.add(Component.translatable("item.integrateddynamics.wrench.mode.config.cards_skipped", + this.cardsSkipped, this.missingBlanks)); + } + return messages; + } + + /** + * @return All messages describing this outcome, joined into a single line. + */ + public MutableComponent getMessage() { + MutableComponent message = Component.empty(); + boolean first = true; + for (MutableComponent part : getMessages()) { + if (!first) { + message.append(" "); + } + first = false; + message.append(part); + } + return message; + } + +} diff --git a/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigSection.java b/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigSection.java new file mode 100644 index 00000000000..67a24d0a584 --- /dev/null +++ b/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigSection.java @@ -0,0 +1,37 @@ +package org.cyclops.integrateddynamics.core.part; + +import com.google.common.collect.Sets; + +import java.util.EnumSet; +import java.util.Locale; +import java.util.Set; + +/** + * The separate sections of a part configuration that can be copied and pasted. + * @author rubensworks + */ +public enum PartConfigSection { + + /** + * Update interval, priority, channel, target side override and target offset. + */ + PART_SETTINGS, + /** + * The statically configured properties (settings) of aspects. + */ + ASPECT_PROPERTIES, + /** + * The variable cards inside the part. + */ + VARIABLE_CARDS; + + /** + * All sections, as copied by the Wrench. + */ + public static final Set ALL = Sets.immutableEnumSet(EnumSet.allOf(PartConfigSection.class)); + + public String getTranslationKey() { + return "item.integrateddynamics.wrench.mode.config.section." + name().toLowerCase(Locale.ENGLISH); + } + +} diff --git a/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigSnapshot.java b/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigSnapshot.java new file mode 100644 index 00000000000..c74439c7c49 --- /dev/null +++ b/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigSnapshot.java @@ -0,0 +1,152 @@ +package org.cyclops.integrateddynamics.core.part; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.Direction; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.Vec3i; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import org.cyclops.integrateddynamics.IntegratedDynamics; + +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** + * An immutable snapshot of the configuration of a part, which can be pasted onto another part. + * + * Only things that a player can configure are stored, + * so no part id, max offset, enabled state, error messages or active aspect. + * + * @param version The version of this snapshot format. + * @param sourcePartType The unique name of the part type this snapshot was taken from. + * @param partSettings The general part settings, if the {@link PartConfigSection#PART_SETTINGS} section is included. + * @param aspectProperties The serialized aspect properties, by aspect unique name. + * @param variableCards All non-empty variable cards. + * @author rubensworks + */ +public record PartConfigSnapshot(int version, + ResourceLocation sourcePartType, + Optional partSettings, + Map aspectProperties, + List variableCards) { + + public static final int VERSION = 1; + + /** + * The inventory name under which the active variable inventory of a part is stored. + * This can not clash with named inventories, as those are derived from resource locations. + */ + public static final String INVENTORY_NAME_ACTIVE = "$active"; + + public static final Codec CODEC_PART_SETTINGS = RecordCodecBuilder.create(builder -> builder + .group( + Codec.INT.fieldOf("updateInterval").forGetter(PartSettings::updateInterval), + Codec.INT.fieldOf("priority").forGetter(PartSettings::priority), + Codec.INT.fieldOf("channel").forGetter(PartSettings::channel), + Direction.CODEC.optionalFieldOf("targetSide").forGetter(PartSettings::targetSide), + Vec3i.CODEC.fieldOf("targetOffset").forGetter(PartSettings::targetOffset) + ) + .apply(builder, PartSettings::new)); + + // Lazy, so that snapshots without variable cards can be (de)serialized without the item registry being present + public static final Codec CODEC_VARIABLE_CARD = Codec.lazyInitialized( + () -> RecordCodecBuilder.create(builder -> builder + .group( + Codec.STRING.fieldOf("inventoryName").forGetter(VariableCard::inventoryName), + Codec.INT.fieldOf("slot").forGetter(VariableCard::slot), + ItemStack.CODEC.fieldOf("itemStack").forGetter(VariableCard::itemStack) + ) + .apply(builder, VariableCard::new))); + + public static final Codec CODEC = RecordCodecBuilder.create(builder -> builder + .group( + Codec.INT.fieldOf("version").forGetter(PartConfigSnapshot::version), + ResourceLocation.CODEC.fieldOf("sourcePartType").forGetter(PartConfigSnapshot::sourcePartType), + CODEC_PART_SETTINGS.optionalFieldOf("partSettings").forGetter(PartConfigSnapshot::partSettings), + Codec.unboundedMap(ResourceLocation.CODEC, CompoundTag.CODEC) + .optionalFieldOf("aspectProperties", Map.of()).forGetter(PartConfigSnapshot::aspectProperties), + CODEC_VARIABLE_CARD.listOf() + .optionalFieldOf("variableCards", List.of()).forGetter(PartConfigSnapshot::variableCards) + ) + .apply(builder, PartConfigSnapshot::new)); + + /** + * @param section A config section. + * @return If this snapshot holds anything for the given section. + */ + public boolean hasSection(PartConfigSection section) { + return switch (section) { + case PART_SETTINGS -> partSettings().isPresent(); + case ASPECT_PROPERTIES -> !aspectProperties().isEmpty(); + case VARIABLE_CARDS -> !variableCards().isEmpty(); + }; + } + + /** + * @return All sections that this snapshot holds something for. + */ + public Set getSections() { + Set sections = EnumSet.noneOf(PartConfigSection.class); + for (PartConfigSection section : PartConfigSection.values()) { + if (hasSection(section)) { + sections.add(section); + } + } + return sections; + } + + /** + * @return If this snapshot holds nothing at all. + */ + public boolean isEmpty() { + return getSections().isEmpty(); + } + + /** + * @param provider A holder lookup provider, used to serialize the variable cards. + * @return The NBT representation of this snapshot. + */ + public CompoundTag toNBT(HolderLookup.Provider provider) { + return (CompoundTag) CODEC.encodeStart(provider.createSerializationContext(NbtOps.INSTANCE), this).getOrThrow(); + } + + /** + * @param provider A holder lookup provider, used to deserialize the variable cards. + * @param tag An NBT representation of a snapshot. + * @return The snapshot, or empty if it could not be read. + */ + public static Optional fromNBT(HolderLookup.Provider provider, CompoundTag tag) { + return CODEC.parse(provider.createSerializationContext(NbtOps.INSTANCE), tag) + .resultOrPartial(error -> IntegratedDynamics.clog(org.apache.logging.log4j.Level.ERROR, + String.format("Could not read a part configuration snapshot: %s", error))); + } + + /** + * The general settings of a part. + * @param updateInterval The tick interval at which the part updates. + * @param priority The priority of the part in its network. + * @param channel The channel of the part in its network. + * @param targetSide The overridden side of the target block, if any. + * @param targetOffset The target position offset. + */ + public record PartSettings(int updateInterval, int priority, int channel, + Optional targetSide, Vec3i targetOffset) { + } + + /** + * A variable card inside one of the inventories of a part. + * @param inventoryName The name of the named inventory, + * or {@link #INVENTORY_NAME_ACTIVE} for the active variable inventory. + * @param slot The slot inside that inventory. + * @param itemStack The card. + */ + public record VariableCard(String inventoryName, int slot, ItemStack itemStack) { + } + +} diff --git a/src/main/java/org/cyclops/integrateddynamics/core/part/PartStateOffsetHandler.java b/src/main/java/org/cyclops/integrateddynamics/core/part/PartStateOffsetHandler.java index cf842e8e97d..05e262efebb 100644 --- a/src/main/java/org/cyclops/integrateddynamics/core/part/PartStateOffsetHandler.java +++ b/src/main/java/org/cyclops/integrateddynamics/core/part/PartStateOffsetHandler.java @@ -35,6 +35,11 @@ */ public class PartStateOffsetHandler

{ + /** + * The name of the named inventory in which the offset variables are stored. + */ + public static final String INVENTORY_NAME = "offsetVariablesInventory"; + public final List> offsetVariableEvaluators = Lists.newArrayList(); public final Int2ObjectMap offsetVariablesSlotMessages = new Int2ObjectArrayMap<>(); public boolean offsetVariablesDirty = true; @@ -80,7 +85,7 @@ public void markOffsetVariablesChanged() { public SimpleInventory getOffsetVariablesInventory(IPartState

partState) { SimpleInventory offsetVariablesInventory = new SimpleInventory(3, 1); - partState.loadInventoryNamed("offsetVariablesInventory", offsetVariablesInventory); + partState.loadInventoryNamed(INVENTORY_NAME, offsetVariablesInventory); return offsetVariablesInventory; } diff --git a/src/main/java/org/cyclops/integrateddynamics/item/ItemWrench.java b/src/main/java/org/cyclops/integrateddynamics/item/ItemWrench.java index 0c1e44c9bad..2eb5a290939 100644 --- a/src/main/java/org/cyclops/integrateddynamics/item/ItemWrench.java +++ b/src/main/java/org/cyclops/integrateddynamics/item/ItemWrench.java @@ -6,6 +6,7 @@ import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.Vec3i; +import net.minecraft.nbt.CompoundTag; import net.minecraft.network.chat.Component; import net.minecraft.network.codec.ByteBufCodecs; import net.minecraft.network.codec.StreamCodec; @@ -26,14 +27,26 @@ import net.minecraft.world.phys.BlockHitResult; import org.cyclops.cyclopscore.helper.MinecraftHelpers; import org.cyclops.integrateddynamics.RegistryEntries; +import org.cyclops.integrateddynamics.api.evaluate.variable.ValueDeseralizationContext; +import org.cyclops.integrateddynamics.api.network.INetwork; +import org.cyclops.integrateddynamics.api.network.IPartNetwork; 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.core.helper.CableHelpers; +import org.cyclops.integrateddynamics.core.helper.NetworkHelpers; +import org.cyclops.integrateddynamics.core.helper.PartConfigHelpers; +import org.cyclops.integrateddynamics.core.helper.PartHelpers; +import org.cyclops.integrateddynamics.core.part.PartConfigApplyResult; +import org.cyclops.integrateddynamics.core.part.PartConfigSection; +import org.cyclops.integrateddynamics.core.part.PartConfigSnapshot; +import org.cyclops.integrateddynamics.core.part.PartTypes; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.stream.Collectors; /** * The default wrench for this mod. @@ -87,6 +100,10 @@ public InteractionResult onItemUseFirst(ItemStack stack, UseOnContext context) { return InteractionResult.FAIL; } } + case CONFIG -> { + // Let the click through to the part, so that its configuration can be copied + return InteractionResult.PASS; + } } } @@ -153,6 +170,17 @@ public void appendHoverText(ItemStack itemStack, Item.TooltipContext context, Li if (itemStack.has(RegistryEntries.DATACOMPONENT_WRENCH_TARGET_DIRECTION)) { list.add(Component.translatable("item.integrateddynamics.wrench.mode.offset_side.side", itemStack.get(RegistryEntries.DATACOMPONENT_WRENCH_TARGET_DIRECTION).getSerializedName()).withStyle(ChatFormatting.GRAY)); } + CompoundTag configTag = itemStack.get(RegistryEntries.DATACOMPONENT_WRENCH_PART_CONFIG); + if (configTag != null && context.registries() != null) { + PartConfigSnapshot.fromNBT(context.registries(), configTag).ifPresent(snapshot -> { + list.add(Component.translatable("item.integrateddynamics.wrench.mode.config.source", + Component.translatable(getSourcePartTypeName(snapshot))).withStyle(ChatFormatting.GRAY)); + list.add(Component.translatable("item.integrateddynamics.wrench.mode.config.sections", + snapshot.getSections().stream() + .map(section -> Component.translatable(section.getTranslationKey()).getString()) + .collect(Collectors.joining(", "))).withStyle(ChatFormatting.GRAY)); + }); + } list.add(Component.translatable(mode.getLabel() + ".info").withStyle(ChatFormatting.ITALIC, ChatFormatting.GRAY)); } @@ -172,6 +200,10 @@ public

, S extends IPartState

> InteractionResult pe } return InteractionResult.SUCCESS; } + case CONFIG -> { + pastePartConfig(partType, partState, itemStack, player, center); + return InteractionResult.SUCCESS; + } case OFFSET_SIDE -> { if (itemStack.has(RegistryEntries.DATACOMPONENT_WRENCH_TARGET_BLOCKPOS) && itemStack.has(RegistryEntries.DATACOMPONENT_WRENCH_TARGET_DIRECTION)) { Vec3i offset = determineOffset(hit, itemStack); @@ -191,6 +223,69 @@ public

, S extends IPartState

> InteractionResult pe return InteractionResult.PASS; } + /** + * Copy the configuration of the part at the given position into the given Wrench. + * @param itemStack The Wrench. + * @param player The player. + * @param center The position of the part. + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public void copyPartConfig(ItemStack itemStack, Player player, PartPos center) { + if (player.level().isClientSide()) { + return; + } + PartHelpers.PartStateHolder partStateHolder = PartHelpers.getPart(center); + if (partStateHolder == null) { + return; + } + IPartType partType = partStateHolder.getPart(); + Level level = center.getPos().getLevel(true); + PartConfigSnapshot snapshot = ((IPartType) partType).snapshotConfig(ValueDeseralizationContext.of(level), + partStateHolder.getState(), PartConfigSection.ALL); + PartConfigHelpers.setSnapshot(level.registryAccess(), itemStack, snapshot); + player.displayClientMessage(Component.translatable("item.integrateddynamics.wrench.mode.config.copied", + Component.translatable(partType.getTranslationKey())), true); + } + + /** + * Paste the configuration inside the given Wrench onto the given part. + * @param partType The part type. + * @param partState The part state. + * @param itemStack The Wrench. + * @param player The player. + * @param center The position of the part. + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + protected void pastePartConfig(IPartType partType, IPartState partState, ItemStack itemStack, + Player player, PartPos center) { + if (player.level().isClientSide()) { + return; + } + Level level = center.getPos().getLevel(true); + PartConfigSnapshot snapshot = PartConfigHelpers.getSnapshot(level.registryAccess(), itemStack).orElse(null); + if (snapshot == null) { + player.displayClientMessage(Component.translatable("item.integrateddynamics.wrench.mode.config.empty"), true); + return; + } + if (!snapshot.sourcePartType().equals(partType.getUniqueName())) { + player.displayClientMessage(Component.translatable("item.integrateddynamics.wrench.mode.config.mismatch", + Component.translatable(getSourcePartTypeName(snapshot))), true); + return; + } + + INetwork network = NetworkHelpers.getNetwork(center).orElse(null); + IPartNetwork partNetwork = NetworkHelpers.getPartNetwork(network).orElse(null); + PartTarget target = ((IPartType) partType).getTarget(center, partState); + PartConfigApplyResult result = ((IPartType) partType).applyConfig(ValueDeseralizationContext.of(level), + network, partNetwork, target, partState, snapshot, PartConfigSection.ALL, player); + player.displayClientMessage(result.getMessage(), true); + } + + protected static String getSourcePartTypeName(PartConfigSnapshot snapshot) { + IPartType partType = PartTypes.REGISTRY.getPartType(snapshot.sourcePartType()); + return partType == null ? snapshot.sourcePartType().toString() : partType.getTranslationKey(); + } + protected Vec3i determineOffset(BlockHitResult hit, ItemStack itemStack) { BlockPos source = hit.getBlockPos().relative(hit.getDirection()); BlockPos targetAbs = itemStack.get(RegistryEntries.DATACOMPONENT_WRENCH_TARGET_BLOCKPOS); @@ -200,7 +295,8 @@ protected Vec3i determineOffset(BlockHitResult hit, ItemStack itemStack) { public static enum Mode implements StringRepresentable { DEFAULT("integrateddynamics:default", "item.integrateddynamics.wrench.mode.default"), OFFSET("integrateddynamics:offset", "item.integrateddynamics.wrench.mode.offset"), - OFFSET_SIDE("integrateddynamics:offset_side", "item.integrateddynamics.wrench.mode.offset_side"); + OFFSET_SIDE("integrateddynamics:offset_side", "item.integrateddynamics.wrench.mode.offset_side"), + CONFIG("integrateddynamics:config", "item.integrateddynamics.wrench.mode.config"); public static final StringRepresentable.EnumCodec CODEC = net.minecraft.util.StringRepresentable.fromEnum(Mode::values); public static final StreamCodec STREAM_CODEC = ByteBufCodecs.idMapper(INT_MODES::get, Mode::ordinal); diff --git a/src/main/resources/assets/integrateddynamics/lang/en_us.json b/src/main/resources/assets/integrateddynamics/lang/en_us.json index 23b0ceacc0e..e50a5620960 100644 --- a/src/main/resources/assets/integrateddynamics/lang/en_us.json +++ b/src/main/resources/assets/integrateddynamics/lang/en_us.json @@ -25,6 +25,12 @@ "gui.integrateddynamics.partsettings.priority": "Priority", "gui.integrateddynamics.partsettings.channel": "Energy Channel", "gui.integrateddynamics.partsettings.channel.disabledinfo": "Network energy consumption is disabled on this server.", + "gui.integrateddynamics.partsettings.config.copy": "Copy the settings into your Wrench", + "gui.integrateddynamics.partsettings.config.paste": "Paste the settings from your Wrench", + "gui.integrateddynamics.aspectsettings.config.copy": "Copy these aspect settings into your Wrench", + "gui.integrateddynamics.aspectsettings.config.paste": "Paste aspect settings from your Wrench", + "gui.integrateddynamics.config.copied": "Configuration was copied into the Wrench", + "gui.integrateddynamics.config.nowrench": "You need a Wrench to copy or paste a configuration", "gui.integrateddynamics.partsettings.side": "Target Side", "gui.integrateddynamics.partoffset.offsets": "Offset relative to the target position", "gui.integrateddynamics.partoffset.offsets.max": "Maximum offset: %s", @@ -159,6 +165,18 @@ "item.integrateddynamics.wrench.mode.offset_side.saved": "Position and Side were saved in Wrench: %s - %s", "item.integrateddynamics.wrench.mode.offset_side.success": "New offset position and side have been set in part", "item.integrateddynamics.wrench.mode.offset_side.side": "Side: %s", + "item.integrateddynamics.wrench.mode.config": "Copy Configuration", + "item.integrateddynamics.wrench.mode.config.info": "Shift + Right-click on a part to copy its configuration and Right-click on another part to paste it", + "item.integrateddynamics.wrench.mode.config.copied": "Configuration of %s was copied into the Wrench", + "item.integrateddynamics.wrench.mode.config.pasted": "Pasted configuration: %s aspect settings and %s variable cards", + "item.integrateddynamics.wrench.mode.config.empty": "No configuration was copied into this Wrench yet", + "item.integrateddynamics.wrench.mode.config.mismatch": "This configuration was copied from a different part: %s", + "item.integrateddynamics.wrench.mode.config.cards_skipped": "Skipped %s variable cards, %s more blank Variable Cards are needed", + "item.integrateddynamics.wrench.mode.config.source": "Copied from: %s", + "item.integrateddynamics.wrench.mode.config.sections": "Includes: %s", + "item.integrateddynamics.wrench.mode.config.section.part_settings": "part settings", + "item.integrateddynamics.wrench.mode.config.section.aspect_properties": "aspect settings", + "item.integrateddynamics.wrench.mode.config.section.variable_cards": "variable cards", "item.integrateddynamics.variable": "Variable Card", "item.integrateddynamics.variable.info": "Clear or copy in a crafting grid", "item.integrateddynamics.variable.warning": "§4§lWARNING: Do NOT copy this item by middle-clicking!", @@ -1841,6 +1859,7 @@ "info_book.integrateddynamics.manual.parts.settings.text3": "The Ticks/Operation allows you to configure the ticking frequency of this part. The higher, the slower it operates.", "info_book.integrateddynamics.manual.parts.settings.text4": "The Priority determines the order in which this part is executed within a single network tick.", "info_book.integrateddynamics.manual.parts.settings.text5": "The Energy Channel indicates the channel from which energy should be consumed when this part ticks. This is only applicable if energy consumption for networks is enabled.", + "info_book.integrateddynamics.manual.parts.settings.text6": "The configuration of a part can be copied to other parts using a &lWrench&r in &lCopy Configuration&r mode. Shift+right-clicking a part copies its settings, aspect settings and variable cards into the Wrench, and right-clicking another part of the same type pastes them. Each pasted variable card consumes one blank &lVariable Card&r from your inventory. The Settings and Aspect Settings screens also have buttons to copy and paste only the settings that they show.", "info_book.integrateddynamics.manual.parts.offsets": "Offsets", "info_book.integrateddynamics.manual.parts.offsets.text1": "By default, parts will target the direct neighboring position. However, when &lPart Enhancements&r are applied, the target position can be changed through the part's &lOffset&r screen.", diff --git a/src/main/resources/data/integrateddynamics/info/on_the_dynamics_of_integration.xml b/src/main/resources/data/integrateddynamics/info/on_the_dynamics_of_integration.xml index b6727826999..1a768edea1e 100644 --- a/src/main/resources/data/integrateddynamics/info/on_the_dynamics_of_integration.xml +++ b/src/main/resources/data/integrateddynamics/info/on_the_dynamics_of_integration.xml @@ -436,6 +436,7 @@ info_book.integrateddynamics.manual.parts.settings.text3 info_book.integrateddynamics.manual.parts.settings.text4 info_book.integrateddynamics.manual.parts.settings.text5 + info_book.integrateddynamics.manual.parts.settings.text6

info_book.integrateddynamics.manual.parts.offsets.text1 diff --git a/src/test/java/org/cyclops/integrateddynamics/core/part/TestPartConfigSnapshot.java b/src/test/java/org/cyclops/integrateddynamics/core/part/TestPartConfigSnapshot.java new file mode 100644 index 00000000000..75aeb0951c4 --- /dev/null +++ b/src/test/java/org/cyclops/integrateddynamics/core/part/TestPartConfigSnapshot.java @@ -0,0 +1,91 @@ +package org.cyclops.integrateddynamics.core.part; + +import net.minecraft.core.Direction; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.Vec3i; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; +import org.junit.Test; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +/** + * Test the serialization of part configuration snapshots. + * + * Variable cards are not covered here, as those require a full registry access, + * they are covered by the game tests instead. + * + * @author rubensworks + */ +public class TestPartConfigSnapshot { + + private static final ResourceLocation PART_TYPE = ResourceLocation.parse("integrateddynamics:redstone_writer"); + private static final ResourceLocation ASPECT = ResourceLocation.parse("integrateddynamics:write_boolean_redstone"); + + protected static PartConfigSnapshot roundTrip(PartConfigSnapshot snapshot) { + CompoundTag tag = snapshot.toNBT(RegistryAccess.EMPTY); + return PartConfigSnapshot.fromNBT(RegistryAccess.EMPTY, tag).orElse(null); + } + + protected static CompoundTag aspectPropertiesTag() { + CompoundTag tag = new CompoundTag(); + tag.putString("dummy", "value"); + return tag; + } + + @Test + public void testRoundTripAllSections() { + PartConfigSnapshot snapshot = new PartConfigSnapshot(PartConfigSnapshot.VERSION, PART_TYPE, + Optional.of(new PartConfigSnapshot.PartSettings(20, 3, 7, + Optional.of(Direction.NORTH), new Vec3i(1, -2, 3))), + Map.of(ASPECT, aspectPropertiesTag()), + List.of()); + + assertThat(roundTrip(snapshot), is(snapshot)); + } + + @Test + public void testRoundTripWithoutPartSettings() { + PartConfigSnapshot snapshot = new PartConfigSnapshot(PartConfigSnapshot.VERSION, PART_TYPE, + Optional.empty(), Map.of(ASPECT, aspectPropertiesTag()), List.of()); + + assertThat(roundTrip(snapshot), is(snapshot)); + } + + @Test + public void testRoundTripWithoutAspectProperties() { + PartConfigSnapshot snapshot = new PartConfigSnapshot(PartConfigSnapshot.VERSION, PART_TYPE, + Optional.of(new PartConfigSnapshot.PartSettings(1, 0, 0, Optional.empty(), Vec3i.ZERO)), + Map.of(), List.of()); + + assertThat(roundTrip(snapshot), is(snapshot)); + } + + @Test + public void testRoundTripEmpty() { + PartConfigSnapshot snapshot = new PartConfigSnapshot(PartConfigSnapshot.VERSION, PART_TYPE, + Optional.empty(), Map.of(), List.of()); + + assertThat(roundTrip(snapshot), is(snapshot)); + assertThat(snapshot.isEmpty(), is(true)); + } + + @Test + public void testSections() { + PartConfigSnapshot snapshot = new PartConfigSnapshot(PartConfigSnapshot.VERSION, PART_TYPE, + Optional.of(new PartConfigSnapshot.PartSettings(1, 0, 0, Optional.empty(), Vec3i.ZERO)), + Map.of(ASPECT, aspectPropertiesTag()), List.of()); + + assertThat(snapshot.hasSection(PartConfigSection.PART_SETTINGS), is(true)); + assertThat(snapshot.hasSection(PartConfigSection.ASPECT_PROPERTIES), is(true)); + assertThat(snapshot.hasSection(PartConfigSection.VARIABLE_CARDS), is(false)); + assertThat(snapshot.getSections(), + is(java.util.Set.of(PartConfigSection.PART_SETTINGS, PartConfigSection.ASPECT_PROPERTIES))); + } + +} From d2e20ede73e73a7e09e667cdf93c4c7da3943df5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 14:57:42 +0000 Subject: [PATCH 02/13] Copy part configurations through Wrench modes instead of gui buttons Replaces the copy and paste buttons in the Settings and Aspect Settings screens with separate Wrench modes, so that copying a subset is a property of the tool rather than of the screen you happen to have open. The Wrench now has four configuration modes: Copy Configuration takes the whole configuration and only pastes onto a part of the same type, while Copy Part Settings, Copy Aspect Settings and Copy Variable Cards each take one section and can be pasted onto any part. Copying now only stores values that differ from the defaults of a freshly placed part, so pasting never resets a setting that was left alone on the copied part. Copying a part that is still fully default reports that there is nothing to copy instead of storing an empty configuration. The Wrench tooltip lists what was copied and how many blank Variable Cards pasting it needs, and the active mode is shown on the Wrench item through a small coloured badge per mode. The paste result is now reported as what was actually applied, with the warnings sent to the chat rather than appended to the action bar line, where they did not fit. Closes #859 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01LBAgcFp6jWcYRWz9N8h7AF --- CHANGELOG-1.21.1.md | 2 + .../gametest/GameTestsWrenchConfig.java | 150 ++++++++++-------- .../api/part/IPartState.java | 9 ++ ...xelShapeComponentsFactoryHandlerParts.java | 2 +- .../ContainerScreenAspectSettings.java | 8 - .../ContainerScreenPartSettings.java | 8 - .../core/helper/PartConfigHelpers.java | 83 +++++++--- .../container/ContainerAspectSettings.java | 143 ----------------- .../container/ContainerPartSettings.java | 67 -------- .../core/part/PartConfigApplyResult.java | 58 ++++--- .../core/part/PartConfigSnapshot.java | 36 +++-- .../core/part/PartStateBase.java | 3 +- .../integrateddynamics/item/ItemWrench.java | 71 ++++++++- .../integrateddynamics/proxy/ClientProxy.java | 18 +++ .../assets/integrateddynamics/lang/en_us.json | 28 ++-- .../models/item/wrench.json | 12 +- .../models/item/wrench_config.json | 6 + .../models/item/wrench_config_aspects.json | 6 + .../models/item/wrench_config_settings.json | 6 + .../models/item/wrench_config_variables.json | 6 + .../models/item/wrench_offset.json | 6 + .../models/item/wrench_offset_side.json | 6 + .../textures/item/wrench_config.png | Bin 0 -> 545 bytes .../textures/item/wrench_config_aspects.png | Bin 0 -> 548 bytes .../textures/item/wrench_config_settings.png | Bin 0 -> 545 bytes .../textures/item/wrench_config_variables.png | Bin 0 -> 546 bytes .../textures/item/wrench_offset.png | Bin 0 -> 544 bytes .../textures/item/wrench_offset_side.png | Bin 0 -> 546 bytes .../core/part/TestPartConfigSnapshot.java | 26 ++- 29 files changed, 396 insertions(+), 364 deletions(-) create mode 100644 src/main/resources/assets/integrateddynamics/models/item/wrench_config.json create mode 100644 src/main/resources/assets/integrateddynamics/models/item/wrench_config_aspects.json create mode 100644 src/main/resources/assets/integrateddynamics/models/item/wrench_config_settings.json create mode 100644 src/main/resources/assets/integrateddynamics/models/item/wrench_config_variables.json create mode 100644 src/main/resources/assets/integrateddynamics/models/item/wrench_offset.json create mode 100644 src/main/resources/assets/integrateddynamics/models/item/wrench_offset_side.json create mode 100644 src/main/resources/assets/integrateddynamics/textures/item/wrench_config.png create mode 100644 src/main/resources/assets/integrateddynamics/textures/item/wrench_config_aspects.png create mode 100644 src/main/resources/assets/integrateddynamics/textures/item/wrench_config_settings.png create mode 100644 src/main/resources/assets/integrateddynamics/textures/item/wrench_config_variables.png create mode 100644 src/main/resources/assets/integrateddynamics/textures/item/wrench_offset.png create mode 100644 src/main/resources/assets/integrateddynamics/textures/item/wrench_offset_side.png diff --git a/CHANGELOG-1.21.1.md b/CHANGELOG-1.21.1.md index e5db9f2aad1..474234f692d 100644 --- a/CHANGELOG-1.21.1.md +++ b/CHANGELOG-1.21.1.md @@ -7,6 +7,8 @@ All notable changes to this project will be documented in this file. ### Added * Allow part configurations to be copied and pasted with the Wrench, Closes #859 + * The Wrench gets modes to copy a whole part configuration, or just its settings, aspect settings or variable cards + * The active Wrench mode is shown on the Wrench item * Allow aspect settings to be determined by variables (#1707), Closes CyclopsMC/IntegratedTunnels#278 * Show modified aspect property values in tooltip (#1706), Closes #1704 diff --git a/src/integrationtest/java/org/cyclops/integrateddynamics/gametest/GameTestsWrenchConfig.java b/src/integrationtest/java/org/cyclops/integrateddynamics/gametest/GameTestsWrenchConfig.java index 487091676a4..db3cec89529 100644 --- a/src/integrationtest/java/org/cyclops/integrateddynamics/gametest/GameTestsWrenchConfig.java +++ b/src/integrationtest/java/org/cyclops/integrateddynamics/gametest/GameTestsWrenchConfig.java @@ -1,54 +1,41 @@ package org.cyclops.integrateddynamics.gametest; -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.GameTest; import net.minecraft.gametest.framework.GameTestHelper; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.world.Container; import net.minecraft.world.InteractionHand; -import net.minecraft.world.SimpleContainer; -import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.GameType; import net.minecraft.world.phys.BlockHitResult; import net.neoforged.neoforge.gametest.GameTestHolder; import net.neoforged.neoforge.gametest.PrefixGameTestTemplate; -import org.apache.commons.lang3.tuple.Triple; import org.cyclops.cyclopscore.inventory.SimpleInventory; import org.cyclops.integrateddynamics.Reference; import org.cyclops.integrateddynamics.RegistryEntries; import org.cyclops.integrateddynamics.api.evaluate.variable.ValueDeseralizationContext; import org.cyclops.integrateddynamics.api.network.INetwork; -import org.cyclops.integrateddynamics.api.part.IPartContainer; 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.IAspect; import org.cyclops.integrateddynamics.core.helper.NetworkHelpers; import org.cyclops.integrateddynamics.core.helper.PartConfigHelpers; import org.cyclops.integrateddynamics.core.helper.PartHelpers; -import org.cyclops.integrateddynamics.core.inventory.container.ContainerAspectSettings; import org.cyclops.integrateddynamics.core.part.PartConfigApplyResult; import org.cyclops.integrateddynamics.core.part.PartConfigSection; import org.cyclops.integrateddynamics.core.part.PartConfigSnapshot; import org.cyclops.integrateddynamics.core.part.PartStateActiveVariableBase; -import org.cyclops.integrateddynamics.core.part.PartTypeBase; import org.cyclops.integrateddynamics.core.part.PartTypes; import org.cyclops.integrateddynamics.item.ItemWrench; import org.cyclops.integrateddynamics.part.aspect.Aspects; import org.cyclops.integrateddynamics.part.aspect.write.AspectWriteBuilders; import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypeBoolean; -import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypeInteger; import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypes; import javax.annotation.Nullable; -import java.util.Map; -import java.util.Optional; import static org.cyclops.integrateddynamics.gametest.GameTestHelpersIntegratedDynamics.createVariableForValue; import static org.cyclops.integrateddynamics.gametest.GameTestHelpersIntegratedDynamics.getEffectiveAspectProperty; @@ -330,67 +317,68 @@ public void testWrenchConfigPasteOffsetOutOfRange(GameTestHelper helper) { }); } - /** - * The aspect settings container, with all value syncing recorded in-memory, - * as there is no client to sync to in game tests. - */ - public static class RecordingContainerAspectSettings extends ContainerAspectSettings { + @GameTest(template = TEMPLATE_EMPTY) + public void testWrenchConfigSettingsModeOnlyCopiesPartSettings(GameTestHelper helper) { + PartPos source = placePart(helper, POS_SOURCE, PartTypes.REDSTONE_WRITER); + PartPos target = placePart(helper, POS_TARGET, PartTypes.REDSTONE_WRITER); + configurePart(helper, source, Vec3i.ZERO); - private final Map recordedValues = Maps.newHashMap(); + Player player = helper.makeMockPlayer(GameType.SURVIVAL); + ItemStack wrench = createWrench(ItemWrench.Mode.CONFIG_SETTINGS); + clickPart(helper, player, wrench, source, true); + clickPart(helper, player, wrench, target, false); - public RecordingContainerAspectSettings(int id, Inventory playerInventory, Container inventory, - Optional target, Optional partContainer, - Optional partType, IAspect aspect) { - super(id, playerInventory, inventory, target, partContainer, partType, aspect); - } + helper.succeedWhen(() -> { + IPartType partType = partType(target); + IPartState state = partState(target); + helper.assertValueEqual(partType.getUpdateInterval(state), 40, "Update interval was not pasted"); + helper.assertValueEqual(partType.getPriority(state), 3, "Priority was not pasted"); + helper.assertValueEqual( + getEffectiveAspectProperty(target, Aspects.Write.Redstone.BOOLEAN, + AspectWriteBuilders.Redstone.PROP_STRONG_POWER), + ValueTypeBoolean.ValueBoolean.of(false), "An aspect setting was pasted"); + }); + } - @Override - public void setValue(int id, CompoundTag value) { - this.recordedValues.put(id, value); - } + @GameTest(template = TEMPLATE_EMPTY) + public void testWrenchConfigAspectsModeOnlyCopiesAspectSettings(GameTestHelper helper) { + PartPos source = placePart(helper, POS_SOURCE, PartTypes.REDSTONE_WRITER); + PartPos target = placePart(helper, POS_TARGET, PartTypes.REDSTONE_WRITER); + configurePart(helper, source, Vec3i.ZERO); + int updateInterval = ((IPartType) partType(target)).getUpdateInterval(partState(target)); - @Override - public CompoundTag getValue(int id) { - return this.recordedValues.get(id); - } - } + Player player = helper.makeMockPlayer(GameType.SURVIVAL); + ItemStack wrench = createWrench(ItemWrench.Mode.CONFIG_ASPECTS); + clickPart(helper, player, wrench, source, true); + clickPart(helper, player, wrench, target, false); - /** - * Construct the aspect settings container for the given part and aspect, - * in the same way as it is constructed when the given player opens the aspect settings gui. - */ - protected static ContainerAspectSettings openAspectSettings(Player player, PartPos partPos, IAspect aspect) { - Triple data = PartHelpers.getContainerPartConstructionData(partPos); - return new RecordingContainerAspectSettings(1, player.getInventory(), new SimpleContainer(0), - Optional.of(data.getRight()), Optional.of(data.getLeft()), Optional.of(data.getMiddle()), aspect); + helper.succeedWhen(() -> { + helper.assertValueEqual( + getEffectiveAspectProperty(target, Aspects.Write.Redstone.BOOLEAN, + AspectWriteBuilders.Redstone.PROP_STRONG_POWER), + ValueTypeBoolean.ValueBoolean.of(true), "The aspect setting was not pasted"); + helper.assertValueEqual(((IPartType) partType(target)).getUpdateInterval(partState(target)), + updateInterval, "A part setting was pasted"); + }); } @GameTest(template = TEMPLATE_EMPTY) - public void testAspectSettingsConfigCopyPasteAcrossAspects(GameTestHelper helper) { - PartPos partPos = placePart(helper, POS_SOURCE, PartTypes.REDSTONE_WRITER); - // The pulse aspect has a strong power setting that the plain aspect also has, - // and a pulse length setting that it does not have. - setAspectProperty(partPos, Aspects.Write.Redstone.BOOLEAN_PULSE, - AspectWriteBuilders.Redstone.PROP_STRONG_POWER, ValueTypeBoolean.ValueBoolean.of(true)); - setAspectProperty(partPos, Aspects.Write.Redstone.BOOLEAN_PULSE, - AspectWriteBuilders.Redstone.PROP_PULSE_LENGTH, ValueTypeInteger.ValueInteger.of(5)); + public void testWrenchConfigSubsetModeAcrossPartTypes(GameTestHelper helper) { + PartPos source = placePart(helper, POS_SOURCE, PartTypes.REDSTONE_WRITER); + PartPos target = placePart(helper, POS_TARGET, PartTypes.REDSTONE_READER); + configurePart(helper, source, Vec3i.ZERO); Player player = helper.makeMockPlayer(GameType.SURVIVAL); - player.setItemInHand(InteractionHand.MAIN_HAND, createWrench(ItemWrench.Mode.CONFIG)); - openAspectSettings(player, partPos, Aspects.Write.Redstone.BOOLEAN_PULSE) - .onButtonClick(ContainerAspectSettings.BUTTON_CONFIG_COPY); - openAspectSettings(player, partPos, Aspects.Write.Redstone.BOOLEAN) - .onButtonClick(ContainerAspectSettings.BUTTON_CONFIG_PASTE); + ItemStack wrench = createWrench(ItemWrench.Mode.CONFIG_SETTINGS); + clickPart(helper, player, wrench, source, true); + clickPart(helper, player, wrench, target, false); helper.succeedWhen(() -> { - helper.assertValueEqual( - getEffectiveAspectProperty(partPos, Aspects.Write.Redstone.BOOLEAN, - AspectWriteBuilders.Redstone.PROP_STRONG_POWER), - ValueTypeBoolean.ValueBoolean.of(true), "The shared setting was not pasted"); - helper.assertValueEqual( - getEffectiveAspectProperty(partPos, Aspects.Write.Redstone.BOOLEAN_PULSE, - AspectWriteBuilders.Redstone.PROP_PULSE_LENGTH), - ValueTypeInteger.ValueInteger.of(5), "The copied aspect was changed"); + // Unlike a whole configuration, a single section can be pasted onto another part type + helper.assertValueEqual(((IPartType) partType(target)).getUpdateInterval(partState(target)), 40, + "Update interval was not pasted"); + helper.assertValueEqual(((IPartType) partType(target)).getPriority(partState(target)), 3, + "Priority was not pasted"); }); } @@ -407,9 +395,47 @@ public void testWrenchConfigSneakDoesNotRemovePart(GameTestHelper helper) { }); } + @GameTest(template = TEMPLATE_EMPTY) + public void testWrenchConfigCopyOnlyStoresNonDefaultValues(GameTestHelper helper) { + // A freshly placed part has nothing configured, so there is nothing to copy + PartPos source = placePart(helper, POS_SOURCE, PartTypes.REDSTONE_WRITER); + + Player player = helper.makeMockPlayer(GameType.SURVIVAL); + ItemStack wrench = createWrench(ItemWrench.Mode.CONFIG); + clickPart(helper, player, wrench, source, true); + + helper.succeedWhen(() -> { + helper.assertTrue(PartConfigHelpers.getSnapshot(helper.getLevel().registryAccess(), wrench).isEmpty(), + "A default part was copied into the wrench"); + }); + } + + @GameTest(template = TEMPLATE_EMPTY) + public void testWrenchConfigPasteKeepsUntouchedSettings(GameTestHelper helper) { + PartPos source = placePart(helper, POS_SOURCE, PartTypes.REDSTONE_WRITER); + PartPos target = placePart(helper, POS_TARGET, PartTypes.REDSTONE_WRITER); + // Only the update interval is non-default on the source part + ((IPartType) partType(source)).setUpdateInterval(partState(source), 40); + // While the target part has a priority that the source part does not have + partState(target).setPriority(7); + + Player player = helper.makeMockPlayer(GameType.SURVIVAL); + ItemStack wrench = createWrench(ItemWrench.Mode.CONFIG); + clickPart(helper, player, wrench, source, true); + clickPart(helper, player, wrench, target, false); + + helper.succeedWhen(() -> { + helper.assertValueEqual(((IPartType) partType(target)).getUpdateInterval(partState(target)), 40, + "Update interval was not pasted"); + helper.assertValueEqual(((IPartType) partType(target)).getPriority(partState(target)), 7, + "The priority of the target part was reset by a setting that was never configured"); + }); + } + @GameTest(template = TEMPLATE_EMPTY) public void testWrenchConfigSurvivesModeCycling(GameTestHelper helper) { PartPos partPos = placePart(helper, POS_SOURCE, PartTypes.REDSTONE_WRITER); + configurePart(helper, partPos, Vec3i.ZERO); Player player = helper.makeMockPlayer(GameType.SURVIVAL); ItemStack wrench = createWrench(ItemWrench.Mode.CONFIG); diff --git a/src/main/java/org/cyclops/integrateddynamics/api/part/IPartState.java b/src/main/java/org/cyclops/integrateddynamics/api/part/IPartState.java index 6c5f7591fbc..9e43834afae 100644 --- a/src/main/java/org/cyclops/integrateddynamics/api/part/IPartState.java +++ b/src/main/java/org/cyclops/integrateddynamics/api/part/IPartState.java @@ -8,6 +8,7 @@ import net.minecraft.world.Container; import net.minecraft.world.item.ItemStack; import org.cyclops.integrateddynamics.api.evaluate.variable.IValue; +import org.cyclops.integrateddynamics.GeneralConfig; import org.cyclops.integrateddynamics.api.evaluate.variable.ValueDeseralizationContext; import org.cyclops.integrateddynamics.api.network.INetwork; import org.cyclops.integrateddynamics.api.network.INetworkElement; @@ -68,6 +69,14 @@ public interface IPartState

{ */ public int getUpdateInterval(); + /** + * @return The tick interval that this part has before a player configures it. + */ + // TODO: make non-default in nextmajor + public default int getDefaultUpdateInterval() { + return GeneralConfig.defaultPartUpdateFreq; + } + /** * Set the priority of this part in the network. * @deprecated Should only be called from {@link org.cyclops.integrateddynamics.api.network.INetwork#setPriorityAndChannel(INetworkElement, int, int)}}! diff --git a/src/main/java/org/cyclops/integrateddynamics/block/shapes/VoxelShapeComponentsFactoryHandlerParts.java b/src/main/java/org/cyclops/integrateddynamics/block/shapes/VoxelShapeComponentsFactoryHandlerParts.java index 25fd9a448b0..e20a8acbd27 100644 --- a/src/main/java/org/cyclops/integrateddynamics/block/shapes/VoxelShapeComponentsFactoryHandlerParts.java +++ b/src/main/java/org/cyclops/integrateddynamics/block/shapes/VoxelShapeComponentsFactoryHandlerParts.java @@ -110,7 +110,7 @@ public BakedModel getBreakingBaseModel(Level world, BlockPos pos) { public InteractionResult onBlockActivated(BlockState state, Level world, BlockPos blockPos, Player player, InteractionHand hand, BlockRayTraceResultComponent hit) { ItemStack heldItem = player.getItemInHand(hand); if(heldItem.getItem() instanceof ItemWrench itemWrench - && itemWrench.getMode(heldItem) == ItemWrench.Mode.CONFIG + && itemWrench.getMode(heldItem).isConfig() && player.isSecondaryUseActive()) { // Copy the configuration of this part into the wrench, instead of removing the part itemWrench.copyPartConfig(heldItem, player, PartPos.of(world, blockPos, direction)); diff --git a/src/main/java/org/cyclops/integrateddynamics/core/client/gui/container/ContainerScreenAspectSettings.java b/src/main/java/org/cyclops/integrateddynamics/core/client/gui/container/ContainerScreenAspectSettings.java index 71860be36c5..acc98f3e825 100644 --- a/src/main/java/org/cyclops/integrateddynamics/core/client/gui/container/ContainerScreenAspectSettings.java +++ b/src/main/java/org/cyclops/integrateddynamics/core/client/gui/container/ContainerScreenAspectSettings.java @@ -117,14 +117,6 @@ public void init() { refreshButtonEnabled(); } }, true)); - addRenderableWidget(new ButtonText(leftPos + 141, topPos + 109, 12, 12, - Component.translatable("gui.integrateddynamics.aspectsettings.config.copy"), Component.literal("C"), - createServerPressable(ContainerAspectSettings.BUTTON_CONFIG_COPY, (button) -> { - saveSetting(); - }), true)); - addRenderableWidget(new ButtonText(leftPos + 155, topPos + 109, 12, 12, - Component.translatable("gui.integrateddynamics.aspectsettings.config.paste"), Component.literal("P"), - createServerPressable(ContainerAspectSettings.BUTTON_CONFIG_PASTE, (button) -> {}), true)); refreshButtonEnabled(); setActiveProperty(activePropertyIndex); diff --git a/src/main/java/org/cyclops/integrateddynamics/core/client/gui/container/ContainerScreenPartSettings.java b/src/main/java/org/cyclops/integrateddynamics/core/client/gui/container/ContainerScreenPartSettings.java index 496a0bf0335..03504a68a65 100644 --- a/src/main/java/org/cyclops/integrateddynamics/core/client/gui/container/ContainerScreenPartSettings.java +++ b/src/main/java/org/cyclops/integrateddynamics/core/client/gui/container/ContainerScreenPartSettings.java @@ -138,14 +138,6 @@ public void init() { addRenderableWidget(buttonSave = new ButtonText(this.leftPos + 178, this.topPos + 8, font.width(save.getVisualOrderText()) + 6, 16, save, save, createServerPressable(ContainerPartSettings.BUTTON_SAVE, b -> onSave()), true)); - addRenderableWidget(new ButtonText(this.leftPos + 178, this.topPos + 30, 14, 14, - Component.translatable("gui.integrateddynamics.partsettings.config.copy"), Component.literal("C"), - // Persist any pending edits first, so that they end up in the wrench as well - createServerPressable(ContainerPartSettings.BUTTON_CONFIG_COPY, b -> onSave()), true)); - addRenderableWidget(new ButtonText(this.leftPos + 194, this.topPos + 30, 14, 14, - Component.translatable("gui.integrateddynamics.partsettings.config.paste"), Component.literal("P"), - createServerPressable(ContainerPartSettings.BUTTON_CONFIG_PASTE, b -> {}), true)); - this.refreshValues(); } diff --git a/src/main/java/org/cyclops/integrateddynamics/core/helper/PartConfigHelpers.java b/src/main/java/org/cyclops/integrateddynamics/core/helper/PartConfigHelpers.java index d1c84236737..f822f0c3d5d 100644 --- a/src/main/java/org/cyclops/integrateddynamics/core/helper/PartConfigHelpers.java +++ b/src/main/java/org/cyclops/integrateddynamics/core/helper/PartConfigHelpers.java @@ -4,6 +4,7 @@ import com.google.common.collect.Maps; import net.minecraft.core.HolderLookup; import net.minecraft.core.NonNullList; +import net.minecraft.core.Vec3i; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; @@ -80,12 +81,10 @@ public static PartConfigSnapshot snapshot(ValueDeseralizationContext valueDesera IPartType partType, IPartState state, Set sections) { Optional partSettings = Optional.empty(); if (sections.contains(PartConfigSection.PART_SETTINGS)) { - partSettings = Optional.of(new PartConfigSnapshot.PartSettings( - partType.getUpdateInterval(state), - partType.getPriority(state), - partType.getChannel(state), - Optional.ofNullable(partType.getTargetSideOverride(state)), - partType.getTargetOffset(state))); + PartConfigSnapshot.PartSettings settings = snapshotPartSettings(partType, state); + if (!settings.isEmpty()) { + partSettings = Optional.of(settings); + } } Map aspectProperties = Maps.newLinkedHashMap(); @@ -94,7 +93,10 @@ public static PartConfigSnapshot snapshot(ValueDeseralizationContext valueDesera if (aspect.hasProperties()) { IAspectProperties properties = state.getAspectProperties(aspect); if (properties != null) { - aspectProperties.put(aspect.getUniqueName(), properties.toNBT(valueDeseralizationContext)); + IAspectProperties modified = filterNonDefaultProperties(properties, aspect); + if (countPropertyTypes(modified) > 0) { + aspectProperties.put(aspect.getUniqueName(), modified.toNBT(valueDeseralizationContext)); + } } } } @@ -126,6 +128,41 @@ public static PartConfigSnapshot snapshot(ValueDeseralizationContext valueDesera partSettings, aspectProperties, variableCards); } + /** + * @param partType A part type. + * @param state A part state. + * @return The settings of the given part that differ from the defaults of a freshly placed part. + */ + @SuppressWarnings("unchecked") + protected static PartConfigSnapshot.PartSettings snapshotPartSettings(IPartType partType, IPartState state) { + int updateInterval = partType.getUpdateInterval(state); + int defaultUpdateInterval = Math.max(partType.getMinimumUpdateInterval(state), state.getDefaultUpdateInterval()); + Vec3i targetOffset = partType.getTargetOffset(state); + return new PartConfigSnapshot.PartSettings( + updateInterval == defaultUpdateInterval ? Optional.empty() : Optional.of(updateInterval), + partType.getPriority(state) == 0 ? Optional.empty() : Optional.of(partType.getPriority(state)), + partType.getChannel(state) == 0 ? Optional.empty() : Optional.of(partType.getChannel(state)), + Optional.ofNullable(partType.getTargetSideOverride(state)), + targetOffset.equals(Vec3i.ZERO) ? Optional.empty() : Optional.of(targetOffset)); + } + + /** + * @param properties The properties of an aspect. + * @param aspect The aspect that they belong to. + * @return Only the properties whose value differs from the aspect's default. + */ + @SuppressWarnings({"unchecked", "deprecation"}) + protected static IAspectProperties filterNonDefaultProperties(IAspectProperties properties, IAspect aspect) { + IAspectProperties defaultProperties = aspect.getDefaultProperties(); + IAspectProperties modified = new AspectProperties(); + for (IAspectPropertyTypeInstance propertyType : properties.getTypes()) { + if (!properties.getValue(propertyType).equals(defaultProperties.getValue(propertyType))) { + modified.setValue(propertyType, properties.getValue(propertyType)); + } + } + return modified; + } + /** * Paste the given snapshot onto the given part. * @param valueDeseralizationContext A value deserialization context. @@ -158,21 +195,31 @@ public static PartConfigApplyResult apply(ValueDeseralizationContext valueDesera return result; } + /** + * Only the settings that the snapshot actually holds are applied, + * so pasting never resets a setting that was left at its default on the copied part. + */ @SuppressWarnings("unchecked") protected static void applyPartSettings(@Nullable INetwork network, PartTarget target, IPartType partType, IPartState state, PartConfigSnapshot.PartSettings settings, PartConfigApplyResult result) { - partType.setUpdateInterval(state, Math.max(partType.getMinimumUpdateInterval(state), settings.updateInterval())); - partType.setTargetSideOverride(state, settings.targetSide().orElse(null)); - if (!partType.setTargetOffset(state, target.getCenter(), settings.targetOffset())) { - result.setOffsetFailed(true); - } - if (network != null) { - network.setPriorityAndChannel(new PartNetworkElement(partType, target.getCenter()), - settings.priority(), settings.channel()); - } else { - state.setPriority(settings.priority()); - state.setChannel(settings.channel()); + settings.updateInterval().ifPresent(updateInterval -> partType.setUpdateInterval(state, + Math.max(partType.getMinimumUpdateInterval(state), updateInterval))); + settings.targetSide().ifPresent(targetSide -> partType.setTargetSideOverride(state, targetSide)); + settings.targetOffset().ifPresent(targetOffset -> { + if (!partType.setTargetOffset(state, target.getCenter(), targetOffset)) { + result.setOffsetFailed(true); + } + }); + if (settings.priority().isPresent() || settings.channel().isPresent()) { + int priority = settings.priority().orElseGet(() -> partType.getPriority(state)); + int channel = settings.channel().orElseGet(() -> partType.getChannel(state)); + if (network != null) { + network.setPriorityAndChannel(new PartNetworkElement(partType, target.getCenter()), priority, channel); + } else { + state.setPriority(priority); + state.setChannel(channel); + } } result.setPartSettingsApplied(true); state.markDirty(); diff --git a/src/main/java/org/cyclops/integrateddynamics/core/inventory/container/ContainerAspectSettings.java b/src/main/java/org/cyclops/integrateddynamics/core/inventory/container/ContainerAspectSettings.java index 1893ce0f6a2..1bdf5a47066 100644 --- a/src/main/java/org/cyclops/integrateddynamics/core/inventory/container/ContainerAspectSettings.java +++ b/src/main/java/org/cyclops/integrateddynamics/core/inventory/container/ContainerAspectSettings.java @@ -33,19 +33,14 @@ import org.cyclops.integrateddynamics.api.part.aspect.property.IAspectPropertyTypeInstance; import org.cyclops.integrateddynamics.core.evaluate.variable.ValueHelpers; import org.cyclops.integrateddynamics.core.helper.NetworkHelpers; -import org.cyclops.integrateddynamics.core.helper.PartConfigHelpers; import org.cyclops.integrateddynamics.core.helper.PartHelpers; import org.cyclops.integrateddynamics.core.inventory.container.slot.SlotVariable; import org.cyclops.integrateddynamics.core.network.event.VariableContentsUpdatedEvent; -import org.cyclops.integrateddynamics.core.part.PartConfigApplyResult; -import org.cyclops.integrateddynamics.core.part.PartConfigSection; -import org.cyclops.integrateddynamics.core.part.PartConfigSnapshot; import org.cyclops.integrateddynamics.core.part.PartStateAspectVariablesHandler; import org.cyclops.integrateddynamics.core.part.aspect.AspectRegistry; import javax.annotation.Nullable; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -56,8 +51,6 @@ public class ContainerAspectSettings extends InventoryContainer { public static final String BUTTON_EXIT = "button_exit"; - public static final String BUTTON_CONFIG_COPY = "button_config_copy"; - public static final String BUTTON_CONFIG_PASTE = "button_config_paste"; public static final int BUTTON_SETTINGS = 1; private static final int PAGE_SIZE = 3; @@ -132,142 +125,6 @@ public ContainerAspectSettings(int id, Inventory playerInventory, Container inve PartHelpers.openContainerPart((ServerPlayer) playerInventory.player, getTarget().get().getCenter(), getPartType().get()); } }); - putButtonAction(ContainerAspectSettings.BUTTON_CONFIG_COPY, (s, containerExtended) -> { - if (!world.isClientSide()) { - copyConfig(); - } - }); - putButtonAction(ContainerAspectSettings.BUTTON_CONFIG_PASTE, (s, containerExtended) -> { - if (!world.isClientSide()) { - pasteConfig(); - } - }); - } - - /** - * Copy the settings of this aspect into the Wrench of the player. - */ - protected void copyConfig() { - ItemStack wrench = PartConfigHelpers.findWrench(player).orElse(ItemStack.EMPTY); - if (wrench.isEmpty()) { - player.displayClientMessage(PartConfigHelpers.getNoWrenchMessage(), true); - return; - } - PartConfigHelpers.setSnapshot(world.registryAccess(), wrench, snapshotAspect()); - player.displayClientMessage(Component.translatable("gui.integrateddynamics.config.copied"), true); - } - - /** - * @return A snapshot that only holds the settings and setting variables of this aspect. - */ - protected PartConfigSnapshot snapshotAspect() { - IPartType partType = getPartType().get(); - IPartState partState = getPartState().get(); - ValueDeseralizationContext valueDeseralizationContext = ValueDeseralizationContext.of(world); - - saveVariablesInventory(partState); - String inventoryName = PartStateAspectVariablesHandler.getInventoryName(aspect); - List variableCards = Lists.newArrayList(); - for (int slot = 0; slot < this.variablesInventory.getContainerSize(); slot++) { - ItemStack itemStack = this.variablesInventory.getItem(slot); - if (!itemStack.isEmpty()) { - variableCards.add(new PartConfigSnapshot.VariableCard(inventoryName, slot, itemStack.copy())); - } - } - - return new PartConfigSnapshot(PartConfigSnapshot.VERSION, partType.getUniqueName(), Optional.empty(), - Map.of(aspect.getUniqueName(), - aspect.getStaticProperties(partType, getTarget().get(), partState).toNBT(valueDeseralizationContext)), - variableCards); - } - - /** - * Paste the aspect settings inside the Wrench of the player onto this aspect. - * - * Settings are matched by property type instead of by aspect, - * so settings can also be copied between different aspects. - */ - protected void pasteConfig() { - ItemStack wrench = PartConfigHelpers.findWrench(player).orElse(ItemStack.EMPTY); - if (wrench.isEmpty()) { - player.displayClientMessage(PartConfigHelpers.getNoWrenchMessage(), true); - return; - } - PartConfigSnapshot snapshot = PartConfigHelpers.getSnapshot(world.registryAccess(), wrench).orElse(null); - if (snapshot == null || (!snapshot.hasSection(PartConfigSection.ASPECT_PROPERTIES) - && !snapshot.hasSection(PartConfigSection.VARIABLE_CARDS))) { - player.displayClientMessage(Component.translatable("item.integrateddynamics.wrench.mode.config.empty"), true); - return; - } - - IPartType partType = getPartType().get(); - PartTarget target = getTarget().get(); - IPartState partState = getPartState().get(); - ValueDeseralizationContext valueDeseralizationContext = ValueDeseralizationContext.of(world); - PartConfigApplyResult result = new PartConfigApplyResult(); - - // Persist any pending slot changes, as the variable slots are rewritten outside of this container below - saveVariablesInventory(partState); - - // Apply all settings of all copied aspects that this aspect also declares - IAspectProperties properties = aspect.getStaticProperties(partType, target, partState).clone(); - int applied = 0; - for (CompoundTag propertiesTag : snapshot.aspectProperties().values()) { - applied += PartConfigHelpers.applyPropertiesByType( - PartConfigHelpers.readProperties(valueDeseralizationContext, propertiesTag), properties, aspect); - } - if (applied > 0) { - aspect.setProperties(partType, target, partState, properties); - } - result.addAppliedProperties(applied); - - // Move the copied setting variables to the slots of the matching properties of this aspect - PartConfigHelpers.applyVariableCards(valueDeseralizationContext, target, partType, partState, - remapVariableCards(snapshot, result), player, result); - - // Reload the variable slots, as they were changed outside of this container - this.variablesInventory.clearContent(); - partState.loadInventoryNamed(PartStateAspectVariablesHandler.getInventoryName(aspect), this.variablesInventory); - this.dirtyInv = false; - - player.displayClientMessage(result.getMessage(), true); - - // Show the pasted values in the gui - initializeValues(); - - // Changing the settings might cause some erroring variables to become valid again, so trigger an update. - NetworkHelpers.getNetwork(target.getCenter()) - .ifPresent(network -> network.getEventBus().post(new VariableContentsUpdatedEvent(network))); - } - - /** - * Rewrite the copied setting variables so that they end up in the slot of the matching property of this aspect. - * @param snapshot A configuration snapshot. - * @param result The outcome to report skipped cards into. - * @return The rewritten cards. - */ - protected List remapVariableCards(PartConfigSnapshot snapshot, - PartConfigApplyResult result) { - String inventoryName = PartStateAspectVariablesHandler.getInventoryName(aspect); - List cards = Lists.newArrayList(); - for (PartConfigSnapshot.VariableCard card : snapshot.variableCards()) { - IAspect sourceAspect = PartStateAspectVariablesHandler.getAspectByInventoryName(card.inventoryName()); - if (sourceAspect == null) { - continue; - } - List sourcePropertyTypes = PartStateAspectVariablesHandler.getPropertyTypes(sourceAspect); - if (card.slot() >= sourcePropertyTypes.size()) { - continue; - } - int slot = this.propertyTypes.indexOf(sourcePropertyTypes.get(card.slot())); - if (slot < 0) { - // This aspect does not have the property that the card was configuring - result.addCardsSkipped(1); - continue; - } - cards.add(new PartConfigSnapshot.VariableCard(inventoryName, slot, card.itemStack())); - } - return cards; } public BiMap getPropertyIds() { diff --git a/src/main/java/org/cyclops/integrateddynamics/core/inventory/container/ContainerPartSettings.java b/src/main/java/org/cyclops/integrateddynamics/core/inventory/container/ContainerPartSettings.java index a65b327e4f2..57cb5556dbd 100644 --- a/src/main/java/org/cyclops/integrateddynamics/core/inventory/container/ContainerPartSettings.java +++ b/src/main/java/org/cyclops/integrateddynamics/core/inventory/container/ContainerPartSettings.java @@ -1,41 +1,31 @@ package org.cyclops.integrateddynamics.core.inventory.container; -import com.google.common.collect.Sets; import net.minecraft.core.Direction; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.RegistryFriendlyByteBuf; -import net.minecraft.network.chat.Component; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.Container; import net.minecraft.world.SimpleContainer; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Player; import net.minecraft.world.inventory.MenuType; -import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; import org.cyclops.cyclopscore.datastructure.DimPos; import org.cyclops.cyclopscore.helper.ValueNotifierHelpers; import org.cyclops.cyclopscore.inventory.container.InventoryContainer; import org.cyclops.integrateddynamics.RegistryEntries; import org.cyclops.integrateddynamics.api.PartStateException; -import org.cyclops.integrateddynamics.api.evaluate.variable.ValueDeseralizationContext; import org.cyclops.integrateddynamics.api.network.INetwork; -import org.cyclops.integrateddynamics.api.network.IPartNetwork; import org.cyclops.integrateddynamics.api.part.IPartContainer; import org.cyclops.integrateddynamics.api.part.IPartState; import org.cyclops.integrateddynamics.api.part.IPartType; import org.cyclops.integrateddynamics.api.part.PartTarget; import org.cyclops.integrateddynamics.core.helper.NetworkHelpers; -import org.cyclops.integrateddynamics.core.helper.PartConfigHelpers; import org.cyclops.integrateddynamics.core.helper.PartHelpers; import org.cyclops.integrateddynamics.core.network.PartNetworkElement; -import org.cyclops.integrateddynamics.core.part.PartConfigApplyResult; -import org.cyclops.integrateddynamics.core.part.PartConfigSection; -import org.cyclops.integrateddynamics.core.part.PartConfigSnapshot; import javax.annotation.Nullable; import java.util.Optional; -import java.util.Set; /** * Container for part settings. @@ -45,15 +35,8 @@ public class ContainerPartSettings extends InventoryContainer { public static final String BUTTON_SAVE = "button_save"; public static final String BUTTON_SETTINGS = "button_settings"; - public static final String BUTTON_CONFIG_COPY = "button_config_copy"; - public static final String BUTTON_CONFIG_PASTE = "button_config_paste"; private static final int PAGE_SIZE = 3; - /** - * The Wrench copies and pastes only the general part settings from this gui. - */ - private static final Set SECTIONS = Sets.immutableEnumSet(PartConfigSection.PART_SETTINGS); - private final PartTarget target; private final Optional partContainer; private final IPartType partType; @@ -96,56 +79,6 @@ public ContainerPartSettings(@Nullable MenuType type, int id, Inventory playe PartHelpers.openContainerPart((ServerPlayer) player, target.getCenter(), getPartType()); } }); - putButtonAction(ContainerPartSettings.BUTTON_CONFIG_COPY, (s, containerExtended) -> { - if(!world.isClientSide()) { - copyConfig(); - } - }); - putButtonAction(ContainerPartSettings.BUTTON_CONFIG_PASTE, (s, containerExtended) -> { - if(!world.isClientSide()) { - pasteConfig(); - } - }); - } - - /** - * Copy the settings of this part into the Wrench of the player. - */ - protected void copyConfig() { - ItemStack wrench = PartConfigHelpers.findWrench(player).orElse(ItemStack.EMPTY); - if (wrench.isEmpty()) { - player.displayClientMessage(PartConfigHelpers.getNoWrenchMessage(), true); - return; - } - PartConfigSnapshot snapshot = getPartType() - .snapshotConfig(ValueDeseralizationContext.of(world), getPartState(), SECTIONS); - PartConfigHelpers.setSnapshot(world.registryAccess(), wrench, snapshot); - player.displayClientMessage(Component.translatable("gui.integrateddynamics.config.copied"), true); - } - - /** - * Paste the settings inside the Wrench of the player onto this part. - */ - protected void pasteConfig() { - ItemStack wrench = PartConfigHelpers.findWrench(player).orElse(ItemStack.EMPTY); - if (wrench.isEmpty()) { - player.displayClientMessage(PartConfigHelpers.getNoWrenchMessage(), true); - return; - } - PartConfigSnapshot snapshot = PartConfigHelpers.getSnapshot(world.registryAccess(), wrench).orElse(null); - if (snapshot == null || !snapshot.hasSection(PartConfigSection.PART_SETTINGS)) { - player.displayClientMessage(Component.translatable("item.integrateddynamics.wrench.mode.config.empty"), true); - return; - } - - INetwork network = NetworkHelpers.getNetwork(getTarget().getCenter()).orElse(null); - IPartNetwork partNetwork = NetworkHelpers.getPartNetwork(network).orElse(null); - PartConfigApplyResult result = getPartType().applyConfig(ValueDeseralizationContext.of(world), network, - partNetwork, getTarget(), getPartState(), snapshot, SECTIONS, player); - player.displayClientMessage(result.getMessage(), true); - - // Show the pasted values in the gui - initializeValues(); } public IPartType getPartType() { diff --git a/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigApplyResult.java b/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigApplyResult.java index dc7dafb4b2e..3feda113cd9 100644 --- a/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigApplyResult.java +++ b/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigApplyResult.java @@ -43,36 +43,50 @@ public void addCardsSkipped(int amount) { } /** - * @return All messages describing this outcome, which can be shown to the player. + * @return A single line summarising what was applied. */ - public List getMessages() { - List messages = Lists.newArrayList(); - messages.add(Component.translatable("item.integrateddynamics.wrench.mode.config.pasted", - this.appliedProperties, this.cardsPasted)); - if (this.offsetFailed) { - messages.add(Component.translatable("item.integrateddynamics.wrench.mode.offset.fail")); + public MutableComponent getMessage() { + // Only report what was actually applied, so that the counts never contradict what the player sees + List applied = Lists.newArrayList(); + if (this.partSettingsApplied) { + applied.add(Component.translatable("item.integrateddynamics.wrench.mode.config.pasted.part_settings")); } - if (this.cardsSkipped > 0) { - messages.add(Component.translatable("item.integrateddynamics.wrench.mode.config.cards_skipped", - this.cardsSkipped, this.missingBlanks)); + if (this.appliedProperties > 0) { + applied.add(Component.translatable("item.integrateddynamics.wrench.mode.config.pasted.aspect_properties", + this.appliedProperties)); + } + if (this.cardsPasted > 0) { + applied.add(Component.translatable("item.integrateddynamics.wrench.mode.config.pasted.variable_cards", + this.cardsPasted)); + } + if (applied.isEmpty()) { + return Component.translatable("item.integrateddynamics.wrench.mode.config.pasted.nothing"); + } + MutableComponent joined = Component.empty(); + for (int i = 0; i < applied.size(); i++) { + if (i > 0) { + joined.append(", "); + } + joined.append(applied.get(i)); } - return messages; + return Component.translatable("item.integrateddynamics.wrench.mode.config.pasted", joined); } /** - * @return All messages describing this outcome, joined into a single line. + * These are kept apart from {@link #getMessage()}, + * as the two together are too long for the single line that the action bar has. + * @return What did not go as the player intended, if anything. */ - public MutableComponent getMessage() { - MutableComponent message = Component.empty(); - boolean first = true; - for (MutableComponent part : getMessages()) { - if (!first) { - message.append(" "); - } - first = false; - message.append(part); + public List getWarnings() { + List warnings = Lists.newArrayList(); + if (this.offsetFailed) { + warnings.add(Component.translatable("item.integrateddynamics.wrench.mode.offset.fail")); + } + if (this.cardsSkipped > 0) { + warnings.add(Component.translatable("item.integrateddynamics.wrench.mode.config.cards_skipped", + this.cardsSkipped, this.missingBlanks)); } - return message; + return warnings; } } diff --git a/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigSnapshot.java b/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigSnapshot.java index c74439c7c49..ca6cb761ca8 100644 --- a/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigSnapshot.java +++ b/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigSnapshot.java @@ -23,11 +23,14 @@ * Only things that a player can configure are stored, * so no part id, max offset, enabled state, error messages or active aspect. * + * Only values that differ from the defaults of the copied part are stored, + * so that pasting only overwrites what was deliberately configured. + * * @param version The version of this snapshot format. * @param sourcePartType The unique name of the part type this snapshot was taken from. - * @param partSettings The general part settings, if the {@link PartConfigSection#PART_SETTINGS} section is included. - * @param aspectProperties The serialized aspect properties, by aspect unique name. - * @param variableCards All non-empty variable cards. + * @param partSettings The non-default general part settings. + * @param aspectProperties The serialized non-default aspect properties, by aspect unique name. + * @param variableCards All variable cards. * @author rubensworks */ public record PartConfigSnapshot(int version, @@ -46,11 +49,11 @@ public record PartConfigSnapshot(int version, public static final Codec CODEC_PART_SETTINGS = RecordCodecBuilder.create(builder -> builder .group( - Codec.INT.fieldOf("updateInterval").forGetter(PartSettings::updateInterval), - Codec.INT.fieldOf("priority").forGetter(PartSettings::priority), - Codec.INT.fieldOf("channel").forGetter(PartSettings::channel), + Codec.INT.optionalFieldOf("updateInterval").forGetter(PartSettings::updateInterval), + Codec.INT.optionalFieldOf("priority").forGetter(PartSettings::priority), + Codec.INT.optionalFieldOf("channel").forGetter(PartSettings::channel), Direction.CODEC.optionalFieldOf("targetSide").forGetter(PartSettings::targetSide), - Vec3i.CODEC.fieldOf("targetOffset").forGetter(PartSettings::targetOffset) + Vec3i.CODEC.optionalFieldOf("targetOffset").forGetter(PartSettings::targetOffset) ) .apply(builder, PartSettings::new)); @@ -76,6 +79,13 @@ public record PartConfigSnapshot(int version, ) .apply(builder, PartConfigSnapshot::new)); + /** + * @return The number of blank Variable Cards that pasting this snapshot needs at most. + */ + public int getRequiredBlankVariables() { + return variableCards().size(); + } + /** * @param section A config section. * @return If this snapshot holds anything for the given section. @@ -135,8 +145,16 @@ public static Optional fromNBT(HolderLookup.Provider provide * @param targetSide The overridden side of the target block, if any. * @param targetOffset The target position offset. */ - public record PartSettings(int updateInterval, int priority, int channel, - Optional targetSide, Vec3i targetOffset) { + public record PartSettings(Optional updateInterval, Optional priority, Optional channel, + Optional targetSide, Optional targetOffset) { + + /** + * @return If no setting at all is stored. + */ + public boolean isEmpty() { + return updateInterval().isEmpty() && priority().isEmpty() && channel().isEmpty() + && targetSide().isEmpty() && targetOffset().isEmpty(); + } } /** diff --git a/src/main/java/org/cyclops/integrateddynamics/core/part/PartStateBase.java b/src/main/java/org/cyclops/integrateddynamics/core/part/PartStateBase.java index b2087141435..02824e5d4ef 100644 --- a/src/main/java/org/cyclops/integrateddynamics/core/part/PartStateBase.java +++ b/src/main/java/org/cyclops/integrateddynamics/core/part/PartStateBase.java @@ -401,7 +401,8 @@ public void removeVolatileCapability(PartCapability capability) { volatileCapabilities.remove(capability); } - protected int getDefaultUpdateInterval() { + @Override + public int getDefaultUpdateInterval() { return GeneralConfig.defaultPartUpdateFreq; } diff --git a/src/main/java/org/cyclops/integrateddynamics/item/ItemWrench.java b/src/main/java/org/cyclops/integrateddynamics/item/ItemWrench.java index 2eb5a290939..a13eb7d30c8 100644 --- a/src/main/java/org/cyclops/integrateddynamics/item/ItemWrench.java +++ b/src/main/java/org/cyclops/integrateddynamics/item/ItemWrench.java @@ -1,6 +1,7 @@ package org.cyclops.integrateddynamics.item; import com.google.common.collect.Maps; +import com.google.common.collect.Sets; import io.netty.buffer.ByteBuf; import net.minecraft.ChatFormatting; import net.minecraft.core.BlockPos; @@ -46,6 +47,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; /** @@ -100,7 +102,7 @@ public InteractionResult onItemUseFirst(ItemStack stack, UseOnContext context) { return InteractionResult.FAIL; } } - case CONFIG -> { + case CONFIG, CONFIG_SETTINGS, CONFIG_ASPECTS, CONFIG_VARIABLES -> { // Let the click through to the part, so that its configuration can be copied return InteractionResult.PASS; } @@ -173,12 +175,18 @@ public void appendHoverText(ItemStack itemStack, Item.TooltipContext context, Li CompoundTag configTag = itemStack.get(RegistryEntries.DATACOMPONENT_WRENCH_PART_CONFIG); if (configTag != null && context.registries() != null) { PartConfigSnapshot.fromNBT(context.registries(), configTag).ifPresent(snapshot -> { + // Only the sections that the current mode pastes are relevant + Set sections = Sets.intersection(snapshot.getSections(), mode.getConfigSections()); list.add(Component.translatable("item.integrateddynamics.wrench.mode.config.source", Component.translatable(getSourcePartTypeName(snapshot))).withStyle(ChatFormatting.GRAY)); list.add(Component.translatable("item.integrateddynamics.wrench.mode.config.sections", - snapshot.getSections().stream() + sections.stream() .map(section -> Component.translatable(section.getTranslationKey()).getString()) .collect(Collectors.joining(", "))).withStyle(ChatFormatting.GRAY)); + if (sections.contains(PartConfigSection.VARIABLE_CARDS) && !snapshot.variableCards().isEmpty()) { + list.add(Component.translatable("item.integrateddynamics.wrench.mode.config.requires", + snapshot.variableCards().size()).withStyle(ChatFormatting.GOLD)); + } }); } list.add(Component.translatable(mode.getLabel() + ".info").withStyle(ChatFormatting.ITALIC, ChatFormatting.GRAY)); @@ -200,7 +208,7 @@ public

, S extends IPartState

> InteractionResult pe } return InteractionResult.SUCCESS; } - case CONFIG -> { + case CONFIG, CONFIG_SETTINGS, CONFIG_ASPECTS, CONFIG_VARIABLES -> { pastePartConfig(partType, partState, itemStack, player, center); return InteractionResult.SUCCESS; } @@ -241,7 +249,13 @@ public void copyPartConfig(ItemStack itemStack, Player player, PartPos center) { IPartType partType = partStateHolder.getPart(); Level level = center.getPos().getLevel(true); PartConfigSnapshot snapshot = ((IPartType) partType).snapshotConfig(ValueDeseralizationContext.of(level), - partStateHolder.getState(), PartConfigSection.ALL); + partStateHolder.getState(), getMode(itemStack).getConfigSections()); + if (snapshot.isEmpty()) { + // Nothing was configured on this part, so there is nothing to paste onto another one + itemStack.remove(RegistryEntries.DATACOMPONENT_WRENCH_PART_CONFIG); + player.displayClientMessage(Component.translatable("item.integrateddynamics.wrench.mode.config.nothing"), true); + return; + } PartConfigHelpers.setSnapshot(level.registryAccess(), itemStack, snapshot); player.displayClientMessage(Component.translatable("item.integrateddynamics.wrench.mode.config.copied", Component.translatable(partType.getTranslationKey())), true); @@ -262,12 +276,15 @@ protected void pastePartConfig(IPartType partType, IPartState partState return; } Level level = center.getPos().getLevel(true); + Set sections = getMode(itemStack).getConfigSections(); PartConfigSnapshot snapshot = PartConfigHelpers.getSnapshot(level.registryAccess(), itemStack).orElse(null); - if (snapshot == null) { + if (snapshot == null || sections.stream().noneMatch(snapshot::hasSection)) { player.displayClientMessage(Component.translatable("item.integrateddynamics.wrench.mode.config.empty"), true); return; } - if (!snapshot.sourcePartType().equals(partType.getUniqueName())) { + // Only a whole configuration has to come from the same part type, + // as the separate sections are matched by aspect and inventory instead. + if (getMode(itemStack).isFullConfig() && !snapshot.sourcePartType().equals(partType.getUniqueName())) { player.displayClientMessage(Component.translatable("item.integrateddynamics.wrench.mode.config.mismatch", Component.translatable(getSourcePartTypeName(snapshot))), true); return; @@ -277,8 +294,10 @@ protected void pastePartConfig(IPartType partType, IPartState partState IPartNetwork partNetwork = NetworkHelpers.getPartNetwork(network).orElse(null); PartTarget target = ((IPartType) partType).getTarget(center, partState); PartConfigApplyResult result = ((IPartType) partType).applyConfig(ValueDeseralizationContext.of(level), - network, partNetwork, target, partState, snapshot, PartConfigSection.ALL, player); + network, partNetwork, target, partState, snapshot, sections, player); player.displayClientMessage(result.getMessage(), true); + // Warnings go to the chat, as they are too long for the action bar and are worth keeping around + result.getWarnings().forEach(warning -> player.displayClientMessage(warning, false)); } protected static String getSourcePartTypeName(PartConfigSnapshot snapshot) { @@ -296,17 +315,30 @@ public static enum Mode implements StringRepresentable { DEFAULT("integrateddynamics:default", "item.integrateddynamics.wrench.mode.default"), OFFSET("integrateddynamics:offset", "item.integrateddynamics.wrench.mode.offset"), OFFSET_SIDE("integrateddynamics:offset_side", "item.integrateddynamics.wrench.mode.offset_side"), - CONFIG("integrateddynamics:config", "item.integrateddynamics.wrench.mode.config"); + CONFIG("integrateddynamics:config", "item.integrateddynamics.wrench.mode.config", + PartConfigSection.ALL), + CONFIG_SETTINGS("integrateddynamics:config_settings", "item.integrateddynamics.wrench.mode.config_settings", + Sets.immutableEnumSet(PartConfigSection.PART_SETTINGS)), + CONFIG_ASPECTS("integrateddynamics:config_aspects", "item.integrateddynamics.wrench.mode.config_aspects", + Sets.immutableEnumSet(PartConfigSection.ASPECT_PROPERTIES)), + CONFIG_VARIABLES("integrateddynamics:config_variables", "item.integrateddynamics.wrench.mode.config_variables", + Sets.immutableEnumSet(PartConfigSection.VARIABLE_CARDS)); public static final StringRepresentable.EnumCodec CODEC = net.minecraft.util.StringRepresentable.fromEnum(Mode::values); public static final StreamCodec STREAM_CODEC = ByteBufCodecs.idMapper(INT_MODES::get, Mode::ordinal); private final String name; private final String label; + private final Set configSections; private Mode(String name, String label) { + this(name, label, Set.of()); + } + + private Mode(String name, String label, Set configSections) { this.name = name; this.label = label; + this.configSections = configSections; NAMED_MODES.put(name, this); INT_MODES.put(ordinal(), this); } @@ -319,6 +351,29 @@ public String getLabel() { return label; } + /** + * @return The configuration sections that this mode copies and pastes. + * Empty for modes that do not deal with part configurations. + */ + public Set getConfigSections() { + return configSections; + } + + /** + * @return If this mode copies and pastes part configurations. + */ + public boolean isConfig() { + return !this.configSections.isEmpty(); + } + + /** + * @return If this mode copies and pastes the whole configuration of a part, + * which is only allowed between parts of the same type. + */ + public boolean isFullConfig() { + return this.configSections.equals(PartConfigSection.ALL); + } + @Override public String getSerializedName() { return getName(); diff --git a/src/main/java/org/cyclops/integrateddynamics/proxy/ClientProxy.java b/src/main/java/org/cyclops/integrateddynamics/proxy/ClientProxy.java index cdfc229171c..8129e9992bb 100644 --- a/src/main/java/org/cyclops/integrateddynamics/proxy/ClientProxy.java +++ b/src/main/java/org/cyclops/integrateddynamics/proxy/ClientProxy.java @@ -2,23 +2,29 @@ import com.mojang.blaze3d.platform.InputConstants; import net.minecraft.client.KeyMapping; +import net.minecraft.client.renderer.item.ItemProperties; import net.minecraft.client.renderer.texture.TextureAtlas; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; import net.neoforged.neoforge.client.event.ClientPlayerNetworkEvent; import net.neoforged.neoforge.client.event.RegisterKeyMappingsEvent; import net.neoforged.neoforge.client.event.TextureAtlasStitchedEvent; import net.neoforged.neoforge.client.settings.KeyConflictContext; import net.neoforged.neoforge.client.settings.KeyModifier; +import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent; import net.neoforged.neoforge.common.NeoForge; import org.cyclops.cyclopscore.client.key.IKeyRegistry; import org.cyclops.cyclopscore.init.ModBase; import org.cyclops.cyclopscore.proxy.ClientProxyComponent; import org.cyclops.integrateddynamics.IntegratedDynamics; import org.cyclops.integrateddynamics.Reference; +import org.cyclops.integrateddynamics.RegistryEntries; import org.cyclops.integrateddynamics.client.render.level.PartOffsetsOverlayRenderer; import org.cyclops.integrateddynamics.core.inventory.container.slot.SlotVariable; import org.cyclops.integrateddynamics.core.network.diagnostics.NetworkDataClient; import org.cyclops.integrateddynamics.core.network.diagnostics.NetworkDiagnosticsPartOverlayRenderer; import org.cyclops.integrateddynamics.core.network.diagnostics.http.DiagnosticsWebServer; +import org.cyclops.integrateddynamics.item.ItemWrench; import org.lwjgl.glfw.GLFW; /** @@ -43,9 +49,21 @@ public class ClientProxy extends ClientProxyComponent { public ClientProxy() { super(new CommonProxy()); IntegratedDynamics._instance.getModEventBus().addListener(this::onPostTextureStitch); + IntegratedDynamics._instance.getModEventBus().addListener(this::onClientSetup); NeoForge.EVENT_BUS.addListener(this::onPlayerLoggedOut); } + public void onClientSetup(FMLClientSetupEvent event) { + // Show the active wrench mode on the item, by picking a model variant for it + event.enqueueWork(() -> ItemProperties.register(RegistryEntries.ITEM_WRENCH.value(), + ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "wrench_mode"), + (itemStack, level, entity, seed) -> { + Item item = itemStack.getItem(); + // Values are divided by ten to fit the clamped 0 to 1 range that item properties have + return item instanceof ItemWrench itemWrench ? itemWrench.getMode(itemStack).ordinal() / 10F : 0F; + })); + } + @Override public ModBase getMod() { return IntegratedDynamics._instance; diff --git a/src/main/resources/assets/integrateddynamics/lang/en_us.json b/src/main/resources/assets/integrateddynamics/lang/en_us.json index e50a5620960..862f67c6f17 100644 --- a/src/main/resources/assets/integrateddynamics/lang/en_us.json +++ b/src/main/resources/assets/integrateddynamics/lang/en_us.json @@ -25,12 +25,6 @@ "gui.integrateddynamics.partsettings.priority": "Priority", "gui.integrateddynamics.partsettings.channel": "Energy Channel", "gui.integrateddynamics.partsettings.channel.disabledinfo": "Network energy consumption is disabled on this server.", - "gui.integrateddynamics.partsettings.config.copy": "Copy the settings into your Wrench", - "gui.integrateddynamics.partsettings.config.paste": "Paste the settings from your Wrench", - "gui.integrateddynamics.aspectsettings.config.copy": "Copy these aspect settings into your Wrench", - "gui.integrateddynamics.aspectsettings.config.paste": "Paste aspect settings from your Wrench", - "gui.integrateddynamics.config.copied": "Configuration was copied into the Wrench", - "gui.integrateddynamics.config.nowrench": "You need a Wrench to copy or paste a configuration", "gui.integrateddynamics.partsettings.side": "Target Side", "gui.integrateddynamics.partoffset.offsets": "Offset relative to the target position", "gui.integrateddynamics.partoffset.offsets.max": "Maximum offset: %s", @@ -166,12 +160,24 @@ "item.integrateddynamics.wrench.mode.offset_side.success": "New offset position and side have been set in part", "item.integrateddynamics.wrench.mode.offset_side.side": "Side: %s", "item.integrateddynamics.wrench.mode.config": "Copy Configuration", - "item.integrateddynamics.wrench.mode.config.info": "Shift + Right-click on a part to copy its configuration and Right-click on another part to paste it", + "item.integrateddynamics.wrench.mode.config_settings": "Copy Part Settings", + "item.integrateddynamics.wrench.mode.config_aspects": "Copy Aspect Settings", + "item.integrateddynamics.wrench.mode.config_variables": "Copy Variable Cards", + "item.integrateddynamics.wrench.mode.config.info": "Shift + Right-click on a part to copy its whole configuration and Right-click on another part of the same type to paste it", + "item.integrateddynamics.wrench.mode.config_settings.info": "Shift + Right-click on a part to copy its update interval, priority, channel, side and offset and Right-click on another part to paste them", + "item.integrateddynamics.wrench.mode.config_aspects.info": "Shift + Right-click on a part to copy its aspect settings and Right-click on another part to paste them", + "item.integrateddynamics.wrench.mode.config_variables.info": "Shift + Right-click on a part to copy its variable cards and Right-click on another part to paste them", "item.integrateddynamics.wrench.mode.config.copied": "Configuration of %s was copied into the Wrench", - "item.integrateddynamics.wrench.mode.config.pasted": "Pasted configuration: %s aspect settings and %s variable cards", - "item.integrateddynamics.wrench.mode.config.empty": "No configuration was copied into this Wrench yet", + "item.integrateddynamics.wrench.mode.config.pasted": "Pasted: %s", + "item.integrateddynamics.wrench.mode.config.pasted.nothing": "Nothing from this Wrench applies to this part", + "item.integrateddynamics.wrench.mode.config.pasted.part_settings": "part settings", + "item.integrateddynamics.wrench.mode.config.pasted.aspect_properties": "%s aspect settings", + "item.integrateddynamics.wrench.mode.config.pasted.variable_cards": "%s variable cards", + "item.integrateddynamics.wrench.mode.config.nothing": "This part has no configuration to copy, everything is still at its default", + "item.integrateddynamics.wrench.mode.config.requires": "Requires %s blank Variable Cards", + "item.integrateddynamics.wrench.mode.config.empty": "No matching configuration was copied into this Wrench yet", "item.integrateddynamics.wrench.mode.config.mismatch": "This configuration was copied from a different part: %s", - "item.integrateddynamics.wrench.mode.config.cards_skipped": "Skipped %s variable cards, %s more blank Variable Cards are needed", + "item.integrateddynamics.wrench.mode.config.cards_skipped": "Skipped %s variable cards, %s more blank Variable Cards needed", "item.integrateddynamics.wrench.mode.config.source": "Copied from: %s", "item.integrateddynamics.wrench.mode.config.sections": "Includes: %s", "item.integrateddynamics.wrench.mode.config.section.part_settings": "part settings", @@ -1859,7 +1865,7 @@ "info_book.integrateddynamics.manual.parts.settings.text3": "The Ticks/Operation allows you to configure the ticking frequency of this part. The higher, the slower it operates.", "info_book.integrateddynamics.manual.parts.settings.text4": "The Priority determines the order in which this part is executed within a single network tick.", "info_book.integrateddynamics.manual.parts.settings.text5": "The Energy Channel indicates the channel from which energy should be consumed when this part ticks. This is only applicable if energy consumption for networks is enabled.", - "info_book.integrateddynamics.manual.parts.settings.text6": "The configuration of a part can be copied to other parts using a &lWrench&r in &lCopy Configuration&r mode. Shift+right-clicking a part copies its settings, aspect settings and variable cards into the Wrench, and right-clicking another part of the same type pastes them. Each pasted variable card consumes one blank &lVariable Card&r from your inventory. The Settings and Aspect Settings screens also have buttons to copy and paste only the settings that they show.", + "info_book.integrateddynamics.manual.parts.settings.text6": "The configuration of a part can be copied to other parts using a &lWrench&r. Shift+right-clicking a part copies it into the Wrench, and right-clicking another part pastes it. The Wrench mode picks what is copied: &lCopy Configuration&r takes everything and only pastes onto a part of the same type, while &lCopy Part Settings&r, &lCopy Aspect Settings&r and &lCopy Variable Cards&r take one part of it and can be pasted onto any part. Only settings that you changed yourself are copied, so pasting never resets anything you left alone. Each pasted variable card consumes one blank &lVariable Card&r from your inventory.", "info_book.integrateddynamics.manual.parts.offsets": "Offsets", "info_book.integrateddynamics.manual.parts.offsets.text1": "By default, parts will target the direct neighboring position. However, when &lPart Enhancements&r are applied, the target position can be changed through the part's &lOffset&r screen.", diff --git a/src/main/resources/assets/integrateddynamics/models/item/wrench.json b/src/main/resources/assets/integrateddynamics/models/item/wrench.json index 8702014c24a..8a3cd8d4b30 100644 --- a/src/main/resources/assets/integrateddynamics/models/item/wrench.json +++ b/src/main/resources/assets/integrateddynamics/models/item/wrench.json @@ -2,5 +2,13 @@ "parent": "cyclopscore:item/flat", "textures": { "layer0": "integrateddynamics:item/wrench" - } -} \ No newline at end of file + }, + "overrides": [ + {"predicate": {"integrateddynamics:wrench_mode": 0.05}, "model": "integrateddynamics:item/wrench_offset"}, + {"predicate": {"integrateddynamics:wrench_mode": 0.15}, "model": "integrateddynamics:item/wrench_offset_side"}, + {"predicate": {"integrateddynamics:wrench_mode": 0.25}, "model": "integrateddynamics:item/wrench_config"}, + {"predicate": {"integrateddynamics:wrench_mode": 0.35}, "model": "integrateddynamics:item/wrench_config_settings"}, + {"predicate": {"integrateddynamics:wrench_mode": 0.45}, "model": "integrateddynamics:item/wrench_config_aspects"}, + {"predicate": {"integrateddynamics:wrench_mode": 0.55}, "model": "integrateddynamics:item/wrench_config_variables"} + ] +} diff --git a/src/main/resources/assets/integrateddynamics/models/item/wrench_config.json b/src/main/resources/assets/integrateddynamics/models/item/wrench_config.json new file mode 100644 index 00000000000..da9cad5e3e7 --- /dev/null +++ b/src/main/resources/assets/integrateddynamics/models/item/wrench_config.json @@ -0,0 +1,6 @@ +{ + "parent": "cyclopscore:item/flat", + "textures": { + "layer0": "integrateddynamics:item/wrench_config" + } +} diff --git a/src/main/resources/assets/integrateddynamics/models/item/wrench_config_aspects.json b/src/main/resources/assets/integrateddynamics/models/item/wrench_config_aspects.json new file mode 100644 index 00000000000..05f461a3c92 --- /dev/null +++ b/src/main/resources/assets/integrateddynamics/models/item/wrench_config_aspects.json @@ -0,0 +1,6 @@ +{ + "parent": "cyclopscore:item/flat", + "textures": { + "layer0": "integrateddynamics:item/wrench_config_aspects" + } +} diff --git a/src/main/resources/assets/integrateddynamics/models/item/wrench_config_settings.json b/src/main/resources/assets/integrateddynamics/models/item/wrench_config_settings.json new file mode 100644 index 00000000000..fb6190d0540 --- /dev/null +++ b/src/main/resources/assets/integrateddynamics/models/item/wrench_config_settings.json @@ -0,0 +1,6 @@ +{ + "parent": "cyclopscore:item/flat", + "textures": { + "layer0": "integrateddynamics:item/wrench_config_settings" + } +} diff --git a/src/main/resources/assets/integrateddynamics/models/item/wrench_config_variables.json b/src/main/resources/assets/integrateddynamics/models/item/wrench_config_variables.json new file mode 100644 index 00000000000..46f4d157bec --- /dev/null +++ b/src/main/resources/assets/integrateddynamics/models/item/wrench_config_variables.json @@ -0,0 +1,6 @@ +{ + "parent": "cyclopscore:item/flat", + "textures": { + "layer0": "integrateddynamics:item/wrench_config_variables" + } +} diff --git a/src/main/resources/assets/integrateddynamics/models/item/wrench_offset.json b/src/main/resources/assets/integrateddynamics/models/item/wrench_offset.json new file mode 100644 index 00000000000..f8b178da126 --- /dev/null +++ b/src/main/resources/assets/integrateddynamics/models/item/wrench_offset.json @@ -0,0 +1,6 @@ +{ + "parent": "cyclopscore:item/flat", + "textures": { + "layer0": "integrateddynamics:item/wrench_offset" + } +} diff --git a/src/main/resources/assets/integrateddynamics/models/item/wrench_offset_side.json b/src/main/resources/assets/integrateddynamics/models/item/wrench_offset_side.json new file mode 100644 index 00000000000..96cfe0a7a3c --- /dev/null +++ b/src/main/resources/assets/integrateddynamics/models/item/wrench_offset_side.json @@ -0,0 +1,6 @@ +{ + "parent": "cyclopscore:item/flat", + "textures": { + "layer0": "integrateddynamics:item/wrench_offset_side" + } +} diff --git a/src/main/resources/assets/integrateddynamics/textures/item/wrench_config.png b/src/main/resources/assets/integrateddynamics/textures/item/wrench_config.png new file mode 100644 index 0000000000000000000000000000000000000000..b972170008d6b10b1f573f3fa7208fc04909b434 GIT binary patch literal 545 zcmV++0^a?JP)>qnCn_&~=6PMHkjZ1kzpCQ8&^c z5H!%hhz*;gjocq}qUqT@XJ=dP?P7FbJ6MnpeDHpJ&-*^#^Snw#*a;jv>INGAMc8}& zwg9M7k+slURsXK)EsUk6ta^0lxpWNN=li1}8V`0TfQpglSY(D-qii2AmYSm1a|D1Z zw}$1(t8wo21#uniRuw&G@hV21+-y~-CWBNmY1LcCCa>cwf3DJcwikDxhtg77ercM; zvxWF3JPAN)HOq1|QdP`rXy3kyxe&ImOhjtVoR((KrGA=xe(?@Jl(xYU8BeUr#n8O8 z4UR~Y&o38tyDYvfL{IP?OEb!%)1_T$$Q(s`hnjl2mz zbyltpDCfR*01B}%W-P*~r_ZdO1^{=Lp9W70XJk#h`C6?Ds^an4LbEz7vnTTS> zBHVp^r{+VJh0Eu<#p8CO8D-Wti}*%gs>#p?ipe;Q-cAbhUuaE7Yy2)|vdS9J6X+7% zEK{jK(tARcGCy#*TvSXGfa%W@+&mtT^Ak4!nCW}RVkWDs45sH6RMpN&jF0c_;cWxk j7XF6AhuXKd@W1#C8p-QjB%iu<00000NkvXXu0mjfbF2SE literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/integrateddynamics/textures/item/wrench_config_aspects.png b/src/main/resources/assets/integrateddynamics/textures/item/wrench_config_aspects.png new file mode 100644 index 0000000000000000000000000000000000000000..41215969f6ee3a9c8665115be81a6ec2a882d91b GIT binary patch literal 548 zcmV+<0^9wGP)BuCg`H*MlS^;p_^_hyp5s@>mmZ_uIr$i zXb=b*WMIUG&e2xx4?5B8+x)g~TYjgD(Ffay1^K`W=kPw~ocDRot3-tDz_G1r!1*u2 z?km>?Kvl}DgskEKy#}n+MvXqfu znr88ACccVH0#I4au^fxmCKfcb`7dHDMC>aQk-9UhW!QCYfM$O{d?WXyeP~oBlB;qy zJSXi#qtfgT$eEpPi*Ga08@fzDONd^s65rfm8;HZ9PGsYJ$d`bIHMWKZ2xv*^+SkI* zf&o8tRxS=I*PadlO7RFrJj(IMPpldzfTufv)7#2PSrgyh7V>(9<#hx7&NJm&su^n} zV;J!$Hy_-ndypmJ{OMltdR%D5D(f3%{A176WcVHBRDvd77p1w+v}Iy-dKa@fWi{vv zc8hLQF-=JMj;c!bI}W!S(=Y&-{`8isJwchjTl*(H55KXP%_+-+>DdKUlXHyX`!{y; mwt#I5f5X889a~HIU;G9fS?gUKA`fK%00003wlHTL0ocV#n?Zz|=0H!~`<$8BO=HK@M(0%bMiGGfE#Xe0LrooIG8cXqbrdAk@L*bWxt10TE}-}AoD_dKr>5w-%ymb!t4e-UvXgOj`AZzRt7w@}H};p6bON=%KurmS37? z@oXVJ4^IM6Udgf)jZ_u$8rs({V$6r_D-)5LGpnW9ajuUhpI^Kqccg7-R3;KDayB$4 zZ9}8d1vJY%-TMjdHcb2x6A4s1Ze&G@>BuCg`H*MlS^;qw5OqiwNr?0_m>npqpqA z2pVW$#D>k$Ms5zBXnHo!+1Zx+x)>eU4i@AKKYTy``+e{KzMm2iwgbnux&i0E2)nP{ z6aZB&u^M@+8sAl;h0)A}RgVllmyZ6se1AAd)BX+xP&Eo1iA^zWRO|yrGZXZB4*_uL z#*jRIHO8GQVcdtiRaMVhysA+kKV1{5%OI0ZTlJ=~!RzGmpR2T;?!^=8p}e>tzckI_ z*+P619S5Mil4B_zt0@*Vv~OR?aK4Qp%-_Yu@m(z&mN^@0gM zb(SypE7u+$0L4TUGZEv&lc!dX6Ts6I#OZD2l&p$>Zwq<7%+i{Pe*cAXE!KiHQ*q2h zjN6ZH)qTjaaPdsHcs(vOqr%!oiNMH9H6HmuDV?Or-$`-y3vCPWI=}PToU#V=gt|mG zD^#nH_8(K_><=7nH&xRFVDj^Ot{)A_*=IQbrtbNe&*qes!Q{-Gs@XY?@$sEKye(kc i!ryT4fNyIH|BK%qVe4H6s?pj20000Zy6 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/integrateddynamics/textures/item/wrench_offset_side.png b/src/main/resources/assets/integrateddynamics/textures/item/wrench_offset_side.png new file mode 100644 index 0000000000000000000000000000000000000000..a427249ee34f01d67043bd95d875cfc14dcb22a8 GIT binary patch literal 546 zcmV+-0^R+IP)1vJY%-TMjdHcb2x6A4s1ZS!G@>BuCMYPn(M!Qdh%PILTbS z8U%s{85pr)bF`6jLnoS^&7GZXdA=@22eyL+`N9w1&;Nek`@ip}M1<|Yv8`^v`7grm zt2YHeRm!YJ-l)cR)o5XCVbZEc2A@ht|6RU69HeP~hXOG50!Lz>nAWTI0b>i3^m-2g zaPj((Jbp3Goy%d|hr5+o%v(HDFOZ+E3Dsq=kWO3mroO?;O9SAKj|^kY(Y*nQrlVTxfcgwT&`?k>_e6@}6=!Nt3^m((D)7GVwaU^VyuT2K0ow zq-azzO-TEXsY><-4!0ZAFaVhP{Ell!Lo#|J2f+Q;KIXGIWo0lmGpA~Hj-!8gYY%S= k*tYOD96aFL+QR?hHy Date: Sat, 5 Sep 2026 16:26:36 +0000 Subject: [PATCH 03/13] Reduce the Wrench configuration modes to Copy All, Settings and Aspect The configuration sections now match the two things a player configures on a part, and each Wrench mode maps onto one of them: * Copy All takes both, and still only pastes onto a part of the same type * Copy Settings takes the update interval, priority, channel, target side and target offset, together with the offset variables * Copy Aspect takes the active variable, the aspect properties and the aspect setting variables Every variable inventory belongs to exactly one section, so the offset variables travel with the settings and the active and aspect setting variables travel with the aspects, rather than all of them forming a section of their own. This replaces the former Copy Aspect Settings and Copy Variable Cards modes, which split the same data along a line that did not match any screen a player uses. Counts in the messages and the tooltip moved to the end of their sentence, so that they read correctly for a single card as well. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01LBAgcFp6jWcYRWz9N8h7AF --- CHANGELOG-1.21.1.md | 2 +- .../gametest/GameTestsWrenchConfig.java | 80 +++++++++++++++++- .../core/helper/PartConfigHelpers.java | 43 +++++----- .../core/part/PartConfigSection.java | 23 +++-- .../core/part/PartConfigSnapshot.java | 25 ++++-- .../integrateddynamics/item/ItemWrench.java | 15 ++-- .../assets/integrateddynamics/lang/en_us.json | 29 +++---- .../models/item/wrench.json | 3 +- .../models/item/wrench_config_aspect.json | 6 ++ .../models/item/wrench_config_aspects.json | 6 -- .../models/item/wrench_config_variables.json | 6 -- ...g_aspects.png => wrench_config_aspect.png} | Bin .../textures/item/wrench_config_variables.png | Bin 546 -> 0 bytes .../core/part/TestPartConfigSnapshot.java | 20 +++-- 14 files changed, 178 insertions(+), 80 deletions(-) create mode 100644 src/main/resources/assets/integrateddynamics/models/item/wrench_config_aspect.json delete mode 100644 src/main/resources/assets/integrateddynamics/models/item/wrench_config_aspects.json delete mode 100644 src/main/resources/assets/integrateddynamics/models/item/wrench_config_variables.json rename src/main/resources/assets/integrateddynamics/textures/item/{wrench_config_aspects.png => wrench_config_aspect.png} (100%) delete mode 100644 src/main/resources/assets/integrateddynamics/textures/item/wrench_config_variables.png diff --git a/CHANGELOG-1.21.1.md b/CHANGELOG-1.21.1.md index 474234f692d..ab8c9def71d 100644 --- a/CHANGELOG-1.21.1.md +++ b/CHANGELOG-1.21.1.md @@ -7,7 +7,7 @@ All notable changes to this project will be documented in this file. ### Added * Allow part configurations to be copied and pasted with the Wrench, Closes #859 - * The Wrench gets modes to copy a whole part configuration, or just its settings, aspect settings or variable cards + * The Wrench gets a Copy All, Copy Settings and Copy Aspect mode * The active Wrench mode is shown on the Wrench item * Allow aspect settings to be determined by variables (#1707), Closes CyclopsMC/IntegratedTunnels#278 * Show modified aspect property values in tooltip (#1706), Closes #1704 diff --git a/src/integrationtest/java/org/cyclops/integrateddynamics/gametest/GameTestsWrenchConfig.java b/src/integrationtest/java/org/cyclops/integrateddynamics/gametest/GameTestsWrenchConfig.java index db3cec89529..b0bc24678b7 100644 --- a/src/integrationtest/java/org/cyclops/integrateddynamics/gametest/GameTestsWrenchConfig.java +++ b/src/integrationtest/java/org/cyclops/integrateddynamics/gametest/GameTestsWrenchConfig.java @@ -28,11 +28,13 @@ import org.cyclops.integrateddynamics.core.part.PartConfigSection; import org.cyclops.integrateddynamics.core.part.PartConfigSnapshot; import org.cyclops.integrateddynamics.core.part.PartStateActiveVariableBase; +import org.cyclops.integrateddynamics.core.part.PartStateOffsetHandler; import org.cyclops.integrateddynamics.core.part.PartTypes; import org.cyclops.integrateddynamics.item.ItemWrench; import org.cyclops.integrateddynamics.part.aspect.Aspects; import org.cyclops.integrateddynamics.part.aspect.write.AspectWriteBuilders; import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypeBoolean; +import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypeInteger; import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypes; import javax.annotation.Nullable; @@ -151,6 +153,19 @@ protected static int countBlankVariables(Player player) { return PartConfigHelpers.countBlankVariables(player); } + protected static void setOffsetVariable(PartPos partPos, int slot, ItemStack variable) { + SimpleInventory inventory = new SimpleInventory(3, 1); + partState(partPos).loadInventoryNamed(PartStateOffsetHandler.INVENTORY_NAME, inventory); + inventory.setItem(slot, variable); + partState(partPos).saveInventoryNamed(PartStateOffsetHandler.INVENTORY_NAME, inventory); + } + + protected static ItemStack getOffsetVariable(PartPos partPos, int slot) { + SimpleInventory inventory = new SimpleInventory(3, 1); + partState(partPos).loadInventoryNamed(PartStateOffsetHandler.INVENTORY_NAME, inventory); + return inventory.getItem(slot); + } + protected static void giveBlankVariables(Player player, int count) { player.getInventory().add(new ItemStack(RegistryEntries.ITEM_VARIABLE.value(), count)); } @@ -341,14 +356,14 @@ public void testWrenchConfigSettingsModeOnlyCopiesPartSettings(GameTestHelper he } @GameTest(template = TEMPLATE_EMPTY) - public void testWrenchConfigAspectsModeOnlyCopiesAspectSettings(GameTestHelper helper) { + public void testWrenchConfigAspectModeOnlyCopiesAspects(GameTestHelper helper) { PartPos source = placePart(helper, POS_SOURCE, PartTypes.REDSTONE_WRITER); PartPos target = placePart(helper, POS_TARGET, PartTypes.REDSTONE_WRITER); configurePart(helper, source, Vec3i.ZERO); int updateInterval = ((IPartType) partType(target)).getUpdateInterval(partState(target)); Player player = helper.makeMockPlayer(GameType.SURVIVAL); - ItemStack wrench = createWrench(ItemWrench.Mode.CONFIG_ASPECTS); + ItemStack wrench = createWrench(ItemWrench.Mode.CONFIG_ASPECT); clickPart(helper, player, wrench, source, true); clickPart(helper, player, wrench, target, false); @@ -362,6 +377,67 @@ public void testWrenchConfigAspectsModeOnlyCopiesAspectSettings(GameTestHelper h }); } + @GameTest(template = TEMPLATE_EMPTY) + public void testWrenchConfigAspectModeCopiesTheActiveVariable(GameTestHelper helper) { + PartPos source = placePart(helper, POS_SOURCE, PartTypes.REDSTONE_WRITER); + PartPos target = placePart(helper, POS_TARGET, PartTypes.REDSTONE_WRITER); + placeVariableInWriter(helper, source, Aspects.Write.Redstone.BOOLEAN, + createVariableForValue(helper.getLevel(), ValueTypes.BOOLEAN, ValueTypeBoolean.ValueBoolean.of(true))); + + Player player = helper.makeMockPlayer(GameType.SURVIVAL); + ItemStack wrench = createWrench(ItemWrench.Mode.CONFIG_ASPECT); + player.setItemInHand(InteractionHand.MAIN_HAND, wrench); + giveBlankVariables(player, 1); + clickPart(helper, player, wrench, source, true); + clickPart(helper, player, wrench, target, false); + + helper.succeedWhen(() -> { + helper.assertTrue(getActiveVariable(target) != null, "The active variable was not pasted"); + helper.assertValueEqual(countBlankVariables(player), 0, "The blank variable card was not consumed"); + }); + } + + @GameTest(template = TEMPLATE_EMPTY) + public void testWrenchConfigSettingsModeCopiesOffsetVariables(GameTestHelper helper) { + PartPos source = placePart(helper, POS_SOURCE, PartTypes.REDSTONE_WRITER); + PartPos target = placePart(helper, POS_TARGET, PartTypes.REDSTONE_WRITER); + // The offset variables are part of the settings, not of the aspects + setOffsetVariable(source, 0, + createVariableForValue(helper.getLevel(), ValueTypes.INTEGER, ValueTypeInteger.ValueInteger.of(1))); + + Player player = helper.makeMockPlayer(GameType.SURVIVAL); + ItemStack wrench = createWrench(ItemWrench.Mode.CONFIG_SETTINGS); + player.setItemInHand(InteractionHand.MAIN_HAND, wrench); + giveBlankVariables(player, 1); + clickPart(helper, player, wrench, source, true); + clickPart(helper, player, wrench, target, false); + + helper.succeedWhen(() -> { + helper.assertTrue(!getOffsetVariable(target, 0).isEmpty(), "The offset variable was not pasted"); + helper.assertValueEqual(countBlankVariables(player), 0, "The blank variable card was not consumed"); + }); + } + + @GameTest(template = TEMPLATE_EMPTY) + public void testWrenchConfigAspectModeSkipsOffsetVariables(GameTestHelper helper) { + PartPos source = placePart(helper, POS_SOURCE, PartTypes.REDSTONE_WRITER); + PartPos target = placePart(helper, POS_TARGET, PartTypes.REDSTONE_WRITER); + setOffsetVariable(source, 0, + createVariableForValue(helper.getLevel(), ValueTypes.INTEGER, ValueTypeInteger.ValueInteger.of(1))); + + Player player = helper.makeMockPlayer(GameType.SURVIVAL); + ItemStack wrench = createWrench(ItemWrench.Mode.CONFIG_ASPECT); + player.setItemInHand(InteractionHand.MAIN_HAND, wrench); + giveBlankVariables(player, 1); + clickPart(helper, player, wrench, source, true); + clickPart(helper, player, wrench, target, false); + + helper.succeedWhen(() -> { + helper.assertTrue(getOffsetVariable(target, 0).isEmpty(), "The offset variable was pasted"); + helper.assertValueEqual(countBlankVariables(player), 1, "A blank variable card was consumed"); + }); + } + @GameTest(template = TEMPLATE_EMPTY) public void testWrenchConfigSubsetModeAcrossPartTypes(GameTestHelper helper) { PartPos source = placePart(helper, POS_SOURCE, PartTypes.REDSTONE_WRITER); diff --git a/src/main/java/org/cyclops/integrateddynamics/core/helper/PartConfigHelpers.java b/src/main/java/org/cyclops/integrateddynamics/core/helper/PartConfigHelpers.java index f822f0c3d5d..bb97ba57113 100644 --- a/src/main/java/org/cyclops/integrateddynamics/core/helper/PartConfigHelpers.java +++ b/src/main/java/org/cyclops/integrateddynamics/core/helper/PartConfigHelpers.java @@ -88,7 +88,7 @@ public static PartConfigSnapshot snapshot(ValueDeseralizationContext valueDesera } Map aspectProperties = Maps.newLinkedHashMap(); - if (sections.contains(PartConfigSection.ASPECT_PROPERTIES)) { + if (sections.contains(PartConfigSection.ASPECT)) { for (IAspect aspect : getAspects(partType)) { if (aspect.hasProperties()) { IAspectProperties properties = state.getAspectProperties(aspect); @@ -102,24 +102,27 @@ public static PartConfigSnapshot snapshot(ValueDeseralizationContext valueDesera } } + // Each variable inventory belongs to one of the sections, so only copy the ones that are included List variableCards = Lists.newArrayList(); - if (sections.contains(PartConfigSection.VARIABLE_CARDS)) { - for (Map.Entry> entry : state.getInventoriesNamed().entrySet()) { - NonNullList inventory = entry.getValue(); - for (int slot = 0; slot < inventory.size(); slot++) { - if (!inventory.get(slot).isEmpty()) { - variableCards.add(new PartConfigSnapshot.VariableCard(entry.getKey(), slot, - inventory.get(slot).copy())); - } + for (Map.Entry> entry : state.getInventoriesNamed().entrySet()) { + if (!sections.contains(PartConfigSection.forInventoryName(entry.getKey()))) { + continue; + } + NonNullList inventory = entry.getValue(); + for (int slot = 0; slot < inventory.size(); slot++) { + if (!inventory.get(slot).isEmpty()) { + variableCards.add(new PartConfigSnapshot.VariableCard(entry.getKey(), slot, + inventory.get(slot).copy())); } } - if (state instanceof PartStateActiveVariableBase activeState) { - SimpleInventory inventory = activeState.getInventory(); - for (int slot = 0; slot < inventory.getContainerSize(); slot++) { - if (!inventory.getItem(slot).isEmpty()) { - variableCards.add(new PartConfigSnapshot.VariableCard( - PartConfigSnapshot.INVENTORY_NAME_ACTIVE, slot, inventory.getItem(slot).copy())); - } + } + if (state instanceof PartStateActiveVariableBase activeState + && sections.contains(PartConfigSection.forInventoryName(PartConfigSnapshot.INVENTORY_NAME_ACTIVE))) { + SimpleInventory inventory = activeState.getInventory(); + for (int slot = 0; slot < inventory.getContainerSize(); slot++) { + if (!inventory.getItem(slot).isEmpty()) { + variableCards.add(new PartConfigSnapshot.VariableCard( + PartConfigSnapshot.INVENTORY_NAME_ACTIVE, slot, inventory.getItem(slot).copy())); } } } @@ -184,13 +187,11 @@ public static PartConfigApplyResult apply(ValueDeseralizationContext valueDesera if (sections.contains(PartConfigSection.PART_SETTINGS) && snapshot.partSettings().isPresent()) { applyPartSettings(network, target, partType, state, snapshot.partSettings().get(), result); } - if (sections.contains(PartConfigSection.ASPECT_PROPERTIES)) { + if (sections.contains(PartConfigSection.ASPECT)) { applyAspectProperties(valueDeseralizationContext, target, partType, state, snapshot, result); } - if (sections.contains(PartConfigSection.VARIABLE_CARDS)) { - applyVariableCards(valueDeseralizationContext, target, partType, state, - snapshot.variableCards(), player, result); - } + applyVariableCards(valueDeseralizationContext, target, partType, state, + snapshot.getVariableCards(sections), player, result); return result; } diff --git a/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigSection.java b/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigSection.java index 67a24d0a584..9c204ed2da8 100644 --- a/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigSection.java +++ b/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigSection.java @@ -13,22 +13,31 @@ public enum PartConfigSection { /** - * Update interval, priority, channel, target side override and target offset. + * Update interval, priority, channel, target side, target offset, and the offset variables. */ PART_SETTINGS, /** - * The statically configured properties (settings) of aspects. + * The active variable, the statically configured aspect properties, and the aspect setting variables. */ - ASPECT_PROPERTIES, + ASPECT; + /** - * The variable cards inside the part. + * All sections, which together form the whole configuration of a part. */ - VARIABLE_CARDS; + public static final Set ALL = Sets.immutableEnumSet(EnumSet.allOf(PartConfigSection.class)); /** - * All sections, as copied by the Wrench. + * @param inventoryName The name of a variable inventory inside a part. + * @return The section that the variables in that inventory belong to. */ - public static final Set ALL = Sets.immutableEnumSet(EnumSet.allOf(PartConfigSection.class)); + public static PartConfigSection forInventoryName(String inventoryName) { + if (PartConfigSnapshot.INVENTORY_NAME_ACTIVE.equals(inventoryName) + || inventoryName.startsWith(PartStateAspectVariablesHandler.INVENTORY_NAME_PREFIX)) { + return ASPECT; + } + // The offset variables, and any inventory that an addon adds, are part-level state + return PART_SETTINGS; + } public String getTranslationKey() { return "item.integrateddynamics.wrench.mode.config.section." + name().toLowerCase(Locale.ENGLISH); diff --git a/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigSnapshot.java b/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigSnapshot.java index ca6cb761ca8..fb2a4c45d66 100644 --- a/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigSnapshot.java +++ b/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigSnapshot.java @@ -30,7 +30,7 @@ * @param sourcePartType The unique name of the part type this snapshot was taken from. * @param partSettings The non-default general part settings. * @param aspectProperties The serialized non-default aspect properties, by aspect unique name. - * @param variableCards All variable cards. + * @param variableCards All variables, of every section. * @author rubensworks */ public record PartConfigSnapshot(int version, @@ -80,10 +80,21 @@ public record PartConfigSnapshot(int version, .apply(builder, PartConfigSnapshot::new)); /** - * @return The number of blank Variable Cards that pasting this snapshot needs at most. + * @param sections The sections that will be pasted. + * @return The variables that this snapshot holds for the given sections. */ - public int getRequiredBlankVariables() { - return variableCards().size(); + public List getVariableCards(Set sections) { + return variableCards().stream() + .filter(card -> sections.contains(PartConfigSection.forInventoryName(card.inventoryName()))) + .toList(); + } + + /** + * @param sections The sections that will be pasted. + * @return The number of blank Variable Cards that pasting those sections needs at most. + */ + public int getRequiredBlankVariables(Set sections) { + return getVariableCards(sections).size(); } /** @@ -91,10 +102,12 @@ public int getRequiredBlankVariables() { * @return If this snapshot holds anything for the given section. */ public boolean hasSection(PartConfigSection section) { + if (!getVariableCards(Set.of(section)).isEmpty()) { + return true; + } return switch (section) { case PART_SETTINGS -> partSettings().isPresent(); - case ASPECT_PROPERTIES -> !aspectProperties().isEmpty(); - case VARIABLE_CARDS -> !variableCards().isEmpty(); + case ASPECT -> !aspectProperties().isEmpty(); }; } diff --git a/src/main/java/org/cyclops/integrateddynamics/item/ItemWrench.java b/src/main/java/org/cyclops/integrateddynamics/item/ItemWrench.java index a13eb7d30c8..e086a481b31 100644 --- a/src/main/java/org/cyclops/integrateddynamics/item/ItemWrench.java +++ b/src/main/java/org/cyclops/integrateddynamics/item/ItemWrench.java @@ -102,7 +102,7 @@ public InteractionResult onItemUseFirst(ItemStack stack, UseOnContext context) { return InteractionResult.FAIL; } } - case CONFIG, CONFIG_SETTINGS, CONFIG_ASPECTS, CONFIG_VARIABLES -> { + case CONFIG, CONFIG_SETTINGS, CONFIG_ASPECT -> { // Let the click through to the part, so that its configuration can be copied return InteractionResult.PASS; } @@ -183,9 +183,10 @@ public void appendHoverText(ItemStack itemStack, Item.TooltipContext context, Li sections.stream() .map(section -> Component.translatable(section.getTranslationKey()).getString()) .collect(Collectors.joining(", "))).withStyle(ChatFormatting.GRAY)); - if (sections.contains(PartConfigSection.VARIABLE_CARDS) && !snapshot.variableCards().isEmpty()) { + int requiredBlanks = snapshot.getRequiredBlankVariables(sections); + if (requiredBlanks > 0) { list.add(Component.translatable("item.integrateddynamics.wrench.mode.config.requires", - snapshot.variableCards().size()).withStyle(ChatFormatting.GOLD)); + requiredBlanks).withStyle(ChatFormatting.GOLD)); } }); } @@ -208,7 +209,7 @@ public

, S extends IPartState

> InteractionResult pe } return InteractionResult.SUCCESS; } - case CONFIG, CONFIG_SETTINGS, CONFIG_ASPECTS, CONFIG_VARIABLES -> { + case CONFIG, CONFIG_SETTINGS, CONFIG_ASPECT -> { pastePartConfig(partType, partState, itemStack, player, center); return InteractionResult.SUCCESS; } @@ -319,10 +320,8 @@ public static enum Mode implements StringRepresentable { PartConfigSection.ALL), CONFIG_SETTINGS("integrateddynamics:config_settings", "item.integrateddynamics.wrench.mode.config_settings", Sets.immutableEnumSet(PartConfigSection.PART_SETTINGS)), - CONFIG_ASPECTS("integrateddynamics:config_aspects", "item.integrateddynamics.wrench.mode.config_aspects", - Sets.immutableEnumSet(PartConfigSection.ASPECT_PROPERTIES)), - CONFIG_VARIABLES("integrateddynamics:config_variables", "item.integrateddynamics.wrench.mode.config_variables", - Sets.immutableEnumSet(PartConfigSection.VARIABLE_CARDS)); + CONFIG_ASPECT("integrateddynamics:config_aspect", "item.integrateddynamics.wrench.mode.config_aspect", + Sets.immutableEnumSet(PartConfigSection.ASPECT)); public static final StringRepresentable.EnumCodec CODEC = net.minecraft.util.StringRepresentable.fromEnum(Mode::values); public static final StreamCodec STREAM_CODEC = ByteBufCodecs.idMapper(INT_MODES::get, Mode::ordinal); diff --git a/src/main/resources/assets/integrateddynamics/lang/en_us.json b/src/main/resources/assets/integrateddynamics/lang/en_us.json index 862f67c6f17..7a9f4e945d5 100644 --- a/src/main/resources/assets/integrateddynamics/lang/en_us.json +++ b/src/main/resources/assets/integrateddynamics/lang/en_us.json @@ -159,30 +159,27 @@ "item.integrateddynamics.wrench.mode.offset_side.saved": "Position and Side were saved in Wrench: %s - %s", "item.integrateddynamics.wrench.mode.offset_side.success": "New offset position and side have been set in part", "item.integrateddynamics.wrench.mode.offset_side.side": "Side: %s", - "item.integrateddynamics.wrench.mode.config": "Copy Configuration", - "item.integrateddynamics.wrench.mode.config_settings": "Copy Part Settings", - "item.integrateddynamics.wrench.mode.config_aspects": "Copy Aspect Settings", - "item.integrateddynamics.wrench.mode.config_variables": "Copy Variable Cards", + "item.integrateddynamics.wrench.mode.config": "Copy All", + "item.integrateddynamics.wrench.mode.config_settings": "Copy Settings", + "item.integrateddynamics.wrench.mode.config_aspect": "Copy Aspect", "item.integrateddynamics.wrench.mode.config.info": "Shift + Right-click on a part to copy its whole configuration and Right-click on another part of the same type to paste it", - "item.integrateddynamics.wrench.mode.config_settings.info": "Shift + Right-click on a part to copy its update interval, priority, channel, side and offset and Right-click on another part to paste them", - "item.integrateddynamics.wrench.mode.config_aspects.info": "Shift + Right-click on a part to copy its aspect settings and Right-click on another part to paste them", - "item.integrateddynamics.wrench.mode.config_variables.info": "Shift + Right-click on a part to copy its variable cards and Right-click on another part to paste them", + "item.integrateddynamics.wrench.mode.config_settings.info": "Shift + Right-click on a part to copy its update interval, priority, channel, side and offsets and Right-click on another part to paste them", + "item.integrateddynamics.wrench.mode.config_aspect.info": "Shift + Right-click on a part to copy its active aspect, its variable cards and its aspect settings and Right-click on another part to paste them", "item.integrateddynamics.wrench.mode.config.copied": "Configuration of %s was copied into the Wrench", "item.integrateddynamics.wrench.mode.config.pasted": "Pasted: %s", "item.integrateddynamics.wrench.mode.config.pasted.nothing": "Nothing from this Wrench applies to this part", - "item.integrateddynamics.wrench.mode.config.pasted.part_settings": "part settings", - "item.integrateddynamics.wrench.mode.config.pasted.aspect_properties": "%s aspect settings", - "item.integrateddynamics.wrench.mode.config.pasted.variable_cards": "%s variable cards", + "item.integrateddynamics.wrench.mode.config.pasted.part_settings": "settings", + "item.integrateddynamics.wrench.mode.config.pasted.aspect_properties": "aspect settings (%s)", + "item.integrateddynamics.wrench.mode.config.pasted.variable_cards": "variable cards (%s)", "item.integrateddynamics.wrench.mode.config.nothing": "This part has no configuration to copy, everything is still at its default", - "item.integrateddynamics.wrench.mode.config.requires": "Requires %s blank Variable Cards", + "item.integrateddynamics.wrench.mode.config.requires": "Blank Variable Cards needed: %s", "item.integrateddynamics.wrench.mode.config.empty": "No matching configuration was copied into this Wrench yet", "item.integrateddynamics.wrench.mode.config.mismatch": "This configuration was copied from a different part: %s", - "item.integrateddynamics.wrench.mode.config.cards_skipped": "Skipped %s variable cards, %s more blank Variable Cards needed", + "item.integrateddynamics.wrench.mode.config.cards_skipped": "Variable cards skipped: %s. Blank Variable Cards still needed: %s", "item.integrateddynamics.wrench.mode.config.source": "Copied from: %s", "item.integrateddynamics.wrench.mode.config.sections": "Includes: %s", - "item.integrateddynamics.wrench.mode.config.section.part_settings": "part settings", - "item.integrateddynamics.wrench.mode.config.section.aspect_properties": "aspect settings", - "item.integrateddynamics.wrench.mode.config.section.variable_cards": "variable cards", + "item.integrateddynamics.wrench.mode.config.section.part_settings": "settings", + "item.integrateddynamics.wrench.mode.config.section.aspect": "aspects", "item.integrateddynamics.variable": "Variable Card", "item.integrateddynamics.variable.info": "Clear or copy in a crafting grid", "item.integrateddynamics.variable.warning": "§4§lWARNING: Do NOT copy this item by middle-clicking!", @@ -1865,7 +1862,7 @@ "info_book.integrateddynamics.manual.parts.settings.text3": "The Ticks/Operation allows you to configure the ticking frequency of this part. The higher, the slower it operates.", "info_book.integrateddynamics.manual.parts.settings.text4": "The Priority determines the order in which this part is executed within a single network tick.", "info_book.integrateddynamics.manual.parts.settings.text5": "The Energy Channel indicates the channel from which energy should be consumed when this part ticks. This is only applicable if energy consumption for networks is enabled.", - "info_book.integrateddynamics.manual.parts.settings.text6": "The configuration of a part can be copied to other parts using a &lWrench&r. Shift+right-clicking a part copies it into the Wrench, and right-clicking another part pastes it. The Wrench mode picks what is copied: &lCopy Configuration&r takes everything and only pastes onto a part of the same type, while &lCopy Part Settings&r, &lCopy Aspect Settings&r and &lCopy Variable Cards&r take one part of it and can be pasted onto any part. Only settings that you changed yourself are copied, so pasting never resets anything you left alone. Each pasted variable card consumes one blank &lVariable Card&r from your inventory.", + "info_book.integrateddynamics.manual.parts.settings.text6": "The configuration of a part can be copied to other parts using a &lWrench&r. Shift+right-clicking a part copies it into the Wrench, and right-clicking another part pastes it. The Wrench mode picks what is copied: &lCopy All&r takes everything and only pastes onto a part of the same type, &lCopy Settings&r takes the update interval, priority, channel, side and offsets, and &lCopy Aspect&r takes the active aspect with its variable card and all aspect settings. The last two can be pasted onto any part. Only settings that you changed yourself are copied, so pasting never resets anything you left alone. Each pasted variable card consumes one blank &lVariable Card&r from your inventory.", "info_book.integrateddynamics.manual.parts.offsets": "Offsets", "info_book.integrateddynamics.manual.parts.offsets.text1": "By default, parts will target the direct neighboring position. However, when &lPart Enhancements&r are applied, the target position can be changed through the part's &lOffset&r screen.", diff --git a/src/main/resources/assets/integrateddynamics/models/item/wrench.json b/src/main/resources/assets/integrateddynamics/models/item/wrench.json index 8a3cd8d4b30..d80d92fd214 100644 --- a/src/main/resources/assets/integrateddynamics/models/item/wrench.json +++ b/src/main/resources/assets/integrateddynamics/models/item/wrench.json @@ -8,7 +8,6 @@ {"predicate": {"integrateddynamics:wrench_mode": 0.15}, "model": "integrateddynamics:item/wrench_offset_side"}, {"predicate": {"integrateddynamics:wrench_mode": 0.25}, "model": "integrateddynamics:item/wrench_config"}, {"predicate": {"integrateddynamics:wrench_mode": 0.35}, "model": "integrateddynamics:item/wrench_config_settings"}, - {"predicate": {"integrateddynamics:wrench_mode": 0.45}, "model": "integrateddynamics:item/wrench_config_aspects"}, - {"predicate": {"integrateddynamics:wrench_mode": 0.55}, "model": "integrateddynamics:item/wrench_config_variables"} + {"predicate": {"integrateddynamics:wrench_mode": 0.45}, "model": "integrateddynamics:item/wrench_config_aspect"} ] } diff --git a/src/main/resources/assets/integrateddynamics/models/item/wrench_config_aspect.json b/src/main/resources/assets/integrateddynamics/models/item/wrench_config_aspect.json new file mode 100644 index 00000000000..39b501ddd38 --- /dev/null +++ b/src/main/resources/assets/integrateddynamics/models/item/wrench_config_aspect.json @@ -0,0 +1,6 @@ +{ + "parent": "cyclopscore:item/flat", + "textures": { + "layer0": "integrateddynamics:item/wrench_config_aspect" + } +} diff --git a/src/main/resources/assets/integrateddynamics/models/item/wrench_config_aspects.json b/src/main/resources/assets/integrateddynamics/models/item/wrench_config_aspects.json deleted file mode 100644 index 05f461a3c92..00000000000 --- a/src/main/resources/assets/integrateddynamics/models/item/wrench_config_aspects.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "parent": "cyclopscore:item/flat", - "textures": { - "layer0": "integrateddynamics:item/wrench_config_aspects" - } -} diff --git a/src/main/resources/assets/integrateddynamics/models/item/wrench_config_variables.json b/src/main/resources/assets/integrateddynamics/models/item/wrench_config_variables.json deleted file mode 100644 index 46f4d157bec..00000000000 --- a/src/main/resources/assets/integrateddynamics/models/item/wrench_config_variables.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "parent": "cyclopscore:item/flat", - "textures": { - "layer0": "integrateddynamics:item/wrench_config_variables" - } -} diff --git a/src/main/resources/assets/integrateddynamics/textures/item/wrench_config_aspects.png b/src/main/resources/assets/integrateddynamics/textures/item/wrench_config_aspect.png similarity index 100% rename from src/main/resources/assets/integrateddynamics/textures/item/wrench_config_aspects.png rename to src/main/resources/assets/integrateddynamics/textures/item/wrench_config_aspect.png diff --git a/src/main/resources/assets/integrateddynamics/textures/item/wrench_config_variables.png b/src/main/resources/assets/integrateddynamics/textures/item/wrench_config_variables.png deleted file mode 100644 index 03e64d6116a004c32b76342fcb99b85cd1efaa7e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 546 zcmV+-0^R+IP)GGfE#Xe0LrooIG8cXqbrdAk@L*bWxt10TE}-}AoD_dKr>5w-%ymb!t4e-UvXgOj`AZzRt7w@}H};p6bON=%KurmS37? z@oXVJ4^IM6Udgf)jZ_u$8rs({V$6r_D-)5LGpnW9ajuUhpI^Kqccg7-R3;KDayB$4 zZ9}8d Date: Sat, 5 Sep 2026 16:45:16 +0000 Subject: [PATCH 04/13] Test that pasted aspect and offset variables also get a new variable id The active variable was already covered, but the aspect setting variables and the offset variables travel through the same copy, so assert their ids change too and that the cards they replace are given back to the player. Both tests were checked against a deliberately broken copy that reuses the original id, so that they can actually fail. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01LBAgcFp6jWcYRWz9N8h7AF --- .../gametest/GameTestsWrenchConfig.java | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/integrationtest/java/org/cyclops/integrateddynamics/gametest/GameTestsWrenchConfig.java b/src/integrationtest/java/org/cyclops/integrateddynamics/gametest/GameTestsWrenchConfig.java index b0bc24678b7..65ab6da4284 100644 --- a/src/integrationtest/java/org/cyclops/integrateddynamics/gametest/GameTestsWrenchConfig.java +++ b/src/integrationtest/java/org/cyclops/integrateddynamics/gametest/GameTestsWrenchConfig.java @@ -21,6 +21,7 @@ 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.IAspect; import org.cyclops.integrateddynamics.core.helper.NetworkHelpers; import org.cyclops.integrateddynamics.core.helper.PartConfigHelpers; import org.cyclops.integrateddynamics.core.helper.PartHelpers; @@ -28,6 +29,7 @@ import org.cyclops.integrateddynamics.core.part.PartConfigSection; import org.cyclops.integrateddynamics.core.part.PartConfigSnapshot; import org.cyclops.integrateddynamics.core.part.PartStateActiveVariableBase; +import org.cyclops.integrateddynamics.core.part.PartStateAspectVariablesHandler; import org.cyclops.integrateddynamics.core.part.PartStateOffsetHandler; import org.cyclops.integrateddynamics.core.part.PartTypes; import org.cyclops.integrateddynamics.item.ItemWrench; @@ -43,6 +45,7 @@ import static org.cyclops.integrateddynamics.gametest.GameTestHelpersIntegratedDynamics.getEffectiveAspectProperty; import static org.cyclops.integrateddynamics.gametest.GameTestHelpersIntegratedDynamics.placeVariableInWriter; import static org.cyclops.integrateddynamics.gametest.GameTestHelpersIntegratedDynamics.setAspectProperty; +import static org.cyclops.integrateddynamics.gametest.GameTestHelpersIntegratedDynamics.setAspectPropertyVariable; /** * Tests for copying and pasting part configurations with the Wrench. @@ -153,6 +156,10 @@ protected static int countBlankVariables(Player player) { return PartConfigHelpers.countBlankVariables(player); } + protected static ItemStack getAspectVariable(PartPos partPos, IAspect aspect, int slot) { + return PartStateAspectVariablesHandler.getVariablesInventory(partState(partPos), aspect).getItem(slot); + } + protected static void setOffsetVariable(PartPos partPos, int slot, ItemStack variable) { SimpleInventory inventory = new SimpleInventory(3, 1); partState(partPos).loadInventoryNamed(PartStateOffsetHandler.INVENTORY_NAME, inventory); @@ -259,6 +266,72 @@ public void testWrenchConfigPasteCardsGetNewIdAndEjectExisting(GameTestHelper he }); } + @GameTest(template = TEMPLATE_EMPTY) + public void testWrenchConfigPasteAspectVariableGetsNewIdAndEjectsExisting(GameTestHelper helper) { + PartPos source = placePart(helper, POS_SOURCE, PartTypes.REDSTONE_WRITER); + PartPos target = placePart(helper, POS_TARGET, PartTypes.REDSTONE_WRITER); + ItemStack sourceVariable = createVariableForValue(helper.getLevel(), ValueTypes.INTEGER, + ValueTypeInteger.ValueInteger.of(5)); + ItemStack targetVariable = createVariableForValue(helper.getLevel(), ValueTypes.INTEGER, + ValueTypeInteger.ValueInteger.of(9)); + setAspectPropertyVariable(source, Aspects.Write.Redstone.BOOLEAN_PULSE, + AspectWriteBuilders.Redstone.PROP_PULSE_LENGTH, sourceVariable); + setAspectPropertyVariable(target, Aspects.Write.Redstone.BOOLEAN_PULSE, + AspectWriteBuilders.Redstone.PROP_PULSE_LENGTH, targetVariable); + int slot = PartStateAspectVariablesHandler.getPropertyTypes(Aspects.Write.Redstone.BOOLEAN_PULSE) + .indexOf(AspectWriteBuilders.Redstone.PROP_PULSE_LENGTH); + int sourceId = getVariableId(helper, sourceVariable); + int ejectedId = getVariableId(helper, targetVariable); + + Player player = helper.makeMockPlayer(GameType.SURVIVAL); + ItemStack wrench = createWrench(ItemWrench.Mode.CONFIG_ASPECT); + player.setItemInHand(InteractionHand.MAIN_HAND, wrench); + giveBlankVariables(player, 3); + clickPart(helper, player, wrench, source, true); + clickPart(helper, player, wrench, target, false); + + helper.succeedWhen(() -> { + ItemStack pasted = getAspectVariable(target, Aspects.Write.Redstone.BOOLEAN_PULSE, slot); + helper.assertTrue(!pasted.isEmpty(), "The aspect setting variable was not pasted"); + helper.assertTrue(getVariableId(helper, pasted) != sourceId, + "The pasted aspect setting variable has the same id as the copied one"); + helper.assertValueEqual(countBlankVariables(player), 2, "Wrong number of blank variable cards consumed"); + helper.assertTrue(hasVariableWithId(helper, player, ejectedId), + "The aspect setting variable that was in the target part was not given back to the player"); + }); + } + + @GameTest(template = TEMPLATE_EMPTY) + public void testWrenchConfigPasteOffsetVariableGetsNewIdAndEjectsExisting(GameTestHelper helper) { + PartPos source = placePart(helper, POS_SOURCE, PartTypes.REDSTONE_WRITER); + PartPos target = placePart(helper, POS_TARGET, PartTypes.REDSTONE_WRITER); + ItemStack sourceVariable = createVariableForValue(helper.getLevel(), ValueTypes.INTEGER, + ValueTypeInteger.ValueInteger.of(2)); + ItemStack targetVariable = createVariableForValue(helper.getLevel(), ValueTypes.INTEGER, + ValueTypeInteger.ValueInteger.of(3)); + setOffsetVariable(source, 1, sourceVariable); + setOffsetVariable(target, 1, targetVariable); + int sourceId = getVariableId(helper, sourceVariable); + int ejectedId = getVariableId(helper, targetVariable); + + Player player = helper.makeMockPlayer(GameType.SURVIVAL); + ItemStack wrench = createWrench(ItemWrench.Mode.CONFIG_SETTINGS); + player.setItemInHand(InteractionHand.MAIN_HAND, wrench); + giveBlankVariables(player, 3); + clickPart(helper, player, wrench, source, true); + clickPart(helper, player, wrench, target, false); + + helper.succeedWhen(() -> { + ItemStack pasted = getOffsetVariable(target, 1); + helper.assertTrue(!pasted.isEmpty(), "The offset variable was not pasted"); + helper.assertTrue(getVariableId(helper, pasted) != sourceId, + "The pasted offset variable has the same id as the copied one"); + helper.assertValueEqual(countBlankVariables(player), 2, "Wrong number of blank variable cards consumed"); + helper.assertTrue(hasVariableWithId(helper, player, ejectedId), + "The offset variable that was in the target part was not given back to the player"); + }); + } + protected static boolean hasVariableWithId(GameTestHelper helper, Player player, int id) { for (int slot = 0; slot < player.getInventory().getContainerSize(); slot++) { ItemStack itemStack = player.getInventory().getItem(slot); From 23d567d2eea87f05c2b6e7272b74862924dba187 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 17:08:09 +0000 Subject: [PATCH 05/13] Shorten the Wrench tooltip and give each mode a meaningful icon The mode explanation moved behind the same shift that already reveals the item info, so the resting tooltip is only the state that matters: the mode, where the configuration came from, what it includes and how many blank Variable Cards pasting it needs. The explanations themselves are shorter, as they used to wrap over three lines. The mode badges on the Wrench are now small glyphs instead of plain coloured squares, so that they say what the mode does rather than having to be memorised by colour: two linked positions for the offset modes, echoing the Part Offsets button, two overlapping sheets for Copy All, a grid of fields for Copy Settings, echoing the Part Settings button, and a round aspect icon for Copy Aspect. Also renames the copied section in the tooltip from "settings" to "part settings". Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01LBAgcFp6jWcYRWz9N8h7AF --- .../integrateddynamics/item/ItemWrench.java | 5 ++++- .../assets/integrateddynamics/lang/en_us.json | 8 ++++---- .../textures/item/wrench_config.png | Bin 545 -> 536 bytes .../textures/item/wrench_config_aspect.png | Bin 548 -> 526 bytes .../textures/item/wrench_config_settings.png | Bin 545 -> 517 bytes .../textures/item/wrench_offset.png | Bin 544 -> 542 bytes .../textures/item/wrench_offset_side.png | Bin 546 -> 549 bytes 7 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/cyclops/integrateddynamics/item/ItemWrench.java b/src/main/java/org/cyclops/integrateddynamics/item/ItemWrench.java index e086a481b31..3c9bd21224a 100644 --- a/src/main/java/org/cyclops/integrateddynamics/item/ItemWrench.java +++ b/src/main/java/org/cyclops/integrateddynamics/item/ItemWrench.java @@ -190,7 +190,10 @@ public void appendHoverText(ItemStack itemStack, Item.TooltipContext context, Li } }); } - list.add(Component.translatable(mode.getLabel() + ".info").withStyle(ChatFormatting.ITALIC, ChatFormatting.GRAY)); + // Hidden behind the same shift that reveals the item info, to keep the resting tooltip short + if (MinecraftHelpers.isShifted()) { + list.add(Component.translatable(mode.getLabel() + ".info").withStyle(ChatFormatting.ITALIC, ChatFormatting.GRAY)); + } } public

, S extends IPartState

> InteractionResult performPartAction(BlockHitResult hit, IPartType partType, IPartState

partState, ItemStack itemStack, Player player, InteractionHand hand, PartPos center) { diff --git a/src/main/resources/assets/integrateddynamics/lang/en_us.json b/src/main/resources/assets/integrateddynamics/lang/en_us.json index 7a9f4e945d5..55cb481e99c 100644 --- a/src/main/resources/assets/integrateddynamics/lang/en_us.json +++ b/src/main/resources/assets/integrateddynamics/lang/en_us.json @@ -162,9 +162,9 @@ "item.integrateddynamics.wrench.mode.config": "Copy All", "item.integrateddynamics.wrench.mode.config_settings": "Copy Settings", "item.integrateddynamics.wrench.mode.config_aspect": "Copy Aspect", - "item.integrateddynamics.wrench.mode.config.info": "Shift + Right-click on a part to copy its whole configuration and Right-click on another part of the same type to paste it", - "item.integrateddynamics.wrench.mode.config_settings.info": "Shift + Right-click on a part to copy its update interval, priority, channel, side and offsets and Right-click on another part to paste them", - "item.integrateddynamics.wrench.mode.config_aspect.info": "Shift + Right-click on a part to copy its active aspect, its variable cards and its aspect settings and Right-click on another part to paste them", + "item.integrateddynamics.wrench.mode.config.info": "Shift + Right-click a part to copy it, Right-click a part of the same type to paste", + "item.integrateddynamics.wrench.mode.config_settings.info": "Shift + Right-click a part to copy its settings and offsets, Right-click a part to paste", + "item.integrateddynamics.wrench.mode.config_aspect.info": "Shift + Right-click a part to copy its aspects, Right-click a part to paste", "item.integrateddynamics.wrench.mode.config.copied": "Configuration of %s was copied into the Wrench", "item.integrateddynamics.wrench.mode.config.pasted": "Pasted: %s", "item.integrateddynamics.wrench.mode.config.pasted.nothing": "Nothing from this Wrench applies to this part", @@ -178,7 +178,7 @@ "item.integrateddynamics.wrench.mode.config.cards_skipped": "Variable cards skipped: %s. Blank Variable Cards still needed: %s", "item.integrateddynamics.wrench.mode.config.source": "Copied from: %s", "item.integrateddynamics.wrench.mode.config.sections": "Includes: %s", - "item.integrateddynamics.wrench.mode.config.section.part_settings": "settings", + "item.integrateddynamics.wrench.mode.config.section.part_settings": "part settings", "item.integrateddynamics.wrench.mode.config.section.aspect": "aspects", "item.integrateddynamics.variable": "Variable Card", "item.integrateddynamics.variable.info": "Clear or copy in a crafting grid", diff --git a/src/main/resources/assets/integrateddynamics/textures/item/wrench_config.png b/src/main/resources/assets/integrateddynamics/textures/item/wrench_config.png index b972170008d6b10b1f573f3fa7208fc04909b434..1b4ee4788201e26fe4efd9169fde3cfa9618ffdf 100644 GIT binary patch delta 510 zcmVT!) zjvaz7dgvf137xgN2OZ2q1k%l*QxIK}l3)w;AjB@!N=oc%uCkgl+l=FE&acDL;LboE z{K13wc)$1i{(DM9*h>ZW^co26*3fd}mH;Swi?zvD%6qT85`RVuX?Opy|B1Bq-sSs9 zKXuVI1>l%vPGqNsn zn$;FLc(I52M1N9Z1H;nVHzeZ=CAl#9MOyoYq&|_9^9Mq1zt6;Q>N-hnUW}~@bK8wR zKmq}Ec4>}J`X=Csu{qU4Qd^LtN1E6uTd;1hdbL;WdpWifMb5lVwiZ^%eOFSBpcr+re2VEWQVptU%2lBB~%3k_GAQZx}EDr0%8l!fyaXt7rn^RRe zr!n8Z_JX>a%GM6=t(pq8!G0Rreh$2iHQJ@78 zbWwDpmx7Vdb%pmu7uH1t(p}e4H_{*wG|<3^4V$Bl+#htJ>DfGIXIt*=Vsv0TSdb5V z@P2&H`##_Eyh=pa2^>4>1{(fF*n9o90H{)twa{Bt|E}sSjDMx3ta^0lxpWNN=li1} z8V`0TfQpglSY(D-qii2AmYSm1a|D1Zw}$1(t8wo21#uniRuw&G@hV21+-y~-CWBNm zY1LcCCa>cwf3DJcwikDxhtg77ercM;vxWF3JPAN)HOq1|QdP`rXy3kyxe&ImOhjtV zoR((KrGA=xet+=}Ka{q?5gAXc%Ei#Uv<;3(lg}>~cDpRTEksZ78h$MoFlH>msi)7Zo(2GSm!Aet z3uk0ay!)HU=_QueP4ovZlyj*XteJ>n#v#B=N44e&Pj}q@9p7j1KSqs=(Dx^(~m002ov KPDHLkU;%;$mHoH? diff --git a/src/main/resources/assets/integrateddynamics/textures/item/wrench_config_aspect.png b/src/main/resources/assets/integrateddynamics/textures/item/wrench_config_aspect.png index 41215969f6ee3a9c8665115be81a6ec2a882d91b..a5d6abf9f6fcdc19bc2ef80e9687d7b8bea00a25 100644 GIT binary patch delta 500 zcmVZwPaxEy4N?X z9bObldsjJnX^8N}tJI2XvTK?i&KKg#>;eF_&F`$|mYa$d6Z7*2vGZB~%0#5)yf@c4 za($T2q$Y{+2Y=EtIwkX~n{qAlNqR=7q%*0>mBS$q?+Y=Sx`SpGr0UdJX>|Dn2?W&n zZ!5el{{q_9*qs_iGgsx*$p}9yHvFhkx;3JL$4@k^JWUHAMppztc?98pjgDwH7i3Ek zu?XdAjrDDt>hu#8Eb88{@o>Ns>x;+4C~hdv!i~#=5^W6!F|9h=I}XX2XKEqy8b>b> z`S_+~*dv+E{juQeHuzeyab3_8XH`BuO&}D)wQVA8Lw}R`LJaqx!^&q{mEy5|f8Zn7 qui8oaW^=S;@V_K`y;0my0KWmD@9XzE*|0JI0000!6!x5C|G%V8n*b(N^vcI??Rg{I+jfey5Ak2iu1Q z`M?Y3@IL39_j%5%M1<|Yv8`&r`7grmE7t`;Rm!Y|U#rG@)qf~qJUwN_W5Z9SbMQ7_ z?+??ozf%F2#R5m7ADJmu?G47$Q}lZe0dV2kh&*~R!L3Un+=qLWspl=8SuBvBsST?0 zAe~BC@n&&@mx+}>skEKy#}n+MvXqfunr88ACccVH0#I4au^fxmCKfcb`7dHDMC>aQ zk-9UhW!QCYfPZFxKzt+jqDt%A&w>FzbXG17D%YM407~%)Mm);#$4{&pCxE9r zfYaN`Nm&!$-WKwDh2?bv{mwJxTB;dqBx4xyC^sM6sDFEqCE@((Uh#TdXvHe)8)f`s z&(&o39pzMlCSMn&xzDs^Vs&~KvpHoo=nHm>Zd5T%NcoPcO7=Srw;R(i0GR&tma9EM znZH~6Cp{0pv6#&%%Y*6J1yz%CjN>1{_)IT^u40WdHyG N07*qoLU z%N8wlDHw_Sr1nM&g@{1fwQijY35GxyLClajTUzMV9Ppu)s_q#bER@0QVk^%kvgzYI+rnHrYIKZ)LO$=u zhxR`2R(JkvrLBLENZ$bN=9>I63?COD@qPLe0B&)ct?!wmj+TM3{8gNlbWk!8IkvtS zYn;71L^z>KY=8WjbdFBQT&^g0=D$hj=!Aq5y4*e!^6>!@qsfQp#;TMLs$>sa0u9kL zbz?otXR`v-$k>`3LN{`9sp~X*mIJ$GcJ7a;QymuosASVP*$mfTzVdUFh!E+~X^OUT zQ%VwRi(on~8zr0l$yw!Y=J8%nHTeGUftdZg@B@jS@Kt|@o@Ct4_k=Qx}*w;B&jtqS3U9yp74b%?t>EcJXqg?e_e h`+P&te--$f{RV=?0TG3ViBJFl002ovPDHLkV1jUm@xcH9 delta 520 zcmV+j0{8ue1fc|wB!B2hL_t(|oTZUpNK;`L#(&?~WSTXN(pq8!G1FGqh#*WF{ei5T zpo^j#y%daut}DDRy09)HknXw;x`{@Cpg{&!Z0H~6Qkw}t2lUd6A)MK4u|ZSArN z;&7Eg_u;n%T%3@Ka~y@}P3Tv=zb@_36 zS~w+Z;@#g&PA{{(ZlK?Lrd&(aV2wl+BNpNI!&@~UvVSaGIMXAZCKp_Gzs_NrOgsPp002ov KPDHLkU;%<4Yz0sN diff --git a/src/main/resources/assets/integrateddynamics/textures/item/wrench_offset.png b/src/main/resources/assets/integrateddynamics/textures/item/wrench_offset.png index 2b861e842e5d3fa0a733abc06455d5495b382cee..139669fc5c2913c46be1fab333fad10c9d620025 100644 GIT binary patch delta 516 zcmV+f0{i`-1fB$tB!A^eL_t(|oTZarNK;W9$3N$?$z0Yf%4&!a#0*_f5KI{hv ze&?L;_xI;}en*K2&6w5{8*uFe*cZMg0IE`EJ@QI5-mAtIrhl_@wm&uUKstx+@blgX zEr&Z5fMpaop7>16s5%2oXXhC39R=Xx)iJsMbe7wfLwJt$D65#aah6daAFCy*(;%D4 z*#5S$$@A3e|E=sjJ%G2bpGs~?wshUbIYfLJj{;Cx`_0NiqL#6sqyP9W=3?CWWFk_x zzUoWtIX_5iAb%+Su?guI9+%nlnw*QwOULlIv<8B5cDKjIJ4Ez_t`O8yQY=?VZtrp= zMAOvCqa-KY;2YZOwFih z<>&G>-?%Gy(Ydk99S28|Ubn`jUS8faj|hRxANZVsJjdN$A5*_Qjd7#-LS7UT;* zd_VvDeeeIipAr$a1IMtzckI_*+P619S5Mil4B_zt0@*Vv~OR<7e{TzUz0A^@iGKfuaxK<^HB)iSM2y>yZqT?VSr+5i9m07*qo IM6N<$g6#tPl>h($ diff --git a/src/main/resources/assets/integrateddynamics/textures/item/wrench_offset_side.png b/src/main/resources/assets/integrateddynamics/textures/item/wrench_offset_side.png index a427249ee34f01d67043bd95d875cfc14dcb22a8..befe157729af3fa08f440e70c80f01cd18d0979c 100644 GIT binary patch delta 524 zcmV+n0`vW%1f>L!B!BElL_t(|oTZapNRv?*$A4#RGM6=rv>avxF+&%WL=h&9=*zsT zi!Sw2F%r72kZ!xvAPXeuqN|9AF2Y@nEYP5c4b#y^Y|6~f-p##xx8?hEF?!+MdlBIS z2cC1z|Nnga&vTTBuoH9bhz+>^1=!bjQvg)C#Cqhls=ZgWEq{z#xm3N_>TZ^@y4(`d^*Xkt6{uHJCs?-SvXVAlbfw3vT2Y> zr!9Y5-{M(v_MGpfK7SaJ!0=sZ9vG3y)S8@&%t`aWh|~u|a(1`Z!aGFthp!RRl2RyDNNn$N zB*f)XCzcX?%oUv?HimoY)Kb!Ruz_EBgX~6;l}r7~v;P19#Y7Av5$ENVs6}!E_}W9b z{f(TIbqO>zkSml~-ZUuOd89nqs<1|C-a)oiaN%@^_#dNBBsHMr4@xOm#CcA2@;153E@z#1aCtm;n O002ovP6b4+LSTZ~=?+o= delta 521 zcmV+k0`~o-1fm3xB!B5iL_t(|oTZUbNK;`LhM#|IGM6=ta<#+=VwSC_5kZ(Vq9E%g zC@8woOTkEpE-SoGy09)HknXzbBIqU>1cC+`7_nh5zwf6+gzdnwt!}{iFT(DtHw8dd%B)7-sK$5IXn$dBVbZEc2A@ht|6RU6 z9HeP~hXOG50!Lz>nAWTI0b>i3^m-2gaPj((Jbp3Goy%d|hr5+o%v(HDFOZ+E3Dsq= zkWO3mroO?;VGAr!^!_piG%GsT6i*F0j8@@tNOG>d^C9%1~ zCWynKPG%E)%$I1X5#z+@6RXDw;OPqD z^tN(JR>i-!g?zEX(waf>-ZSM|tOaYN;uwh-w;$cA`+tyS;li12@p@cndX=?}GJ%oj zY9jKUaym(qzmwAJ7uquMI=}PToU#V=gu0|?R549R`;Vzg_6H8P8`CfVnEL#VYez#e zdLswG{ntL`vpHpDFf}u$YIcsJe|T#TZwuJA@HZSh;M>~5|Kc|vDeGN@gvGr80000< LMNUMnLIPlduI&UD From ba27347668038dde6762d78c2bd23d18bef63038 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 17:34:38 +0000 Subject: [PATCH 06/13] Redraw the offset and copy-all Wrench mode badges The two offset badges were flat blobs that only differed by a single pixel, and their outlines vanished against the dark inventory slot. They become move arrows for the saved offset, and an arrow pointing at a face for the saved offset and side. The copy-all badge no longer uses a generic copy glyph, but combines the badges of the two modes it covers: the settings grid next to the aspect orb. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01LBAgcFp6jWcYRWz9N8h7AF --- .../textures/item/wrench_config.png | Bin 536 -> 556 bytes .../textures/item/wrench_offset.png | Bin 542 -> 542 bytes .../textures/item/wrench_offset_side.png | Bin 549 -> 527 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/src/main/resources/assets/integrateddynamics/textures/item/wrench_config.png b/src/main/resources/assets/integrateddynamics/textures/item/wrench_config.png index 1b4ee4788201e26fe4efd9169fde3cfa9618ffdf..cc32b71bbd3211c46a54f913d9150867a245e8cf 100644 GIT binary patch delta 531 zcmV+u0_^>m1gr#*B!BZsL_t(|oTZUpNRwd{#((cn(UnchY>p`eGG(l&h#*oLQIOeP zM3?hYFp|2?kZy}0g@{0+n{K)Z>Z%bWh|n&I*qAz;!e&`(tG<2nbAQ}U7vk5pFNpBK z;XNQ792B@39bK zv#DdBW6WnafjS%8{6qM(73p#A<(Hm^9~-1D1XN?kApkaGVe+vENAEu{dz6S^@AJ{% zXydqK#MN#mn=kSuW01Q!t)c^ur1!;TwS4up90=&jk$;GqBPZ9tD|3Z&r@Z2@H=-FO ze&z~zCLgQW;2R2wI8Ck|HlrVLB%+eM({HkO-(CFULSd`SYDy=a&9mzCs#g=Y$c5gK zdRP>F{wT!) zjvaz7dgvf137xgN2OZ2q1k%l*QxIK}l3)w;AjB@!N=oc%uCkgl+l=FE&acDL;LboE z{K13wc)$1i{(DM9*h>ZW^co26*3fd}mH;Swi?zvD%6qT85`RVuX?Opy|B1Bq-sSs9 zKXuVI1>l%vPGqNsn zn$;FLc(I52M1N9Z1H;nVHzeZ=CAl#9MOyoYq&|_9^9Mq1zt6;Q>N-hnUW}~@bK8wR zKmq}Ec4>}J`X=Csu{qU4Qd^LtN1E6uTd;1hdbL;WdpWifMb5lVwiZ^%eOFSBpcr+re2VEWQVptU%2lBB~%3k_GAQZx}EDr0%8l!fyaXt7rn^RRe zr!n8Z_JX>adzSq%w-m{IqkM1nAB4+WWz3i;+<0?kn9 zxv2Lb!ZIU}-g@q#Uh*M=5$Hn@8#PCc+BE+#&AWB)u3O$;50_nVciDpnew=^5bH3+% zzCR@*?5FwmbsF&hFQKjfh5)GYHXF0ARqej2{la7}?v5XfJb#gn;c>p*8=>)dhXSz8 zBBzsc%$pVO1(Ufry`hr;Tp1XXhc9NheJx7h)EQ+P1vk$&ixlP^pF@N)N zalJZ~L*2bJb@fnQS&~1x?&f(QzDj%mpqyW0HI;M}i#qz(&tfekyq1Z`uK1)caqw~< z!H6c|vAfb9n}3j*OkOU{ev$Urgajj+Ts#nP^F0tl(d%gXq8Qs1(mTx_L3}=SZaK}z zLJ6p`u{GLTVM)XI6;knoWf z3PzdLUlzulr>bGa0c&Ma3=U0+18!6RKI{hve&?L;_xI;} zen*K2&6w5{8*uFe*cZMg0IE`EJ@QI5-mAtIrn7UlKQ;0|I)8`m@blgXEr&Z5fMpao zp7>16s5%2oXXhC39R=Xx)iJsMbe7wfLwJt$D65#aah6daAFCy*(;%D4*#5S$$@A3e z|E=sjJ%G2bpGs~?wshUbIYfLJj{;Cx`_0NiqL#6sqyP9W=3?CWWFk_xzUoWtIX_5i zASnK^3F#Ofmw(yxnw*QwOULlIv<8B5cDKjIJ4Ez_t`O8yQY=?VZtrp=MAOvC<>&G>-?%Gy(Yd}H0LX1&wQ6fZ`G>eL? zWs6p!%K;;EpA~Iur9@Fkn`jv=OBWGLfd)a$P&uT;jDON*#P^=_=J_VyX(2tvd5MGv z4%~C^ckajizH^m`u#?bs)D6^a2WY;0RREN`MsfU=sy$b=Pk$KBr~LKZ2E1bo!vw_x^Nd}<+ow@xPXW+((eFp3yb{xmf2vm42-Xz#hFS6EfbMV z`N^1N@7W&0aerN6{kNp0Z$L(83UX%ri?s9&NI0&`={+GI9}qE`xP)#@OKGh_cD*4$ zh^DC%b6Gx^WuV5!#zYUgF(Zc$>}JJsV7bK7`Chea-+lnf*)-4F9?Hv8k9<3oh!APl zsf#vpQi>95ieNe}^F^E0!4c&yL!B!BElL_t(|oTZapNRv?*$A4#RGM6=rv>avxF+&%WL=h&9=*zsT zi!Sw2F%r72kZ!xvAPXeuqN|9AF2Y@nEYP5c4b#y^Y|6~f-p##xx8?hEF?!+MdlBIS z2cC1z|Nnga&vTTBuoH9bhz+>^1=!bjQvg)C#Cqhls=ZgWEq{z#xm3N_>TZ^@y4(`d^*Xkt6{uHJCs?-SvXVAlbfw3vT2Y> zr!9Y5-{M(v_MGpfK7SaJ!0=sZ9vG3y)S8@&%t`aWh|~u|a(1`Z!aGFthp!RRl2RyDNNn$N zB*f)XCzcX?%oUv?HimoY)Kb!Ruz_EBgX~6;l}r7~v;P19#Y7Av5$ENVs6}!E_}W9b z{f(TIbqO>zkSml~-ZUuOd89nqs<1|C-a)oiaN%@^_#dNBBsHMr4@xOm#CcA2@;153E@z#1aCtm;n O002ovP6b4+LSTZk1r9#| From b2348403211c7fb8ce4ca277bf393922a9b438b7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 17:47:51 +0000 Subject: [PATCH 07/13] Simplify the two offset Wrench mode badges They become a cube linked to the cube at the offset position, and the same with sides instead of cubes for the mode that also saves a side. Both are outlined in black, so that they stay readable on the light backgrounds that the item is drawn on outside of inventory slots. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01LBAgcFp6jWcYRWz9N8h7AF --- .../textures/item/wrench_offset.png | Bin 542 -> 530 bytes .../textures/item/wrench_offset_side.png | Bin 527 -> 527 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/src/main/resources/assets/integrateddynamics/textures/item/wrench_offset.png b/src/main/resources/assets/integrateddynamics/textures/item/wrench_offset.png index ba11ed9645e852ec280f0b39b10eda80a2d8dfb7..943f22c8dc84b143205db5bb1ce7189cd9ad91a1 100644 GIT binary patch delta 504 zcmV&iVg)m58tx3+yQy2>uJudi90?C})e+saLB0Tz}P%Fjh!=@%_O^lIXw1 z*Smu>bR-mjYn3^Xoo2?W_%9eMr0IAHvWk@!6G4gjaLOz}&0r(#)0|NK$xT*fb% zh}8H8eSw1)`hN%~HHi=3k@kU6nV2uh`KeFRJ}@fbq$cMMggm^D#7OE2nm#AyR)yK^ zCSO4U0d;0^miNXcP$y$cst-+{m!q*}*2^}mnXFvuSNmR$S67kKFCKbw1_2s7HG+{A z&dRF9qsJ-zi;JT2HpHj}^Hv*v$u5EMJDDM7Y>Qx9QHQ!#fCt__fG$}GP uDUyh_iSHd)xy)Z%-gSikwIdzTnuOo+f%#@RGq3Uh0000dzSq%w-m{IqkM1nAB4+WWz z3i;+<0?kn9xv2Lb!ZIU}-g@q#Uh*M=5$Hn@8#PCc+BE+#&AWB)u3O$;50_nVciDpn zew=^5bH3+%zCR@*?5FwmbsF&hFQKjfh5)GYHXF0ARqej2{eQw_F7A#Wj69K!;c>p* z8=>)dhXSz8BBzsc%$pVO1(Ufry`hr;Tp1XXhc9NheJx7h)EQ+P1vk$&ixlP^pF@N)NalJZ~L*2bJb@fnQS&~1x?&f(QzDj%mpqyW0HI;M}i#qz(&tfekyq1Z` zuK1)caqw~TVM z)XI6;knoWf3PzdLUlzulr>bGa0c&Ma3=U0+18!6R<$Xdl7m{?dDQ(S-tZkL}nKy~t zf3Avi?#fZDUX{3_q^h8$8OF zejjE3sXjs{&r&Te$);)gc^bsW*=Yc(tKV4uG{5CoF)=^B6DOC|MDlLhmoY@!j^EHT()D@CuUM#o9LcLWpBoI)?zb^2q z{1a#xV|%Kfq`4>uJHz~_IPl$~bZJ2Cc{264ixgVJ0Lm8#^(1MEv~xn%BpwY@wyG?z zJ6K~=s<~+30qL77zQT=%Q9t%uxNy2xBB5qXyTaEpz-b(TD0I0n+4#y00000NkvXXu0mjfrw#59 delta 493 zcmV}H0LX1&wQ6fZ`G>eL?Ws6p!%K;;E zpA~Iur9@Fkn`jv=OBWGLfd)a$P&uT;jDON*#P^=_=J_VyX(2tvd5MGv4%~C^ckaji zzH^m`u#?bs)D6^a2WY;0RREN`MsfU=sy$b=PZ-Xp{Po@Bh<~*9-r)OClKO+K3c#~1 zj%Fs9v@5|2hVv=9qiq13yV5WBpNw+-VuH|-W6CR;KF+f(%*hR+O&a8LIe)!wukvhq z>2E1bo!vw_x^Nd}<+ow@xPXW+((eFp3yb{xmf2vm42-Xz#hFS6EfbMV`N^1N@7W&0 zab05lx1^}JJsV7bK7`Chea-+lnf*)-4F9?Hv8k9<3oh!APlsf#vpQi>95 zieNe}^F^E0!4c&y Date: Sat, 5 Sep 2026 19:01:10 +0000 Subject: [PATCH 08/13] Relate the two offset Wrench mode badges through a shared cube The offset badge becomes a cube with a line running out to the offset position, and the badge of the mode that also saves a side keeps that same cube, with the side next to it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01LBAgcFp6jWcYRWz9N8h7AF --- .../textures/item/wrench_offset.png | Bin 530 -> 537 bytes .../textures/item/wrench_offset_side.png | Bin 527 -> 523 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/src/main/resources/assets/integrateddynamics/textures/item/wrench_offset.png b/src/main/resources/assets/integrateddynamics/textures/item/wrench_offset.png index 943f22c8dc84b143205db5bb1ce7189cd9ad91a1..c5cd2d62651b23b250e83baa64771b4b0c4a987b 100644 GIT binary patch delta 511 zcmV3l6n|h{a98Nzlm-A_Sow1W8a+Y>}ijX`9x*7vJk^V&8Enc_eva zhaNa^?!DhVf4=WtB_iy_e0#bL_;&+zUcDgzsHx_`%3yi4TqO z{oV*I-EjrrSY=LRW|+0A-UJi*6a%4S09?F2Di5Ddar<(T!10sHF%1{zSY?daok9&9 zDJ;q#U3YOF5?`c008l9{Q~Z|MsaV$0zkU`woAyd3 zA`SjYU*zD0L4Se?O=6>WrE7RxrWQ(ae)@}a4UbDOp~<-e0T=HfF_gT5rq7GHRb_6w z%~KGcPn}ts+jyfdlx7jihjwP&jUytx@^YzV;0W_ajhT{_!ZSl-y=XUQUQ|A{Kk zyvInr^NvwxE(iU+J!0Bb90zi-Q>wD`179G3W7`}yivRpD`NFF5s1QtOp1pdHN5gXK ztw=l?mUuKQo_Ann)BkO`W;IScGVW$ax+7Ab;4h!E{qUT-iRJ(R002ovPDHLkV1l4T B_#^-T delta 504 zcmV1d;@hB!AgSL_t(|oTZaNNK{c2hQB*aGNaa*IWi7Y5X2buh!Qtp(ku$H zXw{;nE?Ou`LhGXTK?`#ck+dsl733-x5e$JYf|#KiNr@TF(MI#V@jah!@|_maV`d&! z;DH0@-us_>&iVg)m58tx3+yQy2>uJudi90?C})e+saLB0Tz}P%Fjh!=@%_O^lIXw1 z*Smu>bR-mjYn3^Xoo2?W_%9eMr0IAHvWk@!6G4gjaLOz}&0r(#)0|NK$xT*fb% zh}8H8eSw1)`hN%~HHi=3k@kU6nV2uh`KeFRJ}@fbq$cMMggm^D#7OE2nm#AyR)yK^ zCSO4U0d;0^miNXcP$y$cst-+{m!q*}*2^}mnXFvuSNmR$S67kKFCKbw1_2s7HG+{A z&dRF9qsJ-zi;JT2HpHj}^Hv*v$u5EMJDDM7Y>Qx9QHQ!#fCt__fG$}GP uDUyh_iSHd)xy)Z%-gSikwIdzTnuOo+f%#@RGq3Uh0000-B z-TG+EvHwCZ&3_49;)8dky?o7~%8y z5PM@?3c#}~9GQR3oL$oeCJVFlMGgUQ@!GK5d64GT#fZDUX{ z3_q^h8$8OFejjE3sXjs{&r&Te$);)gc^bsW*=Yc(tKV4uG{5CoF)=^B6DOC|MDlLhmoY@!j^EHT()D@CuUM#o9LcLWp zBoI)?zb^2q{1a#xV|%Kfq`4>uJHz~_IPl$~bZJ2Cc{264ixgVJ0Lm8#^(1MEv~xn% zBpwY@wyG?zJ6K~=s<~+30qL77zQT=%Q9t%uxNy2xB5t8(OuNSVhD&1No|?|Qz%}xO z-_L093dNN_6#%%Kb-t7wJP(ZcQI#7XBM=PYISy`Hr+WG3IrnFtX-G7Pp`K&dxvUNk r=#E9TN;H9gt?pP<8d>aEpz-b(TD0I0n+4#y00000NkvXXu0mjfw)OG7 From e55e3cf365e18390988812cb9315a9ec0063ec36 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 19:18:02 +0000 Subject: [PATCH 09/13] Draw the offset line of the Wrench mode badge as bare pixels Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01LBAgcFp6jWcYRWz9N8h7AF --- .../textures/item/wrench_offset.png | Bin 537 -> 512 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/src/main/resources/assets/integrateddynamics/textures/item/wrench_offset.png b/src/main/resources/assets/integrateddynamics/textures/item/wrench_offset.png index c5cd2d62651b23b250e83baa64771b4b0c4a987b..cbe58abd36348be1058ec12c9bf82d182b60a20a 100644 GIT binary patch delta 486 zcmVO#uiXvV|}6MKXJ-(g_N+!QT2Gs- zORO=1VHH{Z?O+X#tD`F!T;SI8UjV$m+gSb~06U$conH-UsHW5*$Um45bRitqe0P<3?~TY_T@_qyh|k9V ciTiJnzq!CBbh_eCF8}}l07*qoM6N<$g5&||)c^nh delta 511 zcmV3l6n|h{a98Nzlm-A_Sow1W8a+Y>}ijX`9x*7vJk^V&8Enc_eva zhaNa^?!DhVf4=WtB_iy_e0#bL_;&+zUcDgzsHx_`%3yi4TqO z{oV*I-EjrrSY=LRW|+0A-UJi*6a%4S09?F2Di5Ddar<(T!10sHF%1{zSY?daok9&9 zDJ;q#U3YOF5?`c008l9{Q~Z|MsaV$0zkU`woAyd3 zA`SjYU*zD0L4Se?O=6>WrE7RxrWQ(ae)@}a4UbDOp~<-e0T=HfF_gT5rq7GHRb_6w z%~KGcPn}ts+jyfdlx7jihjwP&jUytx@^YzV;0W_ajhT{_!ZSl-y=XUQUQ|A{Kk zyvInr^NvwxE(iU+J!0Bb90zi-Q>wD`179G3W7`}yivRpD`NFF5s1QtOp1pdHN5gXK ztw=l?mUuKQo_Ann)BkO`W;IScGVW$ax+7Ab;4h!E{qUT-iRJ(R002ovPDHLkV1j&B B_!0mB From 74649c121351a8539369c8506b65c4af4d344efd Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 10 Sep 2026 04:54:14 +0000 Subject: [PATCH 10/13] Act on the review of the Wrench part configuration copying Copy the offset enhancements of a part as well, consuming enhancements from the player just like the variable cards do, and show what a paste needs in the Wrench tooltip. Drop the unused Wrench lookup that was left over from the gui buttons, as the Wrench that a player acts with always comes from the interaction itself. Move the wrench mode item property into an item client config, and replace the lombok accessors of the apply result by plain ones. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01LBAgcFp6jWcYRWz9N8h7AF --- CHANGELOG-1.21.1.md | 1 + .../gametest/GameTestsWrenchConfig.java | 116 +++++++++++++++++- .../core/helper/PartConfigHelpers.java | 114 +++++++++++++---- .../core/part/PartConfigApplyResult.java | 78 +++++++++++- .../core/part/PartConfigSnapshot.java | 25 +++- .../integrateddynamics/item/ItemWrench.java | 5 + .../item/ItemWrenchClientConfig.java | 34 +++++ .../item/ItemWrenchConfig.java | 18 ++- .../integrateddynamics/proxy/ClientProxy.java | 18 --- .../assets/integrateddynamics/lang/en_us.json | 5 +- .../core/part/TestPartConfigSnapshot.java | 33 ++++- 11 files changed, 387 insertions(+), 60 deletions(-) create mode 100644 src/main/java/org/cyclops/integrateddynamics/item/ItemWrenchClientConfig.java diff --git a/CHANGELOG-1.21.1.md b/CHANGELOG-1.21.1.md index ab8c9def71d..ac9ad0676a9 100644 --- a/CHANGELOG-1.21.1.md +++ b/CHANGELOG-1.21.1.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. * Allow part configurations to be copied and pasted with the Wrench, Closes #859 * The Wrench gets a Copy All, Copy Settings and Copy Aspect mode * The active Wrench mode is shown on the Wrench item + * Pasting also raises the maximum offset of a part, by consuming Offset Enhancements * Allow aspect settings to be determined by variables (#1707), Closes CyclopsMC/IntegratedTunnels#278 * Show modified aspect property values in tooltip (#1706), Closes #1704 diff --git a/src/integrationtest/java/org/cyclops/integrateddynamics/gametest/GameTestsWrenchConfig.java b/src/integrationtest/java/org/cyclops/integrateddynamics/gametest/GameTestsWrenchConfig.java index 65ab6da4284..f72f248c720 100644 --- a/src/integrationtest/java/org/cyclops/integrateddynamics/gametest/GameTestsWrenchConfig.java +++ b/src/integrationtest/java/org/cyclops/integrateddynamics/gametest/GameTestsWrenchConfig.java @@ -40,6 +40,7 @@ import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypes; import javax.annotation.Nullable; +import java.util.Set; import static org.cyclops.integrateddynamics.gametest.GameTestHelpersIntegratedDynamics.createVariableForValue; import static org.cyclops.integrateddynamics.gametest.GameTestHelpersIntegratedDynamics.getEffectiveAspectProperty; @@ -117,16 +118,22 @@ protected static void configurePart(GameTestHelper helper, PartPos partPos, Vec3 AspectWriteBuilders.Redstone.PROP_STRONG_POWER, ValueTypeBoolean.ValueBoolean.of(true)); } - @SuppressWarnings({"unchecked", "rawtypes"}) protected static PartConfigApplyResult applyConfig(GameTestHelper helper, PartPos partPos, PartConfigSnapshot snapshot, Player player) { + return applyConfigSections(helper, partPos, snapshot, player, PartConfigSection.ALL); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + protected static PartConfigApplyResult applyConfigSections(GameTestHelper helper, PartPos partPos, + PartConfigSnapshot snapshot, Player player, + Set sections) { IPartType partType = partType(partPos); IPartState state = partState(partPos); INetwork network = NetworkHelpers.getNetwork(partPos).orElse(null); PartTarget target = partType.getTarget(partPos, state); return partType.applyConfig(ValueDeseralizationContext.of(helper.getLevel()), network, NetworkHelpers.getPartNetwork(network).orElse(null), target, state, snapshot, - PartConfigSection.ALL, player); + sections, player); } @SuppressWarnings({"unchecked", "rawtypes"}) @@ -177,6 +184,16 @@ protected static void giveBlankVariables(Player player, int count) { player.getInventory().add(new ItemStack(RegistryEntries.ITEM_VARIABLE.value(), count)); } + protected static void giveOffsetEnhancements(Player player, int count, int value) { + ItemStack itemStack = new ItemStack(RegistryEntries.ITEM_ENHANCEMENT_OFFSET.value(), count); + RegistryEntries.ITEM_ENHANCEMENT_OFFSET.value().setEnhancementValue(itemStack, value); + player.getInventory().add(itemStack); + } + + protected static int countOffsetEnhancements(Player player) { + return PartConfigHelpers.countOffsetEnhancements(player); + } + @GameTest(template = TEMPLATE_EMPTY) public void testWrenchConfigCopyPasteSamePartType(GameTestHelper helper) { PartPos source = placePart(helper, POS_SOURCE, PartTypes.REDSTONE_WRITER); @@ -405,6 +422,101 @@ public void testWrenchConfigPasteOffsetOutOfRange(GameTestHelper helper) { }); } + @GameTest(template = TEMPLATE_EMPTY) + public void testWrenchConfigPasteMaxOffsetConsumesEnhancements(GameTestHelper helper) { + PartPos source = placePart(helper, POS_SOURCE, PartTypes.REDSTONE_WRITER); + PartPos target = placePart(helper, POS_TARGET, PartTypes.REDSTONE_WRITER); + // This gives the source part a maximum offset of 4, and an offset within it + configurePart(helper, source, new Vec3i(3, 0, 0)); + + Player player = helper.makeMockPlayer(GameType.SURVIVAL); + giveOffsetEnhancements(player, 1, 4); + PartConfigApplyResult result = applyConfig(helper, target, snapshotConfig(helper, source), player); + + helper.succeedWhen(() -> { + helper.assertValueEqual(result.getAppliedMaxOffset(), 4, "The maximum offset was not raised"); + helper.assertValueEqual(result.getMissingMaxOffset(), 0, "Offset enhancements were reported as missing"); + helper.assertValueEqual(partState(target).getMaxOffset(), 4, + "The target part did not receive the maximum offset"); + helper.assertValueEqual(countOffsetEnhancements(player), 0, "The offset enhancement was not consumed"); + helper.assertTrue(!result.isOffsetFailed(), "The offset could not be applied"); + helper.assertValueEqual(((IPartType) partType(target)).getTargetOffset(partState(target)), + new Vec3i(3, 0, 0), "The offset was not pasted"); + }); + } + + @GameTest(template = TEMPLATE_EMPTY) + public void testWrenchConfigPasteMaxOffsetWithoutEnhancements(GameTestHelper helper) { + PartPos source = placePart(helper, POS_SOURCE, PartTypes.REDSTONE_WRITER); + PartPos target = placePart(helper, POS_TARGET, PartTypes.REDSTONE_WRITER); + configurePart(helper, source, new Vec3i(3, 0, 0)); + + Player player = helper.makeMockPlayer(GameType.SURVIVAL); + giveOffsetEnhancements(player, 1, 1); + PartConfigApplyResult result = applyConfig(helper, target, snapshotConfig(helper, source), player); + + helper.succeedWhen(() -> { + helper.assertValueEqual(result.getMissingMaxOffset(), 3, "The missing offset value was not reported"); + helper.assertValueEqual(result.getAppliedMaxOffset(), 0, "The maximum offset was raised anyway"); + helper.assertValueEqual(partState(target).getMaxOffset(), 0, "The target part received a maximum offset"); + helper.assertValueEqual(countOffsetEnhancements(player), 1, "The offset enhancement was consumed anyway"); + helper.assertTrue(result.isOffsetFailed(), "The offset failure was not reported"); + }); + } + + @GameTest(template = TEMPLATE_EMPTY) + public void testWrenchConfigPasteMaxOffsetCreativeWithoutEnhancements(GameTestHelper helper) { + PartPos source = placePart(helper, POS_SOURCE, PartTypes.REDSTONE_WRITER); + PartPos target = placePart(helper, POS_TARGET, PartTypes.REDSTONE_WRITER); + configurePart(helper, source, new Vec3i(3, 0, 0)); + + Player player = helper.makeMockPlayer(GameType.CREATIVE); + PartConfigApplyResult result = applyConfig(helper, target, snapshotConfig(helper, source), player); + + helper.succeedWhen(() -> { + helper.assertValueEqual(result.getAppliedMaxOffset(), 4, "The maximum offset was not raised"); + helper.assertValueEqual(partState(target).getMaxOffset(), 4, + "The target part did not receive the maximum offset"); + }); + } + + @GameTest(template = TEMPLATE_EMPTY) + public void testWrenchConfigPasteMaxOffsetKeepsHigherOne(GameTestHelper helper) { + PartPos source = placePart(helper, POS_SOURCE, PartTypes.REDSTONE_WRITER); + PartPos target = placePart(helper, POS_TARGET, PartTypes.REDSTONE_WRITER); + configurePart(helper, source, new Vec3i(3, 0, 0)); + GameTestsOffsets.increaseMaxOffset(helper, target, 8); + + Player player = helper.makeMockPlayer(GameType.SURVIVAL); + giveOffsetEnhancements(player, 1, 4); + PartConfigApplyResult result = applyConfig(helper, target, snapshotConfig(helper, source), player); + + helper.succeedWhen(() -> { + helper.assertValueEqual(result.getAppliedMaxOffset(), 0, "The maximum offset was raised"); + helper.assertValueEqual(partState(target).getMaxOffset(), 8, "The maximum offset was lowered"); + helper.assertValueEqual(countOffsetEnhancements(player), 4, "An offset enhancement was consumed"); + }); + } + + @GameTest(template = TEMPLATE_EMPTY) + public void testWrenchConfigAspectModeSkipsMaxOffset(GameTestHelper helper) { + PartPos source = placePart(helper, POS_SOURCE, PartTypes.REDSTONE_WRITER); + PartPos target = placePart(helper, POS_TARGET, PartTypes.REDSTONE_WRITER); + configurePart(helper, source, new Vec3i(3, 0, 0)); + + Player player = helper.makeMockPlayer(GameType.SURVIVAL); + giveOffsetEnhancements(player, 1, 4); + PartConfigSnapshot snapshot = snapshotConfig(helper, source); + PartConfigApplyResult result = applyConfigSections(helper, target, snapshot, player, + ItemWrench.Mode.CONFIG_ASPECT.getConfigSections()); + + helper.succeedWhen(() -> { + helper.assertValueEqual(result.getAppliedMaxOffset(), 0, "The maximum offset was raised"); + helper.assertValueEqual(partState(target).getMaxOffset(), 0, "The target part received a maximum offset"); + helper.assertValueEqual(countOffsetEnhancements(player), 4, "An offset enhancement was consumed"); + }); + } + @GameTest(template = TEMPLATE_EMPTY) public void testWrenchConfigSettingsModeOnlyCopiesPartSettings(GameTestHelper helper) { PartPos source = placePart(helper, POS_SOURCE, PartTypes.REDSTONE_WRITER); diff --git a/src/main/java/org/cyclops/integrateddynamics/core/helper/PartConfigHelpers.java b/src/main/java/org/cyclops/integrateddynamics/core/helper/PartConfigHelpers.java index bb97ba57113..c27eb6673c1 100644 --- a/src/main/java/org/cyclops/integrateddynamics/core/helper/PartConfigHelpers.java +++ b/src/main/java/org/cyclops/integrateddynamics/core/helper/PartConfigHelpers.java @@ -6,12 +6,12 @@ import net.minecraft.core.NonNullList; import net.minecraft.core.Vec3i; import net.minecraft.nbt.CompoundTag; -import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.Containers; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import org.cyclops.cyclopscore.inventory.SimpleInventory; +import org.cyclops.integrateddynamics.GeneralConfig; import org.cyclops.integrateddynamics.IntegratedDynamics; import org.cyclops.integrateddynamics.RegistryEntries; import org.cyclops.integrateddynamics.api.evaluate.variable.ValueDeseralizationContext; @@ -36,12 +36,12 @@ import org.cyclops.integrateddynamics.core.part.PartTypeAspects; import org.cyclops.integrateddynamics.core.part.aspect.property.AspectProperties; import org.cyclops.integrateddynamics.core.persist.world.LabelsWorldStorage; -import org.cyclops.integrateddynamics.item.ItemWrench; import org.cyclops.integrateddynamics.part.aspect.Aspects; import javax.annotation.Nullable; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Optional; @@ -146,7 +146,8 @@ protected static PartConfigSnapshot.PartSettings snapshotPartSettings(IPartType partType.getPriority(state) == 0 ? Optional.empty() : Optional.of(partType.getPriority(state)), partType.getChannel(state) == 0 ? Optional.empty() : Optional.of(partType.getChannel(state)), Optional.ofNullable(partType.getTargetSideOverride(state)), - targetOffset.equals(Vec3i.ZERO) ? Optional.empty() : Optional.of(targetOffset)); + targetOffset.equals(Vec3i.ZERO) ? Optional.empty() : Optional.of(targetOffset), + state.getMaxOffset() == 0 ? Optional.empty() : Optional.of(state.getMaxOffset())); } /** @@ -185,7 +186,7 @@ public static PartConfigApplyResult apply(ValueDeseralizationContext valueDesera PartConfigApplyResult result = new PartConfigApplyResult(); if (sections.contains(PartConfigSection.PART_SETTINGS) && snapshot.partSettings().isPresent()) { - applyPartSettings(network, target, partType, state, snapshot.partSettings().get(), result); + applyPartSettings(network, target, partType, state, snapshot.partSettings().get(), player, result); } if (sections.contains(PartConfigSection.ASPECT)) { applyAspectProperties(valueDeseralizationContext, target, partType, state, snapshot, result); @@ -203,10 +204,12 @@ public static PartConfigApplyResult apply(ValueDeseralizationContext valueDesera @SuppressWarnings("unchecked") protected static void applyPartSettings(@Nullable INetwork network, PartTarget target, IPartType partType, IPartState state, PartConfigSnapshot.PartSettings settings, - PartConfigApplyResult result) { + Player player, PartConfigApplyResult result) { settings.updateInterval().ifPresent(updateInterval -> partType.setUpdateInterval(state, Math.max(partType.getMinimumUpdateInterval(state), updateInterval))); settings.targetSide().ifPresent(targetSide -> partType.setTargetSideOverride(state, targetSide)); + // Before the target offset, as that one is bounded by the maximum offset + settings.maxOffset().ifPresent(maxOffset -> applyMaxOffset(partType, state, maxOffset, player, result)); settings.targetOffset().ifPresent(targetOffset -> { if (!partType.setTargetOffset(state, target.getCenter(), targetOffset)) { result.setOffsetFailed(true); @@ -227,6 +230,43 @@ protected static void applyPartSettings(@Nullable INetwork network, PartTarget t state.sendUpdate(); } + /** + * Raise the maximum offset of the given part to the given value, + * by consuming offset enhancements from the player. + * + * Enhancements can not be split, so the player can end up spending a bit more value than needed. + * + * @param partType The part type. + * @param state The part state. + * @param maxOffset The maximum offset to raise the part to. + * @param player The player that is pasting. + * @param result The outcome to report into. + */ + protected static void applyMaxOffset(IPartType partType, IPartState state, int maxOffset, + Player player, PartConfigApplyResult result) { + if (!partType.supportsOffsets()) { + return; + } + int required = Math.min(maxOffset, GeneralConfig.maxPartOffset) - state.getMaxOffset(); + if (required <= 0) { + return; + } + + int consumed = required; + if (!player.isCreative()) { + int available = countOffsetEnhancements(player); + if (available < required) { + result.setMissingMaxOffset(required - available); + return; + } + consumed = consumeOffsetEnhancements(player, required); + } + + int before = state.getMaxOffset(); + state.setMaxOffset(Math.min(before + consumed, GeneralConfig.maxPartOffset)); + result.setAppliedMaxOffset(state.getMaxOffset() - before); + } + @SuppressWarnings("unchecked") protected static void applyAspectProperties(ValueDeseralizationContext valueDeseralizationContext, PartTarget target, IPartType partType, IPartState state, PartConfigSnapshot snapshot, @@ -513,23 +553,60 @@ public static void giveOrDrop(Player player, ItemStack itemStack) { } /** - * Find the Wrench that the given player is holding or carrying. * @param player A player. - * @return The Wrench, or empty if the player has none. + * @return The total offset enhancement value in the inventory of the given player. */ - public static Optional findWrench(Player player) { - for (ItemStack itemStack : List.of(player.getMainHandItem(), player.getOffhandItem())) { - if (itemStack.getItem() instanceof ItemWrench) { - return Optional.of(itemStack); - } + public static int countOffsetEnhancements(Player player) { + int count = 0; + for (int slot = 0; slot < player.getInventory().getContainerSize(); slot++) { + count += getOffsetEnhancementValue(player.getInventory().getItem(slot)) + * player.getInventory().getItem(slot).getCount(); } + return count; + } + + /** + * Remove offset enhancements from the inventory of the given player, + * starting at the smallest ones so that as little value as possible is wasted. + * + * @param player A player. + * @param value The offset enhancement value to remove. + * @return The removed value, which can be higher than requested when enhancements did not add up exactly. + */ + public static int consumeOffsetEnhancements(Player player, int value) { + List slots = Lists.newArrayList(); for (int slot = 0; slot < player.getInventory().getContainerSize(); slot++) { + if (getOffsetEnhancementValue(player.getInventory().getItem(slot)) > 0) { + slots.add(slot); + } + } + slots.sort(Comparator.comparingInt(slot -> getOffsetEnhancementValue(player.getInventory().getItem(slot)))); + + int consumed = 0; + for (int slot : slots) { ItemStack itemStack = player.getInventory().getItem(slot); - if (itemStack.getItem() instanceof ItemWrench) { - return Optional.of(itemStack); + int enhancementValue = getOffsetEnhancementValue(itemStack); + while (consumed < value && !itemStack.isEmpty()) { + itemStack.shrink(1); + consumed += enhancementValue; + } + if (itemStack.isEmpty()) { + player.getInventory().setItem(slot, ItemStack.EMPTY); + } + if (consumed >= value) { + break; } } - return Optional.empty(); + return consumed; + } + + /** + * @param itemStack An item stack. + * @return The offset that the given stack enhances a part by, or zero if it is not an offset enhancement. + */ + public static int getOffsetEnhancementValue(ItemStack itemStack) { + return itemStack.is(RegistryEntries.ITEM_ENHANCEMENT_OFFSET.get()) + ? RegistryEntries.ITEM_ENHANCEMENT_OFFSET.get().getEnhancementValue(itemStack) : 0; } /** @@ -552,11 +629,4 @@ public static void setSnapshot(HolderLookup.Provider provider, ItemStack wrench, wrench.set(RegistryEntries.DATACOMPONENT_WRENCH_PART_CONFIG.get(), snapshot.toNBT(provider)); } - /** - * @return A message telling the player that they need a Wrench to copy or paste a configuration. - */ - public static Component getNoWrenchMessage() { - return Component.translatable("gui.integrateddynamics.config.nowrench"); - } - } diff --git a/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigApplyResult.java b/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigApplyResult.java index 3feda113cd9..867d610e58a 100644 --- a/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigApplyResult.java +++ b/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigApplyResult.java @@ -1,8 +1,6 @@ package org.cyclops.integrateddynamics.core.part; import com.google.common.collect.Lists; -import lombok.Getter; -import lombok.Setter; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.MutableComponent; @@ -12,36 +10,96 @@ * The outcome of pasting a {@link PartConfigSnapshot} onto a part. * @author rubensworks */ -@Getter public class PartConfigApplyResult { - @Setter private boolean partSettingsApplied = false; - @Setter private boolean offsetFailed = false; private int appliedProperties = 0; private int skippedProperties = 0; private int cardsPasted = 0; private int cardsSkipped = 0; - @Setter private int missingBlanks = 0; + private int appliedMaxOffset = 0; + private int missingMaxOffset = 0; + + public boolean isPartSettingsApplied() { + return this.partSettingsApplied; + } + + public void setPartSettingsApplied(boolean partSettingsApplied) { + this.partSettingsApplied = partSettingsApplied; + } + + public boolean isOffsetFailed() { + return this.offsetFailed; + } + + public void setOffsetFailed(boolean offsetFailed) { + this.offsetFailed = offsetFailed; + } + + public int getAppliedProperties() { + return this.appliedProperties; + } public void addAppliedProperties(int amount) { this.appliedProperties += amount; } + public int getSkippedProperties() { + return this.skippedProperties; + } + public void addSkippedProperties(int amount) { this.skippedProperties += amount; } + public int getCardsPasted() { + return this.cardsPasted; + } + public void addCardsPasted(int amount) { this.cardsPasted += amount; } + public int getCardsSkipped() { + return this.cardsSkipped; + } + public void addCardsSkipped(int amount) { this.cardsSkipped += amount; } + public int getMissingBlanks() { + return this.missingBlanks; + } + + public void setMissingBlanks(int missingBlanks) { + this.missingBlanks = missingBlanks; + } + + /** + * @return By how much the maximum offset of the part was increased. + */ + public int getAppliedMaxOffset() { + return this.appliedMaxOffset; + } + + public void setAppliedMaxOffset(int appliedMaxOffset) { + this.appliedMaxOffset = appliedMaxOffset; + } + + /** + * @return The offset enhancement value that the player was short of. + */ + public int getMissingMaxOffset() { + return this.missingMaxOffset; + } + + public void setMissingMaxOffset(int missingMaxOffset) { + this.missingMaxOffset = missingMaxOffset; + } + /** * @return A single line summarising what was applied. */ @@ -59,6 +117,10 @@ public MutableComponent getMessage() { applied.add(Component.translatable("item.integrateddynamics.wrench.mode.config.pasted.variable_cards", this.cardsPasted)); } + if (this.appliedMaxOffset > 0) { + applied.add(Component.translatable("item.integrateddynamics.wrench.mode.config.pasted.max_offset", + this.appliedMaxOffset)); + } if (applied.isEmpty()) { return Component.translatable("item.integrateddynamics.wrench.mode.config.pasted.nothing"); } @@ -82,6 +144,10 @@ public List getWarnings() { if (this.offsetFailed) { warnings.add(Component.translatable("item.integrateddynamics.wrench.mode.offset.fail")); } + if (this.missingMaxOffset > 0) { + warnings.add(Component.translatable("item.integrateddynamics.wrench.mode.config.enhancements_missing", + this.missingMaxOffset)); + } if (this.cardsSkipped > 0) { warnings.add(Component.translatable("item.integrateddynamics.wrench.mode.config.cards_skipped", this.cardsSkipped, this.missingBlanks)); diff --git a/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigSnapshot.java b/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigSnapshot.java index fb2a4c45d66..e649a38586b 100644 --- a/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigSnapshot.java +++ b/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigSnapshot.java @@ -21,7 +21,7 @@ * An immutable snapshot of the configuration of a part, which can be pasted onto another part. * * Only things that a player can configure are stored, - * so no part id, max offset, enabled state, error messages or active aspect. + * so no part id, enabled state, error messages or active aspect. * * Only values that differ from the defaults of the copied part are stored, * so that pasting only overwrites what was deliberately configured. @@ -53,7 +53,8 @@ public record PartConfigSnapshot(int version, Codec.INT.optionalFieldOf("priority").forGetter(PartSettings::priority), Codec.INT.optionalFieldOf("channel").forGetter(PartSettings::channel), Direction.CODEC.optionalFieldOf("targetSide").forGetter(PartSettings::targetSide), - Vec3i.CODEC.optionalFieldOf("targetOffset").forGetter(PartSettings::targetOffset) + Vec3i.CODEC.optionalFieldOf("targetOffset").forGetter(PartSettings::targetOffset), + Codec.INT.optionalFieldOf("maxOffset").forGetter(PartSettings::maxOffset) ) .apply(builder, PartSettings::new)); @@ -89,6 +90,20 @@ public List getVariableCards(Set sections) { .toList(); } + /** + * The offset enhancements that a part holds can not be taken out again without breaking the part, + * so pasting them has to consume enhancements from the player, just like the variable cards do. + * + * @param sections The sections that will be pasted. + * @return The offset enhancement value that pasting those sections needs at most. + */ + public int getRequiredMaxOffset(Set sections) { + if (!sections.contains(PartConfigSection.PART_SETTINGS)) { + return 0; + } + return partSettings().flatMap(PartSettings::maxOffset).orElse(0); + } + /** * @param sections The sections that will be pasted. * @return The number of blank Variable Cards that pasting those sections needs at most. @@ -157,16 +172,18 @@ public static Optional fromNBT(HolderLookup.Provider provide * @param channel The channel of the part in its network. * @param targetSide The overridden side of the target block, if any. * @param targetOffset The target position offset. + * @param maxOffset The maximum offset that offset enhancements raised the part to. */ public record PartSettings(Optional updateInterval, Optional priority, Optional channel, - Optional targetSide, Optional targetOffset) { + Optional targetSide, Optional targetOffset, + Optional maxOffset) { /** * @return If no setting at all is stored. */ public boolean isEmpty() { return updateInterval().isEmpty() && priority().isEmpty() && channel().isEmpty() - && targetSide().isEmpty() && targetOffset().isEmpty(); + && targetSide().isEmpty() && targetOffset().isEmpty() && maxOffset().isEmpty(); } } diff --git a/src/main/java/org/cyclops/integrateddynamics/item/ItemWrench.java b/src/main/java/org/cyclops/integrateddynamics/item/ItemWrench.java index 3c9bd21224a..25f0c03c8a8 100644 --- a/src/main/java/org/cyclops/integrateddynamics/item/ItemWrench.java +++ b/src/main/java/org/cyclops/integrateddynamics/item/ItemWrench.java @@ -188,6 +188,11 @@ public void appendHoverText(ItemStack itemStack, Item.TooltipContext context, Li list.add(Component.translatable("item.integrateddynamics.wrench.mode.config.requires", requiredBlanks).withStyle(ChatFormatting.GOLD)); } + int requiredMaxOffset = snapshot.getRequiredMaxOffset(sections); + if (requiredMaxOffset > 0) { + list.add(Component.translatable("item.integrateddynamics.wrench.mode.config.requires_enhancements", + requiredMaxOffset).withStyle(ChatFormatting.GOLD)); + } }); } // Hidden behind the same shift that reveals the item info, to keep the resting tooltip short diff --git a/src/main/java/org/cyclops/integrateddynamics/item/ItemWrenchClientConfig.java b/src/main/java/org/cyclops/integrateddynamics/item/ItemWrenchClientConfig.java new file mode 100644 index 00000000000..759f7e13e3c --- /dev/null +++ b/src/main/java/org/cyclops/integrateddynamics/item/ItemWrenchClientConfig.java @@ -0,0 +1,34 @@ +package org.cyclops.integrateddynamics.item; + +import net.minecraft.client.renderer.item.ItemProperties; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent; +import org.cyclops.cyclopscore.config.extendedconfig.ItemClientConfig; +import org.cyclops.cyclopscore.config.extendedconfig.ItemConfigCommon; +import org.cyclops.integrateddynamics.IntegratedDynamics; +import org.cyclops.integrateddynamics.Reference; + +/** + * Client-side config for {@link ItemWrench}. + * @author rubensworks + */ +public class ItemWrenchClientConfig extends ItemClientConfig { + + public ItemWrenchClientConfig(ItemConfigCommon itemConfig) { + super(itemConfig); + itemConfig.getMod().getModEventBus().addListener(this::onClientSetup); + } + + public void onClientSetup(FMLClientSetupEvent event) { + // Show the active wrench mode on the item, by picking a model variant for it + event.enqueueWork(() -> ItemProperties.register(getItemConfig().getInstance(), + ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "wrench_mode"), + (itemStack, level, entity, seed) -> { + Item item = itemStack.getItem(); + // Values are divided by ten to fit the clamped 0 to 1 range that item properties have + return item instanceof ItemWrench itemWrench ? itemWrench.getMode(itemStack).ordinal() / 10F : 0F; + })); + } + +} diff --git a/src/main/java/org/cyclops/integrateddynamics/item/ItemWrenchConfig.java b/src/main/java/org/cyclops/integrateddynamics/item/ItemWrenchConfig.java index afddbc06ecf..8a15fa7f72d 100644 --- a/src/main/java/org/cyclops/integrateddynamics/item/ItemWrenchConfig.java +++ b/src/main/java/org/cyclops/integrateddynamics/item/ItemWrenchConfig.java @@ -1,14 +1,19 @@ package org.cyclops.integrateddynamics.item; import net.minecraft.world.item.Item; -import org.cyclops.cyclopscore.config.extendedconfig.ItemConfig; +import org.cyclops.cyclopscore.config.extendedconfig.ItemClientConfig; +import org.cyclops.cyclopscore.config.extendedconfig.ItemConfigCommon; import org.cyclops.integrateddynamics.IntegratedDynamics; +import org.jetbrains.annotations.Nullable; /** * Config for a wrench. * @author rubensworks */ -public class ItemWrenchConfig extends ItemConfig { +public class ItemWrenchConfig extends ItemConfigCommon { + + @Nullable + private ItemWrenchClientConfig clientConfig; public ItemWrenchConfig() { super( @@ -19,4 +24,13 @@ public ItemWrenchConfig() { ); } + @Override + @Nullable + public ItemClientConfig getItemClientConfig() { + if (this.clientConfig == null && getMod().getModHelpers().getMinecraftHelpers().isClientSide()) { + this.clientConfig = new ItemWrenchClientConfig(this); + } + return this.clientConfig; + } + } diff --git a/src/main/java/org/cyclops/integrateddynamics/proxy/ClientProxy.java b/src/main/java/org/cyclops/integrateddynamics/proxy/ClientProxy.java index 8129e9992bb..cdfc229171c 100644 --- a/src/main/java/org/cyclops/integrateddynamics/proxy/ClientProxy.java +++ b/src/main/java/org/cyclops/integrateddynamics/proxy/ClientProxy.java @@ -2,29 +2,23 @@ import com.mojang.blaze3d.platform.InputConstants; import net.minecraft.client.KeyMapping; -import net.minecraft.client.renderer.item.ItemProperties; import net.minecraft.client.renderer.texture.TextureAtlas; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.item.Item; import net.neoforged.neoforge.client.event.ClientPlayerNetworkEvent; import net.neoforged.neoforge.client.event.RegisterKeyMappingsEvent; import net.neoforged.neoforge.client.event.TextureAtlasStitchedEvent; import net.neoforged.neoforge.client.settings.KeyConflictContext; import net.neoforged.neoforge.client.settings.KeyModifier; -import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent; import net.neoforged.neoforge.common.NeoForge; import org.cyclops.cyclopscore.client.key.IKeyRegistry; import org.cyclops.cyclopscore.init.ModBase; import org.cyclops.cyclopscore.proxy.ClientProxyComponent; import org.cyclops.integrateddynamics.IntegratedDynamics; import org.cyclops.integrateddynamics.Reference; -import org.cyclops.integrateddynamics.RegistryEntries; import org.cyclops.integrateddynamics.client.render.level.PartOffsetsOverlayRenderer; import org.cyclops.integrateddynamics.core.inventory.container.slot.SlotVariable; import org.cyclops.integrateddynamics.core.network.diagnostics.NetworkDataClient; import org.cyclops.integrateddynamics.core.network.diagnostics.NetworkDiagnosticsPartOverlayRenderer; import org.cyclops.integrateddynamics.core.network.diagnostics.http.DiagnosticsWebServer; -import org.cyclops.integrateddynamics.item.ItemWrench; import org.lwjgl.glfw.GLFW; /** @@ -49,21 +43,9 @@ public class ClientProxy extends ClientProxyComponent { public ClientProxy() { super(new CommonProxy()); IntegratedDynamics._instance.getModEventBus().addListener(this::onPostTextureStitch); - IntegratedDynamics._instance.getModEventBus().addListener(this::onClientSetup); NeoForge.EVENT_BUS.addListener(this::onPlayerLoggedOut); } - public void onClientSetup(FMLClientSetupEvent event) { - // Show the active wrench mode on the item, by picking a model variant for it - event.enqueueWork(() -> ItemProperties.register(RegistryEntries.ITEM_WRENCH.value(), - ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "wrench_mode"), - (itemStack, level, entity, seed) -> { - Item item = itemStack.getItem(); - // Values are divided by ten to fit the clamped 0 to 1 range that item properties have - return item instanceof ItemWrench itemWrench ? itemWrench.getMode(itemStack).ordinal() / 10F : 0F; - })); - } - @Override public ModBase getMod() { return IntegratedDynamics._instance; diff --git a/src/main/resources/assets/integrateddynamics/lang/en_us.json b/src/main/resources/assets/integrateddynamics/lang/en_us.json index 55cb481e99c..3ff505c21ec 100644 --- a/src/main/resources/assets/integrateddynamics/lang/en_us.json +++ b/src/main/resources/assets/integrateddynamics/lang/en_us.json @@ -171,8 +171,11 @@ "item.integrateddynamics.wrench.mode.config.pasted.part_settings": "settings", "item.integrateddynamics.wrench.mode.config.pasted.aspect_properties": "aspect settings (%s)", "item.integrateddynamics.wrench.mode.config.pasted.variable_cards": "variable cards (%s)", + "item.integrateddynamics.wrench.mode.config.pasted.max_offset": "offset enhancements (%s)", "item.integrateddynamics.wrench.mode.config.nothing": "This part has no configuration to copy, everything is still at its default", "item.integrateddynamics.wrench.mode.config.requires": "Blank Variable Cards needed: %s", + "item.integrateddynamics.wrench.mode.config.requires_enhancements": "Offset Enhancement value needed: %s", + "item.integrateddynamics.wrench.mode.config.enhancements_missing": "Offset Enhancement value still needed: %s", "item.integrateddynamics.wrench.mode.config.empty": "No matching configuration was copied into this Wrench yet", "item.integrateddynamics.wrench.mode.config.mismatch": "This configuration was copied from a different part: %s", "item.integrateddynamics.wrench.mode.config.cards_skipped": "Variable cards skipped: %s. Blank Variable Cards still needed: %s", @@ -1862,7 +1865,7 @@ "info_book.integrateddynamics.manual.parts.settings.text3": "The Ticks/Operation allows you to configure the ticking frequency of this part. The higher, the slower it operates.", "info_book.integrateddynamics.manual.parts.settings.text4": "The Priority determines the order in which this part is executed within a single network tick.", "info_book.integrateddynamics.manual.parts.settings.text5": "The Energy Channel indicates the channel from which energy should be consumed when this part ticks. This is only applicable if energy consumption for networks is enabled.", - "info_book.integrateddynamics.manual.parts.settings.text6": "The configuration of a part can be copied to other parts using a &lWrench&r. Shift+right-clicking a part copies it into the Wrench, and right-clicking another part pastes it. The Wrench mode picks what is copied: &lCopy All&r takes everything and only pastes onto a part of the same type, &lCopy Settings&r takes the update interval, priority, channel, side and offsets, and &lCopy Aspect&r takes the active aspect with its variable card and all aspect settings. The last two can be pasted onto any part. Only settings that you changed yourself are copied, so pasting never resets anything you left alone. Each pasted variable card consumes one blank &lVariable Card&r from your inventory.", + "info_book.integrateddynamics.manual.parts.settings.text6": "The configuration of a part can be copied to other parts using a &lWrench&r. Shift+right-clicking a part copies it into the Wrench, and right-clicking another part pastes it. The Wrench mode picks what is copied: &lCopy All&r takes everything and only pastes onto a part of the same type, &lCopy Settings&r takes the update interval, priority, channel, side and offsets, and &lCopy Aspect&r takes the active aspect with its variable card and all aspect settings. The last two can be pasted onto any part. Only settings that you changed yourself are copied, so pasting never resets anything you left alone. Each pasted variable card consumes one blank &lVariable Card&r from your inventory, and raising the maximum offset of a part consumes &lOffset Enhancements&r.", "info_book.integrateddynamics.manual.parts.offsets": "Offsets", "info_book.integrateddynamics.manual.parts.offsets.text1": "By default, parts will target the direct neighboring position. However, when &lPart Enhancements&r are applied, the target position can be changed through the part's &lOffset&r screen.", diff --git a/src/test/java/org/cyclops/integrateddynamics/core/part/TestPartConfigSnapshot.java b/src/test/java/org/cyclops/integrateddynamics/core/part/TestPartConfigSnapshot.java index b0f2ffb4eb6..04c0368ea76 100644 --- a/src/test/java/org/cyclops/integrateddynamics/core/part/TestPartConfigSnapshot.java +++ b/src/test/java/org/cyclops/integrateddynamics/core/part/TestPartConfigSnapshot.java @@ -10,6 +10,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; @@ -42,7 +43,7 @@ protected static CompoundTag aspectPropertiesTag() { public void testRoundTripAllSections() { PartConfigSnapshot snapshot = new PartConfigSnapshot(PartConfigSnapshot.VERSION, PART_TYPE, Optional.of(new PartConfigSnapshot.PartSettings(Optional.of(20), Optional.of(3), Optional.of(7), - Optional.of(Direction.NORTH), Optional.of(new Vec3i(1, -2, 3)))), + Optional.of(Direction.NORTH), Optional.of(new Vec3i(1, -2, 3)), Optional.of(8))), Map.of(ASPECT, aspectPropertiesTag()), List.of()); @@ -61,7 +62,7 @@ public void testRoundTripWithoutPartSettings() { public void testRoundTripWithoutAspectProperties() { PartConfigSnapshot snapshot = new PartConfigSnapshot(PartConfigSnapshot.VERSION, PART_TYPE, Optional.of(new PartConfigSnapshot.PartSettings(Optional.of(1), Optional.empty(), Optional.empty(), - Optional.empty(), Optional.empty())), + Optional.empty(), Optional.empty(), Optional.empty())), Map.of(), List.of()); assertThat(roundTrip(snapshot), is(snapshot)); @@ -79,9 +80,11 @@ public void testRoundTripEmpty() { @Test public void testPartSettingsEmptyWhenEverythingIsDefault() { assertThat(new PartConfigSnapshot.PartSettings(Optional.empty(), Optional.empty(), Optional.empty(), - Optional.empty(), Optional.empty()).isEmpty(), is(true)); + Optional.empty(), Optional.empty(), Optional.empty()).isEmpty(), is(true)); assertThat(new PartConfigSnapshot.PartSettings(Optional.of(1), Optional.empty(), Optional.empty(), - Optional.empty(), Optional.empty()).isEmpty(), is(false)); + Optional.empty(), Optional.empty(), Optional.empty()).isEmpty(), is(false)); + assertThat(new PartConfigSnapshot.PartSettings(Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), Optional.empty(), Optional.of(4)).isEmpty(), is(false)); } @Test @@ -92,6 +95,26 @@ public void testRequiredBlankVariables() { assertThat(snapshot.getRequiredBlankVariables(PartConfigSection.ALL), is(0)); } + @Test + public void testRequiredMaxOffset() { + PartConfigSnapshot snapshot = new PartConfigSnapshot(PartConfigSnapshot.VERSION, PART_TYPE, + Optional.of(new PartConfigSnapshot.PartSettings(Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), Optional.empty(), Optional.of(8))), + Map.of(), List.of()); + + assertThat(snapshot.getRequiredMaxOffset(PartConfigSection.ALL), is(8)); + // The maximum offset is part of the part settings, so the aspect sections alone do not need enhancements + assertThat(snapshot.getRequiredMaxOffset(Set.of(PartConfigSection.ASPECT)), is(0)); + } + + @Test + public void testRequiredMaxOffsetWithoutEnhancements() { + PartConfigSnapshot snapshot = new PartConfigSnapshot(PartConfigSnapshot.VERSION, PART_TYPE, + Optional.empty(), Map.of(), List.of()); + + assertThat(snapshot.getRequiredMaxOffset(PartConfigSection.ALL), is(0)); + } + @Test public void testVariableInventoriesBelongToSections() { // The active variable and the aspect setting variables are aspect state @@ -108,7 +131,7 @@ public void testVariableInventoriesBelongToSections() { public void testSections() { PartConfigSnapshot snapshot = new PartConfigSnapshot(PartConfigSnapshot.VERSION, PART_TYPE, Optional.of(new PartConfigSnapshot.PartSettings(Optional.of(1), Optional.empty(), Optional.empty(), - Optional.empty(), Optional.empty())), + Optional.empty(), Optional.empty(), Optional.empty())), Map.of(ASPECT, aspectPropertiesTag()), List.of()); assertThat(snapshot.hasSection(PartConfigSection.PART_SETTINGS), is(true)); From 7dad1fb41e8476a53bbbd02d67acc6df4936f814 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 10 Sep 2026 05:19:55 +0000 Subject: [PATCH 11/13] Only describe the copied configuration in the modes that paste it A configuration stays in the Wrench when switching modes, so the source part and the included sections were also shown in the modes that do nothing with it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01LBAgcFp6jWcYRWz9N8h7AF --- CHANGELOG-1.21.1.md | 4 ---- .../java/org/cyclops/integrateddynamics/item/ItemWrench.java | 3 ++- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/CHANGELOG-1.21.1.md b/CHANGELOG-1.21.1.md index ac9ad0676a9..9782c636a37 100644 --- a/CHANGELOG-1.21.1.md +++ b/CHANGELOG-1.21.1.md @@ -6,10 +6,6 @@ All notable changes to this project will be documented in this file. ### Added -* Allow part configurations to be copied and pasted with the Wrench, Closes #859 - * The Wrench gets a Copy All, Copy Settings and Copy Aspect mode - * The active Wrench mode is shown on the Wrench item - * Pasting also raises the maximum offset of a part, by consuming Offset Enhancements * Allow aspect settings to be determined by variables (#1707), Closes CyclopsMC/IntegratedTunnels#278 * Show modified aspect property values in tooltip (#1706), Closes #1704 diff --git a/src/main/java/org/cyclops/integrateddynamics/item/ItemWrench.java b/src/main/java/org/cyclops/integrateddynamics/item/ItemWrench.java index 25f0c03c8a8..ad5bfee35ce 100644 --- a/src/main/java/org/cyclops/integrateddynamics/item/ItemWrench.java +++ b/src/main/java/org/cyclops/integrateddynamics/item/ItemWrench.java @@ -173,7 +173,8 @@ public void appendHoverText(ItemStack itemStack, Item.TooltipContext context, Li list.add(Component.translatable("item.integrateddynamics.wrench.mode.offset_side.side", itemStack.get(RegistryEntries.DATACOMPONENT_WRENCH_TARGET_DIRECTION).getSerializedName()).withStyle(ChatFormatting.GRAY)); } CompoundTag configTag = itemStack.get(RegistryEntries.DATACOMPONENT_WRENCH_PART_CONFIG); - if (configTag != null && context.registries() != null) { + // A configuration stays in the Wrench when switching modes, but only says something in the modes that paste it + if (mode.isConfig() && configTag != null && context.registries() != null) { PartConfigSnapshot.fromNBT(context.registries(), configTag).ifPresent(snapshot -> { // Only the sections that the current mode pastes are relevant Set sections = Sets.intersection(snapshot.getSections(), mode.getConfigSections()); From c599bc6b321112afbda2a9f0fa88ce2aa612d65f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 10 Sep 2026 05:46:44 +0000 Subject: [PATCH 12/13] Let part types copy and paste their own state as well Part types, and the part types that addons add in particular, hold state that a configuration snapshot can not know about. They can now store it per section, read it back when pasting, tell the player what a paste took, and state in the tooltip what a paste is going to need. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01LBAgcFp6jWcYRWz9N8h7AF --- .../gametest/GameTestsWrenchConfig.java | 95 +++++++++++++++++++ .../api/part/IPartType.java | 68 +++++++++++++ .../core/helper/PartConfigHelpers.java | 18 +++- .../core/part/PartConfigApplyResult.java | 20 ++++ .../core/part/PartConfigSection.java | 11 ++- .../core/part/PartConfigSnapshot.java | 21 +++- .../integrateddynamics/item/ItemWrench.java | 8 ++ .../core/part/TestPartConfigSnapshot.java | 38 ++++++-- 8 files changed, 266 insertions(+), 13 deletions(-) diff --git a/src/integrationtest/java/org/cyclops/integrateddynamics/gametest/GameTestsWrenchConfig.java b/src/integrationtest/java/org/cyclops/integrateddynamics/gametest/GameTestsWrenchConfig.java index f72f248c720..ec1ecd833a8 100644 --- a/src/integrationtest/java/org/cyclops/integrateddynamics/gametest/GameTestsWrenchConfig.java +++ b/src/integrationtest/java/org/cyclops/integrateddynamics/gametest/GameTestsWrenchConfig.java @@ -3,6 +3,8 @@ import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.Vec3i; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; import net.minecraft.gametest.framework.GameTest; import net.minecraft.gametest.framework.GameTestHelper; import net.minecraft.world.InteractionHand; @@ -40,7 +42,10 @@ import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypes; import javax.annotation.Nullable; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Proxy; import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; import static org.cyclops.integrateddynamics.gametest.GameTestHelpersIntegratedDynamics.createVariableForValue; import static org.cyclops.integrateddynamics.gametest.GameTestHelpersIntegratedDynamics.getEffectiveAspectProperty; @@ -59,6 +64,8 @@ public class GameTestsWrenchConfig { public static final String TEMPLATE_EMPTY = "empty10"; public static final BlockPos POS_SOURCE = BlockPos.ZERO.offset(2, 0, 2); public static final BlockPos POS_TARGET = BlockPos.ZERO.offset(2, 0, 4); + public static final String EXTRA_KEY = "testExtraValue"; + public static final String EXTRA_APPLIED = "test extra"; protected static PartPos placePart(GameTestHelper helper, BlockPos pos, IPartType partType) { helper.setBlock(pos, RegistryEntries.BLOCK_CABLE.value()); @@ -422,6 +429,40 @@ public void testWrenchConfigPasteOffsetOutOfRange(GameTestHelper helper) { }); } + /** + * A part type that behaves like the given one, but that stores and reads back an extra value, + * the way an addon would override these methods on its own part types. + * + * @param delegate The part type to behave like. + * @param key The key to store the extra value under. + * @param value The extra value to store when copying. + * @param applied Where the extra value that is read back when pasting is put. + * @return The part type. + */ + protected static IPartType withExtraConfig(IPartType delegate, String key, int value, + AtomicInteger applied) { + return (IPartType) Proxy.newProxyInstance(GameTestsWrenchConfig.class.getClassLoader(), + new Class[]{IPartType.class}, (proxy, method, args) -> switch (method.getName()) { + case "snapshotConfigExtra" -> { + CompoundTag tag = new CompoundTag(); + // Only one of the sections, to show that they are stored apart + if (args[2] == PartConfigSection.PART_SETTINGS) { + tag.putInt(key, value); + } + yield tag; + } + case "applyConfigExtra" -> { + PartConfigSnapshot snapshot = (PartConfigSnapshot) args[4]; + applied.set(snapshot.getExtraData((PartConfigSection) args[3]).getInt(key)); + ((PartConfigApplyResult) args[6]).addApplied(Component.literal(EXTRA_APPLIED)); + yield null; + } + // Called on the proxy, so that the hooks above are the ones that the helpers reach + case "snapshotConfig", "applyConfig" -> InvocationHandler.invokeDefault(proxy, method, args); + default -> method.invoke(delegate, args); + }); + } + @GameTest(template = TEMPLATE_EMPTY) public void testWrenchConfigPasteMaxOffsetConsumesEnhancements(GameTestHelper helper) { PartPos source = placePart(helper, POS_SOURCE, PartTypes.REDSTONE_WRITER); @@ -517,6 +558,60 @@ public void testWrenchConfigAspectModeSkipsMaxOffset(GameTestHelper helper) { }); } + @GameTest(template = TEMPLATE_EMPTY) + public void testWrenchConfigPartTypeExtraData(GameTestHelper helper) { + PartPos source = placePart(helper, POS_SOURCE, PartTypes.REDSTONE_WRITER); + PartPos target = placePart(helper, POS_TARGET, PartTypes.REDSTONE_WRITER); + + AtomicInteger applied = new AtomicInteger(-1); + PartConfigSnapshot snapshot = ((IPartType) withExtraConfig(partType(source), EXTRA_KEY, 42, applied)) + .snapshotConfig(ValueDeseralizationContext.of(helper.getLevel()), partState(source), + PartConfigSection.ALL); + + Player player = helper.makeMockPlayer(GameType.SURVIVAL); + IPartType targetPartType = (IPartType) withExtraConfig(partType(target), EXTRA_KEY, 0, applied); + IPartState targetState = partState(target); + PartConfigApplyResult result = targetPartType.applyConfig( + ValueDeseralizationContext.of(helper.getLevel()), null, null, + targetPartType.getTarget(target, targetState), targetState, snapshot, + PartConfigSection.ALL, player); + + helper.succeedWhen(() -> { + helper.assertValueEqual(snapshot.getExtraData(PartConfigSection.PART_SETTINGS).getInt(EXTRA_KEY), 42, + "The part type could not store its own state"); + helper.assertTrue(snapshot.getExtraData(PartConfigSection.ASPECT).isEmpty(), + "The stored state leaked into another section"); + helper.assertValueEqual(applied.get(), 42, "The part type did not read its own state back"); + helper.assertTrue(result.getMessage().getString().contains(EXTRA_APPLIED), + "The part type could not report what it pasted"); + }); + } + + @GameTest(template = TEMPLATE_EMPTY) + public void testWrenchConfigPartTypeExtraDataSkipsOtherSections(GameTestHelper helper) { + PartPos source = placePart(helper, POS_SOURCE, PartTypes.REDSTONE_WRITER); + PartPos target = placePart(helper, POS_TARGET, PartTypes.REDSTONE_WRITER); + + AtomicInteger applied = new AtomicInteger(-1); + // The extra state belongs to the part settings, so the aspect mode must not copy or paste it + PartConfigSnapshot snapshot = ((IPartType) withExtraConfig(partType(source), EXTRA_KEY, 42, applied)) + .snapshotConfig(ValueDeseralizationContext.of(helper.getLevel()), partState(source), + ItemWrench.Mode.CONFIG_ASPECT.getConfigSections()); + + Player player = helper.makeMockPlayer(GameType.SURVIVAL); + IPartType targetPartType = (IPartType) withExtraConfig(partType(target), EXTRA_KEY, 0, applied); + IPartState targetState = partState(target); + targetPartType.applyConfig(ValueDeseralizationContext.of(helper.getLevel()), null, null, + targetPartType.getTarget(target, targetState), targetState, snapshot, + ItemWrench.Mode.CONFIG_ASPECT.getConfigSections(), player); + + helper.succeedWhen(() -> { + helper.assertTrue(snapshot.getExtraData(PartConfigSection.PART_SETTINGS).isEmpty(), + "The part settings state was stored by the aspect mode"); + helper.assertValueEqual(applied.get(), -1, "The part type was asked to paste an unstored section"); + }); + } + @GameTest(template = TEMPLATE_EMPTY) public void testWrenchConfigSettingsModeOnlyCopiesPartSettings(GameTestHelper helper) { PartPos source = placePart(helper, POS_SOURCE, PartTypes.REDSTONE_WRITER); diff --git a/src/main/java/org/cyclops/integrateddynamics/api/part/IPartType.java b/src/main/java/org/cyclops/integrateddynamics/api/part/IPartType.java index cc1dd382c5f..e5fa45743d7 100644 --- a/src/main/java/org/cyclops/integrateddynamics/api/part/IPartType.java +++ b/src/main/java/org/cyclops/integrateddynamics/api/part/IPartType.java @@ -246,6 +246,74 @@ public default PartConfigApplyResult applyConfig(ValueDeseralizationContext valu return PartConfigHelpers.apply(valueDeseralizationContext, network, target, this, state, snapshot, sections, player); } + /** + * Take a snapshot of the state that {@link PartConfigSnapshot} does not know about itself. + * + * This is the extension point for part types that hold their own state, + * such as the part types that addons add. + * Implementations should only store what the player configured, + * so that pasting does not overwrite anything that was left at its default, + * and should keep the format stable, as a snapshot can outlive a world reload. + * + * This is only called server-side. + * + * @param valueDeseralizationContext A value deserialization context. + * @param state The state. + * @param section The configuration section that is being copied. + * @return What to store for that section, or an empty tag to store nothing. + */ + // TODO: make non-default in nextmajor + public default CompoundTag snapshotConfigExtra(ValueDeseralizationContext valueDeseralizationContext, S state, + PartConfigSection section) { + return new CompoundTag(); + } + + /** + * Paste back what {@link #snapshotConfigExtra(ValueDeseralizationContext, IPartState, PartConfigSection)} stored. + * + * This is only called for the sections that are being pasted, + * and only when the snapshot holds something for them. + * Since the subset modes of the Wrench can paste onto another part type, + * implementations should check {@link PartConfigSnapshot#sourcePartType()} + * before reading anything that only makes sense for their own part types. + * + * Anything that had to be consumed from the player can be reported through the given result, + * so that the player is told about it. + * + * This is only called server-side. + * + * @param valueDeseralizationContext A value deserialization context. + * @param target The target block. + * @param state The state. + * @param section The configuration section that is being pasted. + * @param snapshot The snapshot that is being pasted, + * holding the stored state in {@link PartConfigSnapshot#getExtraData(PartConfigSection)}. + * @param player The player that is pasting. + * @param result The outcome to report into. + */ + // TODO: make non-default in nextmajor + public default void applyConfigExtra(ValueDeseralizationContext valueDeseralizationContext, PartTarget target, + S state, PartConfigSection section, PartConfigSnapshot snapshot, + Player player, PartConfigApplyResult result) { + + } + + /** + * What a player needs in their inventory before + * {@link #applyConfigExtra(ValueDeseralizationContext, PartTarget, IPartState, PartConfigSection, PartConfigSnapshot, Player, PartConfigApplyResult)} + * can paste everything, to be shown in the tooltip of the Wrench. + * + * This is called on the part type that the snapshot was taken from, on both sides. + * + * @param snapshot The snapshot that is being pasted. + * @param section The configuration section that would be pasted. + * @return One line per requirement, or nothing if pasting needs nothing from the player. + */ + // TODO: make non-default in nextmajor + public default List getConfigExtraRequirements(PartConfigSnapshot snapshot, PartConfigSection section) { + return List.of(); + } + /** * @param state The state * @return If this element should be updated. This method is only called once during network initialization. diff --git a/src/main/java/org/cyclops/integrateddynamics/core/helper/PartConfigHelpers.java b/src/main/java/org/cyclops/integrateddynamics/core/helper/PartConfigHelpers.java index c27eb6673c1..f107f1cad5e 100644 --- a/src/main/java/org/cyclops/integrateddynamics/core/helper/PartConfigHelpers.java +++ b/src/main/java/org/cyclops/integrateddynamics/core/helper/PartConfigHelpers.java @@ -127,8 +127,17 @@ public static PartConfigSnapshot snapshot(ValueDeseralizationContext valueDesera } } + // Give the part type itself the chance to store what this snapshot does not know about + Map extraData = Maps.newLinkedHashMap(); + for (PartConfigSection section : sections) { + CompoundTag tag = partType.snapshotConfigExtra(valueDeseralizationContext, state, section); + if (!tag.isEmpty()) { + extraData.put(section, tag); + } + } + return new PartConfigSnapshot(PartConfigSnapshot.VERSION, partType.getUniqueName(), - partSettings, aspectProperties, variableCards); + partSettings, aspectProperties, variableCards, extraData); } /** @@ -194,6 +203,13 @@ public static PartConfigApplyResult apply(ValueDeseralizationContext valueDesera applyVariableCards(valueDeseralizationContext, target, partType, state, snapshot.getVariableCards(sections), player, result); + // Give the part type itself the chance to paste back what this snapshot does not know about + for (PartConfigSection section : sections) { + if (!snapshot.getExtraData(section).isEmpty()) { + partType.applyConfigExtra(valueDeseralizationContext, target, state, section, snapshot, player, result); + } + } + return result; } diff --git a/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigApplyResult.java b/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigApplyResult.java index 867d610e58a..e762ae4f255 100644 --- a/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigApplyResult.java +++ b/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigApplyResult.java @@ -21,6 +21,8 @@ public class PartConfigApplyResult { private int missingBlanks = 0; private int appliedMaxOffset = 0; private int missingMaxOffset = 0; + private final List extraApplied = Lists.newArrayList(); + private final List extraWarnings = Lists.newArrayList(); public boolean isPartSettingsApplied() { return this.partSettingsApplied; @@ -100,6 +102,22 @@ public void setMissingMaxOffset(int missingMaxOffset) { this.missingMaxOffset = missingMaxOffset; } + /** + * Report something that a part type pasted itself, to be mentioned alongside the rest. + * @param applied What was pasted, phrased to fit in a comma separated list. + */ + public void addApplied(Component applied) { + this.extraApplied.add(applied); + } + + /** + * Report something that a part type could not paste. + * @param warning What did not go as the player intended. + */ + public void addWarning(Component warning) { + this.extraWarnings.add(warning); + } + /** * @return A single line summarising what was applied. */ @@ -121,6 +139,7 @@ public MutableComponent getMessage() { applied.add(Component.translatable("item.integrateddynamics.wrench.mode.config.pasted.max_offset", this.appliedMaxOffset)); } + applied.addAll(this.extraApplied); if (applied.isEmpty()) { return Component.translatable("item.integrateddynamics.wrench.mode.config.pasted.nothing"); } @@ -152,6 +171,7 @@ public List getWarnings() { warnings.add(Component.translatable("item.integrateddynamics.wrench.mode.config.cards_skipped", this.cardsSkipped, this.missingBlanks)); } + this.extraWarnings.forEach(warning -> warnings.add(warning.copy())); return warnings; } diff --git a/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigSection.java b/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigSection.java index 9c204ed2da8..e010b381120 100644 --- a/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigSection.java +++ b/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigSection.java @@ -1,6 +1,8 @@ package org.cyclops.integrateddynamics.core.part; import com.google.common.collect.Sets; +import com.mojang.serialization.Codec; +import net.minecraft.util.StringRepresentable; import java.util.EnumSet; import java.util.Locale; @@ -10,7 +12,7 @@ * The separate sections of a part configuration that can be copied and pasted. * @author rubensworks */ -public enum PartConfigSection { +public enum PartConfigSection implements StringRepresentable { /** * Update interval, priority, channel, target side, target offset, and the offset variables. @@ -26,6 +28,13 @@ public enum PartConfigSection { */ public static final Set ALL = Sets.immutableEnumSet(EnumSet.allOf(PartConfigSection.class)); + public static final Codec CODEC = StringRepresentable.fromEnum(PartConfigSection::values); + + @Override + public String getSerializedName() { + return name().toLowerCase(Locale.ENGLISH); + } + /** * @param inventoryName The name of a variable inventory inside a part. * @return The section that the variables in that inventory belong to. diff --git a/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigSnapshot.java b/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigSnapshot.java index e649a38586b..1227108cae0 100644 --- a/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigSnapshot.java +++ b/src/main/java/org/cyclops/integrateddynamics/core/part/PartConfigSnapshot.java @@ -31,13 +31,15 @@ * @param partSettings The non-default general part settings. * @param aspectProperties The serialized non-default aspect properties, by aspect unique name. * @param variableCards All variables, of every section. + * @param extraData The state that the part type itself stored, by section. * @author rubensworks */ public record PartConfigSnapshot(int version, ResourceLocation sourcePartType, Optional partSettings, Map aspectProperties, - List variableCards) { + List variableCards, + Map extraData) { public static final int VERSION = 1; @@ -76,7 +78,9 @@ public record PartConfigSnapshot(int version, Codec.unboundedMap(ResourceLocation.CODEC, CompoundTag.CODEC) .optionalFieldOf("aspectProperties", Map.of()).forGetter(PartConfigSnapshot::aspectProperties), CODEC_VARIABLE_CARD.listOf() - .optionalFieldOf("variableCards", List.of()).forGetter(PartConfigSnapshot::variableCards) + .optionalFieldOf("variableCards", List.of()).forGetter(PartConfigSnapshot::variableCards), + Codec.unboundedMap(PartConfigSection.CODEC, CompoundTag.CODEC) + .optionalFieldOf("extraData", Map.of()).forGetter(PartConfigSnapshot::extraData) ) .apply(builder, PartConfigSnapshot::new)); @@ -90,6 +94,17 @@ public List getVariableCards(Set sections) { .toList(); } + /** + * Part types can store anything that this snapshot does not know about itself, + * such as the state that an addon adds to its own part types. + * + * @param section A config section. + * @return What the part type stored for the given section, which is empty if it stored nothing. + */ + public CompoundTag getExtraData(PartConfigSection section) { + return extraData().getOrDefault(section, new CompoundTag()); + } + /** * The offset enhancements that a part holds can not be taken out again without breaking the part, * so pasting them has to consume enhancements from the player, just like the variable cards do. @@ -117,7 +132,7 @@ public int getRequiredBlankVariables(Set sections) { * @return If this snapshot holds anything for the given section. */ public boolean hasSection(PartConfigSection section) { - if (!getVariableCards(Set.of(section)).isEmpty()) { + if (!getVariableCards(Set.of(section)).isEmpty() || !getExtraData(section).isEmpty()) { return true; } return switch (section) { diff --git a/src/main/java/org/cyclops/integrateddynamics/item/ItemWrench.java b/src/main/java/org/cyclops/integrateddynamics/item/ItemWrench.java index ad5bfee35ce..33de3c8d86a 100644 --- a/src/main/java/org/cyclops/integrateddynamics/item/ItemWrench.java +++ b/src/main/java/org/cyclops/integrateddynamics/item/ItemWrench.java @@ -194,6 +194,14 @@ public void appendHoverText(ItemStack itemStack, Item.TooltipContext context, Li list.add(Component.translatable("item.integrateddynamics.wrench.mode.config.requires_enhancements", requiredMaxOffset).withStyle(ChatFormatting.GOLD)); } + // Whatever the part type stored itself can need something from the player as well + IPartType sourcePartType = PartTypes.REGISTRY.getPartType(snapshot.sourcePartType()); + if (sourcePartType != null) { + for (PartConfigSection section : sections) { + sourcePartType.getConfigExtraRequirements(snapshot, section) + .forEach(requirement -> list.add(requirement.copy().withStyle(ChatFormatting.GOLD))); + } + } }); } // Hidden behind the same shift that reveals the item info, to keep the resting tooltip short diff --git a/src/test/java/org/cyclops/integrateddynamics/core/part/TestPartConfigSnapshot.java b/src/test/java/org/cyclops/integrateddynamics/core/part/TestPartConfigSnapshot.java index 04c0368ea76..39419582415 100644 --- a/src/test/java/org/cyclops/integrateddynamics/core/part/TestPartConfigSnapshot.java +++ b/src/test/java/org/cyclops/integrateddynamics/core/part/TestPartConfigSnapshot.java @@ -45,7 +45,7 @@ public void testRoundTripAllSections() { Optional.of(new PartConfigSnapshot.PartSettings(Optional.of(20), Optional.of(3), Optional.of(7), Optional.of(Direction.NORTH), Optional.of(new Vec3i(1, -2, 3)), Optional.of(8))), Map.of(ASPECT, aspectPropertiesTag()), - List.of()); + List.of(), Map.of(PartConfigSection.ASPECT, aspectPropertiesTag())); assertThat(roundTrip(snapshot), is(snapshot)); } @@ -53,7 +53,7 @@ public void testRoundTripAllSections() { @Test public void testRoundTripWithoutPartSettings() { PartConfigSnapshot snapshot = new PartConfigSnapshot(PartConfigSnapshot.VERSION, PART_TYPE, - Optional.empty(), Map.of(ASPECT, aspectPropertiesTag()), List.of()); + Optional.empty(), Map.of(ASPECT, aspectPropertiesTag()), List.of(), Map.of()); assertThat(roundTrip(snapshot), is(snapshot)); } @@ -63,7 +63,7 @@ public void testRoundTripWithoutAspectProperties() { PartConfigSnapshot snapshot = new PartConfigSnapshot(PartConfigSnapshot.VERSION, PART_TYPE, Optional.of(new PartConfigSnapshot.PartSettings(Optional.of(1), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty())), - Map.of(), List.of()); + Map.of(), List.of(), Map.of()); assertThat(roundTrip(snapshot), is(snapshot)); } @@ -71,7 +71,7 @@ public void testRoundTripWithoutAspectProperties() { @Test public void testRoundTripEmpty() { PartConfigSnapshot snapshot = new PartConfigSnapshot(PartConfigSnapshot.VERSION, PART_TYPE, - Optional.empty(), Map.of(), List.of()); + Optional.empty(), Map.of(), List.of(), Map.of()); assertThat(roundTrip(snapshot), is(snapshot)); assertThat(snapshot.isEmpty(), is(true)); @@ -90,7 +90,7 @@ public void testPartSettingsEmptyWhenEverythingIsDefault() { @Test public void testRequiredBlankVariables() { PartConfigSnapshot snapshot = new PartConfigSnapshot(PartConfigSnapshot.VERSION, PART_TYPE, - Optional.empty(), Map.of(), List.of()); + Optional.empty(), Map.of(), List.of(), Map.of()); assertThat(snapshot.getRequiredBlankVariables(PartConfigSection.ALL), is(0)); } @@ -100,7 +100,7 @@ public void testRequiredMaxOffset() { PartConfigSnapshot snapshot = new PartConfigSnapshot(PartConfigSnapshot.VERSION, PART_TYPE, Optional.of(new PartConfigSnapshot.PartSettings(Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.of(8))), - Map.of(), List.of()); + Map.of(), List.of(), Map.of()); assertThat(snapshot.getRequiredMaxOffset(PartConfigSection.ALL), is(8)); // The maximum offset is part of the part settings, so the aspect sections alone do not need enhancements @@ -110,11 +110,33 @@ public void testRequiredMaxOffset() { @Test public void testRequiredMaxOffsetWithoutEnhancements() { PartConfigSnapshot snapshot = new PartConfigSnapshot(PartConfigSnapshot.VERSION, PART_TYPE, - Optional.empty(), Map.of(), List.of()); + Optional.empty(), Map.of(), List.of(), Map.of()); assertThat(snapshot.getRequiredMaxOffset(PartConfigSection.ALL), is(0)); } + @Test + public void testRoundTripWithExtraData() { + PartConfigSnapshot snapshot = new PartConfigSnapshot(PartConfigSnapshot.VERSION, PART_TYPE, + Optional.empty(), Map.of(), List.of(), + Map.of(PartConfigSection.PART_SETTINGS, aspectPropertiesTag())); + + assertThat(roundTrip(snapshot), is(snapshot)); + } + + @Test + public void testExtraDataMakesASectionPresent() { + PartConfigSnapshot snapshot = new PartConfigSnapshot(PartConfigSnapshot.VERSION, PART_TYPE, + Optional.empty(), Map.of(), List.of(), + Map.of(PartConfigSection.PART_SETTINGS, aspectPropertiesTag())); + + assertThat(snapshot.isEmpty(), is(false)); + assertThat(snapshot.hasSection(PartConfigSection.PART_SETTINGS), is(true)); + assertThat(snapshot.hasSection(PartConfigSection.ASPECT), is(false)); + assertThat(snapshot.getExtraData(PartConfigSection.PART_SETTINGS), is(aspectPropertiesTag())); + assertThat(snapshot.getExtraData(PartConfigSection.ASPECT).isEmpty(), is(true)); + } + @Test public void testVariableInventoriesBelongToSections() { // The active variable and the aspect setting variables are aspect state @@ -132,7 +154,7 @@ public void testSections() { PartConfigSnapshot snapshot = new PartConfigSnapshot(PartConfigSnapshot.VERSION, PART_TYPE, Optional.of(new PartConfigSnapshot.PartSettings(Optional.of(1), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty())), - Map.of(ASPECT, aspectPropertiesTag()), List.of()); + Map.of(ASPECT, aspectPropertiesTag()), List.of(), Map.of()); assertThat(snapshot.hasSection(PartConfigSection.PART_SETTINGS), is(true)); assertThat(snapshot.hasSection(PartConfigSection.ASPECT), is(true)); From fc98a4b28cbed6b52e96dac3debbcaf446f310d6 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 10 Sep 2026 06:35:09 +0000 Subject: [PATCH 13/13] Keep the default update interval of part states protected Widening it to public broke every part state outside of this mod that overrides it, such as the ones in Integrated Tunnels, so the public accessor that snapshots need is a separate method that delegates to it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01LBAgcFp6jWcYRWz9N8h7AF --- .../cyclops/integrateddynamics/api/part/IPartState.java | 5 ++++- .../integrateddynamics/core/helper/PartConfigHelpers.java | 2 +- .../integrateddynamics/core/part/PartStateBase.java | 8 ++++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/cyclops/integrateddynamics/api/part/IPartState.java b/src/main/java/org/cyclops/integrateddynamics/api/part/IPartState.java index 9e43834afae..9ebe278ab3c 100644 --- a/src/main/java/org/cyclops/integrateddynamics/api/part/IPartState.java +++ b/src/main/java/org/cyclops/integrateddynamics/api/part/IPartState.java @@ -70,10 +70,13 @@ public interface IPartState

{ public int getUpdateInterval(); /** + * This is separate from the protected default update interval that part states can override, + * as making that one public would break every part state outside of this mod that overrides it. + * * @return The tick interval that this part has before a player configures it. */ // TODO: make non-default in nextmajor - public default int getDefaultUpdateInterval() { + public default int getDefaultUpdateIntervalPublic() { return GeneralConfig.defaultPartUpdateFreq; } diff --git a/src/main/java/org/cyclops/integrateddynamics/core/helper/PartConfigHelpers.java b/src/main/java/org/cyclops/integrateddynamics/core/helper/PartConfigHelpers.java index f107f1cad5e..d3c4d512856 100644 --- a/src/main/java/org/cyclops/integrateddynamics/core/helper/PartConfigHelpers.java +++ b/src/main/java/org/cyclops/integrateddynamics/core/helper/PartConfigHelpers.java @@ -148,7 +148,7 @@ public static PartConfigSnapshot snapshot(ValueDeseralizationContext valueDesera @SuppressWarnings("unchecked") protected static PartConfigSnapshot.PartSettings snapshotPartSettings(IPartType partType, IPartState state) { int updateInterval = partType.getUpdateInterval(state); - int defaultUpdateInterval = Math.max(partType.getMinimumUpdateInterval(state), state.getDefaultUpdateInterval()); + int defaultUpdateInterval = Math.max(partType.getMinimumUpdateInterval(state), state.getDefaultUpdateIntervalPublic()); Vec3i targetOffset = partType.getTargetOffset(state); return new PartConfigSnapshot.PartSettings( updateInterval == defaultUpdateInterval ? Optional.empty() : Optional.of(updateInterval), diff --git a/src/main/java/org/cyclops/integrateddynamics/core/part/PartStateBase.java b/src/main/java/org/cyclops/integrateddynamics/core/part/PartStateBase.java index 02824e5d4ef..96005dc6c3d 100644 --- a/src/main/java/org/cyclops/integrateddynamics/core/part/PartStateBase.java +++ b/src/main/java/org/cyclops/integrateddynamics/core/part/PartStateBase.java @@ -401,11 +401,15 @@ public void removeVolatileCapability(PartCapability capability) { volatileCapabilities.remove(capability); } - @Override - public int getDefaultUpdateInterval() { + protected int getDefaultUpdateInterval() { return GeneralConfig.defaultPartUpdateFreq; } + @Override + public int getDefaultUpdateIntervalPublic() { + return getDefaultUpdateInterval(); + } + @Override public void initializeOffsets(PartTarget target) { this.offsetHandler.initializeVariableEvaluators(this.offsetHandler.getOffsetVariablesInventory(this), target);