diff --git a/src/main/java/com/gtnewhorizon/gtnhlib/keybind/IKeyPressedListener.java b/src/main/java/com/gtnewhorizon/gtnhlib/keybind/IKeyPressedListener.java
new file mode 100644
index 0000000..414dc1e
--- /dev/null
+++ b/src/main/java/com/gtnewhorizon/gtnhlib/keybind/IKeyPressedListener.java
@@ -0,0 +1,20 @@
+package com.gtnewhorizon.gtnhlib.keybind;
+
+import net.minecraft.entity.player.EntityPlayerMP;
+
+/**
+ * Server-side listener interface for when a player presses a specific key.
+ *
+ * @author serenibyss
+ * @since 0.6.5
+ */
+public interface IKeyPressedListener {
+
+ /**
+ * Called server-side only when a player presses a specified keybinding.
+ *
+ * @param player The player who pressed the key.
+ * @param keyPressed The key the player pressed.
+ */
+ void onKeyPressed(EntityPlayerMP player, SyncedKeybind keyPressed);
+}
diff --git a/src/main/java/com/gtnewhorizon/gtnhlib/keybind/PacketKeyDown.java b/src/main/java/com/gtnewhorizon/gtnhlib/keybind/PacketKeyDown.java
new file mode 100644
index 0000000..f764294
--- /dev/null
+++ b/src/main/java/com/gtnewhorizon/gtnhlib/keybind/PacketKeyDown.java
@@ -0,0 +1,53 @@
+package com.gtnewhorizon.gtnhlib.keybind;
+
+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 cpw.mods.fml.relauncher.Side;
+import io.netty.buffer.ByteBuf;
+import it.unimi.dsi.fastutil.ints.Int2BooleanMap;
+import it.unimi.dsi.fastutil.ints.Int2BooleanOpenHashMap;
+
+public class PacketKeyDown implements IMessage {
+
+ private Int2BooleanMap updateKeys;
+
+ @SuppressWarnings("unused")
+ public PacketKeyDown() {}
+
+ protected PacketKeyDown(Int2BooleanMap updateKeys) {
+ this.updateKeys = updateKeys;
+ }
+
+ @Override
+ public void fromBytes(ByteBuf buf) {
+ this.updateKeys = new Int2BooleanOpenHashMap();
+ int size = buf.readInt();
+ for (int i = 0; i < size; i++) {
+ updateKeys.put(buf.readInt(), buf.readBoolean());
+ }
+ }
+
+ @Override
+ public void toBytes(ByteBuf buf) {
+ buf.writeInt(updateKeys.size());
+ for (var entry : updateKeys.int2BooleanEntrySet()) {
+ buf.writeInt(entry.getIntKey());
+ buf.writeBoolean(entry.getBooleanValue());
+ }
+ }
+
+ public static class HandlerKeyDown implements IMessageHandler {
+
+ @Override
+ public IMessage onMessage(PacketKeyDown message, MessageContext ctx) {
+ if (ctx.side == Side.SERVER) {
+ for (var entry : message.updateKeys.int2BooleanEntrySet()) {
+ SyncedKeybind keybind = SyncedKeybind.getFromSyncId(entry.getIntKey());
+ keybind.updateKeyDown(entry.getBooleanValue(), ctx.getServerHandler().playerEntity);
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/com/gtnewhorizon/gtnhlib/keybind/PacketKeyPressed.java b/src/main/java/com/gtnewhorizon/gtnhlib/keybind/PacketKeyPressed.java
new file mode 100644
index 0000000..b65272c
--- /dev/null
+++ b/src/main/java/com/gtnewhorizon/gtnhlib/keybind/PacketKeyPressed.java
@@ -0,0 +1,52 @@
+package com.gtnewhorizon.gtnhlib.keybind;
+
+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 cpw.mods.fml.relauncher.Side;
+import io.netty.buffer.ByteBuf;
+import it.unimi.dsi.fastutil.ints.IntArrayList;
+import it.unimi.dsi.fastutil.ints.IntList;
+
+public class PacketKeyPressed implements IMessage {
+
+ private IntList pressedKeys;
+
+ @SuppressWarnings("unused")
+ public PacketKeyPressed() {}
+
+ protected PacketKeyPressed(IntList pressedKeys) {
+ this.pressedKeys = pressedKeys;
+ }
+
+ @Override
+ public void fromBytes(ByteBuf buf) {
+ pressedKeys = new IntArrayList();
+ int size = buf.readInt();
+ for (int i = 0; i < size; i++) {
+ pressedKeys.add(buf.readInt());
+ }
+ }
+
+ @Override
+ public void toBytes(ByteBuf buf) {
+ buf.writeInt(pressedKeys.size());
+ for (int key : pressedKeys) {
+ buf.writeInt(key);
+ }
+ }
+
+ public static class HandlerKeyPressed implements IMessageHandler {
+
+ @Override
+ public IMessage onMessage(PacketKeyPressed message, MessageContext ctx) {
+ if (ctx.side == Side.SERVER) {
+ for (int index : message.pressedKeys) {
+ SyncedKeybind keybind = SyncedKeybind.getFromSyncId(index);
+ keybind.onKeyPressed(ctx.getServerHandler().playerEntity);
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/com/gtnewhorizon/gtnhlib/keybind/SyncedKeybind.java b/src/main/java/com/gtnewhorizon/gtnhlib/keybind/SyncedKeybind.java
new file mode 100644
index 0000000..7151f3c
--- /dev/null
+++ b/src/main/java/com/gtnewhorizon/gtnhlib/keybind/SyncedKeybind.java
@@ -0,0 +1,252 @@
+package com.gtnewhorizon.gtnhlib.keybind;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.WeakHashMap;
+import java.util.function.Supplier;
+
+import net.minecraft.client.settings.KeyBinding;
+import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.entity.player.EntityPlayerMP;
+
+import org.lwjgl.input.Keyboard;
+
+import com.gtnewhorizon.gtnhlib.eventbus.EventBusSubscriber;
+import com.gtnewhorizon.gtnhlib.network.NetworkHandler;
+
+import cpw.mods.fml.client.registry.ClientRegistry;
+import cpw.mods.fml.common.FMLCommonHandler;
+import cpw.mods.fml.common.eventhandler.SubscribeEvent;
+import cpw.mods.fml.common.gameevent.InputEvent;
+import cpw.mods.fml.common.gameevent.TickEvent;
+import cpw.mods.fml.relauncher.Side;
+import cpw.mods.fml.relauncher.SideOnly;
+import it.unimi.dsi.fastutil.ints.Int2BooleanMap;
+import it.unimi.dsi.fastutil.ints.Int2BooleanOpenHashMap;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.ints.IntArrayList;
+import it.unimi.dsi.fastutil.ints.IntList;
+
+/**
+ * Server-backed keybindings, allowing you to read the state of a key press on the server per-player.
+ *
+ * Supports both:
+ *
+ * - "Key held" - Is this key currently held down by the player
+ *
- "Key pressed" - Listener event fired when the player clicks a key
+ *
+ *
+ * @author serenibyss
+ * @since 0.6.5
+ */
+@SuppressWarnings("unused")
+@EventBusSubscriber(side = Side.CLIENT)
+public final class SyncedKeybind {
+
+ private static final Int2ObjectMap KEYBINDS = new Int2ObjectOpenHashMap<>();
+ private static int syncIndex = 0;
+
+ @SideOnly(Side.CLIENT)
+ private KeyBinding keybinding;
+ @SideOnly(Side.CLIENT)
+ private int keyCode;
+ @SideOnly(Side.CLIENT)
+ private boolean isKeyDown;
+
+ private final WeakHashMap mapping = new WeakHashMap<>();
+ private final WeakHashMap> playerListeners = new WeakHashMap<>();
+ private final Set globalListeners = Collections.newSetFromMap(new WeakHashMap<>());
+
+ // Doubly-wrapped supplier for client-side only type
+ private SyncedKeybind(Supplier> keybindingGetter) {
+ if (FMLCommonHandler.instance().getSide().isClient()) {
+ this.keybinding = keybindingGetter.get().get();
+ }
+ KEYBINDS.put(syncIndex++, this);
+ }
+
+ private SyncedKeybind(int keyCode) {
+ if (FMLCommonHandler.instance().getSide().isClient()) {
+ this.keyCode = keyCode;
+ }
+ KEYBINDS.put(syncIndex++, this);
+ }
+
+ private SyncedKeybind(String nameKey, String categoryKey, int keyCode) {
+ if (FMLCommonHandler.instance().getSide().isClient()) {
+ this.keybinding = (KeyBinding) createKeyBinding(nameKey, categoryKey, keyCode);
+ }
+ KEYBINDS.put(syncIndex++, this);
+ }
+
+ /**
+ * Create a Keybind wrapper around a Minecraft {@link KeyBinding}.
+ *
+ * @param mcKeybinding Doubly-wrapped supplier around a keybinding from
+ * {@link net.minecraft.client.settings.GameSettings Minecraft.getMinecraft().gameSettings}.
+ */
+ public static SyncedKeybind createFromMC(Supplier> mcKeybinding) {
+ return new SyncedKeybind(mcKeybinding);
+ }
+
+ /**
+ * Create a new Keybind for a specified key code.
+ *
+ * @param keyCode The key code.
+ */
+ public static SyncedKeybind create(int keyCode) {
+ return new SyncedKeybind(keyCode);
+ }
+
+ /**
+ * Create a new Keybind with server held and pressed syncing to server.
+ * Will automatically create a keybinding entry in the MC settings page.
+ *
+ * @param nameKey Translation key for the keybinding name.
+ * @param categoryKey Translation key for the keybinding options category.
+ * @param keyCode The key code, from {@link Keyboard}.
+ */
+ public static SyncedKeybind createConfigurable(String nameKey, String categoryKey, int keyCode) {
+ return new SyncedKeybind(nameKey, categoryKey, keyCode);
+ }
+
+ /**
+ * Check if a player is currently holding down this key.
+ *
+ * @param player The player to check.
+ *
+ * @return If the key is held.
+ */
+ public boolean isKeyDown(EntityPlayer player) {
+ if (player.worldObj.isRemote) {
+ if (keybinding != null) {
+ return keybinding.getIsKeyPressed();
+ }
+ return Keyboard.isKeyDown(keyCode);
+ }
+ Boolean isKeyDown = mapping.get((EntityPlayerMP) player);
+ return isKeyDown != null ? isKeyDown : false;
+ }
+
+ /**
+ * Registers an {@link IKeyPressedListener} to this key, which will have its {@link IKeyPressedListener#onKeyPressed
+ * onKeyPressed} method called when the provided player presses this key.
+ *
+ * @param player The player who owns this listener.
+ * @param listener The handler for the key clicked event.
+ */
+ public SyncedKeybind registerPlayerListener(EntityPlayerMP player, IKeyPressedListener listener) {
+ Set listenerSet = playerListeners
+ .computeIfAbsent(player, k -> Collections.newSetFromMap(new WeakHashMap<>()));
+ listenerSet.add(listener);
+ return this;
+ }
+
+ /**
+ * Remove a player's listener on this keybinding for a provided player.
+ *
+ * @param player The player who owns this listener.
+ * @param listener The handler for the key clicked event.
+ */
+ public void removePlayerListener(EntityPlayerMP player, IKeyPressedListener listener) {
+ Set listenerSet = playerListeners.get(player);
+ if (listenerSet != null) {
+ listenerSet.remove(listener);
+ }
+ }
+
+ /**
+ * Registers an {@link IKeyPressedListener} to this key, which will have its {@link IKeyPressedListener#onKeyPressed
+ * onKeyPressed} method called when any player presses this key.
+ *
+ * @param listener The handler for the key clicked event.
+ */
+ public SyncedKeybind registerGlobalListener(IKeyPressedListener listener) {
+ globalListeners.add(listener);
+ return this;
+ }
+
+ /**
+ * Remove a global listener on this keybinding.
+ *
+ * @param listener The handler for the key clicked event.
+ */
+ public void removeGlobalListener(IKeyPressedListener listener) {
+ globalListeners.remove(listener);
+ }
+
+ static SyncedKeybind getFromSyncId(int id) {
+ return KEYBINDS.get(id);
+ }
+
+ // Server-side indirection
+ @SideOnly(Side.CLIENT)
+ private Object createKeyBinding(String nameLangKey, String category, int button) {
+ KeyBinding keybinding = new KeyBinding(nameLangKey, button, category);
+ ClientRegistry.registerKeyBinding(keybinding);
+ return keybinding;
+ }
+
+ @SubscribeEvent
+ @SideOnly(Side.CLIENT)
+ public static void onClientTick(TickEvent.ClientTickEvent event) {
+ if (event.phase == TickEvent.Phase.START) {
+ Int2BooleanMap updatingKeyDown = new Int2BooleanOpenHashMap();
+ for (var entry : KEYBINDS.int2ObjectEntrySet()) {
+ SyncedKeybind keybind = entry.getValue();
+ boolean previousKeyDown = keybind.isKeyDown;
+
+ if (keybind.keybinding != null) {
+ keybind.isKeyDown = keybind.keybinding.getIsKeyPressed();
+ } else {
+ keybind.isKeyDown = Keyboard.isKeyDown(keybind.keyCode);
+ }
+
+ if (previousKeyDown != keybind.isKeyDown) {
+ updatingKeyDown.put(entry.getIntKey(), keybind.isKeyDown);
+ }
+ }
+ if (!updatingKeyDown.isEmpty()) {
+ NetworkHandler.instance.sendToServer(new PacketKeyDown(updatingKeyDown));
+ }
+ }
+ }
+
+ // Updated by the packet handler
+ void updateKeyDown(boolean keyDown, EntityPlayerMP player) {
+ this.mapping.put(player, keyDown);
+ }
+
+ @SubscribeEvent
+ @SideOnly(Side.CLIENT)
+ public static void onInputEvent(InputEvent.KeyInputEvent event) {
+ IntList updatingPressed = new IntArrayList();
+ for (var entry : KEYBINDS.int2ObjectEntrySet()) {
+ SyncedKeybind keybind = entry.getValue();
+ if (keybind.keybinding != null && keybind.keybinding.isPressed()) {
+ updatingPressed.add(entry.getIntKey());
+ } else if (Keyboard.getEventKey() == keybind.keyCode) {
+ updatingPressed.add(entry.getIntKey());
+ }
+ }
+ if (!updatingPressed.isEmpty()) {
+ NetworkHandler.instance.sendToServer(new PacketKeyPressed(updatingPressed));
+ }
+ }
+
+ // Updated by the packet handler
+ void onKeyPressed(EntityPlayerMP player) {
+ // Player listeners
+ Set listenerSet = playerListeners.get(player);
+ if (listenerSet != null && !listenerSet.isEmpty()) {
+ for (IKeyPressedListener listener : listenerSet) {
+ listener.onKeyPressed(player, this);
+ }
+ }
+ // Global listeners
+ for (IKeyPressedListener listener : globalListeners) {
+ listener.onKeyPressed(player, this);
+ }
+ }
+}
diff --git a/src/main/java/com/gtnewhorizon/gtnhlib/network/NetworkHandler.java b/src/main/java/com/gtnewhorizon/gtnhlib/network/NetworkHandler.java
index f2dbc47..08027dd 100644
--- a/src/main/java/com/gtnewhorizon/gtnhlib/network/NetworkHandler.java
+++ b/src/main/java/com/gtnewhorizon/gtnhlib/network/NetworkHandler.java
@@ -2,6 +2,8 @@
import com.gtnewhorizon.gtnhlib.GTNHLib;
import com.gtnewhorizon.gtnhlib.config.PacketSyncConfig;
+import com.gtnewhorizon.gtnhlib.keybind.PacketKeyDown;
+import com.gtnewhorizon.gtnhlib.keybind.PacketKeyPressed;
import cpw.mods.fml.common.network.NetworkRegistry;
import cpw.mods.fml.common.network.simpleimpl.SimpleNetworkWrapper;
@@ -18,5 +20,7 @@ public static void init() {
0,
Side.CLIENT);
instance.registerMessage(PacketSyncConfig.Handler.class, PacketSyncConfig.class, 1, Side.CLIENT);
+ instance.registerMessage(PacketKeyDown.HandlerKeyDown.class, PacketKeyDown.class, 2, Side.SERVER);
+ instance.registerMessage(PacketKeyPressed.HandlerKeyPressed.class, PacketKeyPressed.class, 3, Side.SERVER);
}
}