diff --git a/.github/scripts/test_no_error_reports b/.github/scripts/test_no_error_reports old mode 100755 new mode 100644 diff --git a/addon.gradle b/addon.gradle index 32cd6394..30e78921 100644 --- a/addon.gradle +++ b/addon.gradle @@ -1,7 +1,7 @@ javadoc { javadocTool = javaToolchains.javadocToolFor { // Last version with frames, but it also has search - languageVersion = JavaLanguageVersion.of(11) + languageVersion = JavaLanguageVersion.of(21) vendor = JvmVendorSpec.ADOPTIUM } @@ -20,7 +20,6 @@ javadoc { overview "src/main/javadoc/overview.html" links 'https://docs.oracle.com/javase/8/docs/api/' links 'https://skmedix.github.io/ForgeJavaDocs/javadoc/forge/1.7.10-10.13.4.1614/' - addBooleanOption("-frames", true) } } diff --git a/dependencies.gradle b/dependencies.gradle index 9769a6e5..3b157c89 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -4,6 +4,12 @@ dependencies { api("com.github.GTNewHorizons:GTNHLib:0.6.17:dev") implementation("com.github.GTNewHorizons:NotEnoughItems:2.7.29-GTNH:dev") + compileOnly("com.github.GTNewHorizons:ServerUtilities:2.1.41-pre") + testImplementation(platform('org.junit:junit-bom:5.9.2')) testImplementation('org.junit.jupiter:junit-jupiter') + +// runtimeOnlyNonPublishable("com.github.GTNewHorizons:GT5-Unofficial:5.09.51.227:dev") { +// exclude group: "com.github.GTNewHorizons", module: "StructureLib" +// } } diff --git a/gradlew b/gradlew old mode 100755 new mode 100644 diff --git a/src/main/java/com/gtnewhorizon/structurelib/ClientProxy.java b/src/main/java/com/gtnewhorizon/structurelib/ClientProxy.java index 7fdf3bfe..21dc68b4 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/ClientProxy.java +++ b/src/main/java/com/gtnewhorizon/structurelib/ClientProxy.java @@ -20,6 +20,7 @@ import net.minecraft.client.renderer.Tessellator; import net.minecraft.client.renderer.culling.Frustrum; import net.minecraft.client.renderer.texture.TextureMap; import net.minecraft.client.resources.I18n; +import net.minecraft.client.settings.KeyBinding; import net.minecraft.entity.Entity; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; @@ -42,17 +43,23 @@ import org.lwjgl.opengl.GL11; import com.gtnewhorizon.structurelib.entity.fx.WeightlessParticleFX; import com.gtnewhorizon.structurelib.net.RegistryOrderSyncMessage; import com.gtnewhorizon.structurelib.net.SetChannelDataMessage; +import com.gtnewhorizon.structurelib.net.StructureProjectorCycleModeMessage; import cpw.mods.fml.client.config.GuiConfig; import cpw.mods.fml.client.event.ConfigChangedEvent; +import cpw.mods.fml.client.registry.ClientRegistry; import cpw.mods.fml.common.FMLCommonHandler; +import cpw.mods.fml.common.event.FMLInitializationEvent; import cpw.mods.fml.common.event.FMLLoadCompleteEvent; import cpw.mods.fml.common.event.FMLPreInitializationEvent; import cpw.mods.fml.common.eventhandler.EventPriority; import cpw.mods.fml.common.eventhandler.SubscribeEvent; +import cpw.mods.fml.common.gameevent.InputEvent; import cpw.mods.fml.common.gameevent.TickEvent.ClientTickEvent; import cpw.mods.fml.common.gameevent.TickEvent.Phase; import cpw.mods.fml.common.network.FMLNetworkEvent; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; public class ClientProxy extends CommonProxy { @@ -259,6 +266,13 @@ public class ClientProxy extends CommonProxy { MinecraftForge.EVENT_BUS.register(new ForgeEventHandler()); } + @Override + public void init(FMLInitializationEvent e) { + super.init(e); + + MinecraftForge.EVENT_BUS.register(new KeybindHandler()); + } + static void markTextureUsed(IIcon icon) { if (StructureLib.COMPAT instanceof IStructureCompat) ((IStructureCompat) StructureLib.COMPAT).markTextureUsed(icon); @@ -654,4 +668,30 @@ public class ClientProxy extends CommonProxy { p.endSection(); } } + + public static class KeybindHandler { + + private static final KeyBinding CYCLE_PROJECTOR_MODE = new KeyBinding( + "item.structurelib.constructableTrigger.keys.cycleMode", + 0, + "item.structurelib.constructableTrigger.keyCategory"); + + public KeybindHandler() { + FMLCommonHandler.instance().bus().register(this); + // If somebody wants to make a UI element for this, be my guest + ClientRegistry.registerKeyBinding(CYCLE_PROJECTOR_MODE); + } + + @SideOnly(Side.CLIENT) + @SubscribeEvent + public void key(InputEvent.KeyInputEvent event) { + if (CYCLE_PROJECTOR_MODE.isPressed()) { + cycleProjectorMode(); + } + } + + private static void cycleProjectorMode() { + StructureLib.net.sendToServer(new StructureProjectorCycleModeMessage()); + } + } } diff --git a/src/main/java/com/gtnewhorizon/structurelib/CommonProxy.java b/src/main/java/com/gtnewhorizon/structurelib/CommonProxy.java index 72e7e372..8660c8fe 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/CommonProxy.java +++ b/src/main/java/com/gtnewhorizon/structurelib/CommonProxy.java @@ -17,6 +17,7 @@ import net.minecraftforge.common.MinecraftForge; import com.gtnewhorizon.structurelib.net.ErrorHintParticleMessage; import com.gtnewhorizon.structurelib.net.UpdateHintParticleMessage; +import cpw.mods.fml.common.event.FMLInitializationEvent; import cpw.mods.fml.common.event.FMLLoadCompleteEvent; import cpw.mods.fml.common.event.FMLPreInitializationEvent; import cpw.mods.fml.common.eventhandler.SubscribeEvent; @@ -64,6 +65,8 @@ public class CommonProxy { public void preInit(FMLPreInitializationEvent e) {} + public void init(FMLInitializationEvent e) {} + public long getOverworldTime() { return MinecraftServer.getServer().getEntityWorld().getTotalWorldTime(); } diff --git a/src/main/java/com/gtnewhorizon/structurelib/StructureLib.java b/src/main/java/com/gtnewhorizon/structurelib/StructureLib.java index a36ad3f7..d914aeb5 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/StructureLib.java +++ b/src/main/java/com/gtnewhorizon/structurelib/StructureLib.java @@ -27,6 +27,7 @@ import com.gtnewhorizon.structurelib.net.AlignmentMessage; import com.gtnewhorizon.structurelib.net.ErrorHintParticleMessage; import com.gtnewhorizon.structurelib.net.RegistryOrderSyncMessage; import com.gtnewhorizon.structurelib.net.SetChannelDataMessage; +import com.gtnewhorizon.structurelib.net.StructureProjectorCycleModeMessage; import com.gtnewhorizon.structurelib.net.UpdateHintParticleMessage; import com.gtnewhorizon.structurelib.util.InventoryUtility; import com.gtnewhorizon.structurelib.util.XSTR; @@ -34,6 +35,7 @@ import com.gtnewhorizon.structurelib.util.XSTR; import cpw.mods.fml.common.Mod; import cpw.mods.fml.common.SidedProxy; import cpw.mods.fml.common.event.FMLInterModComms; +import cpw.mods.fml.common.event.FMLInitializationEvent; import cpw.mods.fml.common.event.FMLLoadCompleteEvent; import cpw.mods.fml.common.event.FMLPreInitializationEvent; import cpw.mods.fml.common.event.FMLServerStartingEvent; @@ -77,6 +79,11 @@ public class StructureLib { net.registerMessage(SetChannelDataMessage.Handler.class, SetChannelDataMessage.class, 3, Side.SERVER); net.registerMessage(ErrorHintParticleMessage.Handler.class, ErrorHintParticleMessage.class, 4, Side.CLIENT); net.registerMessage(RegistryOrderSyncMessage.Handler.class, RegistryOrderSyncMessage.class, 5, Side.SERVER); + net.registerMessage( + StructureProjectorCycleModeMessage.Handler.class, + StructureProjectorCycleModeMessage.class, + 6, + Side.SERVER); try { DEBUG_MODE = Boolean.parseBoolean(System.getProperty("structurelib.debug")); @@ -128,6 +135,11 @@ public class StructureLib { DevelopHelper.onPreInit(); } + @Mod.EventHandler + public void init(FMLInitializationEvent e) { + proxy.init(e); + } + @Mod.EventHandler public void loadComplete(FMLLoadCompleteEvent e) { proxy.loadComplete(e); diff --git a/src/main/java/com/gtnewhorizon/structurelib/alignment/constructable/ConstructableUtility.java b/src/main/java/com/gtnewhorizon/structurelib/alignment/constructable/ConstructableUtility.java index 92bd596f..4e588c31 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/alignment/constructable/ConstructableUtility.java +++ b/src/main/java/com/gtnewhorizon/structurelib/alignment/constructable/ConstructableUtility.java @@ -57,12 +57,12 @@ public class ConstructableUtility { } IConstructable constructable = null; - if (tTileEntity instanceof IConstructableProvider) - constructable = ((IConstructableProvider) tTileEntity).getConstructable(); - else if (tTileEntity instanceof IConstructable) { - constructable = (IConstructable) tTileEntity; + if (tTileEntity instanceof IConstructableProvider aConstructableProvider) + constructable = aConstructableProvider.getConstructable(); + else if (tTileEntity instanceof IConstructable aConstructable) { + constructable = aConstructable; } else if (IMultiblockInfoContainer.contains(tTileEntity.getClass())) { - ExtendedFacing facing = tTileEntity instanceof IAlignment ? ((IAlignment) tTileEntity).getExtendedFacing() + ExtendedFacing facing = tTileEntity instanceof IAlignment alignment ? alignment.getExtendedFacing() : ExtendedFacing.of(ForgeDirection.getOrientation(aSide)); constructable = IMultiblockInfoContainer.get(tTileEntity.getClass()) .toConstructable(tTileEntity, facing); @@ -70,14 +70,13 @@ public class ConstructableUtility { if (constructable == null) return false; - if (aPlayer instanceof EntityPlayerMP) { - EntityPlayerMP playerMP = (EntityPlayerMP) aPlayer; + if (aPlayer instanceof EntityPlayerMP playerMP) { // server side and sneaking (already checked above) // do construct if (aPlayer.capabilities.isCreativeMode) { constructable.construct(aStack, false); - } else if (constructable instanceof ISurvivalConstructable) { - int built = ((ISurvivalConstructable) constructable).survivalConstruct( + } else if (constructable instanceof ISurvivalConstructable aConstructable) { + int built = aConstructable.survivalConstruct( aStack, ConfigurationHandler.INSTANCE.getAutoPlaceBudget(), ISurvivalBuildEnvironment.create(IItemSource.fromPlayer(playerMP), playerMP)); diff --git a/src/main/java/com/gtnewhorizon/structurelib/item/ItemConstructableTrigger.java b/src/main/java/com/gtnewhorizon/structurelib/item/ItemConstructableTrigger.java index 5246da82..801cfbb5 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/item/ItemConstructableTrigger.java +++ b/src/main/java/com/gtnewhorizon/structurelib/item/ItemConstructableTrigger.java @@ -7,9 +7,14 @@ import static net.minecraft.util.StatCollector.translateToLocalFormatted; import java.util.List; +import net.minecraft.client.renderer.texture.IIconRegister; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.item.Item; import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagInt; +import net.minecraft.util.IIcon; +import net.minecraft.util.StatCollector; import net.minecraft.world.World; import org.lwjgl.input.Keyboard; @@ -20,12 +25,79 @@ import com.gtnewhorizon.structurelib.alignment.constructable.ConstructableUtilit public class ItemConstructableTrigger extends Item { + private static final String TAG_MODE = "mode"; + + private static final IIcon[] textures = new IIcon[3]; + + public enum TriggerMode { + BUILDING, + UPDATING, + REMOVING + } + public ItemConstructableTrigger() { setUnlocalizedName("structurelib.constructableTrigger"); - setTextureName(MOD_ID + ":itemConstructableTrigger"); setCreativeTab(StructureLib.creativeTab); } + public static void cycleMode(ItemStack held) { + if (held.getTagCompound() == null) held.setTagCompound(new NBTTagCompound()); + if (!held.getTagCompound().hasKey(TAG_MODE)) held.getTagCompound().setTag(TAG_MODE, new NBTTagInt(0)); + held.getTagCompound().setInteger(TAG_MODE, (held.getTagCompound().getInteger(TAG_MODE) + 1) % 3); + } + + @Override + public void registerIcons(IIconRegister register) { + textures[0] = register.registerIcon(MOD_ID + ":constructableTrigger/BUILDING"); + textures[1] = register.registerIcon(MOD_ID + ":constructableTrigger/UPDATING"); + textures[2] = register.registerIcon(MOD_ID + ":constructableTrigger/REMOVING"); + } + + @Override + public IIcon getIcon(ItemStack stack, int renderPass, EntityPlayer player, ItemStack usingItem, int useRemaining) { + return getIcon(stack, renderPass); + } + + @Override + public IIcon getIcon(ItemStack stack, int pass) { + if (!stack.hasTagCompound() || !stack.getTagCompound().hasKey(TAG_MODE)) { + return textures[0]; + } + return textures[stack.getTagCompound().getInteger(TAG_MODE)]; + } + + @Override + public String getItemStackDisplayName(ItemStack stack) { + if (!stack.hasTagCompound() || !stack.getTagCompound().hasKey(TAG_MODE)) { + return getDisplayName(0); + } + return getDisplayName(stack.getTagCompound().getInteger(TAG_MODE)); + } + + @Override + public IIcon getIconIndex(ItemStack stack) { + return getIcon(stack, 0); + } + + @Override + public IIcon getIconFromDamage(int damage) { + return textures[damage]; + } + + private String getDisplayName(int mode) { + return StatCollector.translateToLocal("item.structurelib.constructableTrigger.name") + " - " + + StatCollector.translateToLocal("item.structurelib.constructableTrigger.modes." + mode); + } + + public static TriggerMode getMode(ItemStack held) { + if (held == null || !held.hasTagCompound() || !held.getTagCompound().hasKey(TAG_MODE)) { + // default behavior + return TriggerMode.BUILDING; + } + // get user-defined mode + return TriggerMode.values()[held.getTagCompound().getInteger(TAG_MODE)]; + } + @Override public ItemStack onItemRightClick(ItemStack stack, World world, EntityPlayer player) { if (!world.isRemote && getMovingObjectPositionFromPlayer(world, player, true) == null) { diff --git a/src/main/java/com/gtnewhorizon/structurelib/net/StructureProjectorCycleModeMessage.java b/src/main/java/com/gtnewhorizon/structurelib/net/StructureProjectorCycleModeMessage.java new file mode 100644 index 00000000..80858eac --- /dev/null +++ b/src/main/java/com/gtnewhorizon/structurelib/net/StructureProjectorCycleModeMessage.java @@ -0,0 +1,37 @@ +package com.gtnewhorizon.structurelib.net; + +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.item.ItemStack; + +import com.gtnewhorizon.structurelib.item.ItemConstructableTrigger; + +import cpw.mods.fml.common.network.simpleimpl.IMessage; +import cpw.mods.fml.common.network.simpleimpl.IMessageHandler; +import cpw.mods.fml.common.network.simpleimpl.MessageContext; +import io.netty.buffer.ByteBuf; + +public class StructureProjectorCycleModeMessage implements IMessage { + + @Override + public void fromBytes(ByteBuf buf) { + // no op + } + + @Override + public void toBytes(ByteBuf buf) { + // no op + } + + public static class Handler implements IMessageHandler { + + @Override + public IMessage onMessage(StructureProjectorCycleModeMessage message, MessageContext ctx) { + EntityPlayerMP player = ctx.getServerHandler().playerEntity; + final ItemStack held = player.getHeldItem(); + if (held != null && held.getItem() instanceof ItemConstructableTrigger) { + ItemConstructableTrigger.cycleMode(held); + } + return null; + } + } +} diff --git a/src/main/java/com/gtnewhorizon/structurelib/structure/IStructureDefinition.java b/src/main/java/com/gtnewhorizon/structurelib/structure/IStructureDefinition.java index d9cba10d..37adaabb 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/structure/IStructureDefinition.java +++ b/src/main/java/com/gtnewhorizon/structurelib/structure/IStructureDefinition.java @@ -348,34 +348,19 @@ public interface IStructureDefinition { } if (checkBlocksIfNotNullForceCheckAllIfTrue != null) { - boolean success; - if (checkBlocksIfNotNullForceCheckAllIfTrue) { - success = StructureUtility.iterateV2( - elements, - world, - extendedFacing, - basePositionX, - basePositionY, - basePositionZ, - basePositionA, - basePositionB, - basePositionC, - (e, w, x, y, z, a, b, c) -> e.check(object, w, x, y, z), - "check"); - } else { - success = StructureUtility.iterateV2( - elements, - world, - extendedFacing, - basePositionX, - basePositionY, - basePositionZ, - basePositionA, - basePositionB, - basePositionC, - skipBlockUnloaded((e, w, x, y, z, a, b, c) -> e.check(object, w, x, y, z)), - "check force"); - } + boolean success = StructureUtility.iterateV2( + elements, + world, + extendedFacing, + basePositionX, + basePositionY, + basePositionZ, + basePositionA, + basePositionB, + basePositionC, + checkBlocksIfNotNullForceCheckAllIfTrue ? (e, w, x, y, z, a, b, c) -> e.check(object, w, x, y, z) + : skipBlockUnloaded((e, w, x, y, z, a, b, c) -> e.check(object, w, x, y, z)), + checkBlocksIfNotNullForceCheckAllIfTrue ? "check" : "check force"); if (StructureLibAPI.isDebugEnabled() && success) { StructureLib.LOGGER .info("Multi [" + basePositionX + ", " + basePositionY + ", " + basePositionZ + "] pass"); @@ -393,7 +378,7 @@ public interface IStructureDefinition { basePositionA, basePositionB, basePositionC, - ignoreBlockUnloaded((e, w, x, y, z, a, b, c) -> { + hintsOnly ? ignoreBlockUnloaded((e, w, x, y, z, a, b, c) -> { e.spawnHint(object, world, x, y, z, trigger); if (ChannelDataAccessor.hasSubChannel(trigger, StructureLibAPI.CHANNEL_SHOW_ERROR) && !e.couldBeValid(object, world, x, y, z, trigger)) { @@ -417,7 +402,7 @@ public interface IStructureDefinition { e.placeBlock(object, world, x, y, z, trigger); return true; }), - "placeBlock"); + hintsOnly ? "spawnHint" : "placeBlock"); } } return true; diff --git a/src/main/java/com/gtnewhorizon/structurelib/structure/IStructureElement.java b/src/main/java/com/gtnewhorizon/structurelib/structure/IStructureElement.java index 63d1b4b7..c307eaa0 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/structure/IStructureElement.java +++ b/src/main/java/com/gtnewhorizon/structurelib/structure/IStructureElement.java @@ -2,6 +2,8 @@ package com.gtnewhorizon.structurelib.structure; import static com.gtnewhorizon.structurelib.StructureLib.LOGGER; import static com.gtnewhorizon.structurelib.StructureLib.PANIC_MODE; +import static com.gtnewhorizon.structurelib.item.ItemConstructableTrigger.TriggerMode.REMOVING; +import static com.gtnewhorizon.structurelib.structure.StructureUtility.checkForRemoving; import java.util.Arrays; import java.util.Collections; @@ -21,6 +23,7 @@ import net.minecraft.util.IChatComponent; import net.minecraft.world.World; import com.gtnewhorizon.structurelib.StructureLibAPI; +import com.gtnewhorizon.structurelib.item.ItemConstructableTrigger; import com.gtnewhorizon.structurelib.util.ItemStackPredicate; import com.gtnewhorizon.structurelib.util.ItemStackPredicate.NBTMode; @@ -137,12 +140,14 @@ public interface IStructureElement { */ default PlaceResult survivalPlaceBlock(T t, World world, int x, int y, int z, ItemStack trigger, AutoPlaceEnvironment env) { + ItemConstructableTrigger.TriggerMode mode = ItemConstructableTrigger.getMode(trigger); BlocksToPlace e = getBlocksToPlace(t, world, x, y, z, trigger, env); IItemSource source = env.getSource(); EntityPlayer actor = env.getActor(); Consumer chatter = env.getChatter(); if (e != null) { - if (check(t, world, x, y, z)) return PlaceResult.SKIP; + PlaceResult skipOrRejectBasedOnCheck = checkForRemoving(mode, check(t, world, x, y, z), world, x, y, z); + if (skipOrRejectBasedOnCheck != PlaceResult.ACCEPT) return skipOrRejectBasedOnCheck; if (e.getStacks() == null) { ItemStack taken = source.takeOne(e.getPredicate(), true); return StructureUtility.survivalPlaceBlock( @@ -159,7 +164,7 @@ public interface IStructureElement { chatter); } for (ItemStack stack : e.getStacks()) { - if (!source.takeOne(stack, true)) continue; + if (mode != REMOVING && !source.takeOne(stack, true)) continue; return StructureUtility.survivalPlaceBlock( stack, NBTMode.EXACT, diff --git a/src/main/java/com/gtnewhorizon/structurelib/structure/StructureUtility.java b/src/main/java/com/gtnewhorizon/structurelib/structure/StructureUtility.java index 088d527a..a6faa406 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/structure/StructureUtility.java +++ b/src/main/java/com/gtnewhorizon/structurelib/structure/StructureUtility.java @@ -2,6 +2,9 @@ package com.gtnewhorizon.structurelib.structure; import static com.gtnewhorizon.structurelib.StructureLib.LOGGER; import static com.gtnewhorizon.structurelib.StructureLib.PANIC_MODE; +import static com.gtnewhorizon.structurelib.item.ItemConstructableTrigger.TriggerMode.BUILDING; +import static com.gtnewhorizon.structurelib.item.ItemConstructableTrigger.TriggerMode.REMOVING; +import static com.gtnewhorizon.structurelib.item.ItemConstructableTrigger.TriggerMode.UPDATING; import static java.lang.Integer.MIN_VALUE; import java.util.Arrays; @@ -28,6 +31,7 @@ import java.util.function.Supplier; import javax.annotation.Nullable; import net.minecraft.block.Block; +import net.minecraft.entity.item.EntityItem; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.init.Blocks; @@ -51,13 +55,16 @@ import com.gtnewhorizon.structurelib.StructureLib; import com.gtnewhorizon.structurelib.StructureLibAPI; import com.gtnewhorizon.structurelib.alignment.constructable.ChannelDataAccessor; import com.gtnewhorizon.structurelib.alignment.enumerable.ExtendedFacing; +import com.gtnewhorizon.structurelib.item.ItemConstructableTrigger; import com.gtnewhorizon.structurelib.structure.IStructureElement.PlaceResult; import com.gtnewhorizon.structurelib.structure.adders.IBlockAdder; import com.gtnewhorizon.structurelib.structure.adders.ITileAdder; import com.gtnewhorizon.structurelib.util.ItemStackPredicate; import com.gtnewhorizon.structurelib.util.ItemStackPredicate.NBTMode; import com.gtnewhorizon.structurelib.util.Vec3Impl; +import com.gtnewhorizon.structurelib.xmod.ServerUtilitiesCompat; +import cpw.mods.fml.common.Loader; import cpw.mods.fml.common.registry.GameRegistry; /** @@ -146,6 +153,8 @@ public class StructureUtility { private static final String NICE_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz=|!@#$%&()[]{};:<>/?_,.*^'`"; + private static final boolean isSULoaded = Loader.isModLoaded("serverutilities"); + @SuppressWarnings("rawtypes") private static final Map STEP = new HashMap<>(); @@ -206,7 +215,11 @@ public class StructureUtility { @Override public boolean placeBlock(Object o, World world, int x, int y, int z, ItemStack trigger) { - world.setBlock(x, y, z, StructureLibAPI.getBlockHint(), 14, 2); + placeRemoveOrUpdate( + trigger, + () -> world.setBlock(x, y, z, StructureLibAPI.getBlockHint(), 14, 2), + () -> world.setBlock(x, y, z, StructureLibAPI.getBlockHint(), 14, 2), + () -> world.setBlock(x, y, z, Blocks.air, 0, 2)); return true; } @@ -219,11 +232,16 @@ public class StructureUtility { @Override public PlaceResult survivalPlaceBlock(Object o, World world, int x, int y, int z, ItemStack trigger, AutoPlaceEnvironment env) { - if (check(o, world, x, y, z)) return PlaceResult.SKIP; + ItemConstructableTrigger.TriggerMode mode = ItemConstructableTrigger.getMode(trigger); + if (mode != REMOVING && check(o, world, x, y, z)) return PlaceResult.SKIP; // user should place anything here. // maybe make this configurable, but for now we try to take some cobble from user if (env.getSource().takeOne(new ItemStack(Blocks.cobblestone), false)) { - world.setBlock(x, y, z, Blocks.cobblestone, 0, 2); + placeRemoveOrUpdate( + trigger, + () -> world.setBlock(x, y, z, Blocks.cobblestone, 0, 2), + () -> world.setBlock(x, y, z, Blocks.cobblestone, 0, 2), + () -> world.setBlock(x, y, z, Blocks.air, 0, 2)); } return PlaceResult.REJECT; } @@ -330,29 +348,84 @@ public class StructureUtility { public static PlaceResult survivalPlaceBlock(Block block, int meta, World world, int x, int y, int z, IItemSource s, EntityPlayer actor, Consumer chatter) { if (block == null) throw new NullPointerException(); - if (!StructureLibAPI.isBlockTriviallyReplaceable(world, x, y, z, actor)) return PlaceResult.REJECT; + ItemConstructableTrigger.TriggerMode mode = BUILDING; + if (actor.getHeldItem() != null && actor.getHeldItem().getItem() instanceof ItemConstructableTrigger) { + // best effort lookup if the item in the player's hand is a trigger, and if it is, get the mode from the + // ...trigger instead of assuming default behavior + mode = ItemConstructableTrigger.getMode(actor.getHeldItem()); + } + if (mode == BUILDING && !StructureLibAPI.isBlockTriviallyReplaceable(world, x, y, z, actor)) + return PlaceResult.REJECT; + if (mode == UPDATING) { + if (!world.isAirBlock(x, y, z)) { + if (world.getTileEntity(x, y, z) instanceof TileEntity) { + // We're in updating mode and the block we're looking at might be important, so let's just leave it + // alone + // since it's probably not something we want to replace with something else + return PlaceResult.SKIP; + } + Block blockInWorld = world.getBlock(x, y, z); + if (blockInWorld.isAssociatedBlock(block) && blockInWorld.getDamageValue(world, x, y, z) == meta) { + // This is the block we are looking for, move along + return PlaceResult.SKIP; + } + } + } Item itemBlock = Item.getItemFromBlock(block); int itemMeta = itemBlock instanceof ISpecialItemBlock ? ((ISpecialItemBlock) itemBlock).getItemMetaFromBlockMeta(block, meta) : meta; ItemStack stack = new ItemStack(itemBlock, 1, itemMeta); - if (!s.takeOne(stack, true)) { - if (chatter != null) chatter.accept( - new ChatComponentTranslation( - "structurelib.autoplace.error.no_simple_block", - stack.func_151000_E())); - return PlaceResult.REJECT; - } - if (block instanceof ICustomBlockSetting) { - ICustomBlockSetting blockCustom = (ICustomBlockSetting) block; - blockCustom.setBlock(world, x, y, z, meta); - } else if (!stack.copy() - .tryPlaceItemIntoWorld(actor, world, x, y, z, ForgeDirection.UP.ordinal(), 0.5f, 0.5f, 0.5f)) { + if (mode != REMOVING) { + if (!s.takeOne(stack, true)) { + if (chatter != null) chatter.accept( + new ChatComponentTranslation( + "structurelib.autoplace.error.no_simple_block", + stack.func_151000_E())); + return PlaceResult.REJECT; + } + if (block instanceof ICustomBlockSetting) { + ICustomBlockSetting blockCustom = (ICustomBlockSetting) block; + blockCustom.setBlock(world, x, y, z, meta); + } else { + if (mode == UPDATING && !world.isAirBlock(x, y, z) && checkPermission(world, actor, x, y, z)) { + // We're in updating mode and there's a block in our way (grr), so let's remove whatever is there + // ...and drop it to the player + ItemStack toGive = new ItemStack(world.getBlock(x, y, z)); + toGive.setItemDamage(world.getBlock(x, y, z).getDamageValue(world, x, y, z)); + if (!world.setBlock(x, y, z, Blocks.air, 0, 4) || !tryGiveOrDropItem(world, actor, toGive)) { + // We either weren't able to give the player the block that was there, or we weren't able to + // remove + // the block, either way, we need to reject the update. Also, since we hid the block update from + // the client in an effort to make the exchange as seamless as possible, we need to now notify + // the client if we did successfully remove the block. + if (world.isAirBlock(x, y, z)) world.notifyBlockChange(x, y, z, Blocks.air); + return PlaceResult.REJECT; + } + } + if (!stack.copy() + .tryPlaceItemIntoWorld(actor, world, x, y, z, ForgeDirection.UP.ordinal(), 0.5f, 0.5f, 0.5f)) { return PlaceResult.REJECT; } - if (!s.takeOne(stack, false)) { - // rollback + } + } + if (mode == REMOVING || !s.takeOne(stack, false)) { + // If we're in removing mode, or if the user didn't have the block in their inventory, and we're in some + // other mode, let's check some stuff + if (world.isAirBlock(x, y, z)) { + return PlaceResult.SKIP; + } + ItemStack toGive = new ItemStack(world.getBlock(x, y, z)); + toGive.setItemDamage(world.getBlock(x, y, z).getDamageValue(world, x, y, z)); + if (mode == REMOVING && !checkPermission(world, actor, x, y, z)) { + return PlaceResult.REJECT; + } world.setBlockToAir(x, y, z); + if (mode == REMOVING && !tryGiveOrDropItem(world, actor, toGive)) { + // We're in removing mode and something went wrong while trying to give the player the block we removed, + // let's just fail for now + return PlaceResult.REJECT; + } } return PlaceResult.ACCEPT; } @@ -444,24 +517,82 @@ public class StructureUtility { if (stack == null) throw new NullPointerException(); if (stack.stackSize != 1) throw new IllegalArgumentException(); if (!(stack.getItem() instanceof ItemBlock)) throw new IllegalArgumentException(); - if (!StructureLibAPI.isBlockTriviallyReplaceable(world, x, y, z, actor)) return PlaceResult.REJECT; - if (!assumeStackPresent && !s.takeOne(stack, true)) { - if (chatter != null) chatter.accept( - new ChatComponentTranslation("structurelib.autoplace.error.no_item_stack", stack.func_151000_E())); - return PlaceResult.REJECT; + ItemConstructableTrigger.TriggerMode mode = BUILDING; + if (actor.getHeldItem() != null && actor.getHeldItem().getItem() instanceof ItemConstructableTrigger) { + // best effort lookup if the item in the player's hand is a trigger, and if it is, get the mode from the + // ...trigger instead of assuming default behavior + mode = ItemConstructableTrigger.getMode(actor.getHeldItem()); } - if (!stack.copy().tryPlaceItemIntoWorld(actor, world, x, y, z, ForgeDirection.UP.ordinal(), 0.5f, 0.5f, 0.5f)) - return PlaceResult.REJECT; - if (!s.takeOne(stack, false)) - // this is bad! probably an exploit somehow. Let's nullify the block we just placed instead + if (mode != REMOVING) { + if (mode == BUILDING) { + if (!StructureLibAPI.isBlockTriviallyReplaceable(world, x, y, z, actor)) { + return PlaceResult.SKIP; + } + } + if (mode == UPDATING) { + if (!world.isAirBlock(x, y, z)) { + if (world.getTileEntity(x, y, z) instanceof TileEntity) { + return PlaceResult.SKIP; + } + Block blockInWorld = world.getBlock(x, y, z); + ItemStack blockInWorldItem = new ItemStack(blockInWorld); + blockInWorldItem.setItemDamage(blockInWorld.getDamageValue(world, x, y, z)); + if (blockInWorldItem.isItemEqual(stack)) { + // This is the block we are looking for, move along + return PlaceResult.SKIP; + } + } + } + if (!assumeStackPresent && !s.takeOne(stack, true)) { + if (chatter != null) chatter.accept( + new ChatComponentTranslation( + "structurelib.autoplace.error.no_item_stack", + stack.func_151000_E())); + return PlaceResult.REJECT; + } + if (mode == UPDATING && !world.isAirBlock(x, y, z) && checkPermission(world, actor, x, y, z)) { + // We're in updating mode and there's a block in our way (grr), so let's remove whatever is there + // ...and drop it to the player + ItemStack toGive = new ItemStack(world.getBlock(x, y, z)); + toGive.setItemDamage(world.getBlock(x, y, z).getDamageValue(world, x, y, z)); + if (!world.setBlock(x, y, z, Blocks.air, 0, 4) || !tryGiveOrDropItem(world, actor, toGive)) { + // We either weren't able to give the player the block that was there, or we weren't able to remove + // the block, either way, we need to reject the update. Also, since we hid the block update from + // the client in an effort to make the exchange as seamless as possible, we need to now notify + // the client if we did successfully remove the block. + if (world.isAirBlock(x, y, z)) world.notifyBlockChange(x, y, z, Blocks.air); + return PlaceResult.REJECT; + } + } + if (!stack.copy() + .tryPlaceItemIntoWorld(actor, world, x, y, z, ForgeDirection.UP.ordinal(), 0.5f, 0.5f, 0.5f)) { + return PlaceResult.REJECT; + } + } + if (mode == REMOVING || !s.takeOne(stack, false)) { + if (world.isAirBlock(x, y, z)) { + return PlaceResult.SKIP; + } + // If the user didn't have the item in their inventory, or we're in removing mode, we set it to air + ItemStack toGive = new ItemStack(world.getBlock(x, y, z)); + toGive.setItemDamage(world.getBlock(x, y, z).getDamageValue(world, x, y, z)); + if (mode == REMOVING && !checkPermission(world, actor, x, y, z)) { + return PlaceResult.REJECT; + } world.setBlockToAir(x, y, z); + if (mode == REMOVING && !tryGiveOrDropItem(world, actor, toGive)) { + // We were in removing mode and something went wrong while trying to give the player the block we + // ...removed, let's just fail for now + return PlaceResult.REJECT; + } + } return PlaceResult.ACCEPT; } /** * Return a structure element that only allow air (or air equivalents, like Railcraft hidden blocks). You usually - * don't need to call this yourselves. Use {@code -} in shape to automatically use this. Provided nontheless in case - * you want this as a fallback to something else. + * don't need to call this yourselves. Use {@code -} in shape to automatically use this. Provided nonetheless in + * case you want this as a fallback to something else. */ @SuppressWarnings("unchecked") public static IStructureElement isAir() { @@ -470,8 +601,8 @@ public class StructureUtility { /** * Return a structure element that allow anything but air (or air equivalents, like Railcraft hidden blocks). You - * usually don't need to call this yourselves. Use {@code +} in shape to automatically use this. Provided nontheless - * in case you want this as a fallback to something else. + * usually don't need to call this yourselves. Use {@code +} in shape to automatically use this. Provided + * nonetheless in case you want this as a fallback to something else. */ @SuppressWarnings("unchecked") public static IStructureElement notAir() { @@ -707,7 +838,7 @@ public class StructureUtility { List> hints = allKnownTiers == null ? Collections.emptyList() : allKnownTiers; if (hints.stream().anyMatch(Objects::isNull)) throw new IllegalArgumentException(); IStructureElementCheckOnly check = ofBlocksTiered(tierExtractor, notSet, setter, getter); - return new StructureElement_Bridge() { + return new StructureElement_Bridge<>() { @Override public boolean check(T t, World world, int x, int y, int z) { @@ -732,6 +863,11 @@ public class StructureUtility { return Objects.equals(hintTier, worldTier); } + private boolean isInAllKnownTiers(Block b) { + return allKnownTiers != null + && allKnownTiers.stream().anyMatch(pair -> pair.getLeft().isAssociatedBlock(b)); + } + @Override public boolean spawnHint(T t, World world, int x, int y, int z, ItemStack trigger) { Pair hint = getHint(trigger); @@ -764,13 +900,17 @@ public class StructureUtility { @Override public PlaceResult survivalPlaceBlock(T t, World world, int x, int y, int z, ItemStack trigger, AutoPlaceEnvironment env) { + ItemConstructableTrigger.TriggerMode mode = ItemConstructableTrigger.getMode(trigger); Pair hint = getHint(trigger); if (hint == null) return PlaceResult.REJECT; // TODO or SKIP? Block block = world.getBlock(x, y, z); int meta = world.getBlockMetadata(x, y, z); TIER tier = tierExtractor.convert(block, meta); - if (Objects.equals(tier, tierExtractor.convert(hint.getKey(), hint.getValue()))) + if (mode != REMOVING && (Objects.equals(tier, tierExtractor.convert(hint.getKey(), hint.getValue())))) return PlaceResult.SKIP; + if (mode == REMOVING && (!isInAllKnownTiers(block) && !world.isAirBlock(x, y, z))) { + return PlaceResult.REJECT; + } return StructureUtility.survivalPlaceBlock( hint.getKey(), hint.getValue(), @@ -859,7 +999,9 @@ public class StructureUtility { @Override public PlaceResult survivalPlaceBlock(T t, World world, int x, int y, int z, ItemStack trigger, AutoPlaceEnvironment env) { - if (check(t, world, x, y, z)) return PlaceResult.SKIP; + ItemConstructableTrigger.TriggerMode mode = ItemConstructableTrigger.getMode(trigger); + PlaceResult skipOrRejectBasedOnCheck = checkForRemoving(mode, check(t, world, x, y, z), world, x, y, z); + if (skipOrRejectBasedOnCheck != PlaceResult.ACCEPT) return skipOrRejectBasedOnCheck; if (getBlock() == null) return PlaceResult.REJECT; return StructureUtility.survivalPlaceBlock( getBlock(), @@ -946,7 +1088,9 @@ public class StructureUtility { @Deprecated public PlaceResult survivalPlaceBlock(T t, World world, int x, int y, int z, ItemStack trigger, IItemSource s, EntityPlayerMP actor, Consumer chatter) { - if (check(t, world, x, y, z)) return PlaceResult.SKIP; + ItemConstructableTrigger.TriggerMode mode = ItemConstructableTrigger.getMode(trigger); + PlaceResult skipOrRejectBasedOnCheck = checkForRemoving(mode, check(t, world, x, y, z), world, x, y, z); + if (skipOrRejectBasedOnCheck != PlaceResult.ACCEPT) return skipOrRejectBasedOnCheck; if (init()) return StructureUtility.survivalPlaceBlock(block, meta, world, x, y, z, s, actor, chatter); return fallback.survivalPlaceBlock(t, world, x, y, z, trigger, s, actor, chatter); } @@ -954,7 +1098,9 @@ public class StructureUtility { @Override public PlaceResult survivalPlaceBlock(T t, World world, int x, int y, int z, ItemStack trigger, AutoPlaceEnvironment env) { - if (check(t, world, x, y, z)) return PlaceResult.SKIP; + ItemConstructableTrigger.TriggerMode mode = ItemConstructableTrigger.getMode(trigger); + PlaceResult skipOrRejectBasedOnCheck = checkForRemoving(mode, check(t, world, x, y, z), world, x, y, z); + if (skipOrRejectBasedOnCheck != PlaceResult.ACCEPT) return skipOrRejectBasedOnCheck; if (init()) return StructureUtility.survivalPlaceBlock( block, meta, @@ -1582,8 +1728,15 @@ public class StructureUtility { @Deprecated public PlaceResult survivalPlaceBlock(T t, World world, int x, int y, int z, ItemStack trigger, AutoPlaceEnvironment env) { - if (world.getBlock(x, y, z) == defaultBlock && world.getBlockMetadata(x, y, z) == defaultMeta) - return PlaceResult.SKIP; + ItemConstructableTrigger.TriggerMode mode = ItemConstructableTrigger.getMode(trigger); + PlaceResult skipOrRejectBasedOnCheck = checkForRemoving( + mode, + world.getBlock(x, y, z) == defaultBlock && world.getBlockMetadata(x, y, z) == defaultMeta, + world, + x, + y, + z); + if (skipOrRejectBasedOnCheck != PlaceResult.ACCEPT) return skipOrRejectBasedOnCheck; return StructureUtility.survivalPlaceBlock( defaultBlock, defaultMeta, @@ -1628,8 +1781,15 @@ public class StructureUtility { @Deprecated public PlaceResult survivalPlaceBlock(T t, World world, int x, int y, int z, ItemStack trigger, AutoPlaceEnvironment env) { - if (world.getBlock(x, y, z) == defaultBlock && world.getBlockMetadata(x, y, z) == defaultMeta) - return PlaceResult.SKIP; + ItemConstructableTrigger.TriggerMode mode = ItemConstructableTrigger.getMode(trigger); + PlaceResult skipOrRejectBasedOnCheck = checkForRemoving( + mode, + world.getBlock(x, y, z) == defaultBlock && world.getBlockMetadata(x, y, z) == defaultMeta, + world, + x, + y, + z); + if (skipOrRejectBasedOnCheck != PlaceResult.ACCEPT) return skipOrRejectBasedOnCheck; return StructureUtility.survivalPlaceBlock( defaultBlock, defaultMeta, @@ -1888,6 +2048,7 @@ public class StructureUtility { @Deprecated public PlaceResult survivalPlaceBlock(T t, World world, int x, int y, int z, ItemStack trigger, IItemSource s, EntityPlayerMP actor, Consumer chatter) { + ItemConstructableTrigger.TriggerMode mode = ItemConstructableTrigger.getMode(trigger); if (predicate.test(t)) return downstream.survivalPlaceBlock(t, world, x, y, z, trigger, s, actor, chatter); return placeResultWhenDisabled; @@ -1896,6 +2057,7 @@ public class StructureUtility { @Override public PlaceResult survivalPlaceBlock(T t, World world, int x, int y, int z, ItemStack trigger, AutoPlaceEnvironment env) { + ItemConstructableTrigger.TriggerMode mode = ItemConstructableTrigger.getMode(trigger); if (predicate.test(t)) return downstream.survivalPlaceBlock(t, world, x, y, z, trigger, env); return placeResultWhenDisabled; } @@ -3263,4 +3425,57 @@ public class StructureUtility { } return shape; } + + /** + * Performs a creation, removal, or update operation at the given position + * + * @param trigger ItemStack for the trigger item + * @param onCreate Operation to be performed when the mode of the trigger is set to building mode + * @param onUpdate Operation to be performed when the mode of the trigger is set to updating mode + * @param onRemove Operation to be performed when the mode of the trigger is set to removing mode + */ + public static void placeRemoveOrUpdate(ItemStack trigger, Runnable onCreate, Runnable onUpdate, Runnable onRemove) { + ItemConstructableTrigger.TriggerMode mode = ItemConstructableTrigger.getMode(trigger); + switch (mode) { + case BUILDING: + onCreate.run(); + break; + case UPDATING: + onUpdate.run(); + break; + case REMOVING: + onRemove.run(); + break; + } + } + + public static boolean tryGiveOrDropItem(World world, EntityPlayer actor, ItemStack stack) { + if (actor.inventory.addItemStackToInventory(stack)) { + actor.inventoryContainer.detectAndSendChanges(); + return true; + } + EntityItem toSpawn = new EntityItem(world, actor.posX, actor.posY, actor.posZ, stack); + // For some reason these things sometimes spawn with the velocity of an F1 car, so let's prevent that + toSpawn.motionX = 0; + toSpawn.motionY = 0; + toSpawn.motionZ = 0; + return world.spawnEntityInWorld(toSpawn); + } + + public static boolean checkPermission(World world, EntityPlayer actor, int x, int y, int z) { + boolean res = true; + if (isSULoaded) { + res = ServerUtilitiesCompat.checkPermission(world, actor, x, z); + } + res = res && !world.getBlock(x, y, z).isAssociatedBlock(Blocks.bedrock); + // maybe check mining level who knows :shrug: + return res; + } + + public static PlaceResult checkForRemoving(ItemConstructableTrigger.TriggerMode mode, boolean checkResult, + World world, int x, int y, int z) { + if (mode != REMOVING && checkResult) return PlaceResult.SKIP; + else if (mode != BUILDING && !checkResult && !world.isAirBlock(x, y, z)) return PlaceResult.REJECT; + return PlaceResult.ACCEPT; + } } diff --git a/src/main/java/com/gtnewhorizon/structurelib/xmod/ServerUtilitiesCompat.java b/src/main/java/com/gtnewhorizon/structurelib/xmod/ServerUtilitiesCompat.java new file mode 100644 index 00000000..4dace253 --- /dev/null +++ b/src/main/java/com/gtnewhorizon/structurelib/xmod/ServerUtilitiesCompat.java @@ -0,0 +1,29 @@ +package com.gtnewhorizon.structurelib.xmod; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.world.World; + +import cpw.mods.fml.common.Optional; +import serverutils.data.ClaimedChunks; +import serverutils.lib.data.ForgePlayer; +import serverutils.lib.data.ForgeTeam; +import serverutils.lib.data.Universe; +import serverutils.lib.math.ChunkDimPos; + +public class ServerUtilitiesCompat { + + @Optional.Method(modid = "serverutilities") + public static boolean checkPermission(World world, EntityPlayer actor, int x, int z) { + final ForgeTeam owningTeam = ClaimedChunks.instance + .getChunkTeam(new ChunkDimPos(x >> 4, z >> 4, world.provider.dimensionId)); + if (owningTeam == null) { + return true; + } + final ForgePlayer currentPlayer = new ForgePlayer( + Universe.get(), + actor.getUniqueID(), + actor.getCommandSenderName()); + return owningTeam.isAlly(currentPlayer) || owningTeam.isMember(currentPlayer); + } + +} diff --git a/src/main/resources/assets/structurelib/lang/en_US.lang b/src/main/resources/assets/structurelib/lang/en_US.lang index 15ce8cb4..c2f4b194 100644 --- a/src/main/resources/assets/structurelib/lang/en_US.lang +++ b/src/main/resources/assets/structurelib/lang/en_US.lang @@ -22,6 +22,9 @@ item.structurelib.frontRotationTool.desc.1=Rotates only the front panel, item.structurelib.frontRotationTool.desc.2=which allows structure rotation. item.structurelib.constructableTrigger.name=Multiblock Structure Hologram Projector +item.structurelib.constructableTrigger.modes.0=Building Mode +item.structurelib.constructableTrigger.modes.1=Updating Mode +item.structurelib.constructableTrigger.modes.2=Removing Mode item.structurelib.constructableTrigger.desc.0=Triggers Constructable Interface item.structurelib.constructableTrigger.desc.1=Shows multiblock construction schematic, item.structurelib.constructableTrigger.desc.2=use on the multiblock controller. @@ -37,6 +40,12 @@ item.structurelib.constructableTrigger.gui.add=Add item.structurelib.constructableTrigger.gui.set=Set item.structurelib.constructableTrigger.gui.unset=Unset item.structurelib.constructableTrigger.gui.wipe=§4Wipe +item.structurelib.constructableTrigger.gui.error.show=Show Errors +item.structurelib.constructableTrigger.gui.error.hide=Hide Errors +item.structurelib.constructableTrigger.gui.gt_no_hatch.enable=No Hatch +item.structurelib.constructableTrigger.gui.gt_no_hatch.disable=Hatches +item.structurelib.constructableTrigger.keyCategory=Multiblock Structure Hologram Projector Keys +item.structurelib.constructableTrigger.keys.cycleMode=Cycle Projector Mode item.structurelib.constructableTrigger.gui.channels.from=§rMod: §9§o%s item.structurelib.constructableTrigger.gui.info=Many multiblocks support some degree of variation in its structure and use sub channels to indicate which variant to use.\n Sub channels are has a sequence of lower case characters as key and a positive integer as value. Most known sub channels should be available as typeahead completions.\nYou can also drag and drop items from NEI to set the corresponding sub channel value. These items all have a tooltip of §oIndicates value X for sub channel Y§r. diff --git a/src/main/resources/assets/structurelib/textures/items/itemConstructableTrigger.png b/src/main/resources/assets/structurelib/textures/items/constructableTrigger/BUILDING.png similarity index 100% rename from src/main/resources/assets/structurelib/textures/items/itemConstructableTrigger.png rename to src/main/resources/assets/structurelib/textures/items/constructableTrigger/BUILDING.png diff --git a/src/main/resources/assets/structurelib/textures/items/constructableTrigger/REMOVING.png b/src/main/resources/assets/structurelib/textures/items/constructableTrigger/REMOVING.png new file mode 100644 index 00000000..adcb90de Binary files /dev/null and b/src/main/resources/assets/structurelib/textures/items/constructableTrigger/REMOVING.png differ diff --git a/src/main/resources/assets/structurelib/textures/items/constructableTrigger/UPDATING.png b/src/main/resources/assets/structurelib/textures/items/constructableTrigger/UPDATING.png new file mode 100644 index 00000000..c9ed6178 Binary files /dev/null and b/src/main/resources/assets/structurelib/textures/items/constructableTrigger/UPDATING.png differ