From e321979c3c1e8fd5ce99000eaad62957722771a4 Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Thu, 28 Jul 2022 09:01:16 +0200 Subject: [PATCH] Added better client input handling. --- .../java/nl/andrewl/aos2_client/Client.java | 8 +- .../nl/andrewl/aos_core/config/Config.java | 17 +--- .../andrewl/aos_core/model/world/World.java | 8 +- .../ClientOrientationUpdateMessage.java | 11 +++ .../java/nl/andrewl/aos2_server/Server.java | 3 +- .../logic/PlayerActionManager.java | 62 +++++++------- .../aos2_server/logic/PlayerImpulses.java | 42 ++++++++++ .../aos2_server/logic/PlayerInputTracker.java | 82 +++++++++++++++++++ .../aos2_server/model/ServerPlayer.java | 3 +- server/src/main/resources/default-config.yaml | 32 ++++++++ 10 files changed, 212 insertions(+), 56 deletions(-) create mode 100644 core/src/main/java/nl/andrewl/aos_core/net/client/ClientOrientationUpdateMessage.java create mode 100644 server/src/main/java/nl/andrewl/aos2_server/logic/PlayerImpulses.java create mode 100644 server/src/main/java/nl/andrewl/aos2_server/logic/PlayerInputTracker.java create mode 100644 server/src/main/resources/default-config.yaml 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 91848e3..b227eca 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/Client.java +++ b/client/src/main/java/nl/andrewl/aos2_client/Client.java @@ -7,7 +7,6 @@ import nl.andrewl.aos2_client.model.ClientPlayer; import nl.andrewl.aos2_client.model.OtherPlayer; import nl.andrewl.aos2_client.render.GameRenderer; import nl.andrewl.aos2_client.sound.SoundManager; -import nl.andrewl.aos_core.FileUtils; import nl.andrewl.aos_core.config.Config; import nl.andrewl.aos_core.model.Player; import nl.andrewl.aos_core.model.Projectile; @@ -137,7 +136,7 @@ public class Client implements Runnable { gameRenderer.getCamera().setToPlayer(myPlayer); } if (soundManager != null) { - soundManager.updateListener(myPlayer.getPosition(), myPlayer.getVelocity()); + soundManager.updateListener(myPlayer.getEyePosition(), myPlayer.getVelocity()); } lastPlayerUpdate = playerUpdate.timestamp(); } else { @@ -206,7 +205,7 @@ public class Client implements Runnable { } else if (msg instanceof ChatMessage chatMessage) { chat.chatReceived(chatMessage); if (soundManager != null) { - soundManager.play("chat", 1, myPlayer.getPosition(), myPlayer.getVelocity()); + soundManager.play("chat", 1, myPlayer.getEyePosition(), myPlayer.getVelocity()); } } } @@ -268,10 +267,11 @@ public class Client implements Runnable { public static void main(String[] args) throws IOException { List configPaths = Config.getCommonConfigPaths(); + configPaths.add(0, Path.of("client.yaml")); // Add this first so we create client.yaml if needed. if (args.length > 0) { configPaths.add(Path.of(args[0].trim())); } - ClientConfig clientConfig = Config.loadConfig(ClientConfig.class, configPaths, new ClientConfig(), FileUtils.readClasspathFile("default-config.yaml")); + ClientConfig clientConfig = Config.loadConfig(ClientConfig.class, configPaths, new ClientConfig(), "default-config.yaml"); Client client = new Client(clientConfig); client.run(); } diff --git a/core/src/main/java/nl/andrewl/aos_core/config/Config.java b/core/src/main/java/nl/andrewl/aos_core/config/Config.java index cf66df9..b7bf24a 100644 --- a/core/src/main/java/nl/andrewl/aos_core/config/Config.java +++ b/core/src/main/java/nl/andrewl/aos_core/config/Config.java @@ -1,5 +1,6 @@ package nl.andrewl.aos_core.config; +import nl.andrewl.aos_core.FileUtils; import org.yaml.snakeyaml.Yaml; import java.io.IOException; @@ -19,7 +20,7 @@ public final class Config { * @param paths The paths to load from. * @param fallback A default configuration object to use if no config could * be loaded from any of the paths. - * @param defaultConfigFile The default config file to save. + * @param defaultConfigFile The default config file resource to save. * @return The configuration object. * @param The type of the configuration object. */ @@ -35,25 +36,13 @@ public final class Config { } Path outputPath = paths.size() > 0 ? paths.get(0) : Path.of("config.yaml"); try (var writer = Files.newBufferedWriter(outputPath)) { - writer.write(defaultConfigFile); + writer.write(FileUtils.readClasspathFile(defaultConfigFile)); } catch (IOException e) { e.printStackTrace(); } return fallback; } - public static T loadConfig(Class configType, List paths, String defaultConfigFile) { - var cfg = loadConfig(configType, paths, null, defaultConfigFile); - if (cfg == null) { - throw new RuntimeException("Could not load config from any of the supplied paths."); - } - return cfg; - } - - public static T loadConfig(Class configType, T fallback, String defaultConfigFile, Path... paths) { - return loadConfig(configType, List.of(paths), fallback, defaultConfigFile); - } - public static List getCommonConfigPaths() { List paths = new ArrayList<>(); paths.add(Path.of("config.yaml")); diff --git a/core/src/main/java/nl/andrewl/aos_core/model/world/World.java b/core/src/main/java/nl/andrewl/aos_core/model/world/World.java index 08d08ca..2691e15 100644 --- a/core/src/main/java/nl/andrewl/aos_core/model/world/World.java +++ b/core/src/main/java/nl/andrewl/aos_core/model/world/World.java @@ -17,7 +17,7 @@ import java.util.Map; * that players can interact in. */ public class World { - private static final float DELTA = 0.01f; + private static final float DELTA = 0.001f; protected final Map chunkMap = new HashMap<>(); protected ColorPalette palette; @@ -164,8 +164,12 @@ public class World { public Hit getLookingAtPos(Vector3f eyePos, Vector3f eyeDir, float limit) { if (eyeDir.lengthSquared() == 0 || limit <= 0) return null; Vector3f pos = new Vector3f(eyePos); + Vector3f previousPos = new Vector3f(); while (pos.distance(eyePos) < limit) { + previousPos.set(pos); stepToNextBlock(pos, eyeDir); + // If for some reason we couldn't advance to the next block, exit null, so we don't infinitely loop. + if (pos.equals(previousPos)) return null; if (getBlockAt(pos) > 0) { Vector3i hitPos = new Vector3i( (int) Math.floor(pos.x), @@ -234,7 +238,7 @@ public class World { // Testing code! if (diff == 0) { System.out.printf("n = %.8f, nextValue = %.8f, floor(n) - DELTA = %.8f%n", n, nextValue, Math.floor(n) - DELTA); - throw new RuntimeException("EEK"); + return Float.MAX_VALUE; } return Math.abs(diff / dir); } diff --git a/core/src/main/java/nl/andrewl/aos_core/net/client/ClientOrientationUpdateMessage.java b/core/src/main/java/nl/andrewl/aos_core/net/client/ClientOrientationUpdateMessage.java new file mode 100644 index 0000000..cbb0042 --- /dev/null +++ b/core/src/main/java/nl/andrewl/aos_core/net/client/ClientOrientationUpdateMessage.java @@ -0,0 +1,11 @@ +package nl.andrewl.aos_core.net.client; + +import nl.andrewl.record_net.Message; + +/** + * A message that the server sends to clients, to tell them to update their + * player's orientation to the specified values. + */ +public record ClientOrientationUpdateMessage( + float x, float y +) implements Message {} diff --git a/server/src/main/java/nl/andrewl/aos2_server/Server.java b/server/src/main/java/nl/andrewl/aos2_server/Server.java index b930351..d37e0e1 100644 --- a/server/src/main/java/nl/andrewl/aos2_server/Server.java +++ b/server/src/main/java/nl/andrewl/aos2_server/Server.java @@ -177,10 +177,11 @@ public class Server implements Runnable { public static void main(String[] args) throws IOException { List configPaths = Config.getCommonConfigPaths(); + configPaths.add(0, Path.of("server.yaml")); if (args.length > 0) { configPaths.add(Path.of(args[0].trim())); } - ServerConfig cfg = Config.loadConfig(ServerConfig.class, configPaths, new ServerConfig(), null); + ServerConfig cfg = Config.loadConfig(ServerConfig.class, configPaths, new ServerConfig(), "default-config.yaml"); Server server = new Server(cfg); new Thread(server).start(); ServerCli.start(server); diff --git a/server/src/main/java/nl/andrewl/aos2_server/logic/PlayerActionManager.java b/server/src/main/java/nl/andrewl/aos2_server/logic/PlayerActionManager.java index 8bc889f..065dc5d 100644 --- a/server/src/main/java/nl/andrewl/aos2_server/logic/PlayerActionManager.java +++ b/server/src/main/java/nl/andrewl/aos2_server/logic/PlayerActionManager.java @@ -29,8 +29,8 @@ import static nl.andrewl.aos2_server.model.ServerPlayer.RADIUS; */ public class PlayerActionManager { private final ServerPlayer player; + private final PlayerInputTracker input; - private ClientInputState lastInputState; private long lastBlockRemovedAt = 0; private long lastBlockPlacedAt = 0; @@ -45,23 +45,15 @@ public class PlayerActionManager { public PlayerActionManager(ServerPlayer player) { this.player = player; - lastInputState = new ClientInputState( - player.getId(), - false, false, false, false, - false, false, false, - false, false, false, - player.getInventory().getSelectedIndex() - ); + this.input = new PlayerInputTracker(player); } - public ClientInputState getLastInputState() { - return lastInputState; + public PlayerInputTracker getInput() { + return input; } public boolean setLastInputState(ClientInputState lastInputState) { - boolean change = !lastInputState.equals(this.lastInputState); - if (change) this.lastInputState = lastInputState; - return change; + return input.setLastInputState(lastInputState); } public boolean isUpdated() { @@ -70,8 +62,8 @@ public class PlayerActionManager { public void tick(long now, float dt, World world, Server server) { updated = false; // Reset the updated flag. This will be set to true if the player was updated in this tick. - if (player.getInventory().getSelectedIndex() != lastInputState.selectedInventoryIndex()) { - player.getInventory().setSelectedIndex(lastInputState.selectedInventoryIndex()); + if (player.getInventory().getSelectedIndex() != input.selectedInventoryIndex()) { + player.getInventory().setSelectedIndex(input.selectedInventoryIndex()); // Tell the client that their inventory slot has been updated properly. server.getPlayerManager().getHandler(player.getId()).sendDatagramPacket(new InventorySelectedStackMessage(player.getInventory().getSelectedIndex())); updated = true; // Tell everyone else that this player's selected item has changed. @@ -81,7 +73,7 @@ public class PlayerActionManager { if (selectedStack instanceof BlockItemStack b) { tickBlockAction(now, server, world, b); } else if (selectedStack instanceof GunItemStack g) { - tickGunAction(now, server, world, g); + tickGunAction(now, server, g); } if ( @@ -93,18 +85,19 @@ public class PlayerActionManager { lastResupplyAt = now; } - if (player.isCrouching() != lastInputState.crouching()) { - player.setCrouching(lastInputState.crouching()); + if (player.isCrouching() != input.crouching()) { + player.setCrouching(input.crouching()); updated = true; } tickMovement(dt, server, world, server.getConfig().physics); + input.reset(); } - private void tickGunAction(long now, Server server, World world, GunItemStack g) { + private void tickGunAction(long now, Server server, GunItemStack g) { Gun gun = (Gun) g.getType(); if (// Check to see if the player is shooting. - lastInputState.hitting() && + input.hitting() && g.getBulletCount() > 0 && !gunReloading && now - gunLastShotAt > gun.getShotCooldownTime() * 1000 && @@ -126,11 +119,14 @@ public class PlayerActionManager { } else if (gun instanceof Winchester) { shotSound = "shot_winchester_1"; } - server.getPlayerManager().broadcastUdpMessage(new SoundMessage(shotSound, 1, player.getPosition(), player.getVelocity())); + Vector3f soundLocation = new Vector3f(player.getPosition()); + soundLocation.y += 1.4f; + soundLocation.add(player.getViewVector()); + server.getPlayerManager().broadcastUdpMessage(new SoundMessage(shotSound, 1, soundLocation, player.getVelocity())); } if (// Check to see if the player is reloading. - lastInputState.reloading() && + input.reloading() && !gunReloading && g.getClipCount() > 0 ) { @@ -151,7 +147,7 @@ public class PlayerActionManager { } // Check to see if the player released the trigger, for non-automatic weapons. - if (!gun.isAutomatic() && gunNeedsReCock && !lastInputState.hitting()) { + if (!gun.isAutomatic() && gunNeedsReCock && !input.hitting()) { gunNeedsReCock = false; } } @@ -159,7 +155,7 @@ public class PlayerActionManager { private void tickBlockAction(long now, Server server, World world, BlockItemStack stack) { // Check for breaking blocks. if ( - lastInputState.hitting() && + input.hitting() && stack.getAmount() < stack.getType().getMaxAmount() && now - lastBlockRemovedAt > server.getConfig().actions.blockBreakCooldown * 1000 ) { @@ -175,7 +171,7 @@ public class PlayerActionManager { } // Check for placing blocks. if ( - lastInputState.interacting() && + input.interacting() && stack.getAmount() > 0 && now - lastBlockPlacedAt > server.getConfig().actions.blockPlaceCooldown * 1000 ) { @@ -202,8 +198,8 @@ public class PlayerActionManager { tickHorizontalVelocity(config, grounded); if (isGrounded(world)) { - if (lastInputState.jumping()) { - velocity.y = config.jumpVerticalSpeed * (lastInputState.sprinting() ? 1.25f : 1f); + if (input.jumping()) { + velocity.y = config.jumpVerticalSpeed * (input.sprinting() ? 1.25f : 1f); updated = true; } } else { @@ -239,19 +235,19 @@ public class PlayerActionManager { velocity.z == velocity.z ? velocity.z : 0f ); Vector3f acceleration = new Vector3f(0); - if (lastInputState.forward()) acceleration.z -= 1; - if (lastInputState.backward()) acceleration.z += 1; - if (lastInputState.left()) acceleration.x -= 1; - if (lastInputState.right()) acceleration.x += 1; + if (input.forward()) acceleration.z -= 1; + if (input.backward()) acceleration.z += 1; + if (input.left()) acceleration.x -= 1; + if (input.right()) acceleration.x += 1; if (acceleration.lengthSquared() > 0) { acceleration.normalize(); acceleration.rotateAxis(orientation.x, 0, 1, 0); acceleration.mul(config.movementAcceleration); horizontalVelocity.add(acceleration); final float maxSpeed; - if (lastInputState.crouching()) { + if (input.crouching()) { maxSpeed = config.crouchingSpeed; - } else if (lastInputState.sprinting()) { + } else if (input.sprinting()) { maxSpeed = config.sprintingSpeed; } else { maxSpeed = config.walkingSpeed; diff --git a/server/src/main/java/nl/andrewl/aos2_server/logic/PlayerImpulses.java b/server/src/main/java/nl/andrewl/aos2_server/logic/PlayerImpulses.java new file mode 100644 index 0000000..5247c75 --- /dev/null +++ b/server/src/main/java/nl/andrewl/aos2_server/logic/PlayerImpulses.java @@ -0,0 +1,42 @@ +package nl.andrewl.aos2_server.logic; + +import nl.andrewl.aos_core.net.client.ClientInputState; + +public class PlayerImpulses { + public boolean forward; + public boolean backward; + public boolean left; + public boolean right; + public boolean jumping; + public boolean crouching; + public boolean sprinting; + public boolean hitting; + public boolean interacting; + public boolean reloading; + + public void update(ClientInputState s) { + forward = forward || s.forward(); + backward = backward || s.backward(); + left = left || s.left(); + right = right || s.right(); + jumping = jumping || s.jumping(); + crouching = crouching || s.crouching(); + sprinting = sprinting || s.sprinting(); + hitting = hitting || s.hitting(); + interacting = interacting || s.interacting(); + reloading = reloading || s.reloading(); + } + + public void reset() { + forward = false; + backward = false; + left = false; + right = false; + jumping = false; + crouching = false; + sprinting = false; + hitting = false; + interacting = false; + reloading = false; + } +} diff --git a/server/src/main/java/nl/andrewl/aos2_server/logic/PlayerInputTracker.java b/server/src/main/java/nl/andrewl/aos2_server/logic/PlayerInputTracker.java new file mode 100644 index 0000000..e762102 --- /dev/null +++ b/server/src/main/java/nl/andrewl/aos2_server/logic/PlayerInputTracker.java @@ -0,0 +1,82 @@ +package nl.andrewl.aos2_server.logic; + +import nl.andrewl.aos2_server.model.ServerPlayer; +import nl.andrewl.aos_core.net.client.ClientInputState; + +/** + * Wrapper around the various information we have about a player's input state, + * including their last known state, and any impulses they've made since the + * last tick. + */ +public class PlayerInputTracker { + private ClientInputState lastInputState; + private final PlayerImpulses impulsesSinceLastTick; + + public PlayerInputTracker(ServerPlayer player) { + lastInputState = new ClientInputState( + player.getId(), + false, false, false, false, + false, false, false, + false, false, false, + player.getInventory().getSelectedIndex() + ); + this.impulsesSinceLastTick = new PlayerImpulses(); + } + + public boolean setLastInputState(ClientInputState lastInputState) { + boolean updated = !lastInputState.equals(this.lastInputState); + if (updated) { + this.lastInputState = lastInputState; + impulsesSinceLastTick.update(lastInputState); + } + return updated; + } + + public void reset() { + impulsesSinceLastTick.reset(); + } + + public boolean forward() { + return lastInputState.forward() || impulsesSinceLastTick.forward; + } + + public boolean backward() { + return lastInputState.backward() || impulsesSinceLastTick.backward; + } + + public boolean left() { + return lastInputState.left() || impulsesSinceLastTick.left; + } + + public boolean right() { + return lastInputState.right() || impulsesSinceLastTick.right; + } + + public boolean jumping() { + return lastInputState.jumping() || impulsesSinceLastTick.jumping; + } + + public boolean crouching() { + return lastInputState.crouching() || impulsesSinceLastTick.crouching; + } + + public boolean sprinting() { + return lastInputState.sprinting() || impulsesSinceLastTick.sprinting; + } + + public boolean hitting() { + return lastInputState.hitting() || impulsesSinceLastTick.hitting; + } + + public boolean interacting() { + return lastInputState.interacting() || impulsesSinceLastTick.interacting; + } + + public boolean reloading() { + return lastInputState.reloading() || impulsesSinceLastTick.reloading; + } + + public int selectedInventoryIndex() { + return lastInputState.selectedInventoryIndex(); + } +} diff --git a/server/src/main/java/nl/andrewl/aos2_server/model/ServerPlayer.java b/server/src/main/java/nl/andrewl/aos2_server/model/ServerPlayer.java index d6296dc..708a67d 100644 --- a/server/src/main/java/nl/andrewl/aos2_server/model/ServerPlayer.java +++ b/server/src/main/java/nl/andrewl/aos2_server/model/ServerPlayer.java @@ -6,7 +6,6 @@ import nl.andrewl.aos_core.model.item.BlockItemStack; import nl.andrewl.aos_core.model.item.GunItemStack; import nl.andrewl.aos_core.model.item.Inventory; import nl.andrewl.aos_core.model.item.ItemTypes; -import nl.andrewl.aos_core.model.item.gun.Winchester; import nl.andrewl.aos_core.net.client.PlayerUpdateMessage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -69,7 +68,7 @@ public class ServerPlayer extends Player { position.x, position.y, position.z, velocity.x, velocity.y, velocity.z, orientation.x, orientation.y, - actionManager.getLastInputState().crouching(), + actionManager.getInput().crouching(), inventory.getSelectedItemStack().getType().getId() ); } diff --git a/server/src/main/resources/default-config.yaml b/server/src/main/resources/default-config.yaml new file mode 100644 index 0000000..e53a88c --- /dev/null +++ b/server/src/main/resources/default-config.yaml @@ -0,0 +1,32 @@ +# Ace of Shades 2 Server Configuration +port: 25565 +connectionBacklog: 5 +ticksPerSecond: 20.0 +world: worlds.redfort +teams: + - name: Red + color: [0.8, 0, 0] + spawnPoint: A + - name: Blue + color: [0, 0, 0.8] + spawnPoint: B +physics: + gravity: 29.43 + walkingSpeed: 4 + crouchingSpeed: 1.5 + sprintingSpeed: 9 + movementAcceleration: 2 + movementDeceleration: 1 + jumpVerticalSpeed: 8 +actions: + blockBreakCooldown: 0.25 + blockPlaceCooldown: 0.1 + blockBreakReach: 5 + blockPlaceReach: 5 + blockBulletDamageResistance: 3 + blockBulletDamageCooldown: 10 + resupplyCooldown: 30 + resupplyRadius: 3 + teamSpawnProtection: 10 + movementAccuracyDecreaseFactor: 0.01 + friendlyFire: false \ No newline at end of file