diff --git a/client/src/main/java/nl/andrewl/aos2_client/Client.java b/client/src/main/java/nl/andrewl/aos2_client/Client.java index 6cde20a..9aac7f5 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/Client.java +++ b/client/src/main/java/nl/andrewl/aos2_client/Client.java @@ -29,6 +29,7 @@ public class Client implements Runnable { private final ClientConfig config; private final CommunicationHandler communicationHandler; private final InputHandler inputHandler; + private final Camera camera; private GameRenderer gameRenderer; private SoundManager soundManager; private long lastPlayerUpdate = 0; @@ -43,11 +44,12 @@ public class Client implements Runnable { public Client(ClientConfig config) { this.config = config; + this.camera = new Camera(); this.players = new ConcurrentHashMap<>(); this.teams = new ConcurrentHashMap<>(); this.projectiles = new ConcurrentHashMap<>(); this.communicationHandler = new CommunicationHandler(this); - this.inputHandler = new InputHandler(this, communicationHandler); + this.inputHandler = new InputHandler(this, communicationHandler, camera); this.chat = new Chat(); this.mainThreadActions = new ConcurrentLinkedQueue<>(); } @@ -78,7 +80,7 @@ public class Client implements Runnable { return; } - gameRenderer = new GameRenderer(this, inputHandler); + gameRenderer = new GameRenderer(this, inputHandler, camera); soundManager = new SoundManager(); long lastFrameAt = System.currentTimeMillis(); diff --git a/client/src/main/java/nl/andrewl/aos2_client/control/InputContext.java b/client/src/main/java/nl/andrewl/aos2_client/control/InputContext.java new file mode 100644 index 0000000..dbc4ddc --- /dev/null +++ b/client/src/main/java/nl/andrewl/aos2_client/control/InputContext.java @@ -0,0 +1,23 @@ +package nl.andrewl.aos2_client.control; + +/** + * Represents a particular context in which player input is obtained. Different + * contexts do different things with the input. The main game, for example, will + * move the player when WASD keys are pressed, and a chatting context might type + * out the keys the player presses into a chat buffer. Contexts may choose to + * implement only some specified methods here. + */ +public interface InputContext { + default void onEnable() {} + default void onDisable() {} + + default void keyPress(long window, int key, int mods) {} + default void keyRelease(long window, int key, int mods) {} + default void keyRepeat(long window, int key, int mods) {} + default void charInput(long window, int codePoint) {} + + default void mouseButtonPress(long window, int button, int mods) {} + default void mouseButtonRelease(long window, int button, int mods) {} + default void mouseScroll(long window, double xOffset, double yOffset) {} + default void mouseCursorPos(long window, double xPos, double yPos) {} +} diff --git a/client/src/main/java/nl/andrewl/aos2_client/control/InputHandler.java b/client/src/main/java/nl/andrewl/aos2_client/control/InputHandler.java index 571fc2a..c80ecd8 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/control/InputHandler.java +++ b/client/src/main/java/nl/andrewl/aos2_client/control/InputHandler.java @@ -1,14 +1,11 @@ package nl.andrewl.aos2_client.control; +import nl.andrewl.aos2_client.Camera; import nl.andrewl.aos2_client.Client; import nl.andrewl.aos2_client.CommunicationHandler; -import nl.andrewl.aos2_client.model.ClientPlayer; -import nl.andrewl.aos_core.model.item.BlockItemStack; -import nl.andrewl.aos_core.model.item.GunItemStack; -import nl.andrewl.aos_core.model.world.Hit; -import nl.andrewl.aos_core.net.client.BlockColorMessage; -import nl.andrewl.aos_core.net.client.ChatWrittenMessage; -import nl.andrewl.aos_core.net.client.ClientInputState; +import nl.andrewl.aos2_client.control.context.ChattingContext; +import nl.andrewl.aos2_client.control.context.ExitMenuContext; +import nl.andrewl.aos2_client.control.context.NormalContext; /** * Class which manages the player's input, and sending it to the server. @@ -19,236 +16,81 @@ public class InputHandler { private long windowId; - private ClientInputState lastInputState = null; + private final NormalContext normalContext; + private final ChattingContext chattingContext; + private final ExitMenuContext exitMenuContext; - private boolean forward; - private boolean backward; - private boolean left; - private boolean right; - private boolean jumping; - private boolean crouching; - private boolean sprinting; - private boolean hitting; - private boolean interacting; - private boolean reloading; - private int selectedInventoryIndex; + private InputContext activeContext; - private boolean debugEnabled; - - private boolean chatting; - private StringBuffer chatText = new StringBuffer(); - - - public InputHandler(Client client, CommunicationHandler comm) { + public InputHandler(Client client, CommunicationHandler comm, Camera cam) { this.client = client; this.comm = comm; + this.normalContext = new NormalContext(this, cam); + this.chattingContext = new ChattingContext(this); + this.exitMenuContext = new ExitMenuContext(this); + this.activeContext = normalContext; } public void setWindowId(long windowId) { this.windowId = windowId; } - public void updateInputState() { - ClientInputState currentInputState = new ClientInputState( - comm.getClientId(), - forward, backward, left, right, - jumping, crouching, sprinting, - hitting, interacting, reloading, - selectedInventoryIndex - ); - if (!currentInputState.equals(lastInputState)) { - comm.sendDatagramPacket(currentInputState); - lastInputState = currentInputState; - } - - ClientPlayer player = client.getMyPlayer(); + public InputContext getActiveContext() { + return activeContext; } - public boolean isForward() { - return forward; + private void switchToContext(InputContext newContext) { + if (newContext.equals(activeContext)) return; + activeContext.onDisable(); + newContext.onEnable(); + activeContext = newContext; } - public void setForward(boolean forward) { - if (chatting) return; - this.forward = forward; - updateInputState(); + public void switchToNormalContext() { + switchToContext(normalContext); } - public boolean isBackward() { - return backward; + public void switchToChattingContext() { + switchToContext(chattingContext); } - public void setBackward(boolean backward) { - if (chatting) return; - this.backward = backward; - updateInputState(); + public void switchToExitMenuContext() { + switchToContext(exitMenuContext); } - public boolean isLeft() { - return left; + public NormalContext getNormalContext() { + return normalContext; } - public void setLeft(boolean left) { - if (chatting) return; - this.left = left; - updateInputState(); + public ChattingContext getChattingContext() { + return chattingContext; } - public boolean isRight() { - return right; + public ExitMenuContext getExitMenuContext() { + return exitMenuContext; } - public void setRight(boolean right) { - if (chatting) return; - this.right = right; - updateInputState(); + public boolean isNormalContextActive() { + return normalContext.equals(activeContext); } - public boolean isJumping() { - return jumping; + public boolean isChattingContextActive() { + return chattingContext.equals(activeContext); } - public void setJumping(boolean jumping) { - if (chatting) return; - this.jumping = jumping; - updateInputState(); + public boolean isExitMenuContextActive() { + return exitMenuContext.equals(activeContext); } - public boolean isCrouching() { - return crouching; + public Client getClient() { + return client; } - public void setCrouching(boolean crouching) { - if (chatting) return; - this.crouching = crouching; - updateInputState(); + public CommunicationHandler getComm() { + return comm; } - public boolean isSprinting() { - return sprinting; - } - - public void setSprinting(boolean sprinting) { - if (chatting) return; - this.sprinting = sprinting; - updateInputState(); - } - - public boolean isHitting() { - return hitting; - } - - public void setHitting(boolean hitting) { - if (chatting) return; - this.hitting = hitting; - updateInputState(); - } - - public boolean isInteracting() { - return interacting; - } - - public void setInteracting(boolean interacting) { - if (chatting) return; - this.interacting = interacting; - updateInputState(); - } - - public boolean isReloading() { - return reloading; - } - - public void setReloading(boolean reloading) { - if (chatting) return; - this.reloading = reloading; - updateInputState(); - } - - public int getSelectedInventoryIndex() { - return selectedInventoryIndex; - } - - public void setSelectedInventoryIndex(int selectedInventoryIndex) { - if (chatting) return; - this.selectedInventoryIndex = selectedInventoryIndex; - updateInputState(); - } - - public boolean isDebugEnabled() { - return debugEnabled; - } - - public void toggleDebugEnabled() { - this.debugEnabled = !debugEnabled; - } - - public void enableChatting() { - if (chatting) return; - setForward(false); - setBackward(false); - setLeft(false); - setRight(false); - setJumping(false); - setCrouching(false); - setSprinting(false); - setReloading(false); - chatting = true; - chatText = new StringBuffer(); - } - - public boolean isChatting() { - return chatting; - } - - public void cancelChatting() { - chatting = false; - chatText.delete(0, chatText.length()); - } - - public void appendToChat(int codePoint) { - if (!chatting || chatText.length() + 1 > 120) return; - chatText.appendCodePoint(codePoint); - } - - public void appendToChat(String s) { - if (!chatting || chatText.length() + s.length() > 120) return; - chatText.append(s); - } - - public void deleteFromChat() { - if (!chatting || chatText.length() == 0) return; - chatText.deleteCharAt(chatText.length() - 1); - } - - public String getChatText() { - return chatText.toString(); - } - - public void sendChat() { - if (!chatting) return; - String text = chatText.toString().trim(); - cancelChatting(); - if (!text.isBlank()) { - client.getCommunicationHandler().sendMessage(new ChatWrittenMessage(text)); - } - } - - public boolean isScopeEnabled() { - return interacting && - client.getMyPlayer().getInventory().getSelectedItemStack() instanceof GunItemStack; - } - - public void pickBlock() { - var player = client.getMyPlayer(); - if (player.getInventory().getSelectedItemStack() instanceof BlockItemStack stack) { - Hit hit = client.getWorld().getLookingAtPos(player.getEyePosition(), player.getViewVector(), 50); - if (hit != null) { - byte selectedBlock = client.getWorld().getBlockAt(hit.pos().x, hit.pos().y, hit.pos().z); - if (selectedBlock > 0) { - stack.setSelectedValue(selectedBlock); - comm.sendDatagramPacket(new BlockColorMessage(player.getId(), selectedBlock)); - } - } - } + public long getWindowId() { + return windowId; } } diff --git a/client/src/main/java/nl/andrewl/aos2_client/control/PlayerCharacterInputCallback.java b/client/src/main/java/nl/andrewl/aos2_client/control/PlayerCharacterInputCallback.java index e0ff8ec..386be64 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/control/PlayerCharacterInputCallback.java +++ b/client/src/main/java/nl/andrewl/aos2_client/control/PlayerCharacterInputCallback.java @@ -11,8 +11,6 @@ public class PlayerCharacterInputCallback implements GLFWCharCallbackI { @Override public void invoke(long window, int codepoint) { - if (inputHandler.isChatting()) { - inputHandler.appendToChat(codepoint); - } + inputHandler.getActiveContext().charInput(window, codepoint); } } diff --git a/client/src/main/java/nl/andrewl/aos2_client/control/PlayerInputKeyCallback.java b/client/src/main/java/nl/andrewl/aos2_client/control/PlayerInputKeyCallback.java index c18d35b..9a1d37d 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/control/PlayerInputKeyCallback.java +++ b/client/src/main/java/nl/andrewl/aos2_client/control/PlayerInputKeyCallback.java @@ -13,58 +13,10 @@ public class PlayerInputKeyCallback implements GLFWKeyCallbackI { @Override public void invoke(long window, int key, int scancode, int action, int mods) { - if (action == GLFW_PRESS) { - switch (key) { - case GLFW_KEY_W -> inputHandler.setForward(true); - case GLFW_KEY_A -> inputHandler.setLeft(true); - case GLFW_KEY_S -> inputHandler.setBackward(true); - case GLFW_KEY_D -> inputHandler.setRight(true); - case GLFW_KEY_SPACE -> inputHandler.setJumping(true); - case GLFW_KEY_LEFT_CONTROL -> inputHandler.setCrouching(true); - case GLFW_KEY_LEFT_SHIFT -> inputHandler.setSprinting(true); - case GLFW_KEY_R -> inputHandler.setReloading(true); - - case GLFW_KEY_BACKSPACE -> inputHandler.deleteFromChat(); - case GLFW_KEY_ENTER -> inputHandler.sendChat(); - - case GLFW_KEY_1 -> inputHandler.setSelectedInventoryIndex(0); - case GLFW_KEY_2 -> inputHandler.setSelectedInventoryIndex(1); - case GLFW_KEY_3 -> inputHandler.setSelectedInventoryIndex(2); - case GLFW_KEY_4 -> inputHandler.setSelectedInventoryIndex(3); - - case GLFW_KEY_F3 -> inputHandler.toggleDebugEnabled(); - - case GLFW_KEY_ESCAPE -> { - if (inputHandler.isChatting()) { - inputHandler.cancelChatting(); - } else { - glfwSetWindowShouldClose(window, true); - } - } - } - } else if (action == GLFW_RELEASE) { - switch (key) { - case GLFW_KEY_W -> inputHandler.setForward(false); - case GLFW_KEY_A -> inputHandler.setLeft(false); - case GLFW_KEY_S -> inputHandler.setBackward(false); - case GLFW_KEY_D -> inputHandler.setRight(false); - case GLFW_KEY_SPACE -> inputHandler.setJumping(false); - case GLFW_KEY_LEFT_CONTROL -> inputHandler.setCrouching(false); - case GLFW_KEY_LEFT_SHIFT -> inputHandler.setSprinting(false); - case GLFW_KEY_R -> inputHandler.setReloading(false); - - case GLFW_KEY_T -> inputHandler.enableChatting(); - case GLFW_KEY_SLASH -> { - if (!inputHandler.isChatting()) { - inputHandler.enableChatting(); - inputHandler.appendToChat("/"); - } - } - } - } else if (action == GLFW_REPEAT) { - switch (key) { - case GLFW_KEY_BACKSPACE -> inputHandler.deleteFromChat(); - } + switch (action) { + case GLFW_PRESS -> inputHandler.getActiveContext().keyPress(window, key, mods); + case GLFW_RELEASE -> inputHandler.getActiveContext().keyRelease(window, key, mods); + case GLFW_REPEAT -> inputHandler.getActiveContext().keyRepeat(window, key, mods); } } } diff --git a/client/src/main/java/nl/andrewl/aos2_client/control/PlayerInputMouseClickCallback.java b/client/src/main/java/nl/andrewl/aos2_client/control/PlayerInputMouseClickCallback.java index eb8bc4a..943a1c8 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/control/PlayerInputMouseClickCallback.java +++ b/client/src/main/java/nl/andrewl/aos2_client/control/PlayerInputMouseClickCallback.java @@ -2,7 +2,8 @@ package nl.andrewl.aos2_client.control; import org.lwjgl.glfw.GLFWMouseButtonCallbackI; -import static org.lwjgl.glfw.GLFW.*; +import static org.lwjgl.glfw.GLFW.GLFW_PRESS; +import static org.lwjgl.glfw.GLFW.GLFW_RELEASE; /** * Callback that's called when the player clicks with their mouse. @@ -16,17 +17,9 @@ public class PlayerInputMouseClickCallback implements GLFWMouseButtonCallbackI { @Override public void invoke(long window, int button, int action, int mods) { - if (action == GLFW_PRESS) { - switch (button) { - case GLFW_MOUSE_BUTTON_1 -> inputHandler.setHitting(true); - case GLFW_MOUSE_BUTTON_2 -> inputHandler.setInteracting(true); - case GLFW_MOUSE_BUTTON_3 -> inputHandler.pickBlock(); - } - } else if (action == GLFW_RELEASE) { - switch (button) { - case GLFW_MOUSE_BUTTON_1 -> inputHandler.setHitting(false); - case GLFW_MOUSE_BUTTON_2 -> inputHandler.setInteracting(false); - } + switch (action) { + case GLFW_PRESS -> inputHandler.getActiveContext().mouseButtonPress(window, button, mods); + case GLFW_RELEASE -> inputHandler.getActiveContext().mouseButtonRelease(window, button, mods); } } } diff --git a/client/src/main/java/nl/andrewl/aos2_client/control/PlayerInputMouseScrollCallback.java b/client/src/main/java/nl/andrewl/aos2_client/control/PlayerInputMouseScrollCallback.java index 5021391..6609c01 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/control/PlayerInputMouseScrollCallback.java +++ b/client/src/main/java/nl/andrewl/aos2_client/control/PlayerInputMouseScrollCallback.java @@ -1,29 +1,16 @@ package nl.andrewl.aos2_client.control; -import nl.andrewl.aos2_client.Client; -import nl.andrewl.aos2_client.CommunicationHandler; -import nl.andrewl.aos_core.model.item.BlockItemStack; -import nl.andrewl.aos_core.net.client.BlockColorMessage; import org.lwjgl.glfw.GLFWScrollCallbackI; public class PlayerInputMouseScrollCallback implements GLFWScrollCallbackI { - private final Client client; - private final CommunicationHandler comm; + private final InputHandler inputHandler; - public PlayerInputMouseScrollCallback(Client client) { - this.client = client; - this.comm = client.getCommunicationHandler(); + public PlayerInputMouseScrollCallback(InputHandler inputHandler) { + this.inputHandler = inputHandler; } @Override public void invoke(long window, double xoffset, double yoffset) { - if (client.getMyPlayer().getInventory().getSelectedItemStack() instanceof BlockItemStack stack) { - if (yoffset < 0) { - stack.setSelectedValue((byte) (stack.getSelectedValue() - 1)); - } else if (yoffset > 0) { - stack.setSelectedValue((byte) (stack.getSelectedValue() + 1)); - } - comm.sendDatagramPacket(new BlockColorMessage(client.getMyPlayer().getId(), stack.getSelectedValue())); - } + inputHandler.getActiveContext().mouseScroll(window, xoffset, yoffset); } } diff --git a/client/src/main/java/nl/andrewl/aos2_client/control/PlayerViewCursorCallback.java b/client/src/main/java/nl/andrewl/aos2_client/control/PlayerViewCursorCallback.java index fc8d9a0..a9f2e21 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/control/PlayerViewCursorCallback.java +++ b/client/src/main/java/nl/andrewl/aos2_client/control/PlayerViewCursorCallback.java @@ -1,64 +1,17 @@ package nl.andrewl.aos2_client.control; -import nl.andrewl.aos2_client.Camera; -import nl.andrewl.aos2_client.Client; -import nl.andrewl.aos2_client.CommunicationHandler; -import nl.andrewl.aos2_client.config.ClientConfig; -import nl.andrewl.aos_core.net.client.ClientOrientationState; import org.lwjgl.glfw.GLFWCursorPosCallbackI; -import java.util.concurrent.ForkJoinPool; - -import static org.lwjgl.glfw.GLFW.glfwGetCursorPos; - -/** - * Callback that's called when the player's cursor position updates. This means - * the player is looking around. - */ public class PlayerViewCursorCallback implements GLFWCursorPosCallbackI { - /** - * The number of milliseconds to wait before sending orientation updates, - * to prevent overloading the server. - */ - private static final int ORIENTATION_UPDATE_LIMIT = 20; - private final ClientConfig.InputConfig config; - private final Client client; - private final Camera camera; - private final CommunicationHandler comm; - private float lastMouseCursorX; - private float lastMouseCursorY; - private long lastOrientationUpdateSentAt = 0L; + private final InputHandler inputHandler; - public PlayerViewCursorCallback(Client client, Camera cam) { - this.config = client.getConfig().input; - this.client = client; - this.camera = cam; - this.comm = client.getCommunicationHandler(); + public PlayerViewCursorCallback(InputHandler inputHandler) { + this.inputHandler = inputHandler; } @Override public void invoke(long window, double xpos, double ypos) { - double[] xb = new double[1]; - double[] yb = new double[1]; - glfwGetCursorPos(window, xb, yb); - float x = (float) xb[0]; - float y = (float) yb[0]; - float dx = x - lastMouseCursorX; - float dy = y - lastMouseCursorY; - lastMouseCursorX = x; - lastMouseCursorY = y; - float trueSensitivity = config.mouseSensitivity; - if (client.getInputHandler().isScopeEnabled()) trueSensitivity *= 0.1f; - client.getMyPlayer().setOrientation( - client.getMyPlayer().getOrientation().x - dx * trueSensitivity, - client.getMyPlayer().getOrientation().y - dy * trueSensitivity - ); - camera.setOrientationToPlayer(client.getMyPlayer()); - long now = System.currentTimeMillis(); - if (lastOrientationUpdateSentAt + ORIENTATION_UPDATE_LIMIT < now) { - ForkJoinPool.commonPool().submit(() -> comm.sendDatagramPacket(ClientOrientationState.fromPlayer(client.getMyPlayer()))); - lastOrientationUpdateSentAt = now; - } + inputHandler.getActiveContext().mouseCursorPos(window, xpos, ypos); } } diff --git a/client/src/main/java/nl/andrewl/aos2_client/control/context/ChattingContext.java b/client/src/main/java/nl/andrewl/aos2_client/control/context/ChattingContext.java new file mode 100644 index 0000000..7d4de6e --- /dev/null +++ b/client/src/main/java/nl/andrewl/aos2_client/control/context/ChattingContext.java @@ -0,0 +1,88 @@ +package nl.andrewl.aos2_client.control.context; + +import nl.andrewl.aos2_client.control.InputContext; +import nl.andrewl.aos2_client.control.InputHandler; +import nl.andrewl.aos2_client.util.WindowUtils; +import nl.andrewl.aos_core.net.client.ChatWrittenMessage; + +import static org.lwjgl.glfw.GLFW.*; + +public class ChattingContext implements InputContext { + private static final int MAX_LENGTH = 120; + + private final InputHandler inputHandler; + + private final StringBuffer chatBuffer = new StringBuffer(MAX_LENGTH); + + public ChattingContext(InputHandler inputHandler) { + this.inputHandler = inputHandler; + } + + public void appendToChat(int codePoint) { + appendToChat(Character.toString(codePoint)); + } + + public void appendToChat(String content) { + if (chatBuffer.length() + content.length() > MAX_LENGTH) return; + chatBuffer.append(content); + } + + private void deleteFromChat() { + if (chatBuffer.length() > 0) { + chatBuffer.deleteCharAt(chatBuffer.length() - 1); + } + } + + private void clearChatBuffer() { + chatBuffer.delete(0, chatBuffer.length()); + } + + private void sendChat() { + String text = chatBuffer.toString().trim(); + if (!text.isBlank()) { + inputHandler.getComm().sendMessage(new ChatWrittenMessage(text)); + } + inputHandler.switchToNormalContext(); + } + + public String getChatBufferText() { + return new String(chatBuffer); + } + + @Override + public void onEnable() { + clearChatBuffer(); + if (inputHandler.getClient().getConfig().display.captureCursor) { + WindowUtils.freeCursor(inputHandler.getWindowId()); + } + } + + @Override + public void onDisable() { + clearChatBuffer(); + if (inputHandler.getClient().getConfig().display.captureCursor) { + WindowUtils.captureCursor(inputHandler.getWindowId()); + } + } + + @Override + public void keyPress(long window, int key, int mods) { + switch (key) { + case GLFW_KEY_BACKSPACE -> deleteFromChat(); + case GLFW_KEY_ENTER -> sendChat(); + case GLFW_KEY_ESCAPE -> inputHandler.switchToNormalContext(); + } + } + + @Override + public void keyRepeat(long window, int key, int mods) { + switch (key) { + case GLFW_KEY_BACKSPACE -> deleteFromChat(); + } + } + + @Override + public void charInput(long window, int codePoint) { + appendToChat(codePoint); + } +} diff --git a/client/src/main/java/nl/andrewl/aos2_client/control/context/ExitMenuContext.java b/client/src/main/java/nl/andrewl/aos2_client/control/context/ExitMenuContext.java new file mode 100644 index 0000000..fb27dbc --- /dev/null +++ b/client/src/main/java/nl/andrewl/aos2_client/control/context/ExitMenuContext.java @@ -0,0 +1,38 @@ +package nl.andrewl.aos2_client.control.context; + +import nl.andrewl.aos2_client.control.InputContext; +import nl.andrewl.aos2_client.control.InputHandler; +import nl.andrewl.aos2_client.util.WindowUtils; + +import static org.lwjgl.glfw.GLFW.GLFW_KEY_ESCAPE; +import static org.lwjgl.glfw.GLFW.glfwSetWindowShouldClose; + +public class ExitMenuContext implements InputContext { + private final InputHandler inputHandler; + + public ExitMenuContext(InputHandler inputHandler) { + this.inputHandler = inputHandler; + } + + @Override + public void onEnable() { + if (inputHandler.getClient().getConfig().display.captureCursor) { + WindowUtils.freeCursor(inputHandler.getWindowId()); + } + } + + @Override + public void onDisable() { + if (inputHandler.getClient().getConfig().display.captureCursor) { + WindowUtils.captureCursor(inputHandler.getWindowId()); + } + } + + @Override + public void keyPress(long window, int key, int mods) { + switch (key) { + case GLFW_KEY_ESCAPE -> glfwSetWindowShouldClose(window, true); + default -> inputHandler.switchToNormalContext(); + } + } +} diff --git a/client/src/main/java/nl/andrewl/aos2_client/control/context/NormalContext.java b/client/src/main/java/nl/andrewl/aos2_client/control/context/NormalContext.java new file mode 100644 index 0000000..70e9385 --- /dev/null +++ b/client/src/main/java/nl/andrewl/aos2_client/control/context/NormalContext.java @@ -0,0 +1,283 @@ +package nl.andrewl.aos2_client.control.context; + +import nl.andrewl.aos2_client.Camera; +import nl.andrewl.aos2_client.control.InputContext; +import nl.andrewl.aos2_client.control.InputHandler; +import nl.andrewl.aos_core.model.item.BlockItemStack; +import nl.andrewl.aos_core.model.item.GunItemStack; +import nl.andrewl.aos_core.model.item.ItemStack; +import nl.andrewl.aos_core.model.world.Hit; +import nl.andrewl.aos_core.net.client.BlockColorMessage; +import nl.andrewl.aos_core.net.client.ClientInputState; +import nl.andrewl.aos_core.net.client.ClientOrientationState; + +import java.util.concurrent.ForkJoinPool; + +import static org.lwjgl.glfw.GLFW.*; + +/** + * The normal input context that occurs when the player is active in the game. + * This includes moving around, interacting with their inventory, moving their + * view, and so on. + */ +public class NormalContext implements InputContext { + /** + * The number of milliseconds to wait before sending orientation updates, + * to prevent overloading the server. + */ + private static final int ORIENTATION_UPDATE_LIMIT = 20; + + private final InputHandler inputHandler; + private final Camera camera; + + private ClientInputState lastInputState = null; + + private boolean forward; + private boolean backward; + private boolean left; + private boolean right; + private boolean jumping; + private boolean crouching; + private boolean sprinting; + private boolean hitting; + private boolean interacting; + private boolean reloading; + private int selectedInventoryIndex; + + private boolean debugEnabled; + + private float lastMouseCursorX; + private float lastMouseCursorY; + private long lastOrientationUpdateSentAt = 0L; + + public NormalContext(InputHandler inputHandler, Camera camera) { + this.inputHandler = inputHandler; + this.camera = camera; + } + + public void updateInputState() { + var comm = inputHandler.getComm(); + ClientInputState currentInputState = new ClientInputState( + comm.getClientId(), + forward, backward, left, right, + jumping, crouching, sprinting, + hitting, interacting, reloading, + selectedInventoryIndex + ); + if (!currentInputState.equals(lastInputState)) { + comm.sendDatagramPacket(currentInputState); + lastInputState = currentInputState; + } + } + + public void resetInputState() { + forward = false; + backward = false; + left = false; + right = false; + jumping = false; + crouching = false; + sprinting = false; + hitting = false; + interacting = false; + reloading = false; + updateInputState(); + } + + public void setForward(boolean forward) { + this.forward = forward; + updateInputState(); + } + + public void setBackward(boolean backward) { + this.backward = backward; + updateInputState(); + } + + public void setLeft(boolean left) { + this.left = left; + updateInputState(); + } + + public void setRight(boolean right) { + this.right = right; + updateInputState(); + } + + public void setJumping(boolean jumping) { + this.jumping = jumping; + updateInputState(); + } + + public void setCrouching(boolean crouching) { + this.crouching = crouching; + updateInputState(); + } + + public void setSprinting(boolean sprinting) { + this.sprinting = sprinting; + updateInputState(); + } + + public void setHitting(boolean hitting) { + this.hitting = hitting; + updateInputState(); + } + + public void setInteracting(boolean interacting) { + this.interacting = interacting; + updateInputState(); + } + + public void setReloading(boolean reloading) { + this.reloading = reloading; + updateInputState(); + } + + public void setSelectedInventoryIndex(int selectedInventoryIndex) { + this.selectedInventoryIndex = selectedInventoryIndex; + updateInputState(); + } + + public void toggleDebugEnabled() { + this.debugEnabled = !debugEnabled; + } + + public void enableChatting() { + inputHandler.switchToChattingContext(); + } + + @Override + public void onEnable() { + resetInputState(); + } + + @Override + public void onDisable() { + resetInputState(); + } + + @Override + public void keyPress(long window, int key, int mods) { + switch (key) { + case GLFW_KEY_W -> setForward(true); + case GLFW_KEY_A -> setLeft(true); + case GLFW_KEY_S -> setBackward(true); + case GLFW_KEY_D -> setRight(true); + case GLFW_KEY_SPACE -> setJumping(true); + case GLFW_KEY_LEFT_CONTROL -> setCrouching(true); + case GLFW_KEY_LEFT_SHIFT -> setSprinting(true); + case GLFW_KEY_R -> setReloading(true); + + case GLFW_KEY_1 -> setSelectedInventoryIndex(0); + case GLFW_KEY_2 -> setSelectedInventoryIndex(1); + case GLFW_KEY_3 -> setSelectedInventoryIndex(2); + case GLFW_KEY_4 -> setSelectedInventoryIndex(3); + + case GLFW_KEY_F3 -> toggleDebugEnabled(); + case GLFW_KEY_ESCAPE -> inputHandler.switchToExitMenuContext(); + } + } + + @Override + public void keyRelease(long window, int key, int mods) { + switch (key) { + case GLFW_KEY_W -> setForward(false); + case GLFW_KEY_A -> setLeft(false); + case GLFW_KEY_S -> setBackward(false); + case GLFW_KEY_D -> setRight(false); + case GLFW_KEY_SPACE -> setJumping(false); + case GLFW_KEY_LEFT_CONTROL -> setCrouching(false); + case GLFW_KEY_LEFT_SHIFT -> setSprinting(false); + case GLFW_KEY_R -> setReloading(false); + + case GLFW_KEY_T -> enableChatting(); + case GLFW_KEY_SLASH -> { + enableChatting(); + inputHandler.getChattingContext().appendToChat("/"); + } + } + } + + @Override + public void mouseButtonPress(long window, int button, int mods) { + switch (button) { + case GLFW_MOUSE_BUTTON_1 -> setHitting(true); + case GLFW_MOUSE_BUTTON_2 -> setInteracting(true); + case GLFW_MOUSE_BUTTON_3 -> pickBlock(); + } + } + + @Override + public void mouseButtonRelease(long window, int button, int mods) { + switch (button) { + case GLFW_MOUSE_BUTTON_1 -> setHitting(false); + case GLFW_MOUSE_BUTTON_2 -> setInteracting(false); + } + } + + @Override + public void mouseScroll(long window, double xOffset, double yOffset) { + var player = inputHandler.getClient().getMyPlayer(); + ItemStack stack = player.getInventory().getSelectedItemStack(); + if (stack instanceof BlockItemStack blockStack) { + if (yOffset < 0) { + blockStack.setSelectedValue((byte) (blockStack.getSelectedValue() - 1)); + } else if (yOffset > 0) { + blockStack.setSelectedValue((byte) (blockStack.getSelectedValue() + 1)); + } + inputHandler.getComm().sendDatagramPacket(new BlockColorMessage(player.getId(), blockStack.getSelectedValue())); + } + } + + @Override + public void mouseCursorPos(long window, double xPos, double yPos) { + double[] xb = new double[1]; + double[] yb = new double[1]; + glfwGetCursorPos(window, xb, yb); + float x = (float) xb[0]; + float y = (float) yb[0]; + float dx = x - lastMouseCursorX; + float dy = y - lastMouseCursorY; + lastMouseCursorX = x; + lastMouseCursorY = y; + var client = inputHandler.getClient(); + float trueSensitivity = inputHandler.getClient().getConfig().input.mouseSensitivity; + if (isScopeEnabled()) trueSensitivity *= 0.1f; + client.getMyPlayer().setOrientation( + client.getMyPlayer().getOrientation().x - dx * trueSensitivity, + client.getMyPlayer().getOrientation().y - dy * trueSensitivity + ); + camera.setOrientationToPlayer(client.getMyPlayer()); + long now = System.currentTimeMillis(); + if (lastOrientationUpdateSentAt + ORIENTATION_UPDATE_LIMIT < now) { + ForkJoinPool.commonPool().submit(() -> inputHandler.getComm().sendDatagramPacket(ClientOrientationState.fromPlayer(client.getMyPlayer()))); + lastOrientationUpdateSentAt = now; + } + } + + public void pickBlock() { + var client = inputHandler.getClient(); + var comm = inputHandler.getComm(); + var player = client.getMyPlayer(); + if (player.getInventory().getSelectedItemStack() instanceof BlockItemStack stack) { + Hit hit = client.getWorld().getLookingAtPos(player.getEyePosition(), player.getViewVector(), 50); + if (hit != null) { + byte selectedBlock = client.getWorld().getBlockAt(hit.pos().x, hit.pos().y, hit.pos().z); + if (selectedBlock > 0) { + stack.setSelectedValue(selectedBlock); + comm.sendDatagramPacket(new BlockColorMessage(player.getId(), selectedBlock)); + } + } + } + } + + public boolean isScopeEnabled() { + return interacting && + inputHandler.getClient().getMyPlayer().getInventory().getSelectedItemStack() instanceof GunItemStack; + } + + public boolean isDebugEnabled() { + return debugEnabled; + } +} diff --git a/client/src/main/java/nl/andrewl/aos2_client/model/ClientPlayer.java b/client/src/main/java/nl/andrewl/aos2_client/model/ClientPlayer.java index 3550f8b..2491087 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/model/ClientPlayer.java +++ b/client/src/main/java/nl/andrewl/aos2_client/model/ClientPlayer.java @@ -48,7 +48,7 @@ public class ClientPlayer extends Player { .translate(cam.getPosition()) .rotate((float) (cam.getOrientation().x + Math.PI), Camera.UP) .rotate(-cam.getOrientation().y + (float) Math.PI / 2, Camera.RIGHT); - if (inputHandler.isScopeEnabled()) { + if (inputHandler.isNormalContextActive() && inputHandler.getNormalContext().isScopeEnabled()) { heldItemTransform.translate(0, -0.12f, 0); } else { heldItemTransform.translate(-0.35f, -0.4f, 0.5f); diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/GameRenderer.java b/client/src/main/java/nl/andrewl/aos2_client/render/GameRenderer.java index 4991a42..bbee304 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/render/GameRenderer.java +++ b/client/src/main/java/nl/andrewl/aos2_client/render/GameRenderer.java @@ -39,6 +39,7 @@ public class GameRenderer { private final ModelRenderer modelRenderer; private final Camera camera; private final Client client; + private final InputHandler inputHandler; // Standard models for various game components. private final Model playerModel; @@ -56,10 +57,11 @@ public class GameRenderer { private final Matrix4f perspectiveTransform; private final float[] perspectiveTransformData = new float[16]; - public GameRenderer(Client client, InputHandler inputHandler) { + public GameRenderer(Client client, InputHandler inputHandler, Camera camera) { this.config = client.getConfig().display; this.client = client; - this.camera = new Camera(); + this.inputHandler = inputHandler; + this.camera = camera; camera.setToPlayer(client.getMyPlayer()); this.perspectiveTransform = new Matrix4f(); @@ -88,9 +90,9 @@ public class GameRenderer { // Setup callbacks. glfwSetKeyCallback(windowHandle, new PlayerInputKeyCallback(inputHandler)); - glfwSetCursorPosCallback(windowHandle, new PlayerViewCursorCallback(client, camera)); + glfwSetCursorPosCallback(windowHandle, new PlayerViewCursorCallback(inputHandler)); glfwSetMouseButtonCallback(windowHandle, new PlayerInputMouseClickCallback(inputHandler)); - glfwSetScrollCallback(windowHandle, new PlayerInputMouseScrollCallback(client)); + glfwSetScrollCallback(windowHandle, new PlayerInputMouseScrollCallback(inputHandler)); glfwSetCharCallback(windowHandle, new PlayerCharacterInputCallback(inputHandler)); if (config.captureCursor) { glfwSetInputMode(windowHandle, GLFW_CURSOR, GLFW_CURSOR_DISABLED); @@ -168,7 +170,7 @@ public class GameRenderer { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); ClientPlayer myPlayer = client.getMyPlayer(); - if (client.getInputHandler().isScopeEnabled()) { + if (inputHandler.isNormalContextActive() && inputHandler.getNormalContext().isScopeEnabled()) { updatePerspective(15); } else { updatePerspective(config.fov); diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/gui/GuiRenderer.java b/client/src/main/java/nl/andrewl/aos2_client/render/gui/GuiRenderer.java index 4daba8a..00bf9b0 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/render/gui/GuiRenderer.java +++ b/client/src/main/java/nl/andrewl/aos2_client/render/gui/GuiRenderer.java @@ -2,7 +2,6 @@ package nl.andrewl.aos2_client.render.gui; import nl.andrewl.aos2_client.Camera; import nl.andrewl.aos2_client.Client; -import nl.andrewl.aos2_client.model.Chat; import nl.andrewl.aos2_client.model.ClientPlayer; import nl.andrewl.aos2_client.model.OtherPlayer; import nl.andrewl.aos2_client.render.ShaderProgram; @@ -223,15 +222,19 @@ public class GuiRenderer { nvgBeginFrame(vgId, width, height, width / height); nvgSave(vgId); - drawCrosshair(width, height, client.getInputHandler().isScopeEnabled()); + boolean scopeEnabled = client.getInputHandler().getNormalContext().isScopeEnabled(); + drawCrosshair(width, height, scopeEnabled); drawHeldItemStackInfo(width, height, client.getMyPlayer()); - if (!client.getInputHandler().isScopeEnabled()) { + if (!scopeEnabled) { drawChat(width, height, client); drawHealthBar(width, height, client.getMyPlayer()); } - if (client.getInputHandler().isDebugEnabled()) { + if (client.getInputHandler().getNormalContext().isDebugEnabled()) { drawDebugInfo(width, height, client); } + if (client.getInputHandler().isExitMenuContextActive()) { + drawExitMenu(width, height); + } nvgRestore(vgId); nvgEndFrame(vgId); @@ -353,13 +356,13 @@ public class GuiRenderer { y -= 16; } var input = client.getInputHandler(); - if (input.isChatting()) { + if (input.isChattingContextActive()) { nvgFillColor(vgId, GuiUtils.rgba(0, 0, 0, 0.5f, colorA)); nvgBeginPath(vgId); nvgRect(vgId, 0, h - 16, w, 16); nvgFill(vgId); nvgFillColor(vgId, GuiUtils.rgba(1, 1, 1, 1, colorA)); - nvgText(vgId, 5, h - 14, "> " + input.getChatText() + "_"); + nvgText(vgId, 5, h - 14, "> " + input.getChattingContext().getChatBufferText() + "_"); } } @@ -394,4 +397,17 @@ public class GuiRenderer { nvgText(vgId, 5, y, String.format("Looking at: x=%d, y=%d, z=%d", hit.pos().x, hit.pos().y, hit.pos().z)); } } + + private void drawExitMenu(float width, float height) { + nvgFillColor(vgId, GuiUtils.rgba(0, 0, 0, 0.5f, colorA)); + nvgBeginPath(vgId); + nvgRect(vgId, 0, 0, width, height); + nvgFill(vgId); + + nvgFontSize(vgId, 12f); + nvgFontFaceId(vgId, jetbrainsMonoFont); + nvgTextAlign(vgId, NVG_ALIGN_LEFT | NVG_ALIGN_TOP); + nvgFillColor(vgId, GuiUtils.rgba(1, 1, 1, 1, colorA)); + nvgText(vgId, width / 2f, height / 2f, "Press ESC to quit. Press any other key to return to the game."); + } } diff --git a/client/src/main/java/nl/andrewl/aos2_client/util/WindowUtils.java b/client/src/main/java/nl/andrewl/aos2_client/util/WindowUtils.java new file mode 100644 index 0000000..8d45af3 --- /dev/null +++ b/client/src/main/java/nl/andrewl/aos2_client/util/WindowUtils.java @@ -0,0 +1,27 @@ +package nl.andrewl.aos2_client.util; + +import nl.andrewl.aos_core.Pair; +import org.lwjgl.BufferUtils; + +import java.nio.IntBuffer; + +import static org.lwjgl.glfw.GLFW.*; + +public class WindowUtils { + public static Pair getSize(long id) { + IntBuffer wBuf = BufferUtils.createIntBuffer(1); + IntBuffer hBuf = BufferUtils.createIntBuffer(1); + glfwGetWindowSize(id, wBuf, hBuf); + return new Pair<>(wBuf.get(0), hBuf.get(0)); + } + + public static void captureCursor(long id) { + glfwSetInputMode(id, GLFW_CURSOR, GLFW_CURSOR_DISABLED); + } + + public static void freeCursor(long id) { + glfwSetInputMode(id, GLFW_CURSOR, GLFW_CURSOR_NORMAL); + var size = WindowUtils.getSize(id); + glfwSetCursorPos(id, size.first() / 2.0, size.second() / 2.0); + } +}