Skip to content

Commit

Permalink
Synced Keybinds API (#104)
Browse files Browse the repository at this point in the history
  • Loading branch information
serenibyss authored Jan 25, 2025
1 parent 6af1528 commit 656c98d
Show file tree
Hide file tree
Showing 5 changed files with 381 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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 <strong>server-side only</strong> 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);
}
53 changes: 53 additions & 0 deletions src/main/java/com/gtnewhorizon/gtnhlib/keybind/PacketKeyDown.java
Original file line number Diff line number Diff line change
@@ -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<PacketKeyDown, IMessage> {

@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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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<PacketKeyPressed, IMessage> {

@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;
}
}
}
252 changes: 252 additions & 0 deletions src/main/java/com/gtnewhorizon/gtnhlib/keybind/SyncedKeybind.java
Original file line number Diff line number Diff line change
@@ -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. <br>
* <br>
* Supports both:
* <ul>
* <li>"Key held" - Is this key currently held down by the player
* <li>"Key pressed" - Listener event fired when the player clicks a key
* </ul>
*
* @author serenibyss
* @since 0.6.5
*/
@SuppressWarnings("unused")
@EventBusSubscriber(side = Side.CLIENT)
public final class SyncedKeybind {

private static final Int2ObjectMap<SyncedKeybind> 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<EntityPlayerMP, Boolean> mapping = new WeakHashMap<>();
private final WeakHashMap<EntityPlayerMP, Set<IKeyPressedListener>> playerListeners = new WeakHashMap<>();
private final Set<IKeyPressedListener> globalListeners = Collections.newSetFromMap(new WeakHashMap<>());

// Doubly-wrapped supplier for client-side only type
private SyncedKeybind(Supplier<Supplier<KeyBinding>> 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<Supplier<KeyBinding>> 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.<br>
* 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<IKeyPressedListener> 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<IKeyPressedListener> 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<IKeyPressedListener> 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}
}

0 comments on commit 656c98d

Please sign in to comment.