Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Synced Keybinds API #104

Merged
merged 3 commits into from
Jan 25, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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.4
*/
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;
}
}
}
225 changes: 225 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,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. <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.4
*/
@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>> listeners = 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 void registerListener(EntityPlayerMP player, IKeyPressedListener listener) {
Set<IKeyPressedListener> 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<IKeyPressedListener> 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<IKeyPressedListener> listenerSet = listeners.get(player);
if (listenerSet != null && !listenerSet.isEmpty()) {
for (IKeyPressedListener listener : listenerSet) {
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);
}
}