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: + * + * + * @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); } }