Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
Comment thread
Glease marked this conversation as resolved.
sendChannelData(player, holoStack, onCursor);
player.addChatMessage(new ChatComponentTranslation("structurelib.pickchannel.cleared"));
return true;
}

if (targetStack == null) return false; // nothing hovered, cancel silently

Collection<Map.Entry<String, Integer>> 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<String, Integer> 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));
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
33 changes: 33 additions & 0 deletions src/main/java/com/gtnewhorizon/structurelib/ClientProxy.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -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 {

Expand Down Expand Up @@ -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) {
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/com/gtnewhorizon/structurelib/CommonProxy.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/com/gtnewhorizon/structurelib/StructureLib.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package com.gtnewhorizon.structurelib.core;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;

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 {
Expand Down Expand Up @@ -44,6 +47,11 @@ public String getMixinConfig() {

@Override
public List<String> getMixins(Set<String> loadedCoreMods) {
return Arrays.asList("blockChangeNotifier.MixinForgeHooks", "blockChangeNotifier.MixinWorld");
List<String> mixins = new ArrayList<>(
Arrays.asList("blockChangeNotifier.MixinForgeHooks", "blockChangeNotifier.MixinWorld"));
if (FMLLaunchHandler.side() == Side.CLIENT) {
mixins.add("MixinGuiContainer_PickChannel");
}
return mixins;
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,22 @@
public class SetChannelDataMessage implements IMessage {

private final List<Map.Entry<String, Integer>> 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++) {
Expand All @@ -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<String, Integer> e : data) {
ByteBufUtils.writeUTF8String(buf, e.getKey());
Expand All @@ -53,19 +61,19 @@ public static class Handler implements IMessageHandler<SetChannelDataMessage, IM
@Override
public IMessage onMessage(SetChannelDataMessage message, MessageContext ctx) {
EntityPlayerMP player = ctx.getServerHandler().playerEntity;
ItemStack heldItem = player.getHeldItem();
if (heldItem != null && heldItem.getItem() instanceof ItemConstructableTrigger) {
ChannelDataAccessor.wipeChannelData(heldItem);
ItemStack target = message.onCursor ? player.inventory.getItemStack() : player.getHeldItem();
if (target != null && target.getItem() instanceof ItemConstructableTrigger) {
ChannelDataAccessor.wipeChannelData(target);
for (Entry<String, Integer> 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
} else {
StructureLib.LOGGER.warn(
"{} trying to set channel data on {}, which is not a hologram projector!",
player.getUniqueID(),
heldItem);
target);
}
return null;
}
Expand Down
6 changes: 5 additions & 1 deletion src/main/resources/assets/structurelib/lang/en_US.lang
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down
Loading