diff --git a/dependencies.gradle b/dependencies.gradle index 05de3f13..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") - 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 new file mode 100644 index 00000000..a1cad9fe --- /dev/null +++ b/src/main/java/com/gtnewhorizon/structurelib/ChannelPickHandler.java @@ -0,0 +1,88 @@ +package com.gtnewhorizon.structurelib; + +import java.util.Collection; +import java.util.Map; + +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.ChatComponentTranslation; +import net.minecraft.util.MovingObjectPosition; +import net.minecraft.world.World; + +import com.gtnewhorizon.structurelib.alignment.constructable.ChannelDataAccessor; +import com.gtnewhorizon.structurelib.net.SetChannelDataMessage; + +/** + * 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 { + + private ChannelPickHandler() {} + + /** + * Handle a middle-click event while holding a hologram projector on the cursor. + */ + 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. 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); + player.addChatMessage(new ChatComponentTranslation("structurelib.pickchannel.cleared")); + return true; + } + + if (targetStack == null) return false; // nothing hovered, cancel silently + + Collection> channels = ChannelDescription.iterate(targetStack); + 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(); + ChannelDataAccessor.setChannelData(holoStack, first.getKey(), first.getValue()); + sendChannelData(player, holoStack, onCursor); + player.addChatMessage( + new ChatComponentTranslation("structurelib.pickchannel.success", first.getKey(), first.getValue())); + return 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/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; + } +} diff --git a/src/main/java/com/gtnewhorizon/structurelib/ClientProxy.java b/src/main/java/com/gtnewhorizon/structurelib/ClientProxy.java index d2acb607..7237d301 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,6 +41,7 @@ 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; @@ -53,6 +55,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 { @@ -560,6 +564,35 @@ 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); + + final boolean isSneaking = player.isSneaking(); + if (isSneaking && !ChannelWipeConfirmation.confirm(player)) return; + + ChannelPickHandler.handleWorldPick( + player.worldObj, + player, + heldItem, + Minecraft.getMinecraft().objectMouseOver, + 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/core/StructureLibCore.java b/src/main/java/com/gtnewhorizon/structurelib/core/StructureLibCore.java index e8eb6d4b..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,6 +47,11 @@ public String getMixinConfig() { @Override public List getMixins(Set loadedCoreMods) { - return Arrays.asList("blockChangeNotifier.MixinForgeHooks", "blockChangeNotifier.MixinWorld"); + 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 new file mode 100644 index 00000000..a0809f85 --- /dev/null +++ b/src/main/java/com/gtnewhorizon/structurelib/mixins/early/MixinGuiContainer_PickChannel.java @@ -0,0 +1,64 @@ +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.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.ChannelWipeConfirmation; +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); + } + + boolean isSneaking = GuiScreen.isShiftKeyDown(); + if (isSneaking && !ChannelWipeConfirmation.confirm(player)) return; + + ChannelPickHandler.handleMiddleClick(player, cursorStack, hoveredStack, isSneaking); + } + + @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..8a733238 100644 --- a/src/main/resources/assets/structurelib/lang/en_US.lang +++ b/src/main/resources/assets/structurelib/lang/en_US.lang @@ -44,7 +44,11 @@ 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.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!