From 044b77c803a1ab42876ccdb0c3d97832706b7764 Mon Sep 17 00:00:00 2001 From: Alex Vermette Date: Wed, 16 Sep 2026 20:00:11 -0400 Subject: [PATCH 1/6] feat(Hologram): Middle-mouse Pick in GUI --- .../structurelib/ChannelPickHandler.java | 60 ++++++++++++++++++ .../structurelib/core/StructureLibCore.java | 5 +- .../early/MixinGuiContainer_PickChannel.java | 63 +++++++++++++++++++ .../net/SetChannelDataMessage.java | 18 ++++-- .../assets/structurelib/lang/en_US.lang | 4 +- 5 files changed, 143 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/gtnewhorizon/structurelib/ChannelPickHandler.java create mode 100644 src/main/java/com/gtnewhorizon/structurelib/mixins/early/MixinGuiContainer_PickChannel.java diff --git a/src/main/java/com/gtnewhorizon/structurelib/ChannelPickHandler.java b/src/main/java/com/gtnewhorizon/structurelib/ChannelPickHandler.java new file mode 100644 index 00000000..3848ec7d --- /dev/null +++ b/src/main/java/com/gtnewhorizon/structurelib/ChannelPickHandler.java @@ -0,0 +1,60 @@ +package com.gtnewhorizon.structurelib; + +import java.util.Collection; +import java.util.Map; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.resources.I18n; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; +import net.minecraft.util.ChatComponentText; + +import com.gtnewhorizon.structurelib.alignment.constructable.ChannelDataAccessor; +import com.gtnewhorizon.structurelib.net.SetChannelDataMessage; + +/** + * Client-side handler for middle-click channel picking. When the player holds a hologram projector on the cursor and + * middle-clicks a registered indicator item, the corresponding channel is set on the projector. Shift+middle-click + * wipes all channels. + */ +public final class ChannelPickHandler { + + private ChannelPickHandler() {} + + /** + * Handle a middle-click event while holding a hologram projector on the cursor. + * + * @param holoStack the hologram projector on the cursor (already validated) + * @param targetStack the item stack under the mouse from a vanilla slot, may be null + * @return true if the event was handled (caller should cancel) + */ + public static boolean handleMiddleClick(ItemStack holoStack, ItemStack targetStack, final boolean isSneaking) { + EntityPlayer player = Minecraft.getMinecraft().thePlayer; + + // Clear channels on shift+middle-click + if (isSneaking) { + ChannelDataAccessor.wipeChannelData(holoStack); + sendCursorChannelData(holoStack); + player.addChatMessage(new ChatComponentText(I18n.format("structurelib.pickchannel.cleared"))); + return true; + } + + if (targetStack == null) return true; // nothing hovered, cancel silently + + Collection> channels = ChannelDescription.iterate(targetStack); + if (channels.isEmpty()) return true; // no channel mapping, cancel silently + + // Assuming there's only 1 channel per item for now (same behavior as drag and drop) + Map.Entry first = channels.iterator().next(); + ChannelDataAccessor.setChannelData(holoStack, first.getKey(), first.getValue()); + sendCursorChannelData(holoStack); + player.addChatMessage( + new ChatComponentText( + I18n.format("structurelib.pickchannel.success", first.getKey(), first.getValue()))); + return true; + } + + private static void sendCursorChannelData(ItemStack holoStack) { + StructureLib.net.sendToServer(new SetChannelDataMessage(holoStack, true)); + } +} diff --git a/src/main/java/com/gtnewhorizon/structurelib/core/StructureLibCore.java b/src/main/java/com/gtnewhorizon/structurelib/core/StructureLibCore.java index e8eb6d4b..e00a7a6c 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/core/StructureLibCore.java +++ b/src/main/java/com/gtnewhorizon/structurelib/core/StructureLibCore.java @@ -44,6 +44,9 @@ public String getMixinConfig() { @Override public List getMixins(Set loadedCoreMods) { - return Arrays.asList("blockChangeNotifier.MixinForgeHooks", "blockChangeNotifier.MixinWorld"); + return Arrays.asList( + "blockChangeNotifier.MixinForgeHooks", + "blockChangeNotifier.MixinWorld", + "MixinGuiContainer_PickChannel"); } } diff --git a/src/main/java/com/gtnewhorizon/structurelib/mixins/early/MixinGuiContainer_PickChannel.java b/src/main/java/com/gtnewhorizon/structurelib/mixins/early/MixinGuiContainer_PickChannel.java new file mode 100644 index 00000000..48aeb591 --- /dev/null +++ b/src/main/java/com/gtnewhorizon/structurelib/mixins/early/MixinGuiContainer_PickChannel.java @@ -0,0 +1,63 @@ +package com.gtnewhorizon.structurelib.mixins.early; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.inventory.GuiContainer; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.inventory.Slot; +import net.minecraft.item.ItemStack; + +import org.lwjgl.input.Keyboard; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import com.gtnewhorizon.structurelib.ChannelPickHandler; +import com.gtnewhorizon.structurelib.StructureLib; +import com.gtnewhorizon.structurelib.item.ItemConstructableTrigger; + +import codechicken.nei.NEIClientConfig; +import codechicken.nei.guihook.GuiContainerManager; +import cpw.mods.fml.common.Loader; + +@Mixin(value = GuiContainer.class, priority = 100) +public abstract class MixinGuiContainer_PickChannel { + + @Inject(method = "mouseClicked(III)V", at = @At("HEAD"), cancellable = true) + private void slib$onMouseClicked(int mouseX, int mouseY, int mouseButton, CallbackInfo ci) { + if (mouseButton != 2) return; + + EntityPlayer player = Minecraft.getMinecraft().thePlayer; + if (player == null) return; + + ItemStack cursorStack = player.inventory.getItemStack(); + if (cursorStack == null || !(cursorStack.getItem() instanceof ItemConstructableTrigger)) return; + + // Sink middle click as soon as projector is picked up + ci.cancel(); + + ItemStack hoveredStack = null; + try { + Slot slot = slib$invokeGetSlotAtPosition(mouseX, mouseY); + if (slot != null) { + hoveredStack = slot.getStack(); + } + + // try NEI + if (hoveredStack == null && Loader.isModLoaded("NotEnoughItems") && !NEIClientConfig.isHidden()) { + hoveredStack = GuiContainerManager.getStackMouseOver((GuiContainer) (Object) this); + } + } catch (Exception e) { + StructureLib.LOGGER.error("Error while resolving hovered slot for channel pick", e); + } + + ChannelPickHandler.handleMiddleClick( + cursorStack, + hoveredStack, + Keyboard.isKeyDown(Minecraft.getMinecraft().gameSettings.keyBindSneak.getKeyCode())); + } + + @Invoker("getSlotAtPosition") + protected abstract Slot slib$invokeGetSlotAtPosition(int x, int y); +} diff --git a/src/main/java/com/gtnewhorizon/structurelib/net/SetChannelDataMessage.java b/src/main/java/com/gtnewhorizon/structurelib/net/SetChannelDataMessage.java index 5e5d3d53..f0f83835 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/net/SetChannelDataMessage.java +++ b/src/main/java/com/gtnewhorizon/structurelib/net/SetChannelDataMessage.java @@ -23,15 +23,22 @@ public class SetChannelDataMessage implements IMessage { private final List> data = new ArrayList<>(); + private boolean onCursor; public SetChannelDataMessage() {} public SetChannelDataMessage(ItemStack trigger) { + this(trigger, false); + } + + public SetChannelDataMessage(ItemStack trigger, final boolean onCursor) { + this.onCursor = onCursor; ChannelDataAccessor.iterateChannelData(trigger).forEach(data::add); } @Override public void fromBytes(ByteBuf buf) { + onCursor = buf.readBoolean(); data.clear(); int size = ByteBufUtils.readVarShort(buf); for (int i = 0; i < size; i++) { @@ -41,6 +48,7 @@ public void fromBytes(ByteBuf buf) { @Override public void toBytes(ByteBuf buf) { + buf.writeBoolean(onCursor); ByteBufUtils.writeVarShort(buf, data.size()); for (Entry e : data) { ByteBufUtils.writeUTF8String(buf, e.getKey()); @@ -53,11 +61,11 @@ public static class Handler implements IMessageHandler e : message.data) { - ChannelDataAccessor.setChannelData(heldItem, e.getKey(), e.getValue()); + ChannelDataAccessor.setChannelData(target, e.getKey(), e.getValue()); } // since this is a set all channel request from the client, we would assume client already know // what this would look like on the client, so no sync @@ -65,7 +73,7 @@ public IMessage onMessage(SetChannelDataMessage message, MessageContext ctx) { StructureLib.LOGGER.warn( "{} trying to set channel data on {}, which is not a hologram projector!", player.getUniqueID(), - heldItem); + target); } return null; } diff --git a/src/main/resources/assets/structurelib/lang/en_US.lang b/src/main/resources/assets/structurelib/lang/en_US.lang index 529432e4..0ae7f2e8 100644 --- a/src/main/resources/assets/structurelib/lang/en_US.lang +++ b/src/main/resources/assets/structurelib/lang/en_US.lang @@ -44,7 +44,9 @@ item.structurelib.constructableTrigger.gui.info=Many multiblocks support some de channels.structurelib.show_errors=When set, will highlight parts of the multiblock that is definitely wrong structurelib.tooltip.channelvalue=§7Value §b%d§r§7 for subchannel §b%s -structurelib.tooltip.indicator_dnd=Drop into GUI to select this value for this channel +structurelib.tooltip.indicator_dnd=Drop into GUI or middle-click to select this value for this channel +structurelib.pickchannel.success=§aSet §b%s§r§a to §b%d§r +structurelib.pickchannel.cleared=§aAll channels cleared§r structurelib.autoplace.built_stat=Built %s blocks structurelib.autoplace.complete=§aThis multiblock has completed! From ff90efa27b80a7443b1c3219bab491c1f24fcb83 Mon Sep 17 00:00:00 2001 From: Alex Vermette Date: Wed, 16 Sep 2026 22:29:07 -0400 Subject: [PATCH 2/6] feat(Hologram): Middle-mouse Pick in-world + bogo cancel + server-side refact. --- dependencies.gradle | 2 +- .../structurelib/ChannelPickHandler.java | 66 +++++++++++++------ .../structurelib/ClientProxy.java | 39 +++++++++++ .../structurelib/CommonProxy.java | 3 + .../structurelib/StructureLib.java | 6 ++ .../early/MixinGuiContainer_PickChannel.java | 14 ++++ .../structurelib/util/BogoCompat.java | 37 +++++++++++ .../structurelib/util/MiscUtils.java | 26 ++++++++ 8 files changed, 171 insertions(+), 22 deletions(-) create mode 100644 src/main/java/com/gtnewhorizon/structurelib/util/BogoCompat.java diff --git a/dependencies.gradle b/dependencies.gradle index 05de3f13..b8de7799 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -3,7 +3,7 @@ dependencies { api("com.github.GTNewHorizons:GTNHLib:0.11.21:dev") implementation("com.github.GTNewHorizons:NotEnoughItems:2.8.110-GTNH:dev") - + compileOnly("com.github.GTNewHorizons:InventoryBogoSorter:1.3.51-GTNH:dev") testImplementation(platform('org.junit:junit-bom:5.9.2')) testImplementation('org.junit.jupiter:junit-jupiter') testRuntimeOnly ('org.junit.platform:junit-platform-launcher') diff --git a/src/main/java/com/gtnewhorizon/structurelib/ChannelPickHandler.java b/src/main/java/com/gtnewhorizon/structurelib/ChannelPickHandler.java index 3848ec7d..77a4d163 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/ChannelPickHandler.java +++ b/src/main/java/com/gtnewhorizon/structurelib/ChannelPickHandler.java @@ -3,19 +3,24 @@ import java.util.Collection; import java.util.Map; -import net.minecraft.client.Minecraft; -import net.minecraft.client.resources.I18n; +import javax.annotation.Nullable; + +import net.minecraft.block.Block; import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.item.ItemStack; -import net.minecraft.util.ChatComponentText; +import net.minecraft.util.ChatComponentTranslation; +import net.minecraft.util.MovingObjectPosition; +import net.minecraft.world.World; import com.gtnewhorizon.structurelib.alignment.constructable.ChannelDataAccessor; import com.gtnewhorizon.structurelib.net.SetChannelDataMessage; /** - * Client-side handler for middle-click channel picking. When the player holds a hologram projector on the cursor and - * middle-clicks a registered indicator item, the corresponding channel is set on the projector. Shift+middle-click - * wipes all channels. + * Handler for middle-click channel picking. When the player holds a hologram projector on the cursor and middle-clicks + * a registered indicator item, the corresponding channel is set on the projector. The same applies in world when the + * projector is held in hand and a registered indicator block is middle-clicked. Shift+middle-click wipes all channels. + * Was made side neutral to support server-side validation at some point if necessary. */ public final class ChannelPickHandler { @@ -23,38 +28,57 @@ private ChannelPickHandler() {} /** * Handle a middle-click event while holding a hologram projector on the cursor. - * - * @param holoStack the hologram projector on the cursor (already validated) - * @param targetStack the item stack under the mouse from a vanilla slot, may be null - * @return true if the event was handled (caller should cancel) */ - public static boolean handleMiddleClick(ItemStack holoStack, ItemStack targetStack, final boolean isSneaking) { - EntityPlayer player = Minecraft.getMinecraft().thePlayer; + public static boolean handleMiddleClick(EntityPlayer player, ItemStack holoStack, @Nullable ItemStack targetStack, + final boolean isSneaking) { + return handlePick(player, holoStack, targetStack, isSneaking, true); + } + + public static boolean handleWorldPick(World world, EntityPlayer player, ItemStack holoStack, + @Nullable MovingObjectPosition hit, final boolean isSneaking) { + ItemStack targetStack = null; + + if (hit != null) { + Block block = world.getBlock(hit.blockX, hit.blockY, hit.blockZ); + if (!block.isAir(world, hit.blockX, hit.blockY, hit.blockZ)) { + targetStack = block.getPickBlock(hit, world, hit.blockX, hit.blockY, hit.blockZ, player); + } + } + // A null target still goes through so that shift+middle-click wipes even when aiming at nothing + return handlePick(player, holoStack, targetStack, isSneaking, false); + } + + private static boolean handlePick(EntityPlayer player, ItemStack holoStack, @Nullable ItemStack targetStack, + final boolean isSneaking, final boolean onCursor) { // Clear channels on shift+middle-click if (isSneaking) { ChannelDataAccessor.wipeChannelData(holoStack); - sendCursorChannelData(holoStack); - player.addChatMessage(new ChatComponentText(I18n.format("structurelib.pickchannel.cleared"))); + sendChannelData(player, holoStack, onCursor); + player.addChatMessage(new ChatComponentTranslation("structurelib.pickchannel.cleared")); return true; } - if (targetStack == null) return true; // nothing hovered, cancel silently + if (targetStack == null) return false; // nothing hovered, cancel silently Collection> channels = ChannelDescription.iterate(targetStack); - if (channels.isEmpty()) return true; // no channel mapping, cancel silently + if (channels.isEmpty()) return false; // no channel mapping, cancel silently // Assuming there's only 1 channel per item for now (same behavior as drag and drop) Map.Entry first = channels.iterator().next(); ChannelDataAccessor.setChannelData(holoStack, first.getKey(), first.getValue()); - sendCursorChannelData(holoStack); + sendChannelData(player, holoStack, onCursor); player.addChatMessage( - new ChatComponentText( - I18n.format("structurelib.pickchannel.success", first.getKey(), first.getValue()))); + new ChatComponentTranslation("structurelib.pickchannel.success", first.getKey(), first.getValue())); return true; } - private static void sendCursorChannelData(ItemStack holoStack) { - StructureLib.net.sendToServer(new SetChannelDataMessage(holoStack, true)); + /** + * Player is the one triggering this update, so should already be in-sync. + */ + private static void sendChannelData(EntityPlayer player, ItemStack holoStack, boolean onCursor) { + if (!(player instanceof EntityPlayerMP)) { + StructureLib.net.sendToServer(new SetChannelDataMessage(holoStack, onCursor)); + } } } diff --git a/src/main/java/com/gtnewhorizon/structurelib/ClientProxy.java b/src/main/java/com/gtnewhorizon/structurelib/ClientProxy.java index d2acb607..361cc287 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/ClientProxy.java +++ b/src/main/java/com/gtnewhorizon/structurelib/ClientProxy.java @@ -30,6 +30,7 @@ import net.minecraft.util.IIcon; import net.minecraft.util.Vec3; import net.minecraft.world.World; +import net.minecraftforge.client.event.MouseEvent; import net.minecraftforge.client.event.RenderWorldLastEvent; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.config.ConfigElement; @@ -40,12 +41,17 @@ import org.lwjgl.opengl.GL11; import com.gtnewhorizon.structurelib.entity.fx.WeightlessParticleFX; +import com.gtnewhorizon.structurelib.item.ItemConstructableTrigger; import com.gtnewhorizon.structurelib.net.RegistryOrderSyncMessage; import com.gtnewhorizon.structurelib.net.SetChannelDataMessage; +import com.gtnewhorizon.structurelib.util.BogoCompat; +import com.gtnewhorizon.structurelib.util.MiscUtils; import cpw.mods.fml.client.config.GuiConfig; import cpw.mods.fml.client.event.ConfigChangedEvent; import cpw.mods.fml.common.FMLCommonHandler; +import cpw.mods.fml.common.Loader; +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; @@ -53,6 +59,8 @@ 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 +267,11 @@ public void preInit(FMLPreInitializationEvent e) { MinecraftForge.EVENT_BUS.register(new ForgeEventHandler()); } + @Override + public void init(FMLInitializationEvent e) { + if (Loader.isModLoaded("bogosorter")) MinecraftForge.EVENT_BUS.register(new BogoCompat()); + } + static void markTextureUsed(IIcon icon) { if (StructureLib.COMPAT instanceof IStructureCompat) ((IStructureCompat) StructureLib.COMPAT).markTextureUsed(icon); @@ -560,6 +573,32 @@ public void onClientTick(ClientTickEvent e) { public static class ForgeEventHandler { + @SubscribeEvent + @SideOnly(Side.CLIENT) + public void onMouseEvent(MouseEvent event) { + final EntityPlayer player = Minecraft.getMinecraft().thePlayer; + + if (player == null || player.isDead) { + return; + } + + final ItemStack heldItem = player.getHeldItem(); + + if (heldItem == null || !(heldItem.getItem() instanceof ItemConstructableTrigger)) return; + + if (event.button != 2 || !event.buttonstate) return; + + // Sink the middle click as soon as a projector is held, so vanilla pick block does not run + event.setCanceled(true); + + ChannelPickHandler.handleWorldPick( + player.worldObj, + player, + heldItem, + MiscUtils.getHitResult(player), + player.isSneaking()); + } + @SubscribeEvent public void onEntityJoinWorld(EntityJoinWorldEvent e) { if (e.world.isRemote && e.entity instanceof EntityClientPlayerMP) { 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 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 void endHinting(World w) {} 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..4a491d86 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/StructureLib.java +++ b/src/main/java/com/gtnewhorizon/structurelib/StructureLib.java @@ -33,6 +33,7 @@ import cpw.mods.fml.common.Mod; import cpw.mods.fml.common.SidedProxy; +import cpw.mods.fml.common.event.FMLInitializationEvent; import cpw.mods.fml.common.event.FMLInterModComms; import cpw.mods.fml.common.event.FMLLoadCompleteEvent; import cpw.mods.fml.common.event.FMLPreInitializationEvent; @@ -128,6 +129,11 @@ public void preInit(FMLPreInitializationEvent e) { 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/mixins/early/MixinGuiContainer_PickChannel.java b/src/main/java/com/gtnewhorizon/structurelib/mixins/early/MixinGuiContainer_PickChannel.java index 48aeb591..fdf6e66c 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/mixins/early/MixinGuiContainer_PickChannel.java +++ b/src/main/java/com/gtnewhorizon/structurelib/mixins/early/MixinGuiContainer_PickChannel.java @@ -53,11 +53,25 @@ public abstract class MixinGuiContainer_PickChannel { } ChannelPickHandler.handleMiddleClick( + player, cursorStack, hoveredStack, Keyboard.isKeyDown(Minecraft.getMinecraft().gameSettings.keyBindSneak.getKeyCode())); } + /** + * This mixin is to avoid the shift-to-dump-all NEI feature; it triggers on all mouse keys. + */ + // @Inject(method = "mouseMovedOrUp(III)V", at = @At("HEAD"), cancellable = true) + private void slib$onMouseMovedOrUp(int mouseX, int mouseY, int state, CallbackInfo ci) { + if (state != 2) return; + EntityPlayer player = Minecraft.getMinecraft().thePlayer; + if (player == null) return; + ItemStack cursorStack = player.inventory.getItemStack(); + if (cursorStack == null || !(cursorStack.getItem() instanceof ItemConstructableTrigger)) return; + ci.cancel(); + } + @Invoker("getSlotAtPosition") protected abstract Slot slib$invokeGetSlotAtPosition(int x, int y); } diff --git a/src/main/java/com/gtnewhorizon/structurelib/util/BogoCompat.java b/src/main/java/com/gtnewhorizon/structurelib/util/BogoCompat.java new file mode 100644 index 00000000..89c16ac4 --- /dev/null +++ b/src/main/java/com/gtnewhorizon/structurelib/util/BogoCompat.java @@ -0,0 +1,37 @@ +package com.gtnewhorizon.structurelib.util; + +import net.minecraft.item.ItemStack; + +import com.cleanroommc.bogosorter.api.BeforeSortEvent; +import com.gtnewhorizon.structurelib.item.ItemConstructableTrigger; + +import cpw.mods.fml.common.eventhandler.SubscribeEvent; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; + +public class BogoCompat { + + private static final int MOUSE_MIDDLE = -98; + + @SubscribeEvent + @SideOnly(Side.CLIENT) + public void onBeforeSortInGui(final BeforeSortEvent event) { + // Cancel Bogo inventory sort if hologram is picked-up/held. + // Bogo sort is usually bound to middle-click. + // This is just to avoid annoying player experience. + if (!event.isFromKeybind()) return; + + final boolean isInGui = event.isInGui(); + if (event.getSortKeyCode() != MOUSE_MIDDLE) return; + + ItemStack item = null; + + if (isInGui) item = event.getPlayer().inventory.getItemStack(); + else item = event.getPlayer().getHeldItem(); + + if (item == null || !(item.getItem() instanceof ItemConstructableTrigger)) return; + + // Cancel sort + event.setCanceled(true); + } +} diff --git a/src/main/java/com/gtnewhorizon/structurelib/util/MiscUtils.java b/src/main/java/com/gtnewhorizon/structurelib/util/MiscUtils.java index ffc80cf6..9afa4e2e 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/util/MiscUtils.java +++ b/src/main/java/com/gtnewhorizon/structurelib/util/MiscUtils.java @@ -2,7 +2,12 @@ import java.util.Set; +import net.minecraft.client.Minecraft; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.MovingObjectPosition; +import net.minecraft.util.Vec3; /** * not related to a certain mod! @@ -14,4 +19,25 @@ private MiscUtils() {} public static Set getTagKeys(NBTTagCompound tag) { return tag.func_150296_c(); } + + /** + * Gets the standard vanilla hit result for a player. + */ + public static MovingObjectPosition getHitResult(EntityPlayer player) { + double reachDistance = player instanceof EntityPlayerMP mp ? mp.theItemInWorldManager.getBlockReachDistance() + : Minecraft.getMinecraft().playerController.getBlockReachDistance(); + + Vec3 posVec = Vec3.createVectorHelper(player.posX, player.posY + player.getEyeHeight(), player.posZ); + + Vec3 lookVec = player.getLook(1); + + Vec3 modifiedPosVec = posVec.addVector( + lookVec.xCoord * reachDistance, + lookVec.yCoord * reachDistance, + lookVec.zCoord * reachDistance); + + MovingObjectPosition hit = player.worldObj.rayTraceBlocks(posVec, modifiedPosVec, true); + + return hit != null && hit.typeOfHit != MovingObjectPosition.MovingObjectType.BLOCK ? null : hit; + } } From c2c5ae397c7569f9b7fae96d1cfb8bc26a1fa0ea Mon Sep 17 00:00:00 2001 From: Alex Vermette Date: Wed, 16 Sep 2026 23:02:46 -0400 Subject: [PATCH 3/6] fix: commited dead code by accient, removed --- .../mixins/early/MixinGuiContainer_PickChannel.java | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/main/java/com/gtnewhorizon/structurelib/mixins/early/MixinGuiContainer_PickChannel.java b/src/main/java/com/gtnewhorizon/structurelib/mixins/early/MixinGuiContainer_PickChannel.java index fdf6e66c..b3023025 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/mixins/early/MixinGuiContainer_PickChannel.java +++ b/src/main/java/com/gtnewhorizon/structurelib/mixins/early/MixinGuiContainer_PickChannel.java @@ -59,19 +59,6 @@ public abstract class MixinGuiContainer_PickChannel { Keyboard.isKeyDown(Minecraft.getMinecraft().gameSettings.keyBindSneak.getKeyCode())); } - /** - * This mixin is to avoid the shift-to-dump-all NEI feature; it triggers on all mouse keys. - */ - // @Inject(method = "mouseMovedOrUp(III)V", at = @At("HEAD"), cancellable = true) - private void slib$onMouseMovedOrUp(int mouseX, int mouseY, int state, CallbackInfo ci) { - if (state != 2) return; - EntityPlayer player = Minecraft.getMinecraft().thePlayer; - if (player == null) return; - ItemStack cursorStack = player.inventory.getItemStack(); - if (cursorStack == null || !(cursorStack.getItem() instanceof ItemConstructableTrigger)) return; - ci.cancel(); - } - @Invoker("getSlotAtPosition") protected abstract Slot slib$invokeGetSlotAtPosition(int x, int y); } From 088f0f456e16007535684eaaa4c2234fe56f9a12 Mon Sep 17 00:00:00 2001 From: Alex Vermette Date: Sun, 20 Sep 2026 15:50:57 -0400 Subject: [PATCH 4/6] fix(Hologram): Apply review comments and clear add confirmation --- .../structurelib/ChannelPickHandler.java | 8 +++- .../structurelib/ClientProxy.java | 16 +++----- .../structurelib/core/StructureLibCore.java | 13 +++++-- .../early/MixinGuiContainer_PickChannel.java | 12 +++--- .../structurelib/util/BogoCompat.java | 37 ------------------- .../structurelib/util/MiscUtils.java | 26 ------------- .../assets/structurelib/lang/en_US.lang | 2 + 7 files changed, 28 insertions(+), 86 deletions(-) delete mode 100644 src/main/java/com/gtnewhorizon/structurelib/util/BogoCompat.java diff --git a/src/main/java/com/gtnewhorizon/structurelib/ChannelPickHandler.java b/src/main/java/com/gtnewhorizon/structurelib/ChannelPickHandler.java index 77a4d163..a1cad9fe 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/ChannelPickHandler.java +++ b/src/main/java/com/gtnewhorizon/structurelib/ChannelPickHandler.java @@ -51,7 +51,8 @@ public static boolean handleWorldPick(World world, EntityPlayer player, ItemStac private static boolean handlePick(EntityPlayer player, ItemStack holoStack, @Nullable ItemStack targetStack, final boolean isSneaking, final boolean onCursor) { - // Clear channels on shift+middle-click + // Clear channels on shift+middle-click. The "press again to confirm" gesture is gated client-side + // (see ChannelWipeConfirmation) before this method is ever called. if (isSneaking) { ChannelDataAccessor.wipeChannelData(holoStack); sendChannelData(player, holoStack, onCursor); @@ -62,7 +63,10 @@ private static boolean handlePick(EntityPlayer player, ItemStack holoStack, @Nul if (targetStack == null) return false; // nothing hovered, cancel silently Collection> channels = ChannelDescription.iterate(targetStack); - if (channels.isEmpty()) return false; // no channel mapping, cancel silently + if (channels.isEmpty()) { + player.addChatMessage(new ChatComponentTranslation("structurelib.pickchannel.nomapping")); + return false; + } // Assuming there's only 1 channel per item for now (same behavior as drag and drop) Map.Entry first = channels.iterator().next(); diff --git a/src/main/java/com/gtnewhorizon/structurelib/ClientProxy.java b/src/main/java/com/gtnewhorizon/structurelib/ClientProxy.java index 361cc287..7237d301 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/ClientProxy.java +++ b/src/main/java/com/gtnewhorizon/structurelib/ClientProxy.java @@ -44,14 +44,10 @@ import com.gtnewhorizon.structurelib.item.ItemConstructableTrigger; import com.gtnewhorizon.structurelib.net.RegistryOrderSyncMessage; import com.gtnewhorizon.structurelib.net.SetChannelDataMessage; -import com.gtnewhorizon.structurelib.util.BogoCompat; -import com.gtnewhorizon.structurelib.util.MiscUtils; import cpw.mods.fml.client.config.GuiConfig; import cpw.mods.fml.client.event.ConfigChangedEvent; import cpw.mods.fml.common.FMLCommonHandler; -import cpw.mods.fml.common.Loader; -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; @@ -267,11 +263,6 @@ public void preInit(FMLPreInitializationEvent e) { MinecraftForge.EVENT_BUS.register(new ForgeEventHandler()); } - @Override - public void init(FMLInitializationEvent e) { - if (Loader.isModLoaded("bogosorter")) MinecraftForge.EVENT_BUS.register(new BogoCompat()); - } - static void markTextureUsed(IIcon icon) { if (StructureLib.COMPAT instanceof IStructureCompat) ((IStructureCompat) StructureLib.COMPAT).markTextureUsed(icon); @@ -591,12 +582,15 @@ public void onMouseEvent(MouseEvent event) { // Sink the middle click as soon as a projector is held, so vanilla pick block does not run event.setCanceled(true); + final boolean isSneaking = player.isSneaking(); + if (isSneaking && !ChannelWipeConfirmation.confirm(player)) return; + ChannelPickHandler.handleWorldPick( player.worldObj, player, heldItem, - MiscUtils.getHitResult(player), - player.isSneaking()); + Minecraft.getMinecraft().objectMouseOver, + isSneaking); } @SubscribeEvent diff --git a/src/main/java/com/gtnewhorizon/structurelib/core/StructureLibCore.java b/src/main/java/com/gtnewhorizon/structurelib/core/StructureLibCore.java index e00a7a6c..421180ba 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/core/StructureLibCore.java +++ b/src/main/java/com/gtnewhorizon/structurelib/core/StructureLibCore.java @@ -1,5 +1,6 @@ package com.gtnewhorizon.structurelib.core; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -7,7 +8,9 @@ import com.gtnewhorizon.gtnhmixins.IEarlyMixinLoader; +import cpw.mods.fml.relauncher.FMLLaunchHandler; import cpw.mods.fml.relauncher.IFMLLoadingPlugin; +import cpw.mods.fml.relauncher.Side; @IFMLLoadingPlugin.MCVersion("1.7.10") public class StructureLibCore implements IFMLLoadingPlugin, IEarlyMixinLoader { @@ -44,9 +47,11 @@ public String getMixinConfig() { @Override public List getMixins(Set loadedCoreMods) { - return Arrays.asList( - "blockChangeNotifier.MixinForgeHooks", - "blockChangeNotifier.MixinWorld", - "MixinGuiContainer_PickChannel"); + List mixins = new ArrayList<>( + Arrays.asList("blockChangeNotifier.MixinForgeHooks", "blockChangeNotifier.MixinWorld")); + if (FMLLaunchHandler.side() == Side.CLIENT) { + mixins.add("MixinGuiContainer_PickChannel"); + } + return mixins; } } diff --git a/src/main/java/com/gtnewhorizon/structurelib/mixins/early/MixinGuiContainer_PickChannel.java b/src/main/java/com/gtnewhorizon/structurelib/mixins/early/MixinGuiContainer_PickChannel.java index b3023025..a0809f85 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/mixins/early/MixinGuiContainer_PickChannel.java +++ b/src/main/java/com/gtnewhorizon/structurelib/mixins/early/MixinGuiContainer_PickChannel.java @@ -1,12 +1,12 @@ package com.gtnewhorizon.structurelib.mixins.early; import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.gui.inventory.GuiContainer; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.inventory.Slot; import net.minecraft.item.ItemStack; -import org.lwjgl.input.Keyboard; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Invoker; import org.spongepowered.asm.mixin.injection.At; @@ -14,6 +14,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import com.gtnewhorizon.structurelib.ChannelPickHandler; +import com.gtnewhorizon.structurelib.ChannelWipeConfirmation; import com.gtnewhorizon.structurelib.StructureLib; import com.gtnewhorizon.structurelib.item.ItemConstructableTrigger; @@ -52,11 +53,10 @@ public abstract class MixinGuiContainer_PickChannel { StructureLib.LOGGER.error("Error while resolving hovered slot for channel pick", e); } - ChannelPickHandler.handleMiddleClick( - player, - cursorStack, - hoveredStack, - Keyboard.isKeyDown(Minecraft.getMinecraft().gameSettings.keyBindSneak.getKeyCode())); + boolean isSneaking = GuiScreen.isShiftKeyDown(); + if (isSneaking && !ChannelWipeConfirmation.confirm(player)) return; + + ChannelPickHandler.handleMiddleClick(player, cursorStack, hoveredStack, isSneaking); } @Invoker("getSlotAtPosition") diff --git a/src/main/java/com/gtnewhorizon/structurelib/util/BogoCompat.java b/src/main/java/com/gtnewhorizon/structurelib/util/BogoCompat.java deleted file mode 100644 index 89c16ac4..00000000 --- a/src/main/java/com/gtnewhorizon/structurelib/util/BogoCompat.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.gtnewhorizon.structurelib.util; - -import net.minecraft.item.ItemStack; - -import com.cleanroommc.bogosorter.api.BeforeSortEvent; -import com.gtnewhorizon.structurelib.item.ItemConstructableTrigger; - -import cpw.mods.fml.common.eventhandler.SubscribeEvent; -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; - -public class BogoCompat { - - private static final int MOUSE_MIDDLE = -98; - - @SubscribeEvent - @SideOnly(Side.CLIENT) - public void onBeforeSortInGui(final BeforeSortEvent event) { - // Cancel Bogo inventory sort if hologram is picked-up/held. - // Bogo sort is usually bound to middle-click. - // This is just to avoid annoying player experience. - if (!event.isFromKeybind()) return; - - final boolean isInGui = event.isInGui(); - if (event.getSortKeyCode() != MOUSE_MIDDLE) return; - - ItemStack item = null; - - if (isInGui) item = event.getPlayer().inventory.getItemStack(); - else item = event.getPlayer().getHeldItem(); - - if (item == null || !(item.getItem() instanceof ItemConstructableTrigger)) return; - - // Cancel sort - event.setCanceled(true); - } -} diff --git a/src/main/java/com/gtnewhorizon/structurelib/util/MiscUtils.java b/src/main/java/com/gtnewhorizon/structurelib/util/MiscUtils.java index 9afa4e2e..ffc80cf6 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/util/MiscUtils.java +++ b/src/main/java/com/gtnewhorizon/structurelib/util/MiscUtils.java @@ -2,12 +2,7 @@ import java.util.Set; -import net.minecraft.client.Minecraft; -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.util.MovingObjectPosition; -import net.minecraft.util.Vec3; /** * not related to a certain mod! @@ -19,25 +14,4 @@ private MiscUtils() {} public static Set getTagKeys(NBTTagCompound tag) { return tag.func_150296_c(); } - - /** - * Gets the standard vanilla hit result for a player. - */ - public static MovingObjectPosition getHitResult(EntityPlayer player) { - double reachDistance = player instanceof EntityPlayerMP mp ? mp.theItemInWorldManager.getBlockReachDistance() - : Minecraft.getMinecraft().playerController.getBlockReachDistance(); - - Vec3 posVec = Vec3.createVectorHelper(player.posX, player.posY + player.getEyeHeight(), player.posZ); - - Vec3 lookVec = player.getLook(1); - - Vec3 modifiedPosVec = posVec.addVector( - lookVec.xCoord * reachDistance, - lookVec.yCoord * reachDistance, - lookVec.zCoord * reachDistance); - - MovingObjectPosition hit = player.worldObj.rayTraceBlocks(posVec, modifiedPosVec, true); - - return hit != null && hit.typeOfHit != MovingObjectPosition.MovingObjectType.BLOCK ? null : hit; - } } diff --git a/src/main/resources/assets/structurelib/lang/en_US.lang b/src/main/resources/assets/structurelib/lang/en_US.lang index 0ae7f2e8..8a733238 100644 --- a/src/main/resources/assets/structurelib/lang/en_US.lang +++ b/src/main/resources/assets/structurelib/lang/en_US.lang @@ -47,6 +47,8 @@ structurelib.tooltip.channelvalue=§7Value §b%d§r§7 for subchannel §b%s structurelib.tooltip.indicator_dnd=Drop into GUI or middle-click to select this value for this channel structurelib.pickchannel.success=§aSet §b%s§r§a to §b%d§r structurelib.pickchannel.cleared=§aAll channels cleared§r +structurelib.pickchannel.confirmwipe=§6Sneak+Middle-click again to clear all channels +structurelib.pickchannel.nomapping=§cNo channel mapping found for this item structurelib.autoplace.built_stat=Built %s blocks structurelib.autoplace.complete=§aThis multiblock has completed! From 1c66d54f0a57cd430d208fe6669fe60de8c4f5b8 Mon Sep 17 00:00:00 2001 From: Alex Vermette Date: Sun, 20 Sep 2026 18:44:01 -0400 Subject: [PATCH 5/6] fix(missing file): Forgot to actually commit a file... --- .../structurelib/ChannelWipeConfirmation.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/main/java/com/gtnewhorizon/structurelib/ChannelWipeConfirmation.java diff --git a/src/main/java/com/gtnewhorizon/structurelib/ChannelWipeConfirmation.java b/src/main/java/com/gtnewhorizon/structurelib/ChannelWipeConfirmation.java new file mode 100644 index 00000000..9e207793 --- /dev/null +++ b/src/main/java/com/gtnewhorizon/structurelib/ChannelWipeConfirmation.java @@ -0,0 +1,33 @@ +package com.gtnewhorizon.structurelib; + +import net.minecraft.client.Minecraft; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.util.ChatComponentTranslation; + +/** + * Client-side-only confirmation gate for "wipe all channels" shift+middle-click gesture. + */ +public final class ChannelWipeConfirmation { + + private static final long CONFIRM_WINDOW_MS = 5000; + private static long armedAtMs = -1; + + private ChannelWipeConfirmation() {} + + /** + * Call this before invoking {@link ChannelPickHandler} whenever the wipe gesture (shift+middle-click) is detected. + * + * @return true if this is a confirming second press within the window and the wipe should proceed now; false if + * this press only armed the confirmation. + */ + public static boolean confirm(EntityPlayer player) { + long now = Minecraft.getSystemTime(); + if (now - armedAtMs > CONFIRM_WINDOW_MS) { + armedAtMs = now; + player.addChatMessage(new ChatComponentTranslation("structurelib.pickchannel.confirmwipe")); + return false; + } + armedAtMs = -1; + return true; + } +} From 3243d04b2d129f18cc0bc8cf59482a3a893c75c0 Mon Sep 17 00:00:00 2001 From: Alex Vermette Date: Sun, 20 Sep 2026 22:32:47 -0400 Subject: [PATCH 6/6] fix(deps): Remove bogo deps --- dependencies.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index b8de7799..217b4151 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -3,7 +3,6 @@ dependencies { api("com.github.GTNewHorizons:GTNHLib:0.11.21:dev") implementation("com.github.GTNewHorizons:NotEnoughItems:2.8.110-GTNH:dev") - compileOnly("com.github.GTNewHorizons:InventoryBogoSorter:1.3.51-GTNH:dev") testImplementation(platform('org.junit:junit-bom:5.9.2')) testImplementation('org.junit.jupiter:junit-jupiter') testRuntimeOnly ('org.junit.platform:junit-platform-launcher')