Added better client state information, more message reorganization.

This commit is contained in:
Andrew Lalis 2022-07-18 12:36:53 +02:00
parent d83ff8a816
commit 8bd88b849c
37 changed files with 491 additions and 100 deletions

View File

@ -1,6 +1,7 @@
package nl.andrewl.aos2_client;
import nl.andrewl.aos_core.MathUtils;
import nl.andrewl.aos_core.model.Player;
import org.joml.Matrix4f;
import org.joml.Vector2f;
import org.joml.Vector3f;
@ -46,6 +47,11 @@ public class Camera {
this.viewTransform = new Matrix4f();
}
public void setToPlayer(Player p) {
position.set(p.getPosition());
velocity.set(p.getVelocity());
}
public Matrix4f getViewTransform() {
return viewTransform;
}

View File

@ -5,12 +5,15 @@ import nl.andrewl.aos2_client.control.InputHandler;
import nl.andrewl.aos2_client.control.PlayerInputKeyCallback;
import nl.andrewl.aos2_client.control.PlayerInputMouseClickCallback;
import nl.andrewl.aos2_client.control.PlayerViewCursorCallback;
import nl.andrewl.aos2_client.model.ClientPlayer;
import nl.andrewl.aos2_client.render.GameRenderer;
import nl.andrewl.aos_core.config.Config;
import nl.andrewl.aos_core.model.world.ColorPalette;
import nl.andrewl.aos_core.net.*;
import nl.andrewl.aos_core.net.udp.ChunkUpdateMessage;
import nl.andrewl.aos_core.net.udp.PlayerUpdateMessage;
import nl.andrewl.aos_core.net.client.*;
import nl.andrewl.aos_core.net.world.ChunkDataMessage;
import nl.andrewl.aos_core.net.world.ChunkHashMessage;
import nl.andrewl.aos_core.net.world.ChunkUpdateMessage;
import nl.andrewl.aos_core.net.world.WorldInfoMessage;
import nl.andrewl.record_net.Message;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -28,13 +31,13 @@ public class Client implements Runnable {
private final InputHandler inputHandler;
private final GameRenderer gameRenderer;
private int clientId;
private final ClientWorld world;
private ClientPlayer player;
public Client(ClientConfig config) {
this.config = config;
this.communicationHandler = new CommunicationHandler(this);
this.inputHandler = new InputHandler(communicationHandler);
this.inputHandler = new InputHandler(this, communicationHandler);
this.world = new ClientWorld();
this.gameRenderer = new GameRenderer(config.display, world);
}
@ -43,17 +46,30 @@ public class Client implements Runnable {
return config;
}
public ClientPlayer getPlayer() {
return player;
}
/**
* Called by the {@link CommunicationHandler} when a connection is
* established, and we need to begin tracking the player's state.
* @param player The player.
*/
public void setPlayer(ClientPlayer player) {
this.player = player;
}
@Override
public void run() {
try {
this.clientId = communicationHandler.establishConnection();
communicationHandler.establishConnection();
} catch (IOException e) {
log.error("Couldn't connect to the server: {}", e.getMessage());
return;
}
gameRenderer.setupWindow(
new PlayerViewCursorCallback(config.input, gameRenderer.getCamera(), communicationHandler),
new PlayerViewCursorCallback(config.input, this, gameRenderer.getCamera(), communicationHandler),
new PlayerInputKeyCallback(inputHandler),
new PlayerInputMouseClickCallback(inputHandler)
);
@ -75,30 +91,37 @@ public class Client implements Runnable {
public void onMessageReceived(Message msg) {
if (msg instanceof WorldInfoMessage worldInfo) {
world.setPalette(ColorPalette.fromArray(worldInfo.palette()));
}
if (msg instanceof ChunkDataMessage chunkDataMessage) {
} else if (msg instanceof ChunkDataMessage chunkDataMessage) {
world.addChunk(chunkDataMessage);
}
if (msg instanceof ChunkUpdateMessage u) {
} else if (msg instanceof ChunkUpdateMessage u) {
world.updateChunk(u);
// If we received an update for a chunk we don't have, request it!
if (world.getChunkAt(u.getChunkPos()) == null) {
communicationHandler.sendMessage(new ChunkHashMessage(u.cx(), u.cy(), u.cz(), -1));
}
}
if (msg instanceof PlayerUpdateMessage playerUpdate) {
if (playerUpdate.clientId() == clientId) {
} else if (msg instanceof PlayerUpdateMessage playerUpdate) {
if (playerUpdate.clientId() == player.getId()) {
player.getPosition().set(playerUpdate.px(), playerUpdate.py(), playerUpdate.pz());
player.getVelocity().set(playerUpdate.vx(), playerUpdate.vy(), playerUpdate.vz());
gameRenderer.getCamera().setToPlayer(player);
// TODO: Add getEyeHeight() and isCrouching() to main Player class.
float eyeHeight = playerUpdate.crouching() ? 1.3f : 1.7f;
gameRenderer.getCamera().setPosition(playerUpdate.px(), playerUpdate.py() + eyeHeight, playerUpdate.pz());
gameRenderer.getCamera().setVelocity(playerUpdate.vx(), playerUpdate.vy(), playerUpdate.vz());
gameRenderer.getCamera().getPosition().y += eyeHeight;
} else {
world.playerUpdated(playerUpdate);
}
}
if (msg instanceof PlayerJoinMessage joinMessage) {
} else if (msg instanceof ClientInventoryMessage inventoryMessage) {
player.setInventory(inventoryMessage.inv());
System.out.println("Got inventory!");
} else if (msg instanceof InventorySelectedStackMessage selectedStackMessage) {
player.getInventory().setSelectedIndex(selectedStackMessage.index());
System.out.println("Selected item stack: " + player.getInventory().getSelectedItemStack().getType().getName());
} else if (msg instanceof ItemStackMessage itemStackMessage) {
player.getInventory().getItemStacks().set(itemStackMessage.index(), itemStackMessage.stack());
System.out.println("Item stack updated: " + itemStackMessage.index());
} else if (msg instanceof PlayerJoinMessage joinMessage) {
world.playerJoined(joinMessage);
}
if (msg instanceof PlayerLeaveMessage leaveMessage) {
} else if (msg instanceof PlayerLeaveMessage leaveMessage) {
world.playerLeft(leaveMessage);
}
}

View File

@ -5,11 +5,11 @@ import nl.andrewl.aos2_client.render.chunk.ChunkMeshGenerator;
import nl.andrewl.aos_core.model.world.Chunk;
import nl.andrewl.aos_core.model.Player;
import nl.andrewl.aos_core.model.world.World;
import nl.andrewl.aos_core.net.ChunkDataMessage;
import nl.andrewl.aos_core.net.PlayerJoinMessage;
import nl.andrewl.aos_core.net.PlayerLeaveMessage;
import nl.andrewl.aos_core.net.udp.ChunkUpdateMessage;
import nl.andrewl.aos_core.net.udp.PlayerUpdateMessage;
import nl.andrewl.aos_core.net.world.ChunkDataMessage;
import nl.andrewl.aos_core.net.client.PlayerJoinMessage;
import nl.andrewl.aos_core.net.client.PlayerLeaveMessage;
import nl.andrewl.aos_core.net.world.ChunkUpdateMessage;
import nl.andrewl.aos_core.net.client.PlayerUpdateMessage;
import org.joml.Vector3f;
import org.joml.Vector3i;

View File

@ -1,8 +1,12 @@
package nl.andrewl.aos2_client;
import nl.andrewl.aos2_client.model.ClientPlayer;
import nl.andrewl.aos_core.Net;
import nl.andrewl.aos_core.net.*;
import nl.andrewl.aos_core.net.udp.DatagramInit;
import nl.andrewl.aos_core.net.connect.ConnectAcceptMessage;
import nl.andrewl.aos_core.net.connect.ConnectRejectMessage;
import nl.andrewl.aos_core.net.connect.ConnectRequestMessage;
import nl.andrewl.aos_core.net.connect.DatagramInit;
import nl.andrewl.record_net.Message;
import nl.andrewl.record_net.util.ExtendedDataInputStream;
import nl.andrewl.record_net.util.ExtendedDataOutputStream;
@ -33,7 +37,7 @@ public class CommunicationHandler {
this.client = client;
}
public int establishConnection() throws IOException {
public void establishConnection() throws IOException {
if (socket != null && !socket.isClosed()) {
socket.close();
}
@ -54,11 +58,11 @@ public class CommunicationHandler {
}
if (response instanceof ConnectAcceptMessage acceptMessage) {
this.clientId = acceptMessage.clientId();
client.setPlayer(new ClientPlayer(clientId, username));
establishDatagramConnection();
log.info("Connection to server established. My client id is {}.", clientId);
new Thread(new TcpReceiver(in, client::onMessageReceived)).start();
new Thread(new UdpReceiver(datagramSocket, (msg, packet) -> client.onMessageReceived(msg))).start();
return acceptMessage.clientId();
} else {
throw new IOException("Server returned an unexpected message: " + response);
}

View File

@ -13,7 +13,7 @@ public class ClientConfig {
public static class DisplayConfig {
public boolean fullscreen = false;
public boolean captureCursor = true;
public boolean captureCursor = false;
public float fov = 70;
}
}

View File

@ -1,7 +1,8 @@
package nl.andrewl.aos2_client.control;
import nl.andrewl.aos2_client.CommunicationHandler;
import nl.andrewl.aos_core.net.udp.ClientInputState;
import nl.andrewl.aos_core.net.client.ClientInputState;
import nl.andrewl.aos2_client.Client;
import static org.lwjgl.glfw.GLFW.*;
@ -9,16 +10,24 @@ import static org.lwjgl.glfw.GLFW.*;
* Class which manages the player's input, and sending it to the server.
*/
public class InputHandler {
private final Client client;
private final CommunicationHandler comm;
private ClientInputState lastInputState = null;
public InputHandler(CommunicationHandler comm) {
public InputHandler(Client client, CommunicationHandler comm) {
this.client = client;
this.comm = comm;
}
public void updateInputState(long window) {
// TODO: Allow customized keybindings.
int selectedInventoryIndex;
selectedInventoryIndex = client.getPlayer().getInventory().getSelectedIndex();
if (glfwGetKey(window, GLFW_KEY_1) == GLFW_PRESS) selectedInventoryIndex = 0;
if (glfwGetKey(window, GLFW_KEY_2) == GLFW_PRESS) selectedInventoryIndex = 1;
if (glfwGetKey(window, GLFW_KEY_3) == GLFW_PRESS) selectedInventoryIndex = 2;
ClientInputState currentInputState = new ClientInputState(
comm.getClientId(),
glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS,
@ -29,7 +38,8 @@ public class InputHandler {
glfwGetKey(window, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS,
glfwGetKey(window, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS,
glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_1) == GLFW_PRESS,
glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_2) == GLFW_PRESS
glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_2) == GLFW_PRESS,
selectedInventoryIndex
);
if (!currentInputState.equals(lastInputState)) {
comm.sendDatagramPacket(currentInputState);

View File

@ -1,9 +1,10 @@
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.udp.ClientOrientationState;
import nl.andrewl.aos_core.net.client.ClientOrientationState;
import org.lwjgl.glfw.GLFWCursorPosCallbackI;
import java.util.concurrent.ForkJoinPool;
@ -22,15 +23,17 @@ public class PlayerViewCursorCallback implements GLFWCursorPosCallbackI {
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;
public PlayerViewCursorCallback(ClientConfig.InputConfig config, Camera camera, CommunicationHandler comm) {
public PlayerViewCursorCallback(ClientConfig.InputConfig config, Client client, Camera cam, CommunicationHandler comm) {
this.config = config;
this.camera = camera;
this.client = client;
this.camera = cam;
this.comm = comm;
}
@ -45,13 +48,18 @@ public class PlayerViewCursorCallback implements GLFWCursorPosCallbackI {
float dy = y - lastMouseCursorY;
lastMouseCursorX = x;
lastMouseCursorY = y;
camera.setOrientation(
camera.getOrientation().x - dx * config.mouseSensitivity,
camera.getOrientation().y - dy * config.mouseSensitivity
client.getPlayer().setOrientation(
client.getPlayer().getOrientation().x - dx * config.mouseSensitivity,
client.getPlayer().getOrientation().y - dy * config.mouseSensitivity
);
camera.setOrientation(client.getPlayer().getOrientation().x, client.getPlayer().getOrientation().y);
long now = System.currentTimeMillis();
if (lastOrientationUpdateSentAt + ORIENTATION_UPDATE_LIMIT < now) {
ForkJoinPool.commonPool().submit(() -> comm.sendDatagramPacket(new ClientOrientationState(comm.getClientId(), camera.getOrientation().x, camera.getOrientation().y)));
ForkJoinPool.commonPool().submit(() -> comm.sendDatagramPacket(new ClientOrientationState(
client.getPlayer().getId(),
client.getPlayer().getOrientation().x,
client.getPlayer().getOrientation().y
)));
lastOrientationUpdateSentAt = now;
}
}

View File

@ -0,0 +1,25 @@
package nl.andrewl.aos2_client.model;
import nl.andrewl.aos_core.model.Player;
import nl.andrewl.aos_core.model.item.Inventory;
import java.util.ArrayList;
public class ClientPlayer extends Player {
private final Inventory inventory;
public ClientPlayer(int id, String username) {
super(id, username);
this.inventory = new Inventory(new ArrayList<>(), 0);
}
public Inventory getInventory() {
return inventory;
}
public void setInventory(Inventory inv) {
this.inventory.getItemStacks().clear();
this.inventory.getItemStacks().addAll(inv.getItemStacks());
this.inventory.setSelectedIndex(inv.getSelectedIndex());
}
}

View File

@ -1,7 +1,14 @@
package nl.andrewl.aos_core;
import nl.andrewl.aos_core.net.*;
import nl.andrewl.aos_core.net.udp.*;
import nl.andrewl.aos_core.net.client.*;
import nl.andrewl.aos_core.net.connect.ConnectAcceptMessage;
import nl.andrewl.aos_core.net.connect.ConnectRejectMessage;
import nl.andrewl.aos_core.net.connect.ConnectRequestMessage;
import nl.andrewl.aos_core.net.connect.DatagramInit;
import nl.andrewl.aos_core.net.world.ChunkDataMessage;
import nl.andrewl.aos_core.net.world.ChunkHashMessage;
import nl.andrewl.aos_core.net.world.ChunkUpdateMessage;
import nl.andrewl.aos_core.net.world.WorldInfoMessage;
import nl.andrewl.record_net.Message;
import nl.andrewl.record_net.Serializer;
import nl.andrewl.record_net.util.ExtendedDataInputStream;
@ -33,6 +40,10 @@ public final class Net {
serializer.registerType(11, PlayerJoinMessage.class);
serializer.registerType(12, PlayerLeaveMessage.class);
serializer.registerType(13, WorldInfoMessage.class);
// Separate serializers for client inventory messages.
serializer.registerTypeSerializer(14, new InventorySerializer());
serializer.registerTypeSerializer(15, new ItemStackSerializer());
serializer.registerType(16, InventorySelectedStackMessage.class);
}
public static ExtendedDataInputStream getInputStream(InputStream in) {

View File

@ -1,13 +1,23 @@
package nl.andrewl.aos_core.model.item;
public class BlockItemStack extends ItemStack {
private int selectedValue = 1;
private byte selectedValue = 1;
public BlockItemStack(BlockItem item, int amount) {
public BlockItemStack(BlockItem item, int amount, byte selectedValue) {
super(item, amount);
this.selectedValue = selectedValue;
}
public int getSelectedValue() {
public BlockItemStack(BlockItem item) {
this(item, 50, (byte) 1);
}
public byte getSelectedValue() {
return selectedValue;
}
public void setSelectedValue(byte selectedValue) {
if (selectedValue < 1) return;
this.selectedValue = selectedValue;
}
}

View File

@ -4,9 +4,29 @@ public class GunItemStack extends ItemStack {
private int bulletCount;
private int clipCount;
public GunItemStack(Gun gun) {
public GunItemStack(Gun gun, int bulletCount, int clipCount) {
super(gun, 1);
bulletCount = gun.getMaxBulletCount();
clipCount = gun.getMaxClipCount();
this.bulletCount = bulletCount;
this.clipCount = clipCount;
}
public GunItemStack(Gun gun) {
this(gun, gun.getMaxBulletCount(), gun.getMaxClipCount());
}
public int getBulletCount() {
return bulletCount;
}
public void setBulletCount(int bulletCount) {
this.bulletCount = bulletCount;
}
public int getClipCount() {
return clipCount;
}
public void setClipCount(int clipCount) {
this.clipCount = clipCount;
}
}

View File

@ -25,13 +25,17 @@ public class Inventory {
return itemStacks;
}
public int getSelectedIndex() {
return selectedIndex;
}
public ItemStack getSelectedItemStack() {
return itemStacks.get(selectedIndex);
}
public void setSelectedIndex(int newIndex) {
while (newIndex < 0) newIndex += itemStacks.size();
while (newIndex > itemStacks.size() - 1) newIndex -= itemStacks.size();
if (newIndex < 0) newIndex = 0;
if (newIndex > itemStacks.size() - 1) newIndex = itemStacks.size() - 1;
this.selectedIndex = newIndex;
}
}

View File

@ -1,5 +1,10 @@
package nl.andrewl.aos_core.model.item;
import nl.andrewl.record_net.util.ExtendedDataInputStream;
import nl.andrewl.record_net.util.ExtendedDataOutputStream;
import java.io.IOException;
/**
* Represents a stack of items in the player's inventory. This is generally
* a type of item, and the amount of it.
@ -22,6 +27,53 @@ public class ItemStack {
}
public void setAmount(int amount) {
if (amount > -1) {
this.amount = amount;
}
}
public void incrementAmount() {
setAmount(amount + 1);
}
public void decrementAmount() {
setAmount(amount - 1);
}
public static int byteSize(ItemStack stack) {
int bytes = 2 * Integer.BYTES;
if (stack instanceof BlockItemStack) {
bytes += Byte.BYTES;
} else if (stack instanceof GunItemStack) {
bytes += 2 * Integer.BYTES;
}
return bytes;
}
public static void write(ItemStack stack, ExtendedDataOutputStream out) throws IOException {
out.writeInt(stack.type.id);
out.writeInt(stack.amount);
if (stack instanceof BlockItemStack b) {
out.writeByte(b.getSelectedValue());
} else if (stack instanceof GunItemStack g) {
out.writeInt(g.getClipCount());
out.writeInt(g.getBulletCount());
}
}
public static ItemStack read(ExtendedDataInputStream in) throws IOException {
int typeId = in.readInt();
Item item = ItemTypes.get(typeId);
int amount = in.readInt();
if (item instanceof Gun g) {
int clipCount = in.readInt();
int bulletCount = in.readInt();
return new GunItemStack(g, bulletCount, clipCount);
} else if (item instanceof BlockItem b) {
byte selectedValue = in.readByte();
return new BlockItemStack(b, amount, selectedValue);
} else {
throw new IOException("Invalid item stack.");
}
}
}

View File

@ -12,9 +12,12 @@ public final class ItemTypes {
private static final Map<Integer, Item> TYPES_BY_ID = new HashMap<>();
private static final Map<String, Item> TYPES_BY_NAME = new HashMap<>();
public static final BlockItem BLOCK = new BlockItem(1);
public static final Rifle RIFLE = new Rifle(2);
static {
registerType(new BlockItem(1));
registerType(new Rifle(2));
registerType(BLOCK);
registerType(RIFLE);
}
public static void registerType(Item type) {

View File

@ -1,4 +1,4 @@
package nl.andrewl.aos_core.net.udp;
package nl.andrewl.aos_core.net.client;
import nl.andrewl.record_net.Message;
@ -19,5 +19,6 @@ public record ClientInputState(
// Interaction
boolean hitting, // Usually a "left-click" action.
boolean interacting // Usually a "right-click" action.
boolean interacting, // Usually a "right-click" action.
int selectedInventoryIndex // The selected index in the player's inventory.
) implements Message {}

View File

@ -0,0 +1,15 @@
package nl.andrewl.aos_core.net.client;
import nl.andrewl.aos_core.model.item.Inventory;
import nl.andrewl.record_net.Message;
/**
* A message that's sent by the server to a client with information about the
* client's full inventory configuration. Here, we use a custom serializer
* since the inventory object contains a lot of inheritance.
*
* @see InventorySerializer
*/
public record ClientInventoryMessage(
Inventory inv
) implements Message {}

View File

@ -1,4 +1,4 @@
package nl.andrewl.aos_core.net.udp;
package nl.andrewl.aos_core.net.client;
import nl.andrewl.record_net.Message;

View File

@ -0,0 +1,11 @@
package nl.andrewl.aos_core.net.client;
import nl.andrewl.record_net.Message;
/**
* A message that's sent when a player's selected inventory stack changes.
* @param index The selected index.
*/
public record InventorySelectedStackMessage(
int index
) implements Message {}

View File

@ -0,0 +1,56 @@
package nl.andrewl.aos_core.net.client;
import nl.andrewl.aos_core.model.item.Inventory;
import nl.andrewl.aos_core.model.item.ItemStack;
import nl.andrewl.record_net.MessageReader;
import nl.andrewl.record_net.MessageTypeSerializer;
import nl.andrewl.record_net.MessageWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
public class InventorySerializer implements MessageTypeSerializer<ClientInventoryMessage> {
@Override
public Class<ClientInventoryMessage> messageClass() {
return ClientInventoryMessage.class;
}
@Override
public Function<ClientInventoryMessage, Integer> byteSizeFunction() {
return msg -> {
int bytes = Integer.BYTES; // For the stack count size.
for (var stack : msg.inv().getItemStacks()) {
bytes += ItemStack.byteSize(stack);
}
bytes += Integer.BYTES; // Selected index.
return bytes;
};
}
@Override
public MessageReader<ClientInventoryMessage> reader() {
return in -> {
int stacksCount = in.readInt();
List<ItemStack> stacks = new ArrayList<>(stacksCount);
for (int i = 0; i < stacksCount; i++) {
stacks.add(ItemStack.read(in));
}
int selectedIndex = in.readInt();
Inventory inv = new Inventory(stacks, selectedIndex);
return new ClientInventoryMessage(inv);
};
}
@Override
public MessageWriter<ClientInventoryMessage> writer() {
return (msg, out) -> {
Inventory inv = msg.inv();
out.writeInt(inv.getItemStacks().size());
for (var stack : inv.getItemStacks()) {
ItemStack.write(stack, out);
}
out.writeInt(inv.getSelectedIndex());
};
}
}

View File

@ -0,0 +1,18 @@
package nl.andrewl.aos_core.net.client;
import nl.andrewl.aos_core.model.item.Inventory;
import nl.andrewl.aos_core.model.item.ItemStack;
import nl.andrewl.record_net.Message;
/**
* Lightweight packet that's sent when a single item stack in a player's
* inventory updates.
*/
public record ItemStackMessage(
int index,
ItemStack stack
) implements Message {
public ItemStackMessage(Inventory inv) {
this(inv.getSelectedIndex(), inv.getSelectedItemStack());
}
}

View File

@ -0,0 +1,37 @@
package nl.andrewl.aos_core.net.client;
import nl.andrewl.aos_core.model.item.ItemStack;
import nl.andrewl.record_net.MessageReader;
import nl.andrewl.record_net.MessageTypeSerializer;
import nl.andrewl.record_net.MessageWriter;
import java.util.function.Function;
public class ItemStackSerializer implements MessageTypeSerializer<ItemStackMessage> {
@Override
public Class<ItemStackMessage> messageClass() {
return ItemStackMessage.class;
}
@Override
public Function<ItemStackMessage, Integer> byteSizeFunction() {
return msg -> Integer.BYTES + ItemStack.byteSize(msg.stack());
}
@Override
public MessageReader<ItemStackMessage> reader() {
return in -> {
int index = in.readInt();
ItemStack stack = ItemStack.read(in);
return new ItemStackMessage(index, stack);
};
}
@Override
public MessageWriter<ItemStackMessage> writer() {
return (msg, out) -> {
out.writeInt(msg.index());
ItemStack.write(msg.stack(), out);
};
}
}

View File

@ -1,4 +1,4 @@
package nl.andrewl.aos_core.net;
package nl.andrewl.aos_core.net.client;
import nl.andrewl.aos_core.model.Player;
import nl.andrewl.record_net.Message;

View File

@ -1,4 +1,4 @@
package nl.andrewl.aos_core.net;
package nl.andrewl.aos_core.net.client;
import nl.andrewl.record_net.Message;

View File

@ -1,4 +1,4 @@
package nl.andrewl.aos_core.net.udp;
package nl.andrewl.aos_core.net.client;
import nl.andrewl.aos_core.model.Player;
import nl.andrewl.record_net.Message;

View File

@ -1,4 +1,4 @@
package nl.andrewl.aos_core.net;
package nl.andrewl.aos_core.net.connect;
import nl.andrewl.record_net.Message;

View File

@ -1,4 +1,4 @@
package nl.andrewl.aos_core.net;
package nl.andrewl.aos_core.net.connect;
import nl.andrewl.record_net.Message;

View File

@ -1,4 +1,4 @@
package nl.andrewl.aos_core.net;
package nl.andrewl.aos_core.net.connect;
import nl.andrewl.record_net.Message;

View File

@ -1,4 +1,4 @@
package nl.andrewl.aos_core.net.udp;
package nl.andrewl.aos_core.net.connect;
import nl.andrewl.record_net.Message;

View File

@ -1,4 +1,4 @@
package nl.andrewl.aos_core.net;
package nl.andrewl.aos_core.net.world;
import nl.andrewl.aos_core.model.world.Chunk;
import nl.andrewl.record_net.Message;

View File

@ -1,4 +1,4 @@
package nl.andrewl.aos_core.net;
package nl.andrewl.aos_core.net.world;
import nl.andrewl.aos_core.model.world.Chunk;
import nl.andrewl.record_net.Message;

View File

@ -1,4 +1,4 @@
package nl.andrewl.aos_core.net.udp;
package nl.andrewl.aos_core.net.world;
import nl.andrewl.aos_core.model.world.World;
import nl.andrewl.record_net.Message;

View File

@ -1,4 +1,4 @@
package nl.andrewl.aos_core.net;
package nl.andrewl.aos_core.net.world;
import nl.andrewl.aos_core.model.world.World;
import nl.andrewl.record_net.Message;

View File

@ -3,6 +3,14 @@ package nl.andrewl.aos2_server;
import nl.andrewl.aos_core.Net;
import nl.andrewl.aos_core.model.world.Chunk;
import nl.andrewl.aos_core.net.*;
import nl.andrewl.aos_core.net.client.PlayerJoinMessage;
import nl.andrewl.aos_core.net.connect.ConnectAcceptMessage;
import nl.andrewl.aos_core.net.connect.ConnectRejectMessage;
import nl.andrewl.aos_core.net.connect.ConnectRequestMessage;
import nl.andrewl.aos_core.net.client.ClientInventoryMessage;
import nl.andrewl.aos_core.net.world.ChunkDataMessage;
import nl.andrewl.aos_core.net.world.ChunkHashMessage;
import nl.andrewl.aos_core.net.world.WorldInfoMessage;
import nl.andrewl.record_net.Message;
import nl.andrewl.record_net.util.ExtendedDataInputStream;
import nl.andrewl.record_net.util.ExtendedDataOutputStream;
@ -81,6 +89,7 @@ public class ClientCommunicationHandler {
try {
Message msg = Net.read(in);
if (msg instanceof ConnectRequestMessage connectMsg) {
log.debug("Received connect request from player \"{}\"", connectMsg.username());
// Try to set the TCP timeout back to 0 now that we've got the correct request.
socket.setSoTimeout(0);
this.clientAddress = socket.getInetAddress();
@ -90,7 +99,10 @@ public class ClientCommunicationHandler {
log.debug("Sent connect accept message.");
sendTcpMessage(new WorldInfoMessage(server.getWorld()));
// Send join info for all players that are already connected.
// Send player's inventory information.
sendTcpMessage(new ClientInventoryMessage(player.getInventory()));
// Send "join" info about all the players that are already connected, so the client is aware of them.
for (var player : server.getPlayerManager().getPlayers()) {
if (player.getId() != this.player.getId()) {
sendTcpMessage(new PlayerJoinMessage(player));

View File

@ -1,9 +1,9 @@
package nl.andrewl.aos2_server;
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.client.PlayerJoinMessage;
import nl.andrewl.aos_core.net.client.PlayerLeaveMessage;
import nl.andrewl.aos_core.net.connect.DatagramInit;
import nl.andrewl.record_net.Message;
import org.joml.Vector3f;
import org.slf4j.Logger;

View File

@ -6,9 +6,9 @@ import nl.andrewl.aos_core.config.Config;
import nl.andrewl.aos_core.model.world.World;
import nl.andrewl.aos_core.model.world.Worlds;
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.client.ClientInputState;
import nl.andrewl.aos_core.net.client.ClientOrientationState;
import nl.andrewl.aos_core.net.connect.DatagramInit;
import nl.andrewl.record_net.Message;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -66,9 +66,10 @@ public class Server implements Runnable {
} else if (msg instanceof ClientInputState inputState) {
ServerPlayer player = playerManager.getPlayer(inputState.clientId());
if (player != null) {
player.getActionManager().setLastInputState(inputState);
if (player.getActionManager().setLastInputState(inputState)) {
playerManager.broadcastUdpMessage(player.getUpdateMessage());
}
}
} else if (msg instanceof ClientOrientationState orientationState) {
ServerPlayer player = playerManager.getPlayer(orientationState.clientId());
if (player != null) {
@ -86,7 +87,7 @@ public class Server implements Runnable {
ForkJoinPool.commonPool().submit(() -> {
try {
handler.establishConnection();
} catch (IOException e) {
} catch (Exception e) {
e.printStackTrace();
}
});

View File

@ -2,15 +2,20 @@ package nl.andrewl.aos2_server;
import nl.andrewl.aos2_server.logic.PlayerActionManager;
import nl.andrewl.aos_core.model.Player;
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 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.net.client.PlayerUpdateMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
/**
* An extension of the base player class with additional information that's
* needed for the server.
*/
public class ServerPlayer extends Player {
private static final Logger log = LoggerFactory.getLogger(ServerPlayer.class);
@ -26,16 +31,20 @@ public class ServerPlayer extends Player {
public ServerPlayer(int id, String username) {
super(id, username);
this.actionManager = new PlayerActionManager(this);
this.inventory = new Inventory(new ArrayList<>(), 0);
this.actionManager = new PlayerActionManager(this);
inventory.getItemStacks().add(new GunItemStack(ItemTypes.get("Rifle")));
inventory.getItemStacks().add(new BlockItemStack(ItemTypes.get("Block"), 50));
inventory.getItemStacks().add(new BlockItemStack(ItemTypes.get("Block"), 50, (byte) 1));
}
public PlayerActionManager getActionManager() {
return actionManager;
}
public Inventory getInventory() {
return inventory;
}
/**
* Helper method to build an update message for this player, to be sent to
* various clients.

View File

@ -3,9 +3,13 @@ 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.item.BlockItemStack;
import nl.andrewl.aos_core.model.item.ItemTypes;
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 nl.andrewl.aos_core.net.client.ClientInputState;
import nl.andrewl.aos_core.net.client.InventorySelectedStackMessage;
import nl.andrewl.aos_core.net.client.ItemStackMessage;
import nl.andrewl.aos_core.net.world.ChunkUpdateMessage;
import org.joml.Math;
import org.joml.Vector2i;
import org.joml.Vector3f;
@ -30,15 +34,23 @@ public class PlayerActionManager {
public PlayerActionManager(ServerPlayer player) {
this.player = player;
lastInputState = new ClientInputState(player.getId(), false, false, false, false, false, false, false, false, false);
lastInputState = new ClientInputState(
player.getId(),
false, false, false, false,
false, false, false,
false, false,
player.getInventory().getSelectedIndex()
);
}
public ClientInputState getLastInputState() {
return lastInputState;
}
public void setLastInputState(ClientInputState lastInputState) {
this.lastInputState = lastInputState;
public boolean setLastInputState(ClientInputState lastInputState) {
boolean change = !lastInputState.equals(this.lastInputState);
if (change) this.lastInputState = lastInputState;
return change;
}
public boolean isUpdated() {
@ -46,36 +58,65 @@ public class PlayerActionManager {
}
public void tick(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.
long now = System.currentTimeMillis();
if (player.getInventory().getSelectedIndex() != lastInputState.selectedInventoryIndex()) {
player.getInventory().setSelectedIndex(lastInputState.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.
}
if (player.getInventory().getSelectedItemStack().getType().equals(ItemTypes.BLOCK)) {
tickBlockAction(now, server, world);
}
tickMovement(dt, world, server.getConfig().physics);
}
private void tickBlockAction(long now, Server server, World world) {
BlockItemStack stack = (BlockItemStack) player.getInventory().getSelectedItemStack();
// Check for breaking blocks.
if (lastInputState.hitting() && now - lastBlockRemovedAt > server.getConfig().actions.blockRemoveCooldown * 1000) {
if (
lastInputState.hitting() &&
stack.getAmount() < stack.getType().getMaxAmount() &&
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;
stack.incrementAmount();
server.getPlayerManager().getHandler(player.getId()).sendDatagramPacket(new ItemStackMessage(player.getInventory()));
server.getPlayerManager().broadcastUdpMessage(ChunkUpdateMessage.fromWorld(hit.pos(), world));
}
}
// Check for placing blocks.
if (lastInputState.interacting() && now - lastBlockPlacedAt > server.getConfig().actions.blockPlaceCooldown * 1000) {
if (
lastInputState.interacting() &&
stack.getAmount() > 0 &&
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);
if (!isSpaceOccupied(placePos)) { // Ensure that we can't place blocks in space we're occupying.
world.setBlockAt(placePos.x, placePos.y, placePos.z, stack.getSelectedValue());
lastBlockPlacedAt = now;
stack.decrementAmount();
server.getPlayerManager().getHandler(player.getId()).sendDatagramPacket(new ItemStackMessage(player.getInventory()));
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);
@ -178,15 +219,30 @@ public class PlayerActionManager {
return points;
}
private boolean isSpaceOccupied(Vector3i pos) {
var playerPos = player.getPosition();
float playerBodyMinZ = playerPos.z - RADIUS;
float playerBodyMaxZ = playerPos.z + RADIUS;
float playerBodyMinX = playerPos.x - RADIUS;
float playerBodyMaxX = playerPos.x + RADIUS;
float playerBodyMinY = playerPos.y;
float playerBodyMaxY = playerPos.y + getCurrentHeight();
// 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);
return pos.x >= minX && pos.x <= maxX && pos.y >= minY && pos.y <= maxY && pos.z >= minZ && pos.z <= maxZ;
}
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);
@ -205,7 +261,6 @@ public class PlayerActionManager {
// 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;