diff --git a/client/src/main/java/nl/andrewlalis/aos_client/ChatManager.java b/client/src/main/java/nl/andrewlalis/aos_client/ChatManager.java new file mode 100644 index 0000000..94f4d99 --- /dev/null +++ b/client/src/main/java/nl/andrewlalis/aos_client/ChatManager.java @@ -0,0 +1,81 @@ +package nl.andrewlalis.aos_client; + +import nl.andrewlalis.aos_core.net.chat.ChatMessage; +import nl.andrewlalis.aos_core.net.chat.PlayerChatMessage; +import nl.andrewlalis.aos_core.net.data.Sound; +import nl.andrewlalis.aos_core.net.data.SoundType; + +import java.util.LinkedList; +import java.util.List; + +public class ChatManager { + public static final int MAX_CHAT_MESSAGES = 10; + + private final List chatMessages; + private boolean chatting = false; + private final StringBuilder chatBuffer; + + private final SoundManager soundManager; + private MessageTransceiver messageTransceiver; + + public ChatManager(SoundManager soundManager) { + this.soundManager = soundManager; + this.chatMessages = new LinkedList<>(); + this.chatBuffer = new StringBuilder(); + } + + public void bindTransceiver(MessageTransceiver messageTransceiver) { + this.messageTransceiver = messageTransceiver; + } + + public void unbindTransceiver() { + this.messageTransceiver = null; + } + + public synchronized void addChatMessage(ChatMessage message) { + this.chatMessages.add(message); + if (message.getClass() == PlayerChatMessage.class) { + this.soundManager.play(new Sound(null, 1.0f, SoundType.CHAT)); + } + while (this.chatMessages.size() > MAX_CHAT_MESSAGES) { + this.chatMessages.remove(0); + } + } + + public ChatMessage[] getLatestChatMessages() { + return this.chatMessages.toArray(new ChatMessage[0]); + } + + public boolean isChatting() { + return this.chatting; + } + + public void setChatting(boolean chatting) { + this.chatting = chatting; + if (this.chatting) { + this.chatBuffer.setLength(0); + } + } + + public void appendToChat(char c) { + this.chatBuffer.append(c); + } + + public void backspaceChat() { + if (this.chatBuffer.length() > 0) { + this.chatBuffer.setLength(this.chatBuffer.length() - 1); + } + } + + public void sendChat() { + String message = this.chatBuffer.toString().trim(); + if (!message.isBlank() && !message.equals("/") && this.messageTransceiver != null) { + this.messageTransceiver.send(new ChatMessage(message)); + } + this.setChatting(false); + } + + public String getCurrentChatBuffer() { + return this.chatBuffer.toString(); + } +} diff --git a/client/src/main/java/nl/andrewlalis/aos_client/Client.java b/client/src/main/java/nl/andrewlalis/aos_client/Client.java index 15b1d5d..23f776b 100644 --- a/client/src/main/java/nl/andrewlalis/aos_client/Client.java +++ b/client/src/main/java/nl/andrewlalis/aos_client/Client.java @@ -3,50 +3,39 @@ package nl.andrewlalis.aos_client; import nl.andrewlalis.aos_client.view.ConnectDialog; import nl.andrewlalis.aos_client.view.GameFrame; import nl.andrewlalis.aos_client.view.GamePanel; -import nl.andrewlalis.aos_core.model.PlayerControlState; +import nl.andrewlalis.aos_core.model.Player; import nl.andrewlalis.aos_core.model.World; -import nl.andrewlalis.aos_core.net.PlayerControlStateMessage; -import nl.andrewlalis.aos_core.net.chat.ChatMessage; -import nl.andrewlalis.aos_core.net.chat.PlayerChatMessage; +import nl.andrewlalis.aos_core.model.tools.Gun; +import nl.andrewlalis.aos_core.net.data.DataTypes; +import nl.andrewlalis.aos_core.net.data.PlayerDetailUpdate; +import nl.andrewlalis.aos_core.net.data.WorldUpdate; import java.io.IOException; -import java.util.LinkedList; -import java.util.List; /** * The main class for the client, which connects to a server to join and play. */ public class Client { - public static final int MAX_CHAT_MESSAGES = 10; + private final MessageTransceiver messageTransceiver; - private MessageTransceiver messageTransceiver; - - private int playerId; - private PlayerControlState playerControlState; private World world; - - private final List chatMessages; - private boolean chatting = false; - private final StringBuilder chatBuffer; + private Player myPlayer; private final GameRenderer renderer; private final GamePanel gamePanel; private final SoundManager soundManager; + private final ChatManager chatManager; - public Client() { - this.chatMessages = new LinkedList<>(); - this.chatBuffer = new StringBuilder(); + public Client(String serverHost, int serverPort, String username) throws IOException { this.soundManager = new SoundManager(); + this.chatManager = new ChatManager(this.soundManager); this.gamePanel = new GamePanel(this); this.renderer = new GameRenderer(this, gamePanel); - } - - public void connect(String serverHost, int serverPort, String username) throws IOException, ClassNotFoundException { - this.messageTransceiver = new MessageTransceiver(this); - this.messageTransceiver.connectToServer(serverHost, serverPort, username); + this.messageTransceiver = new MessageTransceiver(this, serverHost, serverPort, username); this.messageTransceiver.start(); + this.chatManager.bindTransceiver(this.messageTransceiver); - while (this.playerControlState == null || this.world == null) { + while (this.myPlayer == null || this.world == null) { try { System.out.println("Waiting for server response and player registration..."); Thread.sleep(100); @@ -61,93 +50,75 @@ public class Client { this.renderer.start(); } + public ChatManager getChatManager() { + return chatManager; + } + public World getWorld() { return world; } + public void updateWorld(WorldUpdate update) { + if (this.world == null) return; + this.world.getBullets().clear(); + for (var u : update.getBulletUpdates()) { + this.world.getBullets().add(u.toBullet()); + } + for (var p : update.getPlayerUpdates()) { + Player player = this.world.getPlayers().get(p.getId()); + if (player != null) { + player.setPosition(p.getPosition()); + player.setOrientation(p.getOrientation()); + player.setVelocity(p.getVelocity()); + player.setGun(Gun.forType(p.getGunType())); + } + } + this.soundManager.play(update.getSoundsToPlay()); + } + public void setWorld(World world) { this.world = world; - for (String sound : this.world.getSoundsToPlay()) { - this.soundManager.play(sound); - } } - public void initPlayerData(int playerId) { - this.playerId = playerId; - this.playerControlState = new PlayerControlState(); - this.playerControlState.setPlayerId(playerId); + public void setPlayer(Player player) { + this.myPlayer = player; } - public int getPlayerId() { - return playerId; + public Player getPlayer() { + return myPlayer; } - public PlayerControlState getPlayerState() { - return playerControlState; + public void updatePlayer(PlayerDetailUpdate update) { + if (this.myPlayer == null) return; + this.myPlayer.setHealth(update.getHealth()); + this.myPlayer.setReloading(update.isReloading()); + this.myPlayer.setGun(Gun.forType( + this.myPlayer.getGun().getType(), + update.getGunMaxClipCount(), + update.getGunClipSize(), + update.getGunBulletsPerRound(), + update.getGunCurrentClipBulletCount(), + update.getGunClipCount() + )); } public void sendPlayerState() { try { - this.messageTransceiver.send(new PlayerControlStateMessage(this.playerControlState)); + this.messageTransceiver.sendData(DataTypes.PLAYER_CONTROL_STATE, myPlayer.getId(), myPlayer.getState().toBytes()); } catch (IOException e) { e.printStackTrace(); } } - public synchronized void addChatMessage(ChatMessage message) { - this.chatMessages.add(message); - if (message.getClass() == PlayerChatMessage.class) { - this.soundManager.play("chat.wav"); - } - while (this.chatMessages.size() > MAX_CHAT_MESSAGES) { - this.chatMessages.remove(0); - } - } - - public ChatMessage[] getLatestChatMessages() { - return this.chatMessages.toArray(new ChatMessage[0]); - } - - public boolean isChatting() { - return this.chatting; - } - - public void setChatting(boolean chatting) { - this.chatting = chatting; - if (this.chatting) { - this.chatBuffer.setLength(0); - } - } - - public void appendToChat(char c) { - this.chatBuffer.append(c); - } - - public void backspaceChat() { - if (this.chatBuffer.length() > 0) { - this.chatBuffer.setLength(this.chatBuffer.length() - 1); - } - } - - public void sendChat() { - String message = this.chatBuffer.toString().trim(); - if (!message.isBlank() && !message.equals("/")) { - try { - this.messageTransceiver.send(new PlayerChatMessage(this.playerId, message)); - } catch (IOException e) { - e.printStackTrace(); - } - } - this.setChatting(false); - } - - public String getCurrentChatBuffer() { - return this.chatBuffer.toString(); - } - public void shutdown() { + this.chatManager.unbindTransceiver(); + System.out.println("Chat manager shutdown."); this.messageTransceiver.shutdown(); + System.out.println("Message transceiver shutdown."); this.renderer.shutdown(); + System.out.println("Renderer shutdown."); + this.soundManager.close(); + System.out.println("Sound manager closed."); } diff --git a/client/src/main/java/nl/andrewlalis/aos_client/DataTransceiver.java b/client/src/main/java/nl/andrewlalis/aos_client/DataTransceiver.java new file mode 100644 index 0000000..eeb10c3 --- /dev/null +++ b/client/src/main/java/nl/andrewlalis/aos_client/DataTransceiver.java @@ -0,0 +1,74 @@ +package nl.andrewlalis.aos_client; + +import nl.andrewlalis.aos_core.net.data.DataTypes; +import nl.andrewlalis.aos_core.net.data.PlayerDetailUpdate; +import nl.andrewlalis.aos_core.net.data.WorldUpdate; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.SocketException; +import java.nio.ByteBuffer; + +public class DataTransceiver extends Thread { + private final Client client; + private final DatagramSocket socket; + + private volatile boolean running; + + public DataTransceiver(Client client) throws SocketException { + this.client = client; + this.socket = new DatagramSocket(); + } + + public int getLocalPort() { + return this.socket.getLocalPort(); + } + + public void send(byte[] bytes, InetAddress address, int port) throws IOException { + if (this.socket.isClosed()) return; + var packet = new DatagramPacket(bytes, bytes.length, address, port); + this.socket.send(packet); + } + + public void shutdown() { + this.running = false; + if (!this.socket.isClosed()) { + this.socket.close(); + } + } + + @Override + public void run() { + this.running = true; + System.out.println("Datagram socket opened on " + this.socket.getLocalAddress() + ":" + this.socket.getLocalPort()); + byte[] buffer = new byte[1400]; + DatagramPacket packet = new DatagramPacket(buffer, buffer.length); + while (this.running) { + try { + this.socket.receive(packet); + ByteBuffer b = ByteBuffer.wrap(packet.getData(), 0, packet.getLength()); + byte type = b.get(); + if (type == DataTypes.WORLD_DATA) { + byte[] worldData = new byte[b.remaining()]; + b.get(worldData); + WorldUpdate update = WorldUpdate.fromBytes(worldData); + this.client.updateWorld(update); + } else if (type == DataTypes.PLAYER_DETAIL) { + byte[] detailData = new byte[b.remaining()]; + b.get(detailData); + PlayerDetailUpdate update = PlayerDetailUpdate.fromBytes(detailData); + this.client.updatePlayer(update); + } + } catch (SocketException e) { + if (!e.getMessage().equals("Socket closed")) { + e.printStackTrace(); + } + this.shutdown(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} diff --git a/client/src/main/java/nl/andrewlalis/aos_client/GameRenderer.java b/client/src/main/java/nl/andrewlalis/aos_client/GameRenderer.java index 7ac1933..6773726 100644 --- a/client/src/main/java/nl/andrewlalis/aos_client/GameRenderer.java +++ b/client/src/main/java/nl/andrewlalis/aos_client/GameRenderer.java @@ -35,7 +35,7 @@ public class GameRenderer extends Thread { long now = System.currentTimeMillis(); long msSinceLastFrame = now - lastFrame; if (msSinceLastFrame >= MS_PER_FRAME) { - double elapsedSeconds = msSinceLastFrame / 1000.0; + float elapsedSeconds = msSinceLastFrame / 1000.0f; this.gamePanel.repaint(); this.updateWorld(elapsedSeconds); lastFrame = now; @@ -52,7 +52,7 @@ public class GameRenderer extends Thread { } } - private void updateWorld(double t) { + private void updateWorld(float t) { World world = this.client.getWorld(); for (Player p : world.getPlayers().values()) { p.setPosition(p.getPosition().add(p.getVelocity().mul(t))); diff --git a/client/src/main/java/nl/andrewlalis/aos_client/MessageTransceiver.java b/client/src/main/java/nl/andrewlalis/aos_client/MessageTransceiver.java index 2472f12..503b662 100644 --- a/client/src/main/java/nl/andrewlalis/aos_client/MessageTransceiver.java +++ b/client/src/main/java/nl/andrewlalis/aos_client/MessageTransceiver.java @@ -1,12 +1,15 @@ package nl.andrewlalis.aos_client; -import nl.andrewlalis.aos_core.model.World; import nl.andrewlalis.aos_core.net.*; import nl.andrewlalis.aos_core.net.chat.ChatMessage; import java.io.*; +import java.net.InetAddress; import java.net.Socket; import java.net.SocketException; +import java.nio.ByteBuffer; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; /** * This thread is responsible for handling TCP message communication with the @@ -15,61 +18,84 @@ import java.net.SocketException; public class MessageTransceiver extends Thread { private final Client client; - private Socket socket; - private ObjectOutputStream out; - private ObjectInputStream in; + private final Socket socket; + private final DataTransceiver dataTransceiver; + private final ObjectOutputStream out; + private final ObjectInputStream in; + private final ExecutorService writeService = Executors.newFixedThreadPool(1); private volatile boolean running = true; - public MessageTransceiver(Client client) { + public MessageTransceiver(Client client, String serverHost, int serverPort, String username) throws IOException { this.client = client; - } - - public void connectToServer(String serverHost, int serverPort, String username) throws IOException { this.socket = new Socket(serverHost, serverPort); + this.dataTransceiver = new DataTransceiver(client); this.out = new ObjectOutputStream(this.socket.getOutputStream()); this.in = new ObjectInputStream(this.socket.getInputStream()); - this.send(new IdentMessage(username)); + this.send(new IdentMessage(username, this.dataTransceiver.getLocalPort())); System.out.println("Sent identification packet."); } public void shutdown() { this.running = false; - if (this.socket != null) { - try { - this.socket.close(); - } catch (IOException e) { - e.printStackTrace(); - } + this.dataTransceiver.shutdown(); + this.writeService.shutdown(); + try { + this.out.close(); + this.in.close(); + this.socket.close(); + } catch (IOException e) { + e.printStackTrace(); } } - public synchronized void send(Message message) throws IOException { - this.out.reset(); - this.out.writeObject(message); + public void send(Message message) { + if (this.socket.isClosed()) return; + this.writeService.submit(() -> { + try { + this.out.reset(); + this.out.writeObject(message); + } catch (IOException e) { + e.printStackTrace(); + } + }); + } + + public void sendData(byte type, int playerId, byte[] data) throws IOException { + ByteBuffer buffer = ByteBuffer.allocate(1 + Integer.BYTES + data.length); + buffer.put(type); + buffer.putInt(playerId); + buffer.put(data); + this.dataTransceiver.send(buffer.array(), this.socket.getInetAddress(), this.socket.getPort()); } @Override public void run() { + this.dataTransceiver.start(); while (this.running) { try { Message msg = (Message) this.in.readObject(); if (msg.getType() == Type.PLAYER_REGISTERED) { + System.out.println("Received player registration response from server."); PlayerRegisteredMessage prm = (PlayerRegisteredMessage) msg; - this.client.initPlayerData(prm.getPlayerId()); + this.client.setPlayer(prm.getPlayer()); + this.client.setWorld(prm.getWorld()); } else if (msg.getType() == Type.CHAT) { - this.client.addChatMessage((ChatMessage) msg); - } else if (msg.getType() == Type.WORLD_UPDATE) { - World world = ((WorldUpdateMessage) msg).getWorld(); - this.client.setWorld(world); + this.client.getChatManager().addChatMessage((ChatMessage) msg); + } else if (msg.getType() == Type.PLAYER_JOINED && this.client.getWorld() != null) { + PlayerUpdateMessage pum = (PlayerUpdateMessage) msg; + this.client.getWorld().getPlayers().put(pum.getPlayer().getId(), pum.getPlayer()); + } else if (msg.getType() == Type.PLAYER_LEFT && this.client.getWorld() != null) { + PlayerUpdateMessage pum = (PlayerUpdateMessage) msg; + this.client.getWorld().getPlayers().remove(pum.getPlayer().getId()); } } catch (StreamCorruptedException | EOFException e) { - e.printStackTrace(); - this.running = false; + this.shutdown(); } catch (SocketException e) { if (!e.getMessage().equalsIgnoreCase("Socket closed")) { e.printStackTrace(); } + this.shutdown(); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } diff --git a/client/src/main/java/nl/andrewlalis/aos_client/SoundManager.java b/client/src/main/java/nl/andrewlalis/aos_client/SoundManager.java index cff5895..1020f18 100644 --- a/client/src/main/java/nl/andrewlalis/aos_client/SoundManager.java +++ b/client/src/main/java/nl/andrewlalis/aos_client/SoundManager.java @@ -1,54 +1,82 @@ package nl.andrewlalis.aos_client; +import nl.andrewlalis.aos_core.net.data.Sound; + import javax.sound.sampled.*; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; +import java.io.*; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; public class SoundManager { - private final Map soundData = new HashMap<>(); + private static final int CLIP_COUNT = 10; + private final Map> soundData = new HashMap<>(); + private final Map clipIndexes = new HashMap<>(); - public void play(String sound) { - var clip = this.getClip(sound); - if (clip != null) { - clip.start(); + public void play(List sounds) { + for (Sound sound : sounds) { + this.play(sound); } } + public void play(Sound sound) { + var clip = this.getClip(sound.getType().getSoundName()); + if (clip == null) { + return; + } + clip.setFramePosition(0); + setVolume(clip, sound.getVolume()); + clip.start(); + } + + private void setVolume(Clip clip, float volume) { + volume = Math.max(Math.min(volume, 1.0f), 0.0f); + FloatControl gainControl = (FloatControl) clip.getControl(FloatControl.Type.MASTER_GAIN); + gainControl.setValue(20f * (float) Math.log10(volume)); + } + private Clip getClip(String sound) { - var soundBytes = this.soundData.get(sound); - if (soundBytes == null) { + var clips = this.soundData.get(sound); + if (clips == null) { InputStream is = Client.class.getResourceAsStream("/nl/andrewlalis/aos_client/sound/" + sound); if (is == null) { System.err.println("Could not load sound: " + sound); return null; } - ByteArrayOutputStream bos = new ByteArrayOutputStream(); try { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); is.transferTo(bos); - soundBytes = bos.toByteArray(); - this.soundData.put(sound, soundBytes); - } catch (IOException e) { + byte[] data = bos.toByteArray(); + clips = new ArrayList<>(CLIP_COUNT); + for (int i = 0; i < CLIP_COUNT; i++) { + var ais = AudioSystem.getAudioInputStream(new ByteArrayInputStream(data)); + var clip = AudioSystem.getClip(); + clip.open(ais); + ais.close(); + clips.add(clip); + } + this.soundData.put(sound, clips); + this.clipIndexes.put(sound, 0); + } catch (UnsupportedAudioFileException | LineUnavailableException | IOException e) { e.printStackTrace(); return null; } } - try { - var ais = AudioSystem.getAudioInputStream(new ByteArrayInputStream(soundBytes)); - var clip = AudioSystem.getClip(); - clip.addLineListener(event -> { - if (event.getType() == LineEvent.Type.STOP) { - clip.close(); - } - }); - clip.open(ais); - return clip; - } catch (UnsupportedAudioFileException | IOException | LineUnavailableException e) { - e.printStackTrace(); - return null; + int index = this.clipIndexes.get(sound); + if (index >= CLIP_COUNT) { + index = 0; + } + Clip clip = clips.get(index); + this.clipIndexes.put(sound, index + 1); + return clip; + } + + public void close() { + for (var c : this.soundData.values()) { + for (var clip : c) { + clip.close(); + } } } } diff --git a/client/src/main/java/nl/andrewlalis/aos_client/Tester.java b/client/src/main/java/nl/andrewlalis/aos_client/Tester.java index bf48292..86cfee8 100644 --- a/client/src/main/java/nl/andrewlalis/aos_client/Tester.java +++ b/client/src/main/java/nl/andrewlalis/aos_client/Tester.java @@ -9,12 +9,10 @@ public class Tester { }; public static void main(String[] args) { - for (int i = 0; i < 2; i++) { - Client client = new Client(); + for (int i = 0; i < 6; i++) { try { - client.connect("localhost", 8035, names[ThreadLocalRandom.current().nextInt(names.length)]); - } catch (IOException | ClassNotFoundException e) { - client.shutdown(); + new Client("localhost", 8035, names[ThreadLocalRandom.current().nextInt(names.length)]); + } catch (IOException e) { e.printStackTrace(); } } diff --git a/client/src/main/java/nl/andrewlalis/aos_client/control/PlayerKeyListener.java b/client/src/main/java/nl/andrewlalis/aos_client/control/PlayerKeyListener.java index 6f52e09..e4e9222 100644 --- a/client/src/main/java/nl/andrewlalis/aos_client/control/PlayerKeyListener.java +++ b/client/src/main/java/nl/andrewlalis/aos_client/control/PlayerKeyListener.java @@ -1,5 +1,6 @@ package nl.andrewlalis.aos_client.control; +import nl.andrewlalis.aos_client.ChatManager; import nl.andrewlalis.aos_client.Client; import java.awt.event.KeyAdapter; @@ -7,36 +8,38 @@ import java.awt.event.KeyEvent; public class PlayerKeyListener extends KeyAdapter { private final Client client; + private final ChatManager chatManager; public PlayerKeyListener(Client client) { this.client = client; + this.chatManager = client.getChatManager(); } @Override public void keyTyped(KeyEvent e) { - if (!this.client.isChatting()) { + if (!this.chatManager.isChatting()) { if ((e.getKeyChar() == 't' || e.getKeyChar() == '/')) { - this.client.setChatting(true); - if (e.getKeyChar() == '/') this.client.appendToChat('/'); + this.chatManager.setChatting(true); + if (e.getKeyChar() == '/') this.chatManager.appendToChat('/'); } - } else if (this.client.isChatting()) { + } else if (this.chatManager.isChatting()) { char c = e.getKeyChar(); if (c >= ' ' && c <= '~') { - this.client.appendToChat(c); + this.chatManager.appendToChat(c); } else if (e.getKeyChar() == 8) { - this.client.backspaceChat(); + this.chatManager.backspaceChat(); } else if (e.getKeyChar() == 10) { - this.client.sendChat(); + this.chatManager.sendChat(); } else if (e.getKeyChar() == 27) { - this.client.setChatting(false); + this.chatManager.setChatting(false); } } } @Override public void keyPressed(KeyEvent e) { - if (client.isChatting()) return; - var state = client.getPlayerState(); + if (this.chatManager.isChatting()) return; + var state = client.getPlayer().getState(); if (e.getKeyCode() == KeyEvent.VK_W) { state.setMovingForward(true); } else if (e.getKeyCode() == KeyEvent.VK_S) { @@ -53,8 +56,8 @@ public class PlayerKeyListener extends KeyAdapter { @Override public void keyReleased(KeyEvent e) { - if (client.isChatting()) return; - var state = client.getPlayerState(); + if (this.chatManager.isChatting()) return; + var state = client.getPlayer().getState(); if (e.getKeyCode() == KeyEvent.VK_W) { state.setMovingForward(false); } else if (e.getKeyCode() == KeyEvent.VK_S) { diff --git a/client/src/main/java/nl/andrewlalis/aos_client/control/PlayerMouseListener.java b/client/src/main/java/nl/andrewlalis/aos_client/control/PlayerMouseListener.java index 2cdc89b..3248491 100644 --- a/client/src/main/java/nl/andrewlalis/aos_client/control/PlayerMouseListener.java +++ b/client/src/main/java/nl/andrewlalis/aos_client/control/PlayerMouseListener.java @@ -9,9 +9,14 @@ import java.awt.event.MouseEvent; import java.awt.event.MouseWheelEvent; public class PlayerMouseListener extends MouseInputAdapter { + private static final float MOUSE_UPDATES_PER_SECOND = 30.0f; + private static final long MS_PER_MOUSE_UPDATE = (long) (1000.0f / MOUSE_UPDATES_PER_SECOND); + private final Client client; private final GamePanel gamePanel; + private long lastMouseMove = 0L; + public PlayerMouseListener(Client client, GamePanel gamePanel) { this.client = client; this.gamePanel = gamePanel; @@ -20,7 +25,7 @@ public class PlayerMouseListener extends MouseInputAdapter { @Override public void mousePressed(MouseEvent e) { if (e.getButton() == MouseEvent.BUTTON1) { - client.getPlayerState().setShooting(true); + client.getPlayer().getState().setShooting(true); client.sendPlayerState(); } } @@ -28,7 +33,7 @@ public class PlayerMouseListener extends MouseInputAdapter { @Override public void mouseReleased(MouseEvent e) { if (e.getButton() == MouseEvent.BUTTON1) { - client.getPlayerState().setShooting(false); + client.getPlayer().getState().setShooting(false); client.sendPlayerState(); } } @@ -44,18 +49,26 @@ public class PlayerMouseListener extends MouseInputAdapter { @Override public void mouseMoved(MouseEvent e) { - Vec2 c = new Vec2(this.gamePanel.getWidth() / 2.0, this.gamePanel.getHeight() / 2.0); + Vec2 c = new Vec2(this.gamePanel.getWidth() / 2.0f, this.gamePanel.getHeight() / 2.0f); Vec2 centeredMouseLocation = new Vec2(e.getX(), e.getY()).sub(c); - client.getPlayerState().setMouseLocation(centeredMouseLocation); - client.sendPlayerState(); + client.getPlayer().getState().setMouseLocation(centeredMouseLocation); + long now = System.currentTimeMillis(); + if (now - this.lastMouseMove > MS_PER_MOUSE_UPDATE) { + client.sendPlayerState(); + this.lastMouseMove = now; + } } @Override public void mouseDragged(MouseEvent e) { - Vec2 c = new Vec2(this.gamePanel.getWidth() / 2.0, this.gamePanel.getHeight() / 2.0); + Vec2 c = new Vec2(this.gamePanel.getWidth() / 2.0f, this.gamePanel.getHeight() / 2.0f); Vec2 centeredMouseLocation = new Vec2(e.getX(), e.getY()).sub(c); - client.getPlayerState().setMouseLocation(centeredMouseLocation); - client.getPlayerState().setShooting(true); - client.sendPlayerState(); + client.getPlayer().getState().setMouseLocation(centeredMouseLocation); + client.getPlayer().getState().setShooting(true); + long now = System.currentTimeMillis(); + if (now - this.lastMouseMove > MS_PER_MOUSE_UPDATE) { + client.sendPlayerState(); + this.lastMouseMove = now; + } } } diff --git a/client/src/main/java/nl/andrewlalis/aos_client/view/ConnectDialog.java b/client/src/main/java/nl/andrewlalis/aos_client/view/ConnectDialog.java index b00515c..4b6ae4e 100644 --- a/client/src/main/java/nl/andrewlalis/aos_client/view/ConnectDialog.java +++ b/client/src/main/java/nl/andrewlalis/aos_client/view/ConnectDialog.java @@ -77,6 +77,9 @@ public class ConnectDialog extends JDialog { if (usernameField.getText() == null || usernameField.getText().isBlank()) { warnings.add("Username must not be empty."); } + if (usernameField.getText() != null && usernameField.getText().length() > 16) { + warnings.add("Username is too long."); + } if (addressField.getText() != null && !addressPattern.matcher(addressField.getText()).matches()) { warnings.add("Address must be in the form HOST:PORT."); } @@ -97,11 +100,9 @@ public class ConnectDialog extends JDialog { String host = parts[0].trim(); int port = Integer.parseInt(parts[1]); String username = usernameField.getText(); - Client client = new Client(); try { - client.connect(host, port, username); - } catch (IOException | ClassNotFoundException ex) { - client.shutdown(); + new Client(host, port, username); + } catch (IOException ex) { ex.printStackTrace(); JOptionPane.showMessageDialog(null, "Could not connect:\n" + ex.getMessage(), "Connection Error", JOptionPane.WARNING_MESSAGE); } diff --git a/client/src/main/java/nl/andrewlalis/aos_client/view/GamePanel.java b/client/src/main/java/nl/andrewlalis/aos_client/view/GamePanel.java index fcedac6..3b3d5bd 100644 --- a/client/src/main/java/nl/andrewlalis/aos_client/view/GamePanel.java +++ b/client/src/main/java/nl/andrewlalis/aos_client/view/GamePanel.java @@ -56,7 +56,7 @@ public class GamePanel extends JPanel { } private void drawWorld(Graphics2D g2, World world) { - Player myPlayer = world.getPlayers().get(this.client.getPlayerId()); + Player myPlayer = client.getPlayer(); if (myPlayer == null) return; double scale = this.scales[this.scaleIndex]; AffineTransform pre = g2.getTransform(); @@ -195,7 +195,8 @@ public class GamePanel extends JPanel { private void drawChat(Graphics2D g2, World world) { int height = g2.getFontMetrics().getHeight(); int y = height; - for (ChatMessage message : this.client.getLatestChatMessages()) { + var cm = this.client.getChatManager(); + for (ChatMessage message : cm.getLatestChatMessages()) { Color color = Color.WHITE; String text = message.getText(); if (message instanceof SystemChatMessage sysMsg) { @@ -219,14 +220,14 @@ public class GamePanel extends JPanel { y += height; } - if (this.client.isChatting()) { + if (cm.isChatting()) { g2.setColor(Color.WHITE); - g2.drawString("> " + this.client.getCurrentChatBuffer(), 5, height * 11); + g2.drawString("> " + cm.getCurrentChatBuffer(), 5, height * 11); } } private void drawStatus(Graphics2D g2, World world) { - Player myPlayer = world.getPlayers().get(this.client.getPlayerId()); + Player myPlayer = this.client.getPlayer(); if (myPlayer == null) return; g2.setColor(Color.WHITE); diff --git a/core/src/main/java/module-info.java b/core/src/main/java/module-info.java index 4f0b725..41bd9ae 100644 --- a/core/src/main/java/module-info.java +++ b/core/src/main/java/module-info.java @@ -1,8 +1,13 @@ module aos_core { requires java.desktop; + exports nl.andrewlalis.aos_core.net to aos_server, aos_client; - exports nl.andrewlalis.aos_core.model to aos_server, aos_client; - exports nl.andrewlalis.aos_core.geom to aos_server, aos_client; exports nl.andrewlalis.aos_core.net.chat to aos_client, aos_server; + exports nl.andrewlalis.aos_core.net.data to aos_server, aos_client; + + exports nl.andrewlalis.aos_core.model to aos_server, aos_client; exports nl.andrewlalis.aos_core.model.tools to aos_client, aos_server; + + exports nl.andrewlalis.aos_core.geom to aos_server, aos_client; + exports nl.andrewlalis.aos_core.util to aos_server, aos_client; } \ No newline at end of file diff --git a/core/src/main/java/nl/andrewlalis/aos_core/geom/Vec2.java b/core/src/main/java/nl/andrewlalis/aos_core/geom/Vec2.java index 9b53958..991bdff 100644 --- a/core/src/main/java/nl/andrewlalis/aos_core/geom/Vec2.java +++ b/core/src/main/java/nl/andrewlalis/aos_core/geom/Vec2.java @@ -1,13 +1,15 @@ package nl.andrewlalis.aos_core.geom; +import java.io.DataInputStream; +import java.io.IOException; import java.io.Serializable; import java.util.Random; import java.util.concurrent.ThreadLocalRandom; -public record Vec2(double x, double y) implements Serializable { +public record Vec2(float x, float y) implements Serializable { - public double mag() { - return Math.sqrt(x * x + y * y); + public float mag() { + return (float) Math.sqrt(x * x + y * y); } public Vec2 add(Vec2 other) { @@ -18,16 +20,16 @@ public record Vec2(double x, double y) implements Serializable { return new Vec2(this.x - other.x, this.y - other.y); } - public Vec2 mul(double factor) { + public Vec2 mul(float factor) { return new Vec2(this.x * factor, this.y * factor); } public Vec2 unit() { - double mag = this.mag(); + float mag = this.mag(); return new Vec2(this.x / mag, this.y / mag); } - public double dot(Vec2 other) { + public float dot(Vec2 other) { return this.x * other.x + this.y * other.y; } @@ -39,14 +41,14 @@ public record Vec2(double x, double y) implements Serializable { return new Vec2(this.y, -this.x); } - public double dist(Vec2 other) { + public float dist(Vec2 other) { return other.sub(this).mag(); } public Vec2 rotate(double theta) { return new Vec2( - this.x * Math.cos(theta) - this.y * Math.sin(theta), - this.x * Math.sin(theta) + this.y * Math.cos(theta) + (float) (this.x * Math.cos(theta) - this.y * Math.sin(theta)), + (float) (this.x * Math.sin(theta) + this.y * Math.cos(theta)) ); } @@ -59,10 +61,14 @@ public record Vec2(double x, double y) implements Serializable { return "[ " + x + ", " + y + " ]"; } - public static Vec2 random(double min, double max) { + public static Vec2 random(float min, float max) { Random r = ThreadLocalRandom.current(); - double x = r.nextDouble() * (max - min) + min; - double y = r.nextDouble() * (max - min) + min; + float x = r.nextFloat() * (max - min) + min; + float y = r.nextFloat() * (max - min) + min; return new Vec2(x, y); } + + public static Vec2 read(DataInputStream in) throws IOException { + return new Vec2(in.readFloat(), in.readFloat()); + } } diff --git a/core/src/main/java/nl/andrewlalis/aos_core/model/Barricade.java b/core/src/main/java/nl/andrewlalis/aos_core/model/Barricade.java index e694a24..c499699 100644 --- a/core/src/main/java/nl/andrewlalis/aos_core/model/Barricade.java +++ b/core/src/main/java/nl/andrewlalis/aos_core/model/Barricade.java @@ -21,7 +21,7 @@ public class Barricade implements Serializable { this.size = size; } - public Barricade(double x, double y, double w, double h) { + public Barricade(float x, float y, float w, float h) { this(new Vec2(x, y), new Vec2(w, h)); } diff --git a/core/src/main/java/nl/andrewlalis/aos_core/model/Bullet.java b/core/src/main/java/nl/andrewlalis/aos_core/model/Bullet.java index e723a6a..d4204fa 100644 --- a/core/src/main/java/nl/andrewlalis/aos_core/model/Bullet.java +++ b/core/src/main/java/nl/andrewlalis/aos_core/model/Bullet.java @@ -13,17 +13,21 @@ public class Bullet extends PhysicsObject { public Bullet(Player player) { this.playerId = player.getId(); this.setPosition(player.getPosition() - .add(player.getOrientation().mul(1.5)) + .add(player.getOrientation().mul(1.5f)) .add(player.getOrientation().perp().mul(Player.RADIUS)) ); this.setOrientation(player.getOrientation()); - - Random r = ThreadLocalRandom.current(); - Vec2 perturbation = new Vec2((r.nextDouble() - 0.5) * 2, (r.nextDouble() - 0.5) * 2).mul(player.getGun().getAccuracy()); + Vec2 perturbation = Vec2.random(-1, 1).mul(player.getGun().getAccuracy()); this.setVelocity(this.getOrientation().add(perturbation).mul(player.getGun().getBulletSpeed())); this.gun = player.getGun(); } + public Bullet(Vec2 position, Vec2 velocity) { + super(position, new Vec2(0, -1), velocity); + this.playerId = -1; + this.gun = null; + } + public int getPlayerId() { return playerId; } diff --git a/core/src/main/java/nl/andrewlalis/aos_core/model/Player.java b/core/src/main/java/nl/andrewlalis/aos_core/model/Player.java index c69b698..3cfdf69 100644 --- a/core/src/main/java/nl/andrewlalis/aos_core/model/Player.java +++ b/core/src/main/java/nl/andrewlalis/aos_core/model/Player.java @@ -6,9 +6,9 @@ import nl.andrewlalis.aos_core.model.tools.Gun; import java.util.Objects; public class Player extends PhysicsObject implements Comparable { - public static final double MOVEMENT_SPEED = 10; // Movement speed, in m/s - public static final double RADIUS = 0.5; // Collision radius, in meters. - public static final double RESUPPLY_COOLDOWN = 30; // Seconds between allowing resupply. + public static final float MOVEMENT_SPEED = 10; // Movement speed, in m/s + public static final float RADIUS = 0.5f; // Collision radius, in meters. + public static final float RESUPPLY_COOLDOWN = 30; // Seconds between allowing resupply. public static final float MAX_HEALTH = 100.0f; private final int id; @@ -28,10 +28,10 @@ public class Player extends PhysicsObject implements Comparable { this.name = name; this.team = team; this.state = new PlayerControlState(); - this.state.setPlayerId(this.id); - this.gun = Gun.winchester(); + this.gun = Gun.ak47(); this.health = MAX_HEALTH; this.useWeapon(); + this.lastShot = System.currentTimeMillis(); } public int getId() { @@ -66,8 +66,12 @@ public class Player extends PhysicsObject implements Comparable { this.gun = gun; } - public long getLastShot() { - return lastShot; + public void setHealth(float health) { + this.health = health; + } + + public void setReloading(boolean reloading) { + this.reloading = reloading; } public boolean canUseWeapon() { @@ -75,7 +79,7 @@ public class Player extends PhysicsObject implements Comparable { !this.state.isReloading() && !this.reloading && this.gun.getCurrentClipBulletCount() > 0 && - this.lastShot + this.gun.getShotCooldownTime() * 1000 < System.currentTimeMillis() && + this.lastShot + ((long) (this.gun.getShotCooldownTime() * 1000)) < System.currentTimeMillis() && (this.getTeam() == null || this.getTeam().getSpawnPoint().dist(this.getPosition()) > Team.SPAWN_RADIUS); } diff --git a/core/src/main/java/nl/andrewlalis/aos_core/model/PlayerControlState.java b/core/src/main/java/nl/andrewlalis/aos_core/model/PlayerControlState.java index cc9aa7e..4f7620b 100644 --- a/core/src/main/java/nl/andrewlalis/aos_core/model/PlayerControlState.java +++ b/core/src/main/java/nl/andrewlalis/aos_core/model/PlayerControlState.java @@ -1,12 +1,12 @@ package nl.andrewlalis.aos_core.model; import nl.andrewlalis.aos_core.geom.Vec2; +import nl.andrewlalis.aos_core.net.data.DataTypes; import java.io.Serializable; +import java.nio.ByteBuffer; public class PlayerControlState implements Serializable { - private int playerId; - boolean movingLeft; boolean movingRight; boolean movingForward; @@ -17,14 +17,6 @@ public class PlayerControlState implements Serializable { Vec2 mouseLocation; - public int getPlayerId() { - return playerId; - } - - public void setPlayerId(int playerId) { - this.playerId = playerId; - } - public boolean isMovingLeft() { return movingLeft; } @@ -80,4 +72,46 @@ public class PlayerControlState implements Serializable { public void setMouseLocation(Vec2 mouseLocation) { this.mouseLocation = mouseLocation; } + + @Override + public String toString() { + return "PlayerControlState{" + + "movingLeft=" + movingLeft + + ", movingRight=" + movingRight + + ", movingForward=" + movingForward + + ", movingBackward=" + movingBackward + + ", shooting=" + shooting + + ", reloading=" + reloading + + ", mouseLocation=" + mouseLocation + + '}'; + } + + public byte[] toBytes() { + ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES + 2 * Float.BYTES); + int flags = 0; + if (this.movingLeft) flags |= 1; + if (this.movingRight) flags |= 2; + if (this.movingForward) flags |= 4; + if (this.movingBackward) flags |= 8; + if (this.shooting) flags |= 16; + if (this.reloading) flags |= 32; + buffer.putInt(flags); + buffer.putFloat(this.mouseLocation.x()); + buffer.putFloat(this.mouseLocation.y()); + return buffer.array(); + } + + public static PlayerControlState fromBytes(byte[] bytes) { + var s = new PlayerControlState(); + ByteBuffer buffer = ByteBuffer.wrap(bytes); + int flags = buffer.getInt(); + s.movingLeft = (flags & 1) > 0; + s.movingRight = (flags & 2) > 0; + s.movingForward = (flags & 4) > 0; + s.movingBackward = (flags & 8) > 0; + s.shooting = (flags & 16) > 0; + s.reloading = (flags & 32) > 0; + s.mouseLocation = new Vec2(buffer.getFloat(), buffer.getFloat()); + return s; + } } diff --git a/core/src/main/java/nl/andrewlalis/aos_core/model/Team.java b/core/src/main/java/nl/andrewlalis/aos_core/model/Team.java index 3222d0d..cf77a20 100644 --- a/core/src/main/java/nl/andrewlalis/aos_core/model/Team.java +++ b/core/src/main/java/nl/andrewlalis/aos_core/model/Team.java @@ -8,8 +8,8 @@ import java.util.ArrayList; import java.util.List; public class Team implements Serializable { - public static final double SPAWN_RADIUS = 3; - public static final double SUPPLY_POINT_RADIUS = 2; + public static final float SPAWN_RADIUS = 3; + public static final float SUPPLY_POINT_RADIUS = 2; private final String name; private final java.awt.Color color; diff --git a/core/src/main/java/nl/andrewlalis/aos_core/model/World.java b/core/src/main/java/nl/andrewlalis/aos_core/model/World.java index f9ce69d..f972c9e 100644 --- a/core/src/main/java/nl/andrewlalis/aos_core/model/World.java +++ b/core/src/main/java/nl/andrewlalis/aos_core/model/World.java @@ -7,6 +7,8 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; /** * The main game world, consisting of all players and other objects in the game. @@ -24,8 +26,8 @@ public class World implements Serializable { public World(Vec2 size) { this.size = size; this.teams = new ArrayList<>(); - this.players = new HashMap<>(); - this.bullets = new ArrayList<>(); + this.players = new ConcurrentHashMap<>(); + this.bullets = new CopyOnWriteArrayList<>(); this.barricades = new ArrayList<>(); this.soundsToPlay = new ArrayList<>(); } diff --git a/core/src/main/java/nl/andrewlalis/aos_core/model/tools/Gun.java b/core/src/main/java/nl/andrewlalis/aos_core/model/tools/Gun.java index 0808a22..392efd5 100644 --- a/core/src/main/java/nl/andrewlalis/aos_core/model/tools/Gun.java +++ b/core/src/main/java/nl/andrewlalis/aos_core/model/tools/Gun.java @@ -24,27 +24,27 @@ public class Gun implements Serializable { /** * How accurate shots from this gun are. */ - private final double accuracy; + private final float accuracy; /** * How long (in seconds) to wait after each shot, before another is shot. */ - private final double shotCooldownTime; + private final float shotCooldownTime; /** * How long (in seconds) for reloading a new clip. */ - private final double reloadTime; + private final float reloadTime; /** * How fast the bullet travels (in m/s). */ - private final double bulletSpeed; + private final float bulletSpeed; /** * How much damage the bullet does for a direct hit. */ - private final double baseDamage; + private final float baseDamage; /** * Number of bullets left in the current clip. @@ -55,7 +55,7 @@ public class Gun implements Serializable { */ private int clipCount; - private Gun(GunType type, int maxClipCount, int clipSize, int bulletsPerRound, double accuracy, double shotCooldownTime, double reloadTime, double bulletSpeed, double baseDamage) { + private Gun(GunType type, int maxClipCount, int clipSize, int bulletsPerRound, float accuracy, float shotCooldownTime, float reloadTime, float bulletSpeed, float baseDamage) { this.type = type; this.maxClipCount = maxClipCount; this.clipSize = clipSize; @@ -86,23 +86,23 @@ public class Gun implements Serializable { return bulletsPerRound; } - public double getAccuracy() { + public float getAccuracy() { return accuracy; } - public double getShotCooldownTime() { + public float getShotCooldownTime() { return shotCooldownTime; } - public double getReloadTime() { + public float getReloadTime() { return reloadTime; } - public double getBulletSpeed() { + public float getBulletSpeed() { return bulletSpeed; } - public double getBaseDamage() { + public float getBaseDamage() { return baseDamage; } @@ -137,15 +137,32 @@ public class Gun implements Serializable { } } + /** + * Helper method to obtain a "dummy" gun for client-side rendering. + * TODO: Improve cleanliness so this isn't necessary. + * @param type The type of gun. + * @return The gun. + */ + public static Gun forType(GunType type) { + return new Gun(type, -1, -1, -1, -1, -1, -1, -1, -1); + } + + public static Gun forType(GunType type, int maxClipCount, int clipSize, int bulletsPerRound, int currentClipBulletCount, int clipCount) { + Gun g = new Gun(type, maxClipCount, clipSize, bulletsPerRound, -1, -1, -1, -1, -1); + g.currentClipBulletCount = currentClipBulletCount; + g.clipCount = clipCount; + return g; + } + public static Gun ak47() { - return new Gun(GunType.SMG, 4, 30, 1, 0.10, 0.05, 1.2, 90, 40); + return new Gun(GunType.SMG, 4, 30, 1, 0.10f, 0.05f, 1.2f, 90, 40); } public static Gun m1Garand() { - return new Gun(GunType.RIFLE, 6, 8, 1, 0.02, 0.75, 1.5, 150, 100); + return new Gun(GunType.RIFLE, 6, 8, 1, 0.02f, 0.75f, 1.5f, 150, 100); } public static Gun winchester() { - return new Gun(GunType.SHOTGUN, 8, 4, 3, 0.15, 0.5, 2.0, 75, 60); + return new Gun(GunType.SHOTGUN, 8, 4, 3, 0.15f, 0.5f, 2.0f, 75, 60); } } diff --git a/core/src/main/java/nl/andrewlalis/aos_core/model/tools/GunType.java b/core/src/main/java/nl/andrewlalis/aos_core/model/tools/GunType.java index 48daf97..24cbca9 100644 --- a/core/src/main/java/nl/andrewlalis/aos_core/model/tools/GunType.java +++ b/core/src/main/java/nl/andrewlalis/aos_core/model/tools/GunType.java @@ -1,7 +1,24 @@ package nl.andrewlalis.aos_core.model.tools; public enum GunType { - SHOTGUN, - SMG, - RIFLE + SHOTGUN(0), + SMG(1), + RIFLE(2); + + private final byte code; + + GunType(int code) { + this.code = (byte) code; + } + + public byte getCode() { + return code; + } + + public static GunType get(byte code) { + for (var val : values()) { + if (val.code == code) return val; + } + return null; + } } diff --git a/core/src/main/java/nl/andrewlalis/aos_core/net/IdentMessage.java b/core/src/main/java/nl/andrewlalis/aos_core/net/IdentMessage.java index 647c865..050ea15 100644 --- a/core/src/main/java/nl/andrewlalis/aos_core/net/IdentMessage.java +++ b/core/src/main/java/nl/andrewlalis/aos_core/net/IdentMessage.java @@ -3,12 +3,22 @@ package nl.andrewlalis.aos_core.net; public class IdentMessage extends Message { private final String name; - public IdentMessage(String name) { + /** + * The port that the client will use to send and receive UDP packets. + */ + private final int udpPort; + + public IdentMessage(String name, int udpPort) { super(Type.IDENT); this.name = name; + this.udpPort = udpPort; } public String getName() { return name; } + + public int getUdpPort() { + return this.udpPort; + } } diff --git a/core/src/main/java/nl/andrewlalis/aos_core/net/PlayerRegisteredMessage.java b/core/src/main/java/nl/andrewlalis/aos_core/net/PlayerRegisteredMessage.java index 6dfbbce..bcc4491 100644 --- a/core/src/main/java/nl/andrewlalis/aos_core/net/PlayerRegisteredMessage.java +++ b/core/src/main/java/nl/andrewlalis/aos_core/net/PlayerRegisteredMessage.java @@ -1,14 +1,23 @@ package nl.andrewlalis.aos_core.net; -public class PlayerRegisteredMessage extends Message { - private final int playerId; +import nl.andrewlalis.aos_core.model.Player; +import nl.andrewlalis.aos_core.model.World; - public PlayerRegisteredMessage(int playerId) { +public class PlayerRegisteredMessage extends Message { + private final Player player; + private final World world; + + public PlayerRegisteredMessage(Player player, World world) { super(Type.PLAYER_REGISTERED); - this.playerId = playerId; + this.player = player; + this.world = world; } - public int getPlayerId() { - return playerId; + public Player getPlayer() { + return player; + } + + public World getWorld() { + return world; } } diff --git a/core/src/main/java/nl/andrewlalis/aos_core/net/PlayerUpdateMessage.java b/core/src/main/java/nl/andrewlalis/aos_core/net/PlayerUpdateMessage.java new file mode 100644 index 0000000..86099f5 --- /dev/null +++ b/core/src/main/java/nl/andrewlalis/aos_core/net/PlayerUpdateMessage.java @@ -0,0 +1,16 @@ +package nl.andrewlalis.aos_core.net; + +import nl.andrewlalis.aos_core.model.Player; + +public class PlayerUpdateMessage extends Message { + private final Player player; + + public PlayerUpdateMessage(Type type, Player player) { + super(type); + this.player = player; + } + + public Player getPlayer() { + return player; + } +} diff --git a/core/src/main/java/nl/andrewlalis/aos_core/net/Type.java b/core/src/main/java/nl/andrewlalis/aos_core/net/Type.java index a298e47..7c7c976 100644 --- a/core/src/main/java/nl/andrewlalis/aos_core/net/Type.java +++ b/core/src/main/java/nl/andrewlalis/aos_core/net/Type.java @@ -2,9 +2,10 @@ package nl.andrewlalis.aos_core.net; public enum Type { IDENT, - ACK, PLAYER_REGISTERED, CHAT, PLAYER_CONTROL_STATE, - WORLD_UPDATE + PLAYER_JOINED, + PLAYER_LEFT, + PLAYER_TEAM_CHANGE } diff --git a/core/src/main/java/nl/andrewlalis/aos_core/net/WorldUpdateMessage.java b/core/src/main/java/nl/andrewlalis/aos_core/net/WorldUpdateMessage.java deleted file mode 100644 index 6c29931..0000000 --- a/core/src/main/java/nl/andrewlalis/aos_core/net/WorldUpdateMessage.java +++ /dev/null @@ -1,15 +0,0 @@ -package nl.andrewlalis.aos_core.net; - -import nl.andrewlalis.aos_core.model.World; - -public class WorldUpdateMessage extends Message { - private final World world; - public WorldUpdateMessage(World world) { - super(Type.WORLD_UPDATE); - this.world = world; - } - - public World getWorld() { - return world; - } -} diff --git a/core/src/main/java/nl/andrewlalis/aos_core/net/data/BulletUpdate.java b/core/src/main/java/nl/andrewlalis/aos_core/net/data/BulletUpdate.java new file mode 100644 index 0000000..549cd5f --- /dev/null +++ b/core/src/main/java/nl/andrewlalis/aos_core/net/data/BulletUpdate.java @@ -0,0 +1,50 @@ +package nl.andrewlalis.aos_core.net.data; + +import nl.andrewlalis.aos_core.geom.Vec2; +import nl.andrewlalis.aos_core.model.Bullet; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +public class BulletUpdate { + public static final int BYTES = 4 * Float.BYTES; + + private final Vec2 position; + private final Vec2 velocity; + public BulletUpdate(Bullet bullet) { + this.position = bullet.getPosition(); + this.velocity = bullet.getVelocity(); + } + + private BulletUpdate(Vec2 position, Vec2 velocity) { + this.position = position; + this.velocity = velocity; + } + + public Vec2 getPosition() { + return position; + } + + public Vec2 getVelocity() { + return velocity; + } + + public Bullet toBullet() { + return new Bullet(this.position, this.velocity); + } + + public void write(DataOutputStream out) throws IOException { + out.writeFloat(this.position.x()); + out.writeFloat(this.position.y()); + out.writeFloat(this.velocity.x()); + out.writeFloat(this.velocity.y()); + } + + public static BulletUpdate read(DataInputStream in) throws IOException { + return new BulletUpdate( + Vec2.read(in), + Vec2.read(in) + ); + } +} diff --git a/core/src/main/java/nl/andrewlalis/aos_core/net/data/DataTypes.java b/core/src/main/java/nl/andrewlalis/aos_core/net/data/DataTypes.java new file mode 100644 index 0000000..fe0c899 --- /dev/null +++ b/core/src/main/java/nl/andrewlalis/aos_core/net/data/DataTypes.java @@ -0,0 +1,7 @@ +package nl.andrewlalis.aos_core.net.data; + +public class DataTypes { + public static final byte PLAYER_CONTROL_STATE = 1; + public static final byte WORLD_DATA = 2; + public static final byte PLAYER_DETAIL = 3; +} diff --git a/core/src/main/java/nl/andrewlalis/aos_core/net/data/PlayerDetailUpdate.java b/core/src/main/java/nl/andrewlalis/aos_core/net/data/PlayerDetailUpdate.java new file mode 100644 index 0000000..5c52915 --- /dev/null +++ b/core/src/main/java/nl/andrewlalis/aos_core/net/data/PlayerDetailUpdate.java @@ -0,0 +1,105 @@ +package nl.andrewlalis.aos_core.net.data; + +import nl.andrewlalis.aos_core.model.Player; + +import java.nio.ByteBuffer; + +public class PlayerDetailUpdate { + public static final int BYTES = Float.BYTES + 1 + 5 * Integer.BYTES; + + private final float health; + private final boolean reloading; + + private final int gunMaxClipCount; + private final int gunClipSize; + private final int gunBulletsPerRound; + private final int gunCurrentClipBulletCount; + private final int gunClipCount; + + public PlayerDetailUpdate(Player player) { + this.health = player.getHealth(); + this.reloading = player.isReloading(); + + this.gunMaxClipCount = player.getGun().getMaxClipCount(); + this.gunClipSize = player.getGun().getClipSize(); + this.gunBulletsPerRound = player.getGun().getBulletsPerRound(); + this.gunCurrentClipBulletCount = player.getGun().getCurrentClipBulletCount(); + this.gunClipCount = player.getGun().getClipCount(); + } + + private PlayerDetailUpdate(float health, boolean reloading, int gunMaxClipCount, int gunClipSize, int gunBulletsPerRound, int gunCurrentClipBulletCount, int gunClipCount) { + this.health = health; + this.reloading = reloading; + this.gunMaxClipCount = gunMaxClipCount; + this.gunClipSize = gunClipSize; + this.gunBulletsPerRound = gunBulletsPerRound; + this.gunCurrentClipBulletCount = gunCurrentClipBulletCount; + this.gunClipCount = gunClipCount; + } + + public float getHealth() { + return health; + } + + public boolean isReloading() { + return reloading; + } + + public int getGunMaxClipCount() { + return gunMaxClipCount; + } + + public int getGunClipSize() { + return gunClipSize; + } + + public int getGunBulletsPerRound() { + return gunBulletsPerRound; + } + + public int getGunCurrentClipBulletCount() { + return gunCurrentClipBulletCount; + } + + public int getGunClipCount() { + return gunClipCount; + } + + @Override + public String toString() { + return "PlayerDetailUpdate{" + + "health=" + health + + ", reloading=" + reloading + + ", gunMaxClipCount=" + gunMaxClipCount + + ", gunClipSize=" + gunClipSize + + ", gunBulletsPerRound=" + gunBulletsPerRound + + ", gunCurrentClipBulletCount=" + gunCurrentClipBulletCount + + ", gunClipCount=" + gunClipCount + + '}'; + } + + public byte[] toBytes() { + ByteBuffer buffer = ByteBuffer.allocate(BYTES); + buffer.putFloat(health); + buffer.put((byte) (this.reloading ? 1 : 0)); + buffer.putInt(this.gunMaxClipCount); + buffer.putInt(this.gunClipSize); + buffer.putInt(this.gunBulletsPerRound); + buffer.putInt(this.gunCurrentClipBulletCount); + buffer.putInt(this.gunClipCount); + return buffer.array(); + } + + public static PlayerDetailUpdate fromBytes(byte[] bytes) { + ByteBuffer buffer = ByteBuffer.wrap(bytes); + return new PlayerDetailUpdate( + buffer.getFloat(), + buffer.get() == 1, + buffer.getInt(), + buffer.getInt(), + buffer.getInt(), + buffer.getInt(), + buffer.getInt() + ); + } +} diff --git a/core/src/main/java/nl/andrewlalis/aos_core/net/data/PlayerUpdate.java b/core/src/main/java/nl/andrewlalis/aos_core/net/data/PlayerUpdate.java new file mode 100644 index 0000000..d5e6d01 --- /dev/null +++ b/core/src/main/java/nl/andrewlalis/aos_core/net/data/PlayerUpdate.java @@ -0,0 +1,76 @@ +package nl.andrewlalis.aos_core.net.data; + +import nl.andrewlalis.aos_core.geom.Vec2; +import nl.andrewlalis.aos_core.model.Player; +import nl.andrewlalis.aos_core.model.tools.GunType; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +public class PlayerUpdate { + public static final int BYTES = Integer.BYTES + 6 * Float.BYTES + 1; + + private final int id; + private final Vec2 position; + private final Vec2 orientation; + private final Vec2 velocity; + private final GunType gunType; + + public PlayerUpdate(Player player) { + this.id = player.getId(); + this.position = player.getPosition(); + this.orientation = player.getOrientation(); + this.velocity = player.getVelocity(); + this.gunType = player.getGun().getType(); + } + + public PlayerUpdate(int id, Vec2 position, Vec2 orientation, Vec2 velocity, GunType gunType) { + this.id = id; + this.position = position; + this.orientation = orientation; + this.velocity = velocity; + this.gunType = gunType; + } + + public int getId() { + return id; + } + + public Vec2 getPosition() { + return position; + } + + public Vec2 getOrientation() { + return orientation; + } + + public Vec2 getVelocity() { + return velocity; + } + + public GunType getGunType() { + return gunType; + } + + public void write(DataOutputStream out) throws IOException { + out.writeInt(this.id); + out.writeFloat(this.position.x()); + out.writeFloat(this.position.y()); + out.writeFloat(this.orientation.x()); + out.writeFloat(this.orientation.y()); + out.writeFloat(this.velocity.x()); + out.writeFloat(this.velocity.y()); + out.writeByte(this.gunType.getCode()); + } + + public static PlayerUpdate read(DataInputStream in) throws IOException { + return new PlayerUpdate( + in.readInt(), + Vec2.read(in), + Vec2.read(in), + Vec2.read(in), + GunType.get(in.readByte()) + ); + } +} diff --git a/core/src/main/java/nl/andrewlalis/aos_core/net/data/Sound.java b/core/src/main/java/nl/andrewlalis/aos_core/net/data/Sound.java new file mode 100644 index 0000000..4913d9d --- /dev/null +++ b/core/src/main/java/nl/andrewlalis/aos_core/net/data/Sound.java @@ -0,0 +1,48 @@ +package nl.andrewlalis.aos_core.net.data; + +import nl.andrewlalis.aos_core.geom.Vec2; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +public class Sound { + public static final int BYTES = 3 * Float.BYTES + 1; + + private final Vec2 position; + private final float volume; + private final SoundType type; + + public Sound(Vec2 position, float volume, SoundType type) { + this.position = position; + this.volume = volume; + this.type = type; + } + + public Vec2 getPosition() { + return position; + } + + public float getVolume() { + return volume; + } + + public SoundType getType() { + return this.type; + } + + public void write(DataOutputStream out) throws IOException { + out.writeFloat(this.position.x()); + out.writeFloat(this.position.y()); + out.writeFloat(this.volume); + out.writeByte(this.type.getCode()); + } + + public static Sound read(DataInputStream in) throws IOException { + return new Sound( + Vec2.read(in), + in.readFloat(), + SoundType.get(in.readByte()) + ); + } +} diff --git a/core/src/main/java/nl/andrewlalis/aos_core/net/data/SoundType.java b/core/src/main/java/nl/andrewlalis/aos_core/net/data/SoundType.java new file mode 100644 index 0000000..285ccc1 --- /dev/null +++ b/core/src/main/java/nl/andrewlalis/aos_core/net/data/SoundType.java @@ -0,0 +1,49 @@ +package nl.andrewlalis.aos_core.net.data; + +import java.util.HashMap; +import java.util.Map; + +/** + * Encoding of all server-initiated game sounds with a unique byte value, for + * efficient transmission to clients. + */ +public enum SoundType { + SHOT_SMG(0, "ak47shot1.wav"), + SHOT_RIFLE(1, "m1garand-shot1.wav"), + SHOT_SHOTGUN(2, "shotgun-shot1.wav"), + RELOAD(3, "reload.wav"), + CHAT(4, "chat.wav"), + DEATH(5, "death.wav"), + BULLET_IMPACT_1(6, "bullet_impact_1.wav"), + BULLET_IMPACT_2(7, "bullet_impact_2.wav"), + BULLET_IMPACT_3(8, "bullet_impact_3.wav"), + BULLET_IMPACT_4(9, "bullet_impact_4.wav"), + BULLET_IMPACT_5(10, "bullet_impact_5.wav"); + + private final byte code; + private final String soundName; + + SoundType(int code, String soundName) { + this.code = (byte) code; + this.soundName = soundName; + } + + public byte getCode() { + return code; + } + + public String getSoundName() { + return soundName; + } + + private static final Map typeIndex = new HashMap<>(); + static { + for (var val : values()) { + typeIndex.put(val.getCode(), val); + } + } + + public static SoundType get(byte code) { + return typeIndex.get(code); + } +} diff --git a/core/src/main/java/nl/andrewlalis/aos_core/net/data/WorldUpdate.java b/core/src/main/java/nl/andrewlalis/aos_core/net/data/WorldUpdate.java new file mode 100644 index 0000000..b59adf4 --- /dev/null +++ b/core/src/main/java/nl/andrewlalis/aos_core/net/data/WorldUpdate.java @@ -0,0 +1,112 @@ +package nl.andrewlalis.aos_core.net.data; + +import nl.andrewlalis.aos_core.model.Bullet; +import nl.andrewlalis.aos_core.model.Player; + +import java.io.*; +import java.util.ArrayList; +import java.util.List; + +/** + * The minimal data that's sent to each client after every game tick. This + * contains the most basic information on updates to bullets, object movement, + * and sounds that need to be played, and other simple things. + *

+ * This update doesn't contain all data about players and the world, and + * this extra data is sent periodically to keep clients up-to-date without + * sending too much data. + *

+ */ +public class WorldUpdate { + private final List playerUpdates; + private final List bulletUpdates; + private final List soundsToPlay; + + public WorldUpdate() { + this(new ArrayList<>(), new ArrayList<>(), new ArrayList<>()); + } + + private WorldUpdate(List playerUpdates, List bulletUpdates, List soundsToPlay) { + this.playerUpdates = playerUpdates; + this.bulletUpdates = bulletUpdates; + this.soundsToPlay = soundsToPlay; + } + + public void clear() { + this.playerUpdates.clear(); + this.bulletUpdates.clear(); + this.soundsToPlay.clear(); + } + + public void addPlayer(Player p) { + this.playerUpdates.add(new PlayerUpdate(p)); + } + + public void addBullet(Bullet b) { + this.bulletUpdates.add(new BulletUpdate(b)); + } + + public void addSound(Sound sound) { + this.soundsToPlay.add(sound); + } + + public List getPlayerUpdates() { + return playerUpdates; + } + + public List getBulletUpdates() { + return bulletUpdates; + } + + public List getSoundsToPlay() { + return soundsToPlay; + } + + public byte[] toBytes() throws IOException { + int size = 3 * Integer.BYTES + // List size integers. + this.playerUpdates.size() * PlayerUpdate.BYTES + + this.bulletUpdates.size() * BulletUpdate.BYTES + + this.soundsToPlay.size() * Sound.BYTES; + ByteArrayOutputStream out = new ByteArrayOutputStream(size); + DataOutputStream dataOut = new DataOutputStream(out); + dataOut.writeInt(this.playerUpdates.size()); + for (var u : this.playerUpdates) { + u.write(dataOut); + } + dataOut.writeInt(this.bulletUpdates.size()); + for (var u : this.bulletUpdates) { + u.write(dataOut); + } + dataOut.writeInt(this.soundsToPlay.size()); + for (var u : this.soundsToPlay) { + u.write(dataOut); + } + byte[] data = out.toByteArray(); + dataOut.close(); + return data; + } + + public static WorldUpdate fromBytes(byte[] data) throws IOException { + ByteArrayInputStream in = new ByteArrayInputStream(data); + DataInputStream dataIn = new DataInputStream(in); + + int players = dataIn.readInt(); + List playerUpdates = new ArrayList<>(players); + for (int i = 0; i < players; i++) { + playerUpdates.add(PlayerUpdate.read(dataIn)); + } + int bullets = dataIn.readInt(); + List bulletUpdates = new ArrayList<>(bullets); + for (int i = 0; i < bullets; i++) { + bulletUpdates.add(BulletUpdate.read(dataIn)); + } + int sounds = dataIn.readInt(); + List soundsToPlay = new ArrayList<>(sounds); + for (int i = 0; i < sounds; i++) { + soundsToPlay.add(Sound.read(dataIn)); + } + var obj = new WorldUpdate(playerUpdates, bulletUpdates, soundsToPlay); + dataIn.close(); + return obj; + } +} diff --git a/core/src/main/java/nl/andrewlalis/aos_core/util/ByteUtils.java b/core/src/main/java/nl/andrewlalis/aos_core/util/ByteUtils.java index 1214f38..f27d38a 100644 --- a/core/src/main/java/nl/andrewlalis/aos_core/util/ByteUtils.java +++ b/core/src/main/java/nl/andrewlalis/aos_core/util/ByteUtils.java @@ -45,4 +45,11 @@ public class ByteUtils { if (n != length) throw new IOException("Could not read enough bytes to read string."); return new String(strBytes, StandardCharsets.UTF_8); } + + public static byte[] prefix(byte pre, byte[] data) { + byte[] full = new byte[data.length + 1]; + full[0] = pre; + System.arraycopy(data, 0, full, 1, data.length); + return full; + } } diff --git a/server/src/main/java/nl/andrewlalis/aos_server/ChatManager.java b/server/src/main/java/nl/andrewlalis/aos_server/ChatManager.java new file mode 100644 index 0000000..231eeb9 --- /dev/null +++ b/server/src/main/java/nl/andrewlalis/aos_server/ChatManager.java @@ -0,0 +1,44 @@ +package nl.andrewlalis.aos_server; + +import nl.andrewlalis.aos_core.model.Player; +import nl.andrewlalis.aos_core.net.chat.ChatMessage; +import nl.andrewlalis.aos_core.net.chat.PlayerChatMessage; +import nl.andrewlalis.aos_server.command.ResetCommand; +import nl.andrewlalis.aos_server.command.chat.ChatCommand; +import nl.andrewlalis.aos_server.command.chat.GunCommand; + +import java.util.Arrays; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * This chat manager is responsible for dealing with incoming player chats and + * potentially executing commands, or simply relaying the chats on to the rest + * of the players. + */ +public class ChatManager { + private final Server server; + private final Map chatCommands; + + public ChatManager(Server server) { + this.server = server; + this.chatCommands = new ConcurrentHashMap<>(); + this.chatCommands.put("gun", new GunCommand()); + this.chatCommands.put("reset", new ResetCommand(server)); + } + + public void handlePlayerChat(ClientHandler handler, Player player, ChatMessage msg) { + if (player == null) return; + if (msg.getText().startsWith("/")) { + String[] words = msg.getText().substring(1).split("\\s+"); + if (words.length == 0) return; + String command = words[0]; + ChatCommand cmd = this.chatCommands.get(command); + if (cmd != null) { + cmd.execute(handler, player, Arrays.copyOfRange(words, 1, words.length)); + } + } else { + this.server.broadcastMessage(new PlayerChatMessage(player.getId(), msg.getText())); + } + } +} diff --git a/server/src/main/java/nl/andrewlalis/aos_server/ClientHandler.java b/server/src/main/java/nl/andrewlalis/aos_server/ClientHandler.java index fb69e58..5931a06 100644 --- a/server/src/main/java/nl/andrewlalis/aos_server/ClientHandler.java +++ b/server/src/main/java/nl/andrewlalis/aos_server/ClientHandler.java @@ -1,15 +1,19 @@ package nl.andrewlalis.aos_server; +import nl.andrewlalis.aos_core.model.Player; import nl.andrewlalis.aos_core.net.IdentMessage; import nl.andrewlalis.aos_core.net.Message; -import nl.andrewlalis.aos_core.net.PlayerControlStateMessage; +import nl.andrewlalis.aos_core.net.PlayerRegisteredMessage; import nl.andrewlalis.aos_core.net.Type; import nl.andrewlalis.aos_core.net.chat.ChatMessage; +import java.io.EOFException; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; +import java.net.InetAddress; import java.net.Socket; +import java.net.SocketException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -23,29 +27,41 @@ public class ClientHandler extends Thread { private final ObjectOutputStream out; private final ObjectInputStream in; - private int playerId; + private Player player; + private final InetAddress clientAddress; + private int clientUdpPort = -1; private volatile boolean running = true; public ClientHandler(Server server, Socket socket) throws IOException { this.server = server; this.socket = socket; + this.clientAddress = this.socket.getInetAddress(); this.out = new ObjectOutputStream(socket.getOutputStream()); this.in = new ObjectInputStream(socket.getInputStream()); } - public int getPlayerId() { - return playerId; + public Player getPlayer() { + return player; + } + + public InetAddress getClientAddress() { + return clientAddress; + } + + public int getClientUdpPort() { + return clientUdpPort; } public void shutdown() { this.running = false; + this.sendingQueue.shutdown(); try { this.in.close(); this.out.close(); this.socket.close(); } catch (IOException e) { - System.err.println("Could not close streams when shutting down client handler for player " + this.playerId + ": " + e.getMessage()); + System.err.println("Could not close streams when shutting down client handler for player " + this.player.getId() + ": " + e.getMessage()); } } @@ -63,29 +79,29 @@ public class ClientHandler extends Thread { @Override public void run() { - try { - while (this.running) { - try { - Message msg = (Message) this.in.readObject(); - if (msg.getType() == Type.IDENT) { - IdentMessage ident = (IdentMessage) msg; - this.playerId = this.server.registerNewPlayer(ident.getName(), this); - } else if (msg.getType() == Type.CHAT) { - this.server.handlePlayerChat(this, this.playerId, (ChatMessage) msg); - } else if (msg.getType() == Type.PLAYER_CONTROL_STATE) { - this.server.updatePlayerState(((PlayerControlStateMessage) msg).getPlayerControlState()); - } - } catch (ClassNotFoundException e) { + while (this.running) { + try { + Message msg = (Message) this.in.readObject(); + if (msg.getType() == Type.IDENT) { + IdentMessage ident = (IdentMessage) msg; + this.player = this.server.registerNewPlayer(ident.getName()); + this.clientUdpPort = ident.getUdpPort(); + this.send(new PlayerRegisteredMessage(this.player, this.server.getWorld())); + } else if (msg.getType() == Type.CHAT) { + this.server.getChatManager().handlePlayerChat(this, this.player, (ChatMessage) msg); + } + } catch (SocketException e) { + if (e.getMessage().equals("Socket closed")) { + this.shutdown(); + } else { e.printStackTrace(); } + } catch (EOFException e) { + this.shutdown(); + } catch (IOException | ClassNotFoundException e) { + e.printStackTrace(); + this.shutdown(); } - } catch (IOException e) { - // Ignore this exception, consider the client disconnected. - } - try { - this.socket.close(); - } catch (IOException e) { - e.printStackTrace(); } this.server.clientDisconnected(this); } diff --git a/server/src/main/java/nl/andrewlalis/aos_server/DataTransceiver.java b/server/src/main/java/nl/andrewlalis/aos_server/DataTransceiver.java new file mode 100644 index 0000000..1e9a299 --- /dev/null +++ b/server/src/main/java/nl/andrewlalis/aos_server/DataTransceiver.java @@ -0,0 +1,66 @@ +package nl.andrewlalis.aos_server; + +import nl.andrewlalis.aos_core.model.PlayerControlState; +import nl.andrewlalis.aos_core.net.data.DataTypes; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.SocketException; +import java.nio.ByteBuffer; + +public class DataTransceiver extends Thread { + private final DatagramSocket socket; + private final Server server; + + private volatile boolean running; + + public DataTransceiver(Server server, int port) throws SocketException { + this.socket = new DatagramSocket(port); + this.server = server; + } + + public void send(byte[] bytes, InetAddress address, int port) { + DatagramPacket packet = new DatagramPacket(bytes, bytes.length, address, port); + try { + this.socket.send(packet); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void shutdown() { + this.running = false; + if (!this.socket.isClosed()) { + this.socket.close(); + } + } + + @Override + public void run() { + this.running = true; + byte[] buffer = new byte[1400]; + DatagramPacket packet = new DatagramPacket(buffer, buffer.length); + while (this.running) { + try { + this.socket.receive(packet); + ByteBuffer b = ByteBuffer.wrap(packet.getData(), 0, packet.getLength()); + byte type = b.get(); + int playerId = b.getInt(); + if (type == DataTypes.PLAYER_CONTROL_STATE) { + if (playerId < 1) continue; + byte[] stateBuffer = new byte[b.remaining()]; + b.get(stateBuffer); + this.server.updatePlayerState(playerId, PlayerControlState.fromBytes(stateBuffer)); + } + } catch (SocketException e) { + if (!e.getMessage().equals("Socket closed")) { + e.printStackTrace(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} diff --git a/server/src/main/java/nl/andrewlalis/aos_server/Server.java b/server/src/main/java/nl/andrewlalis/aos_server/Server.java index 39a7e45..5d861d7 100644 --- a/server/src/main/java/nl/andrewlalis/aos_server/Server.java +++ b/server/src/main/java/nl/andrewlalis/aos_server/Server.java @@ -2,20 +2,20 @@ package nl.andrewlalis.aos_server; import nl.andrewlalis.aos_core.geom.Vec2; import nl.andrewlalis.aos_core.model.*; -import nl.andrewlalis.aos_core.model.tools.Gun; import nl.andrewlalis.aos_core.net.Message; -import nl.andrewlalis.aos_core.net.PlayerRegisteredMessage; -import nl.andrewlalis.aos_core.net.WorldUpdateMessage; -import nl.andrewlalis.aos_core.net.chat.ChatMessage; -import nl.andrewlalis.aos_core.net.chat.PlayerChatMessage; +import nl.andrewlalis.aos_core.net.PlayerUpdateMessage; +import nl.andrewlalis.aos_core.net.Type; import nl.andrewlalis.aos_core.net.chat.SystemChatMessage; +import nl.andrewlalis.aos_core.net.data.DataTypes; +import nl.andrewlalis.aos_core.net.data.PlayerDetailUpdate; +import nl.andrewlalis.aos_core.net.data.WorldUpdate; +import nl.andrewlalis.aos_core.util.ByteUtils; import java.awt.*; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; -import java.util.Arrays; import java.util.List; import java.util.Scanner; import java.util.concurrent.CopyOnWriteArrayList; @@ -26,19 +26,23 @@ public class Server { private final List clientHandlers; private final ServerSocket serverSocket; + private final DataTransceiver dataTransceiver; private final World world; private final WorldUpdater worldUpdater; private final ServerCli cli; + private final ChatManager chatManager; private volatile boolean running; public Server(int port) throws IOException { this.clientHandlers = new CopyOnWriteArrayList<>(); this.serverSocket = new ServerSocket(port); + this.dataTransceiver = new DataTransceiver(this, port); this.cli = new ServerCli(this); this.world = new World(new Vec2(50, 70)); this.initWorld(); this.worldUpdater = new WorldUpdater(this, this.world); + this.chatManager = new ChatManager(this); } private void initWorld() { @@ -68,6 +72,10 @@ public class Server { return world; } + public ChatManager getChatManager() { + return chatManager; + } + public void acceptClientConnection() { try { Socket socket = this.serverSocket.accept(); @@ -82,7 +90,7 @@ public class Server { } } - public int registerNewPlayer(String name, ClientHandler handler) { + public Player registerNewPlayer(String name) { int id = ThreadLocalRandom.current().nextInt(1, Integer.MAX_VALUE); Team team = null; for (Team t : this.world.getTeams()) { @@ -94,11 +102,10 @@ public class Server { } Player p = new Player(id, name, team); this.world.getPlayers().put(p.getId(), p); - handler.send(new PlayerRegisteredMessage(id)); String message = p.getName() + " connected."; this.broadcastMessage(new SystemChatMessage(SystemChatMessage.Level.INFO, message)); System.out.println(message); - p.setPosition(new Vec2(this.world.getSize().x() / 2.0, this.world.getSize().y() / 2.0)); + p.setPosition(new Vec2(this.world.getSize().x() / 2.0f, this.world.getSize().y() / 2.0f)); if (team != null) { team.getPlayers().add(p); p.setPosition(team.getSpawnPoint()); @@ -106,11 +113,12 @@ public class Server { message = name + " joined team " + team.getName() + "."; this.broadcastMessage(new SystemChatMessage(SystemChatMessage.Level.INFO, message)); } - return id; + this.broadcastMessage(new PlayerUpdateMessage(Type.PLAYER_JOINED, p)); + return p; } public void clientDisconnected(ClientHandler clientHandler) { - Player player = this.world.getPlayers().get(clientHandler.getPlayerId()); + Player player = clientHandler.getPlayer(); this.clientHandlers.remove(clientHandler); clientHandler.shutdown(); this.world.getPlayers().remove(player.getId()); @@ -120,25 +128,37 @@ public class Server { String message = player.getName() + " disconnected."; this.broadcastMessage(new SystemChatMessage(SystemChatMessage.Level.INFO, message)); System.out.println(message); + this.broadcastMessage(new PlayerUpdateMessage(Type.PLAYER_LEFT, player)); } public void kickPlayer(Player player) { for (ClientHandler handler : this.clientHandlers) { - if (handler.getPlayerId() == player.getId()) { + if (handler.getPlayer().getId() == player.getId()) { handler.shutdown(); return; } } } - public void sendWorldToClients() { - for (ClientHandler handler : this.clientHandlers) { - handler.send(new WorldUpdateMessage(this.world)); + public void sendWorldUpdate(WorldUpdate update) { + try { + byte[] data = update.toBytes(); + byte[] finalData = new byte[data.length + 1]; + finalData[0] = DataTypes.WORLD_DATA; + System.arraycopy(data, 0, finalData, 1, data.length); + for (ClientHandler handler : this.clientHandlers) { + if (handler.getClientUdpPort() == -1) continue; + this.dataTransceiver.send(finalData, handler.getClientAddress(), handler.getClientUdpPort()); + byte[] detailData = ByteUtils.prefix(DataTypes.PLAYER_DETAIL, new PlayerDetailUpdate(handler.getPlayer()).toBytes()); + this.dataTransceiver.send(detailData, handler.getClientAddress(), handler.getClientUdpPort()); + } + } catch (IOException e) { + e.printStackTrace(); } } - public void updatePlayerState(PlayerControlState state) { - Player p = this.world.getPlayers().get(state.getPlayerId()); + public void updatePlayerState(int playerId, PlayerControlState state) { + Player p = this.world.getPlayers().get(playerId); if (p != null) { p.setState(state); } @@ -151,6 +171,7 @@ public class Server { p.respawn(); } } + broadcastMessage(new SystemChatMessage(SystemChatMessage.Level.INFO, "Game has been reset.")); } public void broadcastMessage(Message message) { @@ -159,40 +180,6 @@ public class Server { } } - public void handlePlayerChat(ClientHandler handler, int playerId, ChatMessage msg) { - Player p = this.world.getPlayers().get(playerId); - if (p == null) return; - if (msg.getText().startsWith("/")) { - String[] words = msg.getText().substring(1).split("\\s+"); - if (words.length == 0) return; - String command = words[0]; - String[] args = Arrays.copyOfRange(words, 1, words.length); - this.handleCommand(handler, p, command, args); - } else { - this.broadcastMessage(new PlayerChatMessage(p.getId(), msg.getText())); - } - } - - public void handleCommand(ClientHandler handler, Player player, String command, String[] args) { - if (command.equalsIgnoreCase("gun")) { - if (args.length < 1) { - return; - } - String gunName = args[0]; - if (gunName.equalsIgnoreCase("smg")) { - player.setGun(Gun.ak47()); - } else if (gunName.equalsIgnoreCase("rifle")) { - player.setGun(Gun.m1Garand()); - } else if (gunName.equalsIgnoreCase("shotgun")) { - player.setGun(Gun.winchester()); - } - handler.send(new SystemChatMessage(SystemChatMessage.Level.INFO, "Changed gun to " + player.getGun().getType().name() + ".")); - } else if (command.equalsIgnoreCase("reset")) { - this.resetGame(); - this.broadcastMessage(new SystemChatMessage(SystemChatMessage.Level.INFO, "Game has been reset.")); - } - } - public void shutdown() { this.running = false; try { @@ -207,17 +194,21 @@ public class Server { public void run() { this.running = true; + this.dataTransceiver.start(); this.worldUpdater.start(); this.cli.start(); System.out.println("Started AOS-Server TCP on port " + this.serverSocket.getLocalPort() + "; now accepting connections."); while (this.running) { this.acceptClientConnection(); } + this.shutdown(); System.out.println("Stopped accepting new client connections."); this.worldUpdater.shutdown(); System.out.println("Stopped world updater."); this.cli.shutdown(); System.out.println("Stopped CLI interface."); + this.dataTransceiver.shutdown(); + System.out.println("Stopped data transceiver."); } diff --git a/server/src/main/java/nl/andrewlalis/aos_server/WorldUpdater.java b/server/src/main/java/nl/andrewlalis/aos_server/WorldUpdater.java index 02c92f4..49319b7 100644 --- a/server/src/main/java/nl/andrewlalis/aos_server/WorldUpdater.java +++ b/server/src/main/java/nl/andrewlalis/aos_server/WorldUpdater.java @@ -4,6 +4,9 @@ import nl.andrewlalis.aos_core.geom.Vec2; import nl.andrewlalis.aos_core.model.*; import nl.andrewlalis.aos_core.model.tools.GunType; import nl.andrewlalis.aos_core.net.chat.SystemChatMessage; +import nl.andrewlalis.aos_core.net.data.Sound; +import nl.andrewlalis.aos_core.net.data.SoundType; +import nl.andrewlalis.aos_core.net.data.WorldUpdate; import java.util.ArrayList; import java.util.List; @@ -15,11 +18,15 @@ public class WorldUpdater extends Thread { private final Server server; private final World world; + + private final WorldUpdate worldUpdate; + private volatile boolean running = true; public WorldUpdater(Server server, World world) { this.server = server; this.world = world; + this.worldUpdate = new WorldUpdate(); } public void shutdown() { @@ -33,7 +40,7 @@ public class WorldUpdater extends Thread { long now = System.currentTimeMillis(); long msSinceLastTick = now - lastTick; if (msSinceLastTick >= MS_PER_TICK) { - double elapsedSeconds = msSinceLastTick / 1000.0; + float elapsedSeconds = msSinceLastTick / 1000.0f; this.tick(elapsedSeconds); lastTick = now; } @@ -48,21 +55,22 @@ public class WorldUpdater extends Thread { } } - private void tick(double t) { - world.getSoundsToPlay().clear(); + private void tick(float t) { + this.worldUpdate.clear(); this.updateBullets(t); this.updatePlayers(t); - this.server.sendWorldToClients(); + this.server.sendWorldUpdate(this.worldUpdate); } - private void updatePlayers(double t) { + private void updatePlayers(float t) { for (Player p : this.world.getPlayers().values()) { this.updatePlayerMovement(p, t); this.updatePlayerShooting(p); + this.worldUpdate.addPlayer(p); } } - private void updatePlayerMovement(Player p, double t) { + private void updatePlayerMovement(Player p, float t) { if (p.getState().getMouseLocation() != null && p.getState().getMouseLocation().mag() > 0) { Vec2 newOrientation = p.getState().getMouseLocation().unit(); if (p.getTeam() != null) { @@ -71,8 +79,8 @@ public class WorldUpdater extends Thread { } p.setOrientation(newOrientation); } - double vx = 0; - double vy = 0; + float vx = 0; + float vy = 0; if (p.getState().isMovingForward()) vy += Player.MOVEMENT_SPEED; if (p.getState().isMovingBackward()) vy -= Player.MOVEMENT_SPEED; if (p.getState().isMovingLeft()) vx -= Player.MOVEMENT_SPEED; @@ -83,15 +91,15 @@ public class WorldUpdater extends Thread { } Vec2 leftVector = forwardVector.perp(); Vec2 newPos = p.getPosition().add(forwardVector.mul(vy * t)).add(leftVector.mul(vx * t)); - double nx = newPos.x(); - double ny = newPos.y(); + float nx = newPos.x(); + float ny = newPos.y(); for (Barricade b : world.getBarricades()) { // TODO: Improve barricade collision smoothness. - double x1 = b.getPosition().x(); - double x2 = x1 + b.getSize().x(); - double y1 = b.getPosition().y(); - double y2 = y1 + b.getSize().y(); + float x1 = b.getPosition().x(); + float x2 = x1 + b.getSize().x(); + float y1 = b.getPosition().y(); + float y2 = y1 + b.getSize().y(); if (nx + Player.RADIUS > x1 && nx - Player.RADIUS < x2 && ny + Player.RADIUS > y1 && ny - Player.RADIUS < y2) { double distanceLeft = Math.abs(nx - x1); double distanceRight = Math.abs(nx - x2); @@ -123,15 +131,17 @@ public class WorldUpdater extends Thread { private void updatePlayerShooting(Player p) { if (p.canUseWeapon()) { for (int i = 0; i < p.getGun().getBulletsPerRound(); i++) { - this.world.getBullets().add(new Bullet(p)); + Bullet b = new Bullet(p); + this.world.getBullets().add(b); + this.worldUpdate.addBullet(b); } - String sound = "ak47shot1.wav"; + SoundType soundType = SoundType.SHOT_SMG; if (p.getGun().getType() == GunType.RIFLE) { - sound = "m1garand-shot1.wav"; + soundType = SoundType.SHOT_RIFLE; } else if (p.getGun().getType() == GunType.SHOTGUN) { - sound = "shotgun-shot1.wav"; + soundType = SoundType.SHOT_SHOTGUN; } - this.world.getSoundsToPlay().add(sound); + this.worldUpdate.addSound(new Sound(p.getPosition(), 1.0f, soundType)); p.useWeapon(); } if (p.getState().isReloading() && !p.isReloading() && p.getGun().canReload()) { @@ -139,11 +149,11 @@ public class WorldUpdater extends Thread { } if (p.isReloading() && p.isReloadingComplete()) { p.finishReloading(); - this.world.getSoundsToPlay().add("reload.wav"); + this.worldUpdate.addSound(new Sound(p.getPosition(), 1.0f, SoundType.RELOAD)); } } - private void updateBullets(double t) { + private void updateBullets(float t) { List bulletsToRemove = new ArrayList<>(); for (Bullet b : this.world.getBullets()) { Vec2 oldPos = b.getPosition(); @@ -151,26 +161,30 @@ public class WorldUpdater extends Thread { Vec2 pos = b.getPosition(); if (pos.x() < 0 || pos.y() < 0 || pos.x() > this.world.getSize().x() || pos.y() > this.world.getSize().y()) { bulletsToRemove.add(b); + continue; } + boolean removed = false; for (Barricade bar : this.world.getBarricades()) { if ( pos.x() > bar.getPosition().x() && pos.x() < bar.getPosition().x() + bar.getSize().x() && pos.y() > bar.getPosition().y() && pos.y() < bar.getPosition().y() + bar.getSize().y() ) { - int n = ThreadLocalRandom.current().nextInt(1, 6); - this.world.getSoundsToPlay().add("bullet_impact_" + n + ".wav"); + int code = ThreadLocalRandom.current().nextInt(SoundType.BULLET_IMPACT_1.getCode(), SoundType.BULLET_IMPACT_5.getCode() + 1); + this.worldUpdate.addSound(new Sound(b.getPosition(), 1.0f, SoundType.get((byte) code))); bulletsToRemove.add(b); + removed = true; break; } } + if (removed) continue; - double x1 = oldPos.x(); - double x2 = b.getPosition().x(); - double y1 = oldPos.y(); - double y2 = b.getPosition().y(); - double lineDist = oldPos.dist(b.getPosition()); + float x1 = oldPos.x(); + float x2 = b.getPosition().x(); + float y1 = oldPos.y(); + float y2 = b.getPosition().y(); + float lineDist = oldPos.dist(b.getPosition()); for (Player p : this.world.getPlayers().values()) { - double n = ((p.getPosition().x() - x1) * (x2 - x1) + (p.getPosition().y() - y1) * (y2 - y1)) / lineDist; + float n = ((p.getPosition().x() - x1) * (x2 - x1) + (p.getPosition().y() - y1) * (y2 - y1)) / lineDist; n = Math.max(Math.min(n, 1), 0); double dist = p.getPosition().dist(new Vec2(x1 + n * (x2 - x1), y1 + n * (y2 - y1))); if (dist < Player.RADIUS && (p.getTeam() == null || p.getTeam().getSpawnPoint().dist(p.getPosition()) > Team.SPAWN_RADIUS)) { @@ -181,7 +195,7 @@ public class WorldUpdater extends Thread { if (p.getHealth() == 0.0f) { Player shooter = this.world.getPlayers().get(b.getPlayerId()); this.server.broadcastMessage(new SystemChatMessage(SystemChatMessage.Level.SEVERE, p.getName() + " was shot by " + shooter.getName() + ".")); - world.getSoundsToPlay().add("death.wav"); + this.worldUpdate.addSound(new Sound(p.getPosition(), 1.0f, SoundType.DEATH)); if (shooter.getTeam() != null) { shooter.getTeam().incrementScore(); } @@ -189,6 +203,7 @@ public class WorldUpdater extends Thread { } } } + this.worldUpdate.addBullet(b); } this.world.getBullets().removeAll(bulletsToRemove); } diff --git a/server/src/main/java/nl/andrewlalis/aos_server/command/ResetCommand.java b/server/src/main/java/nl/andrewlalis/aos_server/command/ResetCommand.java index 9394298..15e2344 100644 --- a/server/src/main/java/nl/andrewlalis/aos_server/command/ResetCommand.java +++ b/server/src/main/java/nl/andrewlalis/aos_server/command/ResetCommand.java @@ -1,8 +1,11 @@ package nl.andrewlalis.aos_server.command; +import nl.andrewlalis.aos_core.model.Player; +import nl.andrewlalis.aos_server.ClientHandler; import nl.andrewlalis.aos_server.Server; +import nl.andrewlalis.aos_server.command.chat.ChatCommand; -public class ResetCommand implements Command { +public class ResetCommand implements Command, ChatCommand { private final Server server; public ResetCommand(Server server) { @@ -14,4 +17,9 @@ public class ResetCommand implements Command { this.server.resetGame(); System.out.println("Reset the game."); } + + @Override + public void execute(ClientHandler handler, Player player, String[] args) { + this.server.resetGame(); + } } diff --git a/server/src/main/java/nl/andrewlalis/aos_server/command/chat/ChatCommand.java b/server/src/main/java/nl/andrewlalis/aos_server/command/chat/ChatCommand.java new file mode 100644 index 0000000..89f967a --- /dev/null +++ b/server/src/main/java/nl/andrewlalis/aos_server/command/chat/ChatCommand.java @@ -0,0 +1,8 @@ +package nl.andrewlalis.aos_server.command.chat; + +import nl.andrewlalis.aos_core.model.Player; +import nl.andrewlalis.aos_server.ClientHandler; + +public interface ChatCommand { + void execute(ClientHandler handler, Player player, String[] args); +} diff --git a/server/src/main/java/nl/andrewlalis/aos_server/command/chat/GunCommand.java b/server/src/main/java/nl/andrewlalis/aos_server/command/chat/GunCommand.java new file mode 100644 index 0000000..65ce95f --- /dev/null +++ b/server/src/main/java/nl/andrewlalis/aos_server/command/chat/GunCommand.java @@ -0,0 +1,24 @@ +package nl.andrewlalis.aos_server.command.chat; + +import nl.andrewlalis.aos_core.model.Player; +import nl.andrewlalis.aos_core.model.tools.Gun; +import nl.andrewlalis.aos_core.net.chat.SystemChatMessage; +import nl.andrewlalis.aos_server.ClientHandler; + +public class GunCommand implements ChatCommand { + @Override + public void execute(ClientHandler handler, Player player, String[] args) { + if (args.length < 1) { + return; + } + String gunName = args[0]; + if (gunName.equalsIgnoreCase("smg")) { + player.setGun(Gun.ak47()); + } else if (gunName.equalsIgnoreCase("rifle")) { + player.setGun(Gun.m1Garand()); + } else if (gunName.equalsIgnoreCase("shotgun")) { + player.setGun(Gun.winchester()); + } + handler.send(new SystemChatMessage(SystemChatMessage.Level.INFO, "Changed gun to " + player.getGun().getType().name() + ".")); + } +}