From 7a15b67c61f3b49f15812cf98c6c88dcdb92014b Mon Sep 17 00:00:00 2001 From: Maya <10861407+serenibyss@users.noreply.github.com> Date: Thu, 23 Jan 2025 14:44:48 -0600 Subject: [PATCH 1/3] Synced Keybinds API --- .../gtnhlib/keybind/IKeyPressedListener.java | 20 ++ .../gtnhlib/keybind/PacketKeyDown.java | 53 +++++ .../gtnhlib/keybind/PacketKeyPressed.java | 52 ++++ .../gtnhlib/keybind/SyncedKeybind.java | 225 ++++++++++++++++++ .../gtnhlib/network/NetworkHandler.java | 4 + 5 files changed, 354 insertions(+) create mode 100644 src/main/java/com/gtnewhorizon/gtnhlib/keybind/IKeyPressedListener.java create mode 100644 src/main/java/com/gtnewhorizon/gtnhlib/keybind/PacketKeyDown.java create mode 100644 src/main/java/com/gtnewhorizon/gtnhlib/keybind/PacketKeyPressed.java create mode 100644 src/main/java/com/gtnewhorizon/gtnhlib/keybind/SyncedKeybind.java 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..a7fc1b6 --- /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.4 + */ +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..d8386db --- /dev/null +++ b/src/main/java/com/gtnewhorizon/gtnhlib/keybind/SyncedKeybind.java @@ -0,0 +1,225 @@ +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.4 + */ +@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> listeners = 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 void registerListener(EntityPlayerMP player, IKeyPressedListener listener) { + Set listenerSet = listeners + .computeIfAbsent(player, k -> Collections.newSetFromMap(new WeakHashMap<>())); + listenerSet.add(listener); + } + + /** + * Remove a player's listener on this keybinding. + * + * @param player The player who owns this listener. + * @param listener The handler for the key clicked event. + */ + public void removeListener(EntityPlayerMP player, IKeyPressedListener listener) { + Set listenerSet = listeners.get(player); + if (listenerSet != null) { + listenerSet.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) { + Set listenerSet = listeners.get(player); + if (listenerSet != null && !listenerSet.isEmpty()) { + for (IKeyPressedListener listener : listenerSet) { + 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); } } From 9a0113900323f24619564e5b38f71b22400fc069 Mon Sep 17 00:00:00 2001 From: Maya <10861407+serenibyss@users.noreply.github.com> Date: Sat, 25 Jan 2025 00:59:08 -0600 Subject: [PATCH 2/3] add global listener system --- .../gtnhlib/keybind/SyncedKeybind.java | 39 +++++++++++++++---- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/gtnewhorizon/gtnhlib/keybind/SyncedKeybind.java b/src/main/java/com/gtnewhorizon/gtnhlib/keybind/SyncedKeybind.java index d8386db..884c89b 100644 --- a/src/main/java/com/gtnewhorizon/gtnhlib/keybind/SyncedKeybind.java +++ b/src/main/java/com/gtnewhorizon/gtnhlib/keybind/SyncedKeybind.java @@ -55,7 +55,8 @@ public final class SyncedKeybind { private boolean isKeyDown; private final WeakHashMap mapping = new WeakHashMap<>(); - private final WeakHashMap> listeners = 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) { @@ -135,25 +136,44 @@ public boolean isKeyDown(EntityPlayer player) { * @param player The player who owns this listener. * @param listener The handler for the key clicked event. */ - public void registerListener(EntityPlayerMP player, IKeyPressedListener listener) { - Set listenerSet = listeners + public void registerPlayerListener(EntityPlayerMP player, IKeyPressedListener listener) { + Set listenerSet = playerListeners .computeIfAbsent(player, k -> Collections.newSetFromMap(new WeakHashMap<>())); listenerSet.add(listener); } /** - * Remove a player's listener on this keybinding. + * 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 removeListener(EntityPlayerMP player, IKeyPressedListener listener) { - Set listenerSet = listeners.get(player); + 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 void registerGlobalListener(IKeyPressedListener listener) { + globalListeners.add(listener); + } + + /** + * 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); } @@ -215,11 +235,16 @@ public static void onInputEvent(InputEvent.KeyInputEvent event) { // Updated by the packet handler void onKeyPressed(EntityPlayerMP player) { - Set listenerSet = listeners.get(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); + } } } From 994aa71ee8fac3a5615aa56395caa8f8bbd0679e Mon Sep 17 00:00:00 2001 From: Maya <10861407+serenibyss@users.noreply.github.com> Date: Sat, 25 Jan 2025 09:22:55 -0600 Subject: [PATCH 3/3] reviews, update `@since` javadoc --- .../gtnewhorizon/gtnhlib/keybind/IKeyPressedListener.java | 2 +- .../com/gtnewhorizon/gtnhlib/keybind/SyncedKeybind.java | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/gtnewhorizon/gtnhlib/keybind/IKeyPressedListener.java b/src/main/java/com/gtnewhorizon/gtnhlib/keybind/IKeyPressedListener.java index a7fc1b6..414dc1e 100644 --- a/src/main/java/com/gtnewhorizon/gtnhlib/keybind/IKeyPressedListener.java +++ b/src/main/java/com/gtnewhorizon/gtnhlib/keybind/IKeyPressedListener.java @@ -6,7 +6,7 @@ * Server-side listener interface for when a player presses a specific key. * * @author serenibyss - * @since 0.6.4 + * @since 0.6.5 */ public interface IKeyPressedListener { diff --git a/src/main/java/com/gtnewhorizon/gtnhlib/keybind/SyncedKeybind.java b/src/main/java/com/gtnewhorizon/gtnhlib/keybind/SyncedKeybind.java index 884c89b..7151f3c 100644 --- a/src/main/java/com/gtnewhorizon/gtnhlib/keybind/SyncedKeybind.java +++ b/src/main/java/com/gtnewhorizon/gtnhlib/keybind/SyncedKeybind.java @@ -38,7 +38,7 @@ * * * @author serenibyss - * @since 0.6.4 + * @since 0.6.5 */ @SuppressWarnings("unused") @EventBusSubscriber(side = Side.CLIENT) @@ -136,10 +136,11 @@ public boolean isKeyDown(EntityPlayer player) { * @param player The player who owns this listener. * @param listener The handler for the key clicked event. */ - public void registerPlayerListener(EntityPlayerMP player, IKeyPressedListener listener) { + public SyncedKeybind registerPlayerListener(EntityPlayerMP player, IKeyPressedListener listener) { Set listenerSet = playerListeners .computeIfAbsent(player, k -> Collections.newSetFromMap(new WeakHashMap<>())); listenerSet.add(listener); + return this; } /** @@ -161,8 +162,9 @@ public void removePlayerListener(EntityPlayerMP player, IKeyPressedListener list * * @param listener The handler for the key clicked event. */ - public void registerGlobalListener(IKeyPressedListener listener) { + public SyncedKeybind registerGlobalListener(IKeyPressedListener listener) { globalListeners.add(listener); + return this; } /**