From d83ff8a8162fde71a1230ac5db6236f076d9c552 Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Mon, 18 Jul 2022 01:10:29 +0200 Subject: [PATCH] Updated server-side logic and added better inventory models. --- .../nl/andrewl/aos_core/model/Player.java | 4 +- .../aos_core/model/item/BlockItem.java | 11 + .../aos_core/model/item/BlockItemStack.java | 13 + .../nl/andrewl/aos_core/model/item/Gun.java | 72 ++++ .../aos_core/model/item/GunItemStack.java | 12 + .../aos_core/model/item/Inventory.java | 4 + .../model/item/{ItemType.java => Item.java} | 21 +- .../aos_core/model/item/ItemStack.java | 6 +- .../aos_core/model/item/ItemTypes.java | 24 +- .../aos_core/model/item/gun/Rifle.java | 20 ++ .../aos_core/net/udp/PlayerUpdateMessage.java | 3 +- .../nl/andrewl/aos2_server/PlayerManager.java | 9 +- .../java/nl/andrewl/aos2_server/Server.java | 20 +- .../nl/andrewl/aos2_server/ServerPlayer.java | 321 ++--------------- .../logic/PlayerActionManager.java | 329 ++++++++++++++++++ .../aos2_server/{ => logic}/WorldUpdater.java | 16 +- 16 files changed, 536 insertions(+), 349 deletions(-) create mode 100644 core/src/main/java/nl/andrewl/aos_core/model/item/BlockItem.java create mode 100644 core/src/main/java/nl/andrewl/aos_core/model/item/BlockItemStack.java create mode 100644 core/src/main/java/nl/andrewl/aos_core/model/item/Gun.java create mode 100644 core/src/main/java/nl/andrewl/aos_core/model/item/GunItemStack.java rename core/src/main/java/nl/andrewl/aos_core/model/item/{ItemType.java => Item.java} (50%) create mode 100644 core/src/main/java/nl/andrewl/aos_core/model/item/gun/Rifle.java create mode 100644 server/src/main/java/nl/andrewl/aos2_server/logic/PlayerActionManager.java rename server/src/main/java/nl/andrewl/aos2_server/{ => logic}/WorldUpdater.java (74%) diff --git a/core/src/main/java/nl/andrewl/aos_core/model/Player.java b/core/src/main/java/nl/andrewl/aos_core/model/Player.java index 2b49b64..506eb85 100644 --- a/core/src/main/java/nl/andrewl/aos_core/model/Player.java +++ b/core/src/main/java/nl/andrewl/aos_core/model/Player.java @@ -102,9 +102,9 @@ public class Player { public Vector3f getRightVector() { float x = orientation.x - (float) (Math.PI / 2); return new Vector3f( - sin(orientation.x), + sin(x), 0, - cos(orientation.x) + cos(x) ).normalize(); } } diff --git a/core/src/main/java/nl/andrewl/aos_core/model/item/BlockItem.java b/core/src/main/java/nl/andrewl/aos_core/model/item/BlockItem.java new file mode 100644 index 0000000..89c7c37 --- /dev/null +++ b/core/src/main/java/nl/andrewl/aos_core/model/item/BlockItem.java @@ -0,0 +1,11 @@ +package nl.andrewl.aos_core.model.item; + +/** + * A block item that contains information about what type of block value is + * currently selected. + */ +public class BlockItem extends Item { + public BlockItem(int id) { + super(id, "Block", 100); + } +} diff --git a/core/src/main/java/nl/andrewl/aos_core/model/item/BlockItemStack.java b/core/src/main/java/nl/andrewl/aos_core/model/item/BlockItemStack.java new file mode 100644 index 0000000..ab417e6 --- /dev/null +++ b/core/src/main/java/nl/andrewl/aos_core/model/item/BlockItemStack.java @@ -0,0 +1,13 @@ +package nl.andrewl.aos_core.model.item; + +public class BlockItemStack extends ItemStack { + private int selectedValue = 1; + + public BlockItemStack(BlockItem item, int amount) { + super(item, amount); + } + + public int getSelectedValue() { + return selectedValue; + } +} diff --git a/core/src/main/java/nl/andrewl/aos_core/model/item/Gun.java b/core/src/main/java/nl/andrewl/aos_core/model/item/Gun.java new file mode 100644 index 0000000..00cd1aa --- /dev/null +++ b/core/src/main/java/nl/andrewl/aos_core/model/item/Gun.java @@ -0,0 +1,72 @@ +package nl.andrewl.aos_core.model.item; + +import nl.andrewl.aos_core.MathUtils; + +/** + * The base class for all types of guns. + */ +public class Gun extends Item { + private final int maxClipCount; + private final int maxBulletCount; + private final int bulletsPerRound; + private final float accuracy; + private final float shotCooldownTime; + private final float reloadTime; + private final float baseDamage; + private final float recoil; + + public Gun( + int id, + String name, + int maxClipCount, + int maxBulletCount, + int bulletsPerRound, + float accuracy, + float shotCooldownTime, + float reloadTime, + float baseDamage, + float recoil + ) { + super(id, name, 1); + this.maxClipCount = maxClipCount; + this.maxBulletCount = maxBulletCount; + this.bulletsPerRound = bulletsPerRound; + this.accuracy = MathUtils.clamp(accuracy, 0, 1); + this.shotCooldownTime = shotCooldownTime; + this.reloadTime = reloadTime; + this.baseDamage = baseDamage; + this.recoil = recoil; + } + + public int getMaxClipCount() { + return maxClipCount; + } + + public int getMaxBulletCount() { + return maxBulletCount; + } + + public int getBulletsPerRound() { + return bulletsPerRound; + } + + public float getAccuracy() { + return accuracy; + } + + public float getShotCooldownTime() { + return shotCooldownTime; + } + + public float getReloadTime() { + return reloadTime; + } + + public float getBaseDamage() { + return baseDamage; + } + + public float getRecoil() { + return recoil; + } +} diff --git a/core/src/main/java/nl/andrewl/aos_core/model/item/GunItemStack.java b/core/src/main/java/nl/andrewl/aos_core/model/item/GunItemStack.java new file mode 100644 index 0000000..4558625 --- /dev/null +++ b/core/src/main/java/nl/andrewl/aos_core/model/item/GunItemStack.java @@ -0,0 +1,12 @@ +package nl.andrewl.aos_core.model.item; + +public class GunItemStack extends ItemStack { + private int bulletCount; + private int clipCount; + + public GunItemStack(Gun gun) { + super(gun, 1); + bulletCount = gun.getMaxBulletCount(); + clipCount = gun.getMaxClipCount(); + } +} diff --git a/core/src/main/java/nl/andrewl/aos_core/model/item/Inventory.java b/core/src/main/java/nl/andrewl/aos_core/model/item/Inventory.java index bd7207b..52c3692 100644 --- a/core/src/main/java/nl/andrewl/aos_core/model/item/Inventory.java +++ b/core/src/main/java/nl/andrewl/aos_core/model/item/Inventory.java @@ -21,6 +21,10 @@ public class Inventory { this.selectedIndex = selectedIndex; } + public List getItemStacks() { + return itemStacks; + } + public ItemStack getSelectedItemStack() { return itemStacks.get(selectedIndex); } diff --git a/core/src/main/java/nl/andrewl/aos_core/model/item/ItemType.java b/core/src/main/java/nl/andrewl/aos_core/model/item/Item.java similarity index 50% rename from core/src/main/java/nl/andrewl/aos_core/model/item/ItemType.java rename to core/src/main/java/nl/andrewl/aos_core/model/item/Item.java index 613dc28..9b5ef73 100644 --- a/core/src/main/java/nl/andrewl/aos_core/model/item/ItemType.java +++ b/core/src/main/java/nl/andrewl/aos_core/model/item/Item.java @@ -3,12 +3,23 @@ package nl.andrewl.aos_core.model.item; /** * Represents a type of item that a player can have. */ -public class ItemType { - private final int id; - private final String name; - private final int maxAmount; +public class Item { + /** + * The item's unique id. + */ + protected final int id; - public ItemType(int id, String name, int maxAmount) { + /** + * The item's unique name. + */ + protected final String name; + + /** + * The maximum amount of this item that can be in a stack at once. + */ + protected final int maxAmount; + + public Item(int id, String name, int maxAmount) { this.id = id; this.name = name; this.maxAmount = maxAmount; diff --git a/core/src/main/java/nl/andrewl/aos_core/model/item/ItemStack.java b/core/src/main/java/nl/andrewl/aos_core/model/item/ItemStack.java index 61f0f03..a920fec 100644 --- a/core/src/main/java/nl/andrewl/aos_core/model/item/ItemStack.java +++ b/core/src/main/java/nl/andrewl/aos_core/model/item/ItemStack.java @@ -5,15 +5,15 @@ package nl.andrewl.aos_core.model.item; * a type of item, and the amount of it. */ public class ItemStack { - private final ItemType type; + private final Item type; private int amount; - public ItemStack(ItemType type, int amount) { + public ItemStack(Item type, int amount) { this.type = type; this.amount = amount; } - public Object getType() { + public Item getType() { return type; } diff --git a/core/src/main/java/nl/andrewl/aos_core/model/item/ItemTypes.java b/core/src/main/java/nl/andrewl/aos_core/model/item/ItemTypes.java index 71960b3..23da11c 100644 --- a/core/src/main/java/nl/andrewl/aos_core/model/item/ItemTypes.java +++ b/core/src/main/java/nl/andrewl/aos_core/model/item/ItemTypes.java @@ -1,5 +1,7 @@ package nl.andrewl.aos_core.model.item; +import nl.andrewl.aos_core.model.item.gun.Rifle; + import java.util.HashMap; import java.util.Map; @@ -7,14 +9,26 @@ import java.util.Map; * Global constant set of registered item types. */ public final class ItemTypes { - public static final Map TYPES_MAP = new HashMap<>(); + private static final Map TYPES_BY_ID = new HashMap<>(); + private static final Map TYPES_BY_NAME = new HashMap<>(); static { - registerType(new ItemType(1, "Rifle", 1)); - registerType(new ItemType(2, "Block", 100)); + registerType(new BlockItem(1)); + registerType(new Rifle(2)); } - public static void registerType(ItemType type) { - TYPES_MAP.put(type.getId(), type); + public static void registerType(Item type) { + TYPES_BY_ID.put(type.getId(), type); + TYPES_BY_NAME.put(type.getName(), type); + } + + @SuppressWarnings("unchecked") + public static T get(int id) { + return (T) TYPES_BY_ID.get(id); + } + + @SuppressWarnings("unchecked") + public static T get(String name) { + return (T) TYPES_BY_NAME.get(name); } } diff --git a/core/src/main/java/nl/andrewl/aos_core/model/item/gun/Rifle.java b/core/src/main/java/nl/andrewl/aos_core/model/item/gun/Rifle.java new file mode 100644 index 0000000..5cc10a6 --- /dev/null +++ b/core/src/main/java/nl/andrewl/aos_core/model/item/gun/Rifle.java @@ -0,0 +1,20 @@ +package nl.andrewl.aos_core.model.item.gun; + +import nl.andrewl.aos_core.model.item.Gun; + +public class Rifle extends Gun { + public Rifle(int id) { + super( + id, + "Rifle", + 5, + 8, + 1, + 0.97f, + 0.8f, + 2.5f, + 80f, + 50f + ); + } +} diff --git a/core/src/main/java/nl/andrewl/aos_core/net/udp/PlayerUpdateMessage.java b/core/src/main/java/nl/andrewl/aos_core/net/udp/PlayerUpdateMessage.java index 803540b..034caf8 100644 --- a/core/src/main/java/nl/andrewl/aos_core/net/udp/PlayerUpdateMessage.java +++ b/core/src/main/java/nl/andrewl/aos_core/net/udp/PlayerUpdateMessage.java @@ -12,7 +12,8 @@ public record PlayerUpdateMessage( float px, float py, float pz, float vx, float vy, float vz, float ox, float oy, - boolean crouching + boolean crouching, + int selectedItemId ) implements Message { public void apply(Player p) { diff --git a/server/src/main/java/nl/andrewl/aos2_server/PlayerManager.java b/server/src/main/java/nl/andrewl/aos2_server/PlayerManager.java index 842e81f..a8b5d57 100644 --- a/server/src/main/java/nl/andrewl/aos2_server/PlayerManager.java +++ b/server/src/main/java/nl/andrewl/aos2_server/PlayerManager.java @@ -4,7 +4,6 @@ import nl.andrewl.aos_core.Net; import nl.andrewl.aos_core.net.PlayerJoinMessage; import nl.andrewl.aos_core.net.PlayerLeaveMessage; import nl.andrewl.aos_core.net.udp.DatagramInit; -import nl.andrewl.aos_core.net.udp.PlayerUpdateMessage; import nl.andrewl.record_net.Message; import org.joml.Vector3f; import org.slf4j.Logger; @@ -32,13 +31,7 @@ public class PlayerManager { log.info("Registered player \"{}\" with id {}", player.getUsername(), player.getId()); player.setPosition(new Vector3f(0, 64, 0)); broadcastTcpMessageToAllBut(new PlayerJoinMessage(player), player); - broadcastUdpMessage(new PlayerUpdateMessage( - player.getId(), - player.getPosition().x, player.getPosition().y, player.getPosition().z, - player.getVelocity().x, player.getVelocity().y, player.getVelocity().z, - player.getOrientation().x, player.getOrientation().y, - player.getLastInputState().crouching() - )); + broadcastUdpMessage(player.getUpdateMessage()); return player; } 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 72223d5..293887e 100644 --- a/server/src/main/java/nl/andrewl/aos2_server/Server.java +++ b/server/src/main/java/nl/andrewl/aos2_server/Server.java @@ -1,6 +1,7 @@ package nl.andrewl.aos2_server; import nl.andrewl.aos2_server.config.ServerConfig; +import nl.andrewl.aos2_server.logic.WorldUpdater; import nl.andrewl.aos_core.config.Config; import nl.andrewl.aos_core.model.world.World; import nl.andrewl.aos_core.model.world.Worlds; @@ -8,7 +9,6 @@ import nl.andrewl.aos_core.net.UdpReceiver; import nl.andrewl.aos_core.net.udp.ClientInputState; import nl.andrewl.aos_core.net.udp.ClientOrientationState; import nl.andrewl.aos_core.net.udp.DatagramInit; -import nl.andrewl.aos_core.net.udp.PlayerUpdateMessage; import nl.andrewl.record_net.Message; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -66,26 +66,14 @@ public class Server implements Runnable { } else if (msg instanceof ClientInputState inputState) { ServerPlayer player = playerManager.getPlayer(inputState.clientId()); if (player != null) { - player.setLastInputState(inputState); - playerManager.broadcastUdpMessage(new PlayerUpdateMessage( - player.getId(), - player.getPosition().x, player.getPosition().y, player.getPosition().z, - player.getVelocity().x, player.getVelocity().y, player.getVelocity().z, - player.getOrientation().x, player.getOrientation().y, - player.getLastInputState().crouching() - )); + player.getActionManager().setLastInputState(inputState); + playerManager.broadcastUdpMessage(player.getUpdateMessage()); } } else if (msg instanceof ClientOrientationState orientationState) { ServerPlayer player = playerManager.getPlayer(orientationState.clientId()); if (player != null) { player.setOrientation(orientationState.x(), orientationState.y()); - playerManager.broadcastUdpMessageToAllBut(new PlayerUpdateMessage( - player.getId(), - player.getPosition().x, player.getPosition().y, player.getPosition().z, - player.getVelocity().x, player.getVelocity().y, player.getVelocity().z, - player.getOrientation().x, player.getOrientation().y, - player.getLastInputState().crouching() - ), player); + playerManager.broadcastUdpMessageToAllBut(player.getUpdateMessage(), player); } } } diff --git a/server/src/main/java/nl/andrewl/aos2_server/ServerPlayer.java b/server/src/main/java/nl/andrewl/aos2_server/ServerPlayer.java index f35e5c1..8b6d2ba 100644 --- a/server/src/main/java/nl/andrewl/aos2_server/ServerPlayer.java +++ b/server/src/main/java/nl/andrewl/aos2_server/ServerPlayer.java @@ -1,14 +1,10 @@ package nl.andrewl.aos2_server; -import nl.andrewl.aos2_server.config.ServerConfig; +import nl.andrewl.aos2_server.logic.PlayerActionManager; import nl.andrewl.aos_core.model.Player; -import nl.andrewl.aos_core.model.world.World; -import nl.andrewl.aos_core.net.udp.ChunkUpdateMessage; -import nl.andrewl.aos_core.net.udp.ClientInputState; -import org.joml.Math; -import org.joml.Vector2i; -import org.joml.Vector3f; -import org.joml.Vector3i; +import nl.andrewl.aos_core.model.item.*; +import nl.andrewl.aos_core.model.item.gun.Rifle; +import nl.andrewl.aos_core.net.udp.PlayerUpdateMessage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,303 +21,34 @@ public class ServerPlayer extends Player { public static final float WIDTH = 0.75f; public static final float RADIUS = WIDTH / 2f; - private ClientInputState lastInputState; - private long lastBlockRemovedAt = 0; - private long lastBlockPlacedAt = 0; - - private boolean updated = false; + private final PlayerActionManager actionManager; + private final Inventory inventory; public ServerPlayer(int id, String username) { super(id, username); - // Initialize with a default state of no input. - lastInputState = new ClientInputState(id, false, false, false, false, false, false, false, false, false); + this.actionManager = new PlayerActionManager(this); + this.inventory = new Inventory(new ArrayList<>(), 0); + inventory.getItemStacks().add(new GunItemStack(ItemTypes.get("Rifle"))); + inventory.getItemStacks().add(new BlockItemStack(ItemTypes.get("Block"), 50)); } - public ClientInputState getLastInputState() { - return lastInputState; - } - - public void setLastInputState(ClientInputState inputState) { - this.lastInputState = inputState; - } - - public boolean isUpdated() { - return updated; - } - - public void tick(float dt, World world, Server server) { - long now = System.currentTimeMillis(); - // Check for breaking blocks. - if (lastInputState.hitting() && now - lastBlockRemovedAt > server.getConfig().actions.blockRemoveCooldown * 1000) { - Vector3f eyePos = new Vector3f(position); - eyePos.y += getEyeHeight(); - var hit = world.getLookingAtPos(eyePos, viewVector, 10); - if (hit != null) { - world.setBlockAt(hit.pos().x, hit.pos().y, hit.pos().z, (byte) 0); - lastBlockRemovedAt = now; - server.getPlayerManager().broadcastUdpMessage(ChunkUpdateMessage.fromWorld(hit.pos(), world)); - } - } - // Check for placing blocks. - if (lastInputState.interacting() && now - lastBlockPlacedAt > server.getConfig().actions.blockPlaceCooldown * 1000) { - Vector3f eyePos = new Vector3f(position); - eyePos.y += getEyeHeight(); - var hit = world.getLookingAtPos(eyePos, viewVector, 10); - if (hit != null) { - Vector3i placePos = new Vector3i(hit.pos()); - placePos.add(hit.norm()); - world.setBlockAt(placePos.x, placePos.y, placePos.z, (byte) 1); - lastBlockPlacedAt = now; - server.getPlayerManager().broadcastUdpMessage(ChunkUpdateMessage.fromWorld(placePos, world)); - } - } - tickMovement(dt, world, server.getConfig().physics); - } - - private void tickMovement(float dt, World world, ServerConfig.PhysicsConfig config) { - updated = false; // Reset the updated flag. This will be set to true if the player was updated in this tick. - boolean grounded = isGrounded(world); - tickHorizontalVelocity(config, grounded); - - if (isGrounded(world)) { - if (lastInputState.jumping()) { - velocity.y = config.jumpVerticalSpeed * (lastInputState.sprinting() ? 1.25f : 1f); - updated = true; - } - } else { - velocity.y -= config.gravity * dt; - updated = true; - } - - // Apply updated velocity to the player. - if (velocity.lengthSquared() > 0) { - Vector3f movement = new Vector3f(velocity).mul(dt); - // Check for collisions if we try to move according to what the player wants. - checkBlockCollisions(movement, world); - position.add(movement); - updated = true; - } - } - - private void tickHorizontalVelocity(ServerConfig.PhysicsConfig config, boolean doDeceleration) { - Vector3f horizontalVelocity = new Vector3f( - velocity.x == velocity.x ? velocity.x : 0f, - 0, - 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 (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()) { - maxSpeed = config.crouchingSpeed; - } else if (lastInputState.sprinting()) { - maxSpeed = config.sprintingSpeed; - } else { - maxSpeed = config.walkingSpeed; - } - if (horizontalVelocity.length() > maxSpeed) { - horizontalVelocity.normalize(maxSpeed); - } - updated = true; - } else if (doDeceleration && horizontalVelocity.lengthSquared() > 0) { - Vector3f deceleration = new Vector3f(horizontalVelocity) - .negate().normalize() - .mul(Math.min(horizontalVelocity.length(), config.movementDeceleration)); - horizontalVelocity.add(deceleration); - if (horizontalVelocity.length() < 0.1f) { - horizontalVelocity.set(0); - } - updated = true; - } - - // Update the player's velocity with what we've computed. - velocity.x = horizontalVelocity.x; - velocity.z = horizontalVelocity.z; - } - - private boolean isGrounded(World world) { - // Player must be flat on the top of a block. - if (Math.floor(position.y) != position.y) return false; - // Check to see if there's a block under any of the spaces the player is over. - return getHorizontalSpaceOccupied(position).stream() - .anyMatch(point -> world.getBlockAt(point.x, position.y - 0.1f, point.y) != 0); + public PlayerActionManager getActionManager() { + return actionManager; } /** - * Gets the list of all spaces occupied by a player's position, in the - * horizontal XZ plane. This can be between 1 and 4 spaces, depending on - * if the player's position is overlapping with a few blocks. - * @param pos The position. - * @return The list of 2d positions occupied. + * Helper method to build an update message for this player, to be sent to + * various clients. + * @return The update message. */ - private List getHorizontalSpaceOccupied(Vector3f pos) { - // Get the list of 2d x,z coordinates that we overlap with. - List points = new ArrayList<>(4); // Due to the size of radius, there can only be a max of 4 blocks. - int minX = (int) Math.floor(pos.x - RADIUS); - int minZ = (int) Math.floor(pos.z - RADIUS); - int maxX = (int) Math.floor(pos.x + RADIUS); - int maxZ = (int) Math.floor(pos.z + RADIUS); - for (int x = minX; x <= maxX; x++) { - for (int z = minZ; z <= maxZ; z++) { - points.add(new Vector2i(x, z)); - } - } - return points; + public PlayerUpdateMessage getUpdateMessage() { + return new PlayerUpdateMessage( + id, + position.x, position.y, position.z, + velocity.x, velocity.y, velocity.z, + orientation.x, orientation.y, + actionManager.getLastInputState().crouching(), + inventory.getSelectedItemStack().getType().getId() + ); } - - private void checkBlockCollisions(Vector3f movement, World world) { - final Vector3f nextTickPosition = new Vector3f(position).add(movement); -// System.out.printf("Pos:\t\t%.3f, %.3f, %.3f%nmov:\t\t%.3f, %.3f, %.3f%nNexttick:\t%.3f, %.3f, %.3f%n", -// position.x, position.y, position.z, -// movement.x, movement.y, movement.z, -// nextTickPosition.x, nextTickPosition.y, nextTickPosition.z -// ); - float height = getCurrentHeight(); - float delta = 0.00001f; - final Vector3f stepSize = new Vector3f(movement).normalize(1.0f); - // The number of steps we'll make towards the next tick position. - int stepCount = (int) Math.ceil(movement.length()); - if (stepCount == 0) return; // No movement, so exit. - final Vector3f nextPos = new Vector3f(position); - final Vector3f lastPos = new Vector3f(position); - for (int i = 0; i < stepCount; i++) { - lastPos.set(nextPos); - nextPos.add(stepSize); - // If we shot past the next tick position, clamp it to that. - if (new Vector3f(nextPos).sub(position).length() > movement.length()) { - nextPos.set(nextTickPosition); - } - - // Check if we collide with anything at this new position. - - - float playerBodyPrevMinZ = lastPos.z - RADIUS; - float playerBodyPrevMaxZ = lastPos.z + RADIUS; - float playerBodyPrevMinX = lastPos.x - RADIUS; - float playerBodyPrevMaxX = lastPos.x + RADIUS; - float playerBodyPrevMinY = lastPos.y; - float playerBodyPrevMaxY = lastPos.y + height; - - float playerBodyMinZ = nextPos.z - RADIUS; - float playerBodyMaxZ = nextPos.z + RADIUS; - float playerBodyMinX = nextPos.x - RADIUS; - float playerBodyMaxX = nextPos.x + RADIUS; - float playerBodyMinY = nextPos.y; - float playerBodyMaxY = nextPos.y + height; - - // Compute the bounds of all blocks the player is intersecting with. - int minX = (int) Math.floor(playerBodyMinX); - int minZ = (int) Math.floor(playerBodyMinZ); - int minY = (int) Math.floor(playerBodyMinY); - int maxX = (int) Math.floor(playerBodyMaxX); - int maxZ = (int) Math.floor(playerBodyMaxZ); - int maxY = (int) Math.floor(playerBodyMaxY); - - for (int x = minX; x <= maxX; x++) { - for (int z = minZ; z <= maxZ; z++) { - for (int y = minY; y <= maxY; y++) { - byte block = world.getBlockAt(x, y, z); - if (block <= 0) continue; // We're not colliding with this block. - float blockMinY = (float) y; - float blockMaxY = (float) y + 1; - float blockMinX = (float) x; - float blockMaxX = (float) x + 1; - float blockMinZ = (float) z; - float blockMaxZ = (float) z + 1; - - /* - To determine if the player is moving into the -Z side of a block: - - The player's max z position went from < blockMinZ to >= blockMinZ. - - The block to the -Z direction is air. - */ - boolean collidingWithNegativeZ = playerBodyPrevMaxZ < blockMinZ && playerBodyMaxZ >= blockMinZ && world.getBlockAt(x, y, z - 1) <= 0; - if (collidingWithNegativeZ) { - position.z = blockMinZ - RADIUS - delta; - velocity.z = 0; - movement.z = 0; - } - - /* - To determine if the player is moving into the +Z side of a block: - - The player's min z position went from >= blockMaxZ to < blockMaxZ. - - The block to the +Z direction is air. - */ - boolean collidingWithPositiveZ = playerBodyPrevMinZ >= blockMaxZ && playerBodyMinZ < blockMaxZ && world.getBlockAt(x, y, z + 1) <= 0; - if (collidingWithPositiveZ) { - position.z = blockMaxZ + RADIUS + delta; - velocity.z = 0; - movement.z = 0; - } - - /* - To determine if the player is moving into the -X side of a block: - - The player's max x position went from < blockMinX to >= blockMinX - - The block to the -X direction is air. - */ - boolean collidingWithNegativeX = playerBodyPrevMaxX < blockMinX && playerBodyMaxX >= blockMinX && world.getBlockAt(x - 1, y, z) <= 0; - if (collidingWithNegativeX) { - position.x = blockMinX - RADIUS - delta; - velocity.x = 0; - movement.x = 0; - } - - /* - To determine if the player is moving into the +X side of a block: - - The player's min x position went from >= blockMaxX to < blockMaxX. - - The block to the +X direction is air. - */ - boolean collidingWithPositiveX = playerBodyPrevMinX >= blockMaxX && playerBodyMinX < blockMaxX && world.getBlockAt(x + 1, y, z) <= 0; - if (collidingWithPositiveX) { - position.x = blockMaxX + RADIUS + delta; - velocity.x = 0; - movement.x = 0; - } - - /* - To determine if the player is moving down onto a block: - - The player's min y position went from >= blockMaxY to < blockMaxY - - The block above the current one is air. - */ - boolean collidingWithFloor = playerBodyPrevMinY >= blockMaxY && playerBodyMinY < blockMaxY && world.getBlockAt(x, y + 1, z) <= 0; - if (collidingWithFloor) { - position.y = blockMaxY; - velocity.y = 0; - movement.y = 0; - } - - /* - To determine if the player is moving up into a block: - - The player's y position went from below blockMinY to >= blockMinY - - The block below the current one is air. - */ - boolean collidingWithCeiling = playerBodyPrevMaxY < blockMinY && playerBodyMaxY >= blockMinY && world.getBlockAt(x, y - 1, z) <= 0; - if (collidingWithCeiling) { - position.y = blockMinY - height - delta; - velocity.y = 0; - movement.y = 0; - } - - updated = true; - } - } - } - } - } - - public float getCurrentHeight() { - return lastInputState.crouching() ? HEIGHT_CROUCH : HEIGHT; - } - - public float getEyeHeight() { - return lastInputState.crouching() ? EYE_HEIGHT_CROUCH : EYE_HEIGHT; - } - } 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 new file mode 100644 index 0000000..dc5a4b2 --- /dev/null +++ b/server/src/main/java/nl/andrewl/aos2_server/logic/PlayerActionManager.java @@ -0,0 +1,329 @@ +package nl.andrewl.aos2_server.logic; + +import nl.andrewl.aos2_server.Server; +import nl.andrewl.aos2_server.ServerPlayer; +import nl.andrewl.aos2_server.config.ServerConfig; +import nl.andrewl.aos_core.model.world.World; +import nl.andrewl.aos_core.net.udp.ChunkUpdateMessage; +import nl.andrewl.aos_core.net.udp.ClientInputState; +import org.joml.Math; +import org.joml.Vector2i; +import org.joml.Vector3f; +import org.joml.Vector3i; + +import java.util.ArrayList; +import java.util.List; + +import static nl.andrewl.aos2_server.ServerPlayer.*; + +/** + * Component that manages a server player's current actions and movement. + */ +public class PlayerActionManager { + private final ServerPlayer player; + + private ClientInputState lastInputState; + private long lastBlockRemovedAt = 0; + private long lastBlockPlacedAt = 0; + + private boolean updated = false; + + public PlayerActionManager(ServerPlayer player) { + this.player = player; + lastInputState = new ClientInputState(player.getId(), false, false, false, false, false, false, false, false, false); + } + + public ClientInputState getLastInputState() { + return lastInputState; + } + + public void setLastInputState(ClientInputState lastInputState) { + this.lastInputState = lastInputState; + } + + public boolean isUpdated() { + return updated; + } + + public void tick(float dt, World world, Server server) { + long now = System.currentTimeMillis(); + // Check for breaking blocks. + if (lastInputState.hitting() && now - lastBlockRemovedAt > server.getConfig().actions.blockRemoveCooldown * 1000) { + Vector3f eyePos = new Vector3f(player.getPosition()); + eyePos.y += getEyeHeight(); + var hit = world.getLookingAtPos(eyePos, player.getViewVector(), 10); + if (hit != null) { + world.setBlockAt(hit.pos().x, hit.pos().y, hit.pos().z, (byte) 0); + lastBlockRemovedAt = now; + server.getPlayerManager().broadcastUdpMessage(ChunkUpdateMessage.fromWorld(hit.pos(), world)); + } + } + // Check for placing blocks. + if (lastInputState.interacting() && now - lastBlockPlacedAt > server.getConfig().actions.blockPlaceCooldown * 1000) { + Vector3f eyePos = new Vector3f(player.getPosition()); + eyePos.y += getEyeHeight(); + var hit = world.getLookingAtPos(eyePos, player.getViewVector(), 10); + if (hit != null) { + Vector3i placePos = new Vector3i(hit.pos()); + placePos.add(hit.norm()); + world.setBlockAt(placePos.x, placePos.y, placePos.z, (byte) 1); + lastBlockPlacedAt = now; + server.getPlayerManager().broadcastUdpMessage(ChunkUpdateMessage.fromWorld(placePos, world)); + } + } + tickMovement(dt, world, server.getConfig().physics); + } + + private void tickMovement(float dt, World world, ServerConfig.PhysicsConfig config) { + updated = false; // Reset the updated flag. This will be set to true if the player was updated in this tick. + var velocity = player.getVelocity(); + var position = player.getPosition(); + boolean grounded = isGrounded(world); + tickHorizontalVelocity(config, grounded); + + if (isGrounded(world)) { + if (lastInputState.jumping()) { + velocity.y = config.jumpVerticalSpeed * (lastInputState.sprinting() ? 1.25f : 1f); + updated = true; + } + } else { + velocity.y -= config.gravity * dt; + updated = true; + } + + // Apply updated velocity to the player. + if (velocity.lengthSquared() > 0) { + Vector3f movement = new Vector3f(velocity).mul(dt); + // Check for collisions if we try to move according to what the player wants. + checkBlockCollisions(movement, world); + position.add(movement); + updated = true; + } + } + + private void tickHorizontalVelocity(ServerConfig.PhysicsConfig config, boolean doDeceleration) { + var velocity = player.getVelocity(); + var orientation = player.getOrientation(); + Vector3f horizontalVelocity = new Vector3f( + velocity.x == velocity.x ? velocity.x : 0f, + 0, + 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 (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()) { + maxSpeed = config.crouchingSpeed; + } else if (lastInputState.sprinting()) { + maxSpeed = config.sprintingSpeed; + } else { + maxSpeed = config.walkingSpeed; + } + if (horizontalVelocity.length() > maxSpeed) { + horizontalVelocity.normalize(maxSpeed); + } + updated = true; + } else if (doDeceleration && horizontalVelocity.lengthSquared() > 0) { + Vector3f deceleration = new Vector3f(horizontalVelocity) + .negate().normalize() + .mul(Math.min(horizontalVelocity.length(), config.movementDeceleration)); + horizontalVelocity.add(deceleration); + if (horizontalVelocity.length() < 0.1f) { + horizontalVelocity.set(0); + } + updated = true; + } + + // Update the player's velocity with what we've computed. + velocity.x = horizontalVelocity.x; + velocity.z = horizontalVelocity.z; + } + + private boolean isGrounded(World world) { + var position = player.getPosition(); + // Player must be flat on the top of a block. + if (Math.floor(position.y) != position.y) return false; + // Check to see if there's a block under any of the spaces the player is over. + return getHorizontalSpaceOccupied(position).stream() + .anyMatch(point -> world.getBlockAt(point.x, position.y - 0.1f, point.y) != 0); + } + + /** + * Gets the list of all spaces occupied by a player's position, in the + * horizontal XZ plane. This can be between 1 and 4 spaces, depending on + * if the player's position is overlapping with a few blocks. + * @param pos The position. + * @return The list of 2d positions occupied. + */ + private List getHorizontalSpaceOccupied(Vector3f pos) { + // Get the list of 2d x,z coordinates that we overlap with. + List points = new ArrayList<>(4); // Due to the size of radius, there can only be a max of 4 blocks. + int minX = (int) Math.floor(pos.x - RADIUS); + int minZ = (int) Math.floor(pos.z - RADIUS); + int maxX = (int) Math.floor(pos.x + RADIUS); + int maxZ = (int) Math.floor(pos.z + RADIUS); + for (int x = minX; x <= maxX; x++) { + for (int z = minZ; z <= maxZ; z++) { + points.add(new Vector2i(x, z)); + } + } + return points; + } + + private void checkBlockCollisions(Vector3f movement, World world) { + var position = player.getPosition(); + var velocity = player.getVelocity(); + final Vector3f nextTickPosition = new Vector3f(position).add(movement); +// System.out.printf("Pos:\t\t%.3f, %.3f, %.3f%nmov:\t\t%.3f, %.3f, %.3f%nNexttick:\t%.3f, %.3f, %.3f%n", +// position.x, position.y, position.z, +// movement.x, movement.y, movement.z, +// nextTickPosition.x, nextTickPosition.y, nextTickPosition.z +// ); + float height = getCurrentHeight(); + float delta = 0.00001f; + final Vector3f stepSize = new Vector3f(movement).normalize(1.0f); + // The number of steps we'll make towards the next tick position. + int stepCount = (int) Math.ceil(movement.length()); + if (stepCount == 0) return; // No movement, so exit. + final Vector3f nextPos = new Vector3f(position); + final Vector3f lastPos = new Vector3f(position); + for (int i = 0; i < stepCount; i++) { + lastPos.set(nextPos); + nextPos.add(stepSize); + // If we shot past the next tick position, clamp it to that. + if (new Vector3f(nextPos).sub(position).length() > movement.length()) { + nextPos.set(nextTickPosition); + } + + // Check if we collide with anything at this new position. + + + float playerBodyPrevMinZ = lastPos.z - RADIUS; + float playerBodyPrevMaxZ = lastPos.z + RADIUS; + float playerBodyPrevMinX = lastPos.x - RADIUS; + float playerBodyPrevMaxX = lastPos.x + RADIUS; + float playerBodyPrevMinY = lastPos.y; + float playerBodyPrevMaxY = lastPos.y + height; + + float playerBodyMinZ = nextPos.z - RADIUS; + float playerBodyMaxZ = nextPos.z + RADIUS; + float playerBodyMinX = nextPos.x - RADIUS; + float playerBodyMaxX = nextPos.x + RADIUS; + float playerBodyMinY = nextPos.y; + float playerBodyMaxY = nextPos.y + height; + + // Compute the bounds of all blocks the player is intersecting with. + int minX = (int) Math.floor(playerBodyMinX); + int minZ = (int) Math.floor(playerBodyMinZ); + int minY = (int) Math.floor(playerBodyMinY); + int maxX = (int) Math.floor(playerBodyMaxX); + int maxZ = (int) Math.floor(playerBodyMaxZ); + int maxY = (int) Math.floor(playerBodyMaxY); + + for (int x = minX; x <= maxX; x++) { + for (int z = minZ; z <= maxZ; z++) { + for (int y = minY; y <= maxY; y++) { + byte block = world.getBlockAt(x, y, z); + if (block <= 0) continue; // We're not colliding with this block. + float blockMinY = (float) y; + float blockMaxY = (float) y + 1; + float blockMinX = (float) x; + float blockMaxX = (float) x + 1; + float blockMinZ = (float) z; + float blockMaxZ = (float) z + 1; + + /* + To determine if the player is moving into the -Z side of a block: + - The player's max z position went from < blockMinZ to >= blockMinZ. + - The block to the -Z direction is air. + */ + boolean collidingWithNegativeZ = playerBodyPrevMaxZ < blockMinZ && playerBodyMaxZ >= blockMinZ && world.getBlockAt(x, y, z - 1) <= 0; + if (collidingWithNegativeZ) { + position.z = blockMinZ - RADIUS - delta; + velocity.z = 0; + movement.z = 0; + } + + /* + To determine if the player is moving into the +Z side of a block: + - The player's min z position went from >= blockMaxZ to < blockMaxZ. + - The block to the +Z direction is air. + */ + boolean collidingWithPositiveZ = playerBodyPrevMinZ >= blockMaxZ && playerBodyMinZ < blockMaxZ && world.getBlockAt(x, y, z + 1) <= 0; + if (collidingWithPositiveZ) { + position.z = blockMaxZ + RADIUS + delta; + velocity.z = 0; + movement.z = 0; + } + + /* + To determine if the player is moving into the -X side of a block: + - The player's max x position went from < blockMinX to >= blockMinX + - The block to the -X direction is air. + */ + boolean collidingWithNegativeX = playerBodyPrevMaxX < blockMinX && playerBodyMaxX >= blockMinX && world.getBlockAt(x - 1, y, z) <= 0; + if (collidingWithNegativeX) { + position.x = blockMinX - RADIUS - delta; + velocity.x = 0; + movement.x = 0; + } + + /* + To determine if the player is moving into the +X side of a block: + - The player's min x position went from >= blockMaxX to < blockMaxX. + - The block to the +X direction is air. + */ + boolean collidingWithPositiveX = playerBodyPrevMinX >= blockMaxX && playerBodyMinX < blockMaxX && world.getBlockAt(x + 1, y, z) <= 0; + if (collidingWithPositiveX) { + position.x = blockMaxX + RADIUS + delta; + velocity.x = 0; + movement.x = 0; + } + + /* + To determine if the player is moving down onto a block: + - The player's min y position went from >= blockMaxY to < blockMaxY + - The block above the current one is air. + */ + boolean collidingWithFloor = playerBodyPrevMinY >= blockMaxY && playerBodyMinY < blockMaxY && world.getBlockAt(x, y + 1, z) <= 0; + if (collidingWithFloor) { + position.y = blockMaxY; + velocity.y = 0; + movement.y = 0; + } + + /* + To determine if the player is moving up into a block: + - The player's y position went from below blockMinY to >= blockMinY + - The block below the current one is air. + */ + boolean collidingWithCeiling = playerBodyPrevMaxY < blockMinY && playerBodyMaxY >= blockMinY && world.getBlockAt(x, y - 1, z) <= 0; + if (collidingWithCeiling) { + position.y = blockMinY - height - delta; + velocity.y = 0; + movement.y = 0; + } + + updated = true; + } + } + } + } + } + + public float getCurrentHeight() { + return lastInputState.crouching() ? HEIGHT_CROUCH : HEIGHT; + } + + public float getEyeHeight() { + return lastInputState.crouching() ? EYE_HEIGHT_CROUCH : EYE_HEIGHT; + } +} diff --git a/server/src/main/java/nl/andrewl/aos2_server/WorldUpdater.java b/server/src/main/java/nl/andrewl/aos2_server/logic/WorldUpdater.java similarity index 74% rename from server/src/main/java/nl/andrewl/aos2_server/WorldUpdater.java rename to server/src/main/java/nl/andrewl/aos2_server/logic/WorldUpdater.java index e3086e7..095a58a 100644 --- a/server/src/main/java/nl/andrewl/aos2_server/WorldUpdater.java +++ b/server/src/main/java/nl/andrewl/aos2_server/logic/WorldUpdater.java @@ -1,9 +1,7 @@ -package nl.andrewl.aos2_server; +package nl.andrewl.aos2_server.logic; -import nl.andrewl.aos_core.net.udp.PlayerUpdateMessage; +import nl.andrewl.aos2_server.Server; import org.joml.Math; -import org.joml.Matrix4f; -import org.joml.Vector3f; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -56,14 +54,8 @@ public class WorldUpdater implements Runnable { private void tick() { for (var player : server.getPlayerManager().getPlayers()) { - player.tick(secondsPerTick, server.getWorld(), server); - if (player.isUpdated()) server.getPlayerManager().broadcastUdpMessage(new PlayerUpdateMessage( - player.getId(), - player.getPosition().x, player.getPosition().y, player.getPosition().z, - player.getVelocity().x, player.getVelocity().y, player.getVelocity().z, - player.getOrientation().x, player.getOrientation().y, - player.getLastInputState().crouching() - )); + player.getActionManager().tick(secondsPerTick, server.getWorld(), server); + if (player.getActionManager().isUpdated()) server.getPlayerManager().broadcastUdpMessage(player.getUpdateMessage()); } } }