Updated with commands, improved UDP, etc. Still need to make UDP hole-punching thing.

This commit is contained in:
Andrew Lalis 2021-06-22 08:19:14 +02:00
parent fa001996ff
commit 09f5630a0c
42 changed files with 1298 additions and 366 deletions

View File

@ -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<ChatMessage> 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();
}
}

View File

@ -3,50 +3,39 @@ package nl.andrewlalis.aos_client;
import nl.andrewlalis.aos_client.view.ConnectDialog; import nl.andrewlalis.aos_client.view.ConnectDialog;
import nl.andrewlalis.aos_client.view.GameFrame; import nl.andrewlalis.aos_client.view.GameFrame;
import nl.andrewlalis.aos_client.view.GamePanel; 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.model.World;
import nl.andrewlalis.aos_core.net.PlayerControlStateMessage; import nl.andrewlalis.aos_core.model.tools.Gun;
import nl.andrewlalis.aos_core.net.chat.ChatMessage; import nl.andrewlalis.aos_core.net.data.DataTypes;
import nl.andrewlalis.aos_core.net.chat.PlayerChatMessage; import nl.andrewlalis.aos_core.net.data.PlayerDetailUpdate;
import nl.andrewlalis.aos_core.net.data.WorldUpdate;
import java.io.IOException; 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. * The main class for the client, which connects to a server to join and play.
*/ */
public class Client { 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 World world;
private Player myPlayer;
private final List<ChatMessage> chatMessages;
private boolean chatting = false;
private final StringBuilder chatBuffer;
private final GameRenderer renderer; private final GameRenderer renderer;
private final GamePanel gamePanel; private final GamePanel gamePanel;
private final SoundManager soundManager; private final SoundManager soundManager;
private final ChatManager chatManager;
public Client() { public Client(String serverHost, int serverPort, String username) throws IOException {
this.chatMessages = new LinkedList<>();
this.chatBuffer = new StringBuilder();
this.soundManager = new SoundManager(); this.soundManager = new SoundManager();
this.chatManager = new ChatManager(this.soundManager);
this.gamePanel = new GamePanel(this); this.gamePanel = new GamePanel(this);
this.renderer = new GameRenderer(this, gamePanel); this.renderer = new GameRenderer(this, gamePanel);
} this.messageTransceiver = new MessageTransceiver(this, serverHost, serverPort, username);
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.start(); this.messageTransceiver.start();
this.chatManager.bindTransceiver(this.messageTransceiver);
while (this.playerControlState == null || this.world == null) { while (this.myPlayer == null || this.world == null) {
try { try {
System.out.println("Waiting for server response and player registration..."); System.out.println("Waiting for server response and player registration...");
Thread.sleep(100); Thread.sleep(100);
@ -61,93 +50,75 @@ public class Client {
this.renderer.start(); this.renderer.start();
} }
public ChatManager getChatManager() {
return chatManager;
}
public World getWorld() { public World getWorld() {
return world; 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) { public void setWorld(World world) {
this.world = world; this.world = world;
for (String sound : this.world.getSoundsToPlay()) {
this.soundManager.play(sound);
}
} }
public void initPlayerData(int playerId) { public void setPlayer(Player player) {
this.playerId = playerId; this.myPlayer = player;
this.playerControlState = new PlayerControlState();
this.playerControlState.setPlayerId(playerId);
} }
public int getPlayerId() { public Player getPlayer() {
return playerId; return myPlayer;
} }
public PlayerControlState getPlayerState() { public void updatePlayer(PlayerDetailUpdate update) {
return playerControlState; 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() { public void sendPlayerState() {
try { try {
this.messageTransceiver.send(new PlayerControlStateMessage(this.playerControlState)); this.messageTransceiver.sendData(DataTypes.PLAYER_CONTROL_STATE, myPlayer.getId(), myPlayer.getState().toBytes());
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); 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() { public void shutdown() {
this.chatManager.unbindTransceiver();
System.out.println("Chat manager shutdown.");
this.messageTransceiver.shutdown(); this.messageTransceiver.shutdown();
System.out.println("Message transceiver shutdown.");
this.renderer.shutdown(); this.renderer.shutdown();
System.out.println("Renderer shutdown.");
this.soundManager.close();
System.out.println("Sound manager closed.");
} }

View File

@ -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();
}
}
}
}

View File

@ -35,7 +35,7 @@ public class GameRenderer extends Thread {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
long msSinceLastFrame = now - lastFrame; long msSinceLastFrame = now - lastFrame;
if (msSinceLastFrame >= MS_PER_FRAME) { if (msSinceLastFrame >= MS_PER_FRAME) {
double elapsedSeconds = msSinceLastFrame / 1000.0; float elapsedSeconds = msSinceLastFrame / 1000.0f;
this.gamePanel.repaint(); this.gamePanel.repaint();
this.updateWorld(elapsedSeconds); this.updateWorld(elapsedSeconds);
lastFrame = now; 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(); World world = this.client.getWorld();
for (Player p : world.getPlayers().values()) { for (Player p : world.getPlayers().values()) {
p.setPosition(p.getPosition().add(p.getVelocity().mul(t))); p.setPosition(p.getPosition().add(p.getVelocity().mul(t)));

View File

@ -1,12 +1,15 @@
package nl.andrewlalis.aos_client; package nl.andrewlalis.aos_client;
import nl.andrewlalis.aos_core.model.World;
import nl.andrewlalis.aos_core.net.*; import nl.andrewlalis.aos_core.net.*;
import nl.andrewlalis.aos_core.net.chat.ChatMessage; import nl.andrewlalis.aos_core.net.chat.ChatMessage;
import java.io.*; import java.io.*;
import java.net.InetAddress;
import java.net.Socket; import java.net.Socket;
import java.net.SocketException; 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 * This thread is responsible for handling TCP message communication with the
@ -15,61 +18,84 @@ import java.net.SocketException;
public class MessageTransceiver extends Thread { public class MessageTransceiver extends Thread {
private final Client client; private final Client client;
private Socket socket; private final Socket socket;
private ObjectOutputStream out; private final DataTransceiver dataTransceiver;
private ObjectInputStream in; private final ObjectOutputStream out;
private final ObjectInputStream in;
private final ExecutorService writeService = Executors.newFixedThreadPool(1);
private volatile boolean running = true; private volatile boolean running = true;
public MessageTransceiver(Client client) { public MessageTransceiver(Client client, String serverHost, int serverPort, String username) throws IOException {
this.client = client; this.client = client;
}
public void connectToServer(String serverHost, int serverPort, String username) throws IOException {
this.socket = new Socket(serverHost, serverPort); this.socket = new Socket(serverHost, serverPort);
this.dataTransceiver = new DataTransceiver(client);
this.out = new ObjectOutputStream(this.socket.getOutputStream()); this.out = new ObjectOutputStream(this.socket.getOutputStream());
this.in = new ObjectInputStream(this.socket.getInputStream()); 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."); System.out.println("Sent identification packet.");
} }
public void shutdown() { public void shutdown() {
this.running = false; this.running = false;
if (this.socket != null) { this.dataTransceiver.shutdown();
this.writeService.shutdown();
try { try {
this.out.close();
this.in.close();
this.socket.close(); this.socket.close();
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
} }
} }
}
public synchronized void send(Message message) throws IOException { public void send(Message message) {
if (this.socket.isClosed()) return;
this.writeService.submit(() -> {
try {
this.out.reset(); this.out.reset();
this.out.writeObject(message); 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 @Override
public void run() { public void run() {
this.dataTransceiver.start();
while (this.running) { while (this.running) {
try { try {
Message msg = (Message) this.in.readObject(); Message msg = (Message) this.in.readObject();
if (msg.getType() == Type.PLAYER_REGISTERED) { if (msg.getType() == Type.PLAYER_REGISTERED) {
System.out.println("Received player registration response from server.");
PlayerRegisteredMessage prm = (PlayerRegisteredMessage) msg; 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) { } else if (msg.getType() == Type.CHAT) {
this.client.addChatMessage((ChatMessage) msg); this.client.getChatManager().addChatMessage((ChatMessage) msg);
} else if (msg.getType() == Type.WORLD_UPDATE) { } else if (msg.getType() == Type.PLAYER_JOINED && this.client.getWorld() != null) {
World world = ((WorldUpdateMessage) msg).getWorld(); PlayerUpdateMessage pum = (PlayerUpdateMessage) msg;
this.client.setWorld(world); 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) { } catch (StreamCorruptedException | EOFException e) {
e.printStackTrace(); this.shutdown();
this.running = false;
} catch (SocketException e) { } catch (SocketException e) {
if (!e.getMessage().equalsIgnoreCase("Socket closed")) { if (!e.getMessage().equalsIgnoreCase("Socket closed")) {
e.printStackTrace(); e.printStackTrace();
} }
this.shutdown();
} catch (IOException | ClassNotFoundException e) { } catch (IOException | ClassNotFoundException e) {
e.printStackTrace(); e.printStackTrace();
} }

View File

@ -1,54 +1,82 @@
package nl.andrewlalis.aos_client; package nl.andrewlalis.aos_client;
import nl.andrewlalis.aos_core.net.data.Sound;
import javax.sound.sampled.*; import javax.sound.sampled.*;
import java.io.ByteArrayInputStream; import java.io.*;
import java.io.ByteArrayOutputStream; import java.util.ArrayList;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
public class SoundManager { public class SoundManager {
private final Map<String, byte[]> soundData = new HashMap<>(); private static final int CLIP_COUNT = 10;
private final Map<String, List<Clip>> soundData = new HashMap<>();
private final Map<String, Integer> clipIndexes = new HashMap<>();
public void play(String sound) { public void play(List<Sound> sounds) {
var clip = this.getClip(sound); for (Sound sound : sounds) {
if (clip != null) { 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(); 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) { private Clip getClip(String sound) {
var soundBytes = this.soundData.get(sound); var clips = this.soundData.get(sound);
if (soundBytes == null) { if (clips == null) {
InputStream is = Client.class.getResourceAsStream("/nl/andrewlalis/aos_client/sound/" + sound); InputStream is = Client.class.getResourceAsStream("/nl/andrewlalis/aos_client/sound/" + sound);
if (is == null) { if (is == null) {
System.err.println("Could not load sound: " + sound); System.err.println("Could not load sound: " + sound);
return null; return null;
} }
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try { try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
is.transferTo(bos); is.transferTo(bos);
soundBytes = bos.toByteArray(); byte[] data = bos.toByteArray();
this.soundData.put(sound, soundBytes); clips = new ArrayList<>(CLIP_COUNT);
} catch (IOException e) { 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(); e.printStackTrace();
return null; return null;
} }
} }
try { int index = this.clipIndexes.get(sound);
var ais = AudioSystem.getAudioInputStream(new ByteArrayInputStream(soundBytes)); if (index >= CLIP_COUNT) {
var clip = AudioSystem.getClip(); index = 0;
clip.addLineListener(event -> { }
if (event.getType() == LineEvent.Type.STOP) { 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(); clip.close();
} }
});
clip.open(ais);
return clip;
} catch (UnsupportedAudioFileException | IOException | LineUnavailableException e) {
e.printStackTrace();
return null;
} }
} }
} }

View File

@ -9,12 +9,10 @@ public class Tester {
}; };
public static void main(String[] args) { public static void main(String[] args) {
for (int i = 0; i < 2; i++) { for (int i = 0; i < 6; i++) {
Client client = new Client();
try { try {
client.connect("localhost", 8035, names[ThreadLocalRandom.current().nextInt(names.length)]); new Client("localhost", 8035, names[ThreadLocalRandom.current().nextInt(names.length)]);
} catch (IOException | ClassNotFoundException e) { } catch (IOException e) {
client.shutdown();
e.printStackTrace(); e.printStackTrace();
} }
} }

View File

@ -1,5 +1,6 @@
package nl.andrewlalis.aos_client.control; package nl.andrewlalis.aos_client.control;
import nl.andrewlalis.aos_client.ChatManager;
import nl.andrewlalis.aos_client.Client; import nl.andrewlalis.aos_client.Client;
import java.awt.event.KeyAdapter; import java.awt.event.KeyAdapter;
@ -7,36 +8,38 @@ import java.awt.event.KeyEvent;
public class PlayerKeyListener extends KeyAdapter { public class PlayerKeyListener extends KeyAdapter {
private final Client client; private final Client client;
private final ChatManager chatManager;
public PlayerKeyListener(Client client) { public PlayerKeyListener(Client client) {
this.client = client; this.client = client;
this.chatManager = client.getChatManager();
} }
@Override @Override
public void keyTyped(KeyEvent e) { public void keyTyped(KeyEvent e) {
if (!this.client.isChatting()) { if (!this.chatManager.isChatting()) {
if ((e.getKeyChar() == 't' || e.getKeyChar() == '/')) { if ((e.getKeyChar() == 't' || e.getKeyChar() == '/')) {
this.client.setChatting(true); this.chatManager.setChatting(true);
if (e.getKeyChar() == '/') this.client.appendToChat('/'); if (e.getKeyChar() == '/') this.chatManager.appendToChat('/');
} }
} else if (this.client.isChatting()) { } else if (this.chatManager.isChatting()) {
char c = e.getKeyChar(); char c = e.getKeyChar();
if (c >= ' ' && c <= '~') { if (c >= ' ' && c <= '~') {
this.client.appendToChat(c); this.chatManager.appendToChat(c);
} else if (e.getKeyChar() == 8) { } else if (e.getKeyChar() == 8) {
this.client.backspaceChat(); this.chatManager.backspaceChat();
} else if (e.getKeyChar() == 10) { } else if (e.getKeyChar() == 10) {
this.client.sendChat(); this.chatManager.sendChat();
} else if (e.getKeyChar() == 27) { } else if (e.getKeyChar() == 27) {
this.client.setChatting(false); this.chatManager.setChatting(false);
} }
} }
} }
@Override @Override
public void keyPressed(KeyEvent e) { public void keyPressed(KeyEvent e) {
if (client.isChatting()) return; if (this.chatManager.isChatting()) return;
var state = client.getPlayerState(); var state = client.getPlayer().getState();
if (e.getKeyCode() == KeyEvent.VK_W) { if (e.getKeyCode() == KeyEvent.VK_W) {
state.setMovingForward(true); state.setMovingForward(true);
} else if (e.getKeyCode() == KeyEvent.VK_S) { } else if (e.getKeyCode() == KeyEvent.VK_S) {
@ -53,8 +56,8 @@ public class PlayerKeyListener extends KeyAdapter {
@Override @Override
public void keyReleased(KeyEvent e) { public void keyReleased(KeyEvent e) {
if (client.isChatting()) return; if (this.chatManager.isChatting()) return;
var state = client.getPlayerState(); var state = client.getPlayer().getState();
if (e.getKeyCode() == KeyEvent.VK_W) { if (e.getKeyCode() == KeyEvent.VK_W) {
state.setMovingForward(false); state.setMovingForward(false);
} else if (e.getKeyCode() == KeyEvent.VK_S) { } else if (e.getKeyCode() == KeyEvent.VK_S) {

View File

@ -9,9 +9,14 @@ import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent; import java.awt.event.MouseWheelEvent;
public class PlayerMouseListener extends MouseInputAdapter { 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 Client client;
private final GamePanel gamePanel; private final GamePanel gamePanel;
private long lastMouseMove = 0L;
public PlayerMouseListener(Client client, GamePanel gamePanel) { public PlayerMouseListener(Client client, GamePanel gamePanel) {
this.client = client; this.client = client;
this.gamePanel = gamePanel; this.gamePanel = gamePanel;
@ -20,7 +25,7 @@ public class PlayerMouseListener extends MouseInputAdapter {
@Override @Override
public void mousePressed(MouseEvent e) { public void mousePressed(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON1) { if (e.getButton() == MouseEvent.BUTTON1) {
client.getPlayerState().setShooting(true); client.getPlayer().getState().setShooting(true);
client.sendPlayerState(); client.sendPlayerState();
} }
} }
@ -28,7 +33,7 @@ public class PlayerMouseListener extends MouseInputAdapter {
@Override @Override
public void mouseReleased(MouseEvent e) { public void mouseReleased(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON1) { if (e.getButton() == MouseEvent.BUTTON1) {
client.getPlayerState().setShooting(false); client.getPlayer().getState().setShooting(false);
client.sendPlayerState(); client.sendPlayerState();
} }
} }
@ -44,18 +49,26 @@ public class PlayerMouseListener extends MouseInputAdapter {
@Override @Override
public void mouseMoved(MouseEvent e) { 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); Vec2 centeredMouseLocation = new Vec2(e.getX(), e.getY()).sub(c);
client.getPlayerState().setMouseLocation(centeredMouseLocation); client.getPlayer().getState().setMouseLocation(centeredMouseLocation);
long now = System.currentTimeMillis();
if (now - this.lastMouseMove > MS_PER_MOUSE_UPDATE) {
client.sendPlayerState(); client.sendPlayerState();
this.lastMouseMove = now;
}
} }
@Override @Override
public void mouseDragged(MouseEvent e) { 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); Vec2 centeredMouseLocation = new Vec2(e.getX(), e.getY()).sub(c);
client.getPlayerState().setMouseLocation(centeredMouseLocation); client.getPlayer().getState().setMouseLocation(centeredMouseLocation);
client.getPlayerState().setShooting(true); client.getPlayer().getState().setShooting(true);
long now = System.currentTimeMillis();
if (now - this.lastMouseMove > MS_PER_MOUSE_UPDATE) {
client.sendPlayerState(); client.sendPlayerState();
this.lastMouseMove = now;
}
} }
} }

View File

@ -77,6 +77,9 @@ public class ConnectDialog extends JDialog {
if (usernameField.getText() == null || usernameField.getText().isBlank()) { if (usernameField.getText() == null || usernameField.getText().isBlank()) {
warnings.add("Username must not be empty."); 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()) { if (addressField.getText() != null && !addressPattern.matcher(addressField.getText()).matches()) {
warnings.add("Address must be in the form HOST:PORT."); warnings.add("Address must be in the form HOST:PORT.");
} }
@ -97,11 +100,9 @@ public class ConnectDialog extends JDialog {
String host = parts[0].trim(); String host = parts[0].trim();
int port = Integer.parseInt(parts[1]); int port = Integer.parseInt(parts[1]);
String username = usernameField.getText(); String username = usernameField.getText();
Client client = new Client();
try { try {
client.connect(host, port, username); new Client(host, port, username);
} catch (IOException | ClassNotFoundException ex) { } catch (IOException ex) {
client.shutdown();
ex.printStackTrace(); ex.printStackTrace();
JOptionPane.showMessageDialog(null, "Could not connect:\n" + ex.getMessage(), "Connection Error", JOptionPane.WARNING_MESSAGE); JOptionPane.showMessageDialog(null, "Could not connect:\n" + ex.getMessage(), "Connection Error", JOptionPane.WARNING_MESSAGE);
} }

View File

@ -56,7 +56,7 @@ public class GamePanel extends JPanel {
} }
private void drawWorld(Graphics2D g2, World world) { private void drawWorld(Graphics2D g2, World world) {
Player myPlayer = world.getPlayers().get(this.client.getPlayerId()); Player myPlayer = client.getPlayer();
if (myPlayer == null) return; if (myPlayer == null) return;
double scale = this.scales[this.scaleIndex]; double scale = this.scales[this.scaleIndex];
AffineTransform pre = g2.getTransform(); AffineTransform pre = g2.getTransform();
@ -195,7 +195,8 @@ public class GamePanel extends JPanel {
private void drawChat(Graphics2D g2, World world) { private void drawChat(Graphics2D g2, World world) {
int height = g2.getFontMetrics().getHeight(); int height = g2.getFontMetrics().getHeight();
int y = height; int y = height;
for (ChatMessage message : this.client.getLatestChatMessages()) { var cm = this.client.getChatManager();
for (ChatMessage message : cm.getLatestChatMessages()) {
Color color = Color.WHITE; Color color = Color.WHITE;
String text = message.getText(); String text = message.getText();
if (message instanceof SystemChatMessage sysMsg) { if (message instanceof SystemChatMessage sysMsg) {
@ -219,14 +220,14 @@ public class GamePanel extends JPanel {
y += height; y += height;
} }
if (this.client.isChatting()) { if (cm.isChatting()) {
g2.setColor(Color.WHITE); 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) { private void drawStatus(Graphics2D g2, World world) {
Player myPlayer = world.getPlayers().get(this.client.getPlayerId()); Player myPlayer = this.client.getPlayer();
if (myPlayer == null) return; if (myPlayer == null) return;
g2.setColor(Color.WHITE); g2.setColor(Color.WHITE);

View File

@ -1,8 +1,13 @@
module aos_core { module aos_core {
requires java.desktop; requires java.desktop;
exports nl.andrewlalis.aos_core.net to aos_server, aos_client; 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.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.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;
} }

View File

@ -1,13 +1,15 @@
package nl.andrewlalis.aos_core.geom; package nl.andrewlalis.aos_core.geom;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.Serializable; import java.io.Serializable;
import java.util.Random; import java.util.Random;
import java.util.concurrent.ThreadLocalRandom; 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() { public float mag() {
return Math.sqrt(x * x + y * y); return (float) Math.sqrt(x * x + y * y);
} }
public Vec2 add(Vec2 other) { 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); 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); return new Vec2(this.x * factor, this.y * factor);
} }
public Vec2 unit() { public Vec2 unit() {
double mag = this.mag(); float mag = this.mag();
return new Vec2(this.x / mag, this.y / 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; 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); return new Vec2(this.y, -this.x);
} }
public double dist(Vec2 other) { public float dist(Vec2 other) {
return other.sub(this).mag(); return other.sub(this).mag();
} }
public Vec2 rotate(double theta) { public Vec2 rotate(double theta) {
return new Vec2( return new Vec2(
this.x * Math.cos(theta) - this.y * Math.sin(theta), (float) (this.x * Math.cos(theta) - this.y * Math.sin(theta)),
this.x * Math.sin(theta) + this.y * Math.cos(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 + " ]"; return "[ " + x + ", " + y + " ]";
} }
public static Vec2 random(double min, double max) { public static Vec2 random(float min, float max) {
Random r = ThreadLocalRandom.current(); Random r = ThreadLocalRandom.current();
double x = r.nextDouble() * (max - min) + min; float x = r.nextFloat() * (max - min) + min;
double y = r.nextDouble() * (max - min) + min; float y = r.nextFloat() * (max - min) + min;
return new Vec2(x, y); return new Vec2(x, y);
} }
public static Vec2 read(DataInputStream in) throws IOException {
return new Vec2(in.readFloat(), in.readFloat());
}
} }

View File

@ -21,7 +21,7 @@ public class Barricade implements Serializable {
this.size = size; 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)); this(new Vec2(x, y), new Vec2(w, h));
} }

View File

@ -13,17 +13,21 @@ public class Bullet extends PhysicsObject {
public Bullet(Player player) { public Bullet(Player player) {
this.playerId = player.getId(); this.playerId = player.getId();
this.setPosition(player.getPosition() this.setPosition(player.getPosition()
.add(player.getOrientation().mul(1.5)) .add(player.getOrientation().mul(1.5f))
.add(player.getOrientation().perp().mul(Player.RADIUS)) .add(player.getOrientation().perp().mul(Player.RADIUS))
); );
this.setOrientation(player.getOrientation()); this.setOrientation(player.getOrientation());
Vec2 perturbation = Vec2.random(-1, 1).mul(player.getGun().getAccuracy());
Random r = ThreadLocalRandom.current();
Vec2 perturbation = new Vec2((r.nextDouble() - 0.5) * 2, (r.nextDouble() - 0.5) * 2).mul(player.getGun().getAccuracy());
this.setVelocity(this.getOrientation().add(perturbation).mul(player.getGun().getBulletSpeed())); this.setVelocity(this.getOrientation().add(perturbation).mul(player.getGun().getBulletSpeed()));
this.gun = player.getGun(); 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() { public int getPlayerId() {
return playerId; return playerId;
} }

View File

@ -6,9 +6,9 @@ import nl.andrewlalis.aos_core.model.tools.Gun;
import java.util.Objects; import java.util.Objects;
public class Player extends PhysicsObject implements Comparable<Player> { public class Player extends PhysicsObject implements Comparable<Player> {
public static final double MOVEMENT_SPEED = 10; // Movement speed, in m/s public static final float MOVEMENT_SPEED = 10; // Movement speed, in m/s
public static final double RADIUS = 0.5; // Collision radius, in meters. public static final float RADIUS = 0.5f; // Collision radius, in meters.
public static final double RESUPPLY_COOLDOWN = 30; // Seconds between allowing resupply. public static final float RESUPPLY_COOLDOWN = 30; // Seconds between allowing resupply.
public static final float MAX_HEALTH = 100.0f; public static final float MAX_HEALTH = 100.0f;
private final int id; private final int id;
@ -28,10 +28,10 @@ public class Player extends PhysicsObject implements Comparable<Player> {
this.name = name; this.name = name;
this.team = team; this.team = team;
this.state = new PlayerControlState(); this.state = new PlayerControlState();
this.state.setPlayerId(this.id); this.gun = Gun.ak47();
this.gun = Gun.winchester();
this.health = MAX_HEALTH; this.health = MAX_HEALTH;
this.useWeapon(); this.useWeapon();
this.lastShot = System.currentTimeMillis();
} }
public int getId() { public int getId() {
@ -66,8 +66,12 @@ public class Player extends PhysicsObject implements Comparable<Player> {
this.gun = gun; this.gun = gun;
} }
public long getLastShot() { public void setHealth(float health) {
return lastShot; this.health = health;
}
public void setReloading(boolean reloading) {
this.reloading = reloading;
} }
public boolean canUseWeapon() { public boolean canUseWeapon() {
@ -75,7 +79,7 @@ public class Player extends PhysicsObject implements Comparable<Player> {
!this.state.isReloading() && !this.state.isReloading() &&
!this.reloading && !this.reloading &&
this.gun.getCurrentClipBulletCount() > 0 && 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); (this.getTeam() == null || this.getTeam().getSpawnPoint().dist(this.getPosition()) > Team.SPAWN_RADIUS);
} }

View File

@ -1,12 +1,12 @@
package nl.andrewlalis.aos_core.model; package nl.andrewlalis.aos_core.model;
import nl.andrewlalis.aos_core.geom.Vec2; import nl.andrewlalis.aos_core.geom.Vec2;
import nl.andrewlalis.aos_core.net.data.DataTypes;
import java.io.Serializable; import java.io.Serializable;
import java.nio.ByteBuffer;
public class PlayerControlState implements Serializable { public class PlayerControlState implements Serializable {
private int playerId;
boolean movingLeft; boolean movingLeft;
boolean movingRight; boolean movingRight;
boolean movingForward; boolean movingForward;
@ -17,14 +17,6 @@ public class PlayerControlState implements Serializable {
Vec2 mouseLocation; Vec2 mouseLocation;
public int getPlayerId() {
return playerId;
}
public void setPlayerId(int playerId) {
this.playerId = playerId;
}
public boolean isMovingLeft() { public boolean isMovingLeft() {
return movingLeft; return movingLeft;
} }
@ -80,4 +72,46 @@ public class PlayerControlState implements Serializable {
public void setMouseLocation(Vec2 mouseLocation) { public void setMouseLocation(Vec2 mouseLocation) {
this.mouseLocation = 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;
}
} }

View File

@ -8,8 +8,8 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
public class Team implements Serializable { public class Team implements Serializable {
public static final double SPAWN_RADIUS = 3; public static final float SPAWN_RADIUS = 3;
public static final double SUPPLY_POINT_RADIUS = 2; public static final float SUPPLY_POINT_RADIUS = 2;
private final String name; private final String name;
private final java.awt.Color color; private final java.awt.Color color;

View File

@ -7,6 +7,8 @@ import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; 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. * 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) { public World(Vec2 size) {
this.size = size; this.size = size;
this.teams = new ArrayList<>(); this.teams = new ArrayList<>();
this.players = new HashMap<>(); this.players = new ConcurrentHashMap<>();
this.bullets = new ArrayList<>(); this.bullets = new CopyOnWriteArrayList<>();
this.barricades = new ArrayList<>(); this.barricades = new ArrayList<>();
this.soundsToPlay = new ArrayList<>(); this.soundsToPlay = new ArrayList<>();
} }

View File

@ -24,27 +24,27 @@ public class Gun implements Serializable {
/** /**
* How accurate shots from this gun are. * 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. * 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. * 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). * 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. * 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. * Number of bullets left in the current clip.
@ -55,7 +55,7 @@ public class Gun implements Serializable {
*/ */
private int clipCount; 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.type = type;
this.maxClipCount = maxClipCount; this.maxClipCount = maxClipCount;
this.clipSize = clipSize; this.clipSize = clipSize;
@ -86,23 +86,23 @@ public class Gun implements Serializable {
return bulletsPerRound; return bulletsPerRound;
} }
public double getAccuracy() { public float getAccuracy() {
return accuracy; return accuracy;
} }
public double getShotCooldownTime() { public float getShotCooldownTime() {
return shotCooldownTime; return shotCooldownTime;
} }
public double getReloadTime() { public float getReloadTime() {
return reloadTime; return reloadTime;
} }
public double getBulletSpeed() { public float getBulletSpeed() {
return bulletSpeed; return bulletSpeed;
} }
public double getBaseDamage() { public float getBaseDamage() {
return baseDamage; 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() { 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() { 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() { 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);
} }
} }

View File

@ -1,7 +1,24 @@
package nl.andrewlalis.aos_core.model.tools; package nl.andrewlalis.aos_core.model.tools;
public enum GunType { public enum GunType {
SHOTGUN, SHOTGUN(0),
SMG, SMG(1),
RIFLE 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;
}
} }

View File

@ -3,12 +3,22 @@ package nl.andrewlalis.aos_core.net;
public class IdentMessage extends Message { public class IdentMessage extends Message {
private final String name; 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); super(Type.IDENT);
this.name = name; this.name = name;
this.udpPort = udpPort;
} }
public String getName() { public String getName() {
return name; return name;
} }
public int getUdpPort() {
return this.udpPort;
}
} }

View File

@ -1,14 +1,23 @@
package nl.andrewlalis.aos_core.net; package nl.andrewlalis.aos_core.net;
import nl.andrewlalis.aos_core.model.Player;
import nl.andrewlalis.aos_core.model.World;
public class PlayerRegisteredMessage extends Message { public class PlayerRegisteredMessage extends Message {
private final int playerId; private final Player player;
private final World world;
public PlayerRegisteredMessage(int playerId) { public PlayerRegisteredMessage(Player player, World world) {
super(Type.PLAYER_REGISTERED); super(Type.PLAYER_REGISTERED);
this.playerId = playerId; this.player = player;
this.world = world;
} }
public int getPlayerId() { public Player getPlayer() {
return playerId; return player;
}
public World getWorld() {
return world;
} }
} }

View File

@ -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;
}
}

View File

@ -2,9 +2,10 @@ package nl.andrewlalis.aos_core.net;
public enum Type { public enum Type {
IDENT, IDENT,
ACK,
PLAYER_REGISTERED, PLAYER_REGISTERED,
CHAT, CHAT,
PLAYER_CONTROL_STATE, PLAYER_CONTROL_STATE,
WORLD_UPDATE PLAYER_JOINED,
PLAYER_LEFT,
PLAYER_TEAM_CHANGE
} }

View File

@ -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;
}
}

View File

@ -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)
);
}
}

View File

@ -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;
}

View File

@ -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()
);
}
}

View File

@ -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())
);
}
}

View File

@ -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())
);
}
}

View File

@ -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<Byte, SoundType> typeIndex = new HashMap<>();
static {
for (var val : values()) {
typeIndex.put(val.getCode(), val);
}
}
public static SoundType get(byte code) {
return typeIndex.get(code);
}
}

View File

@ -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.
* <p>
* 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.
* </p>
*/
public class WorldUpdate {
private final List<PlayerUpdate> playerUpdates;
private final List<BulletUpdate> bulletUpdates;
private final List<Sound> soundsToPlay;
public WorldUpdate() {
this(new ArrayList<>(), new ArrayList<>(), new ArrayList<>());
}
private WorldUpdate(List<PlayerUpdate> playerUpdates, List<BulletUpdate> bulletUpdates, List<Sound> 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<PlayerUpdate> getPlayerUpdates() {
return playerUpdates;
}
public List<BulletUpdate> getBulletUpdates() {
return bulletUpdates;
}
public List<Sound> 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<PlayerUpdate> playerUpdates = new ArrayList<>(players);
for (int i = 0; i < players; i++) {
playerUpdates.add(PlayerUpdate.read(dataIn));
}
int bullets = dataIn.readInt();
List<BulletUpdate> bulletUpdates = new ArrayList<>(bullets);
for (int i = 0; i < bullets; i++) {
bulletUpdates.add(BulletUpdate.read(dataIn));
}
int sounds = dataIn.readInt();
List<Sound> 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;
}
}

View File

@ -45,4 +45,11 @@ public class ByteUtils {
if (n != length) throw new IOException("Could not read enough bytes to read string."); if (n != length) throw new IOException("Could not read enough bytes to read string.");
return new String(strBytes, StandardCharsets.UTF_8); 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;
}
} }

View File

@ -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<String, ChatCommand> 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()));
}
}
}

View File

@ -1,15 +1,19 @@
package nl.andrewlalis.aos_server; 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.IdentMessage;
import nl.andrewlalis.aos_core.net.Message; 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.Type;
import nl.andrewlalis.aos_core.net.chat.ChatMessage; import nl.andrewlalis.aos_core.net.chat.ChatMessage;
import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import java.io.ObjectInputStream; import java.io.ObjectInputStream;
import java.io.ObjectOutputStream; import java.io.ObjectOutputStream;
import java.net.InetAddress;
import java.net.Socket; import java.net.Socket;
import java.net.SocketException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@ -23,29 +27,41 @@ public class ClientHandler extends Thread {
private final ObjectOutputStream out; private final ObjectOutputStream out;
private final ObjectInputStream in; private final ObjectInputStream in;
private int playerId; private Player player;
private final InetAddress clientAddress;
private int clientUdpPort = -1;
private volatile boolean running = true; private volatile boolean running = true;
public ClientHandler(Server server, Socket socket) throws IOException { public ClientHandler(Server server, Socket socket) throws IOException {
this.server = server; this.server = server;
this.socket = socket; this.socket = socket;
this.clientAddress = this.socket.getInetAddress();
this.out = new ObjectOutputStream(socket.getOutputStream()); this.out = new ObjectOutputStream(socket.getOutputStream());
this.in = new ObjectInputStream(socket.getInputStream()); this.in = new ObjectInputStream(socket.getInputStream());
} }
public int getPlayerId() { public Player getPlayer() {
return playerId; return player;
}
public InetAddress getClientAddress() {
return clientAddress;
}
public int getClientUdpPort() {
return clientUdpPort;
} }
public void shutdown() { public void shutdown() {
this.running = false; this.running = false;
this.sendingQueue.shutdown();
try { try {
this.in.close(); this.in.close();
this.out.close(); this.out.close();
this.socket.close(); this.socket.close();
} catch (IOException e) { } 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 @Override
public void run() { public void run() {
try {
while (this.running) { while (this.running) {
try { try {
Message msg = (Message) this.in.readObject(); Message msg = (Message) this.in.readObject();
if (msg.getType() == Type.IDENT) { if (msg.getType() == Type.IDENT) {
IdentMessage ident = (IdentMessage) msg; IdentMessage ident = (IdentMessage) msg;
this.playerId = this.server.registerNewPlayer(ident.getName(), this); 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) { } else if (msg.getType() == Type.CHAT) {
this.server.handlePlayerChat(this, this.playerId, (ChatMessage) msg); this.server.getChatManager().handlePlayerChat(this, this.player, (ChatMessage) msg);
} else if (msg.getType() == Type.PLAYER_CONTROL_STATE) {
this.server.updatePlayerState(((PlayerControlStateMessage) msg).getPlayerControlState());
} }
} catch (ClassNotFoundException e) { } catch (SocketException e) {
if (e.getMessage().equals("Socket closed")) {
this.shutdown();
} else {
e.printStackTrace(); e.printStackTrace();
} }
} } catch (EOFException e) {
} catch (IOException e) { this.shutdown();
// Ignore this exception, consider the client disconnected. } catch (IOException | ClassNotFoundException e) {
}
try {
this.socket.close();
} catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
this.shutdown();
}
} }
this.server.clientDisconnected(this); this.server.clientDisconnected(this);
} }

View File

@ -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();
}
}
}
}

View File

@ -2,20 +2,20 @@ package nl.andrewlalis.aos_server;
import nl.andrewlalis.aos_core.geom.Vec2; import nl.andrewlalis.aos_core.geom.Vec2;
import nl.andrewlalis.aos_core.model.*; 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.Message;
import nl.andrewlalis.aos_core.net.PlayerRegisteredMessage; import nl.andrewlalis.aos_core.net.PlayerUpdateMessage;
import nl.andrewlalis.aos_core.net.WorldUpdateMessage; import nl.andrewlalis.aos_core.net.Type;
import nl.andrewlalis.aos_core.net.chat.ChatMessage;
import nl.andrewlalis.aos_core.net.chat.PlayerChatMessage;
import nl.andrewlalis.aos_core.net.chat.SystemChatMessage; 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.awt.*;
import java.io.IOException; import java.io.IOException;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.net.Socket; import java.net.Socket;
import java.net.SocketException; import java.net.SocketException;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Scanner; import java.util.Scanner;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
@ -26,19 +26,23 @@ public class Server {
private final List<ClientHandler> clientHandlers; private final List<ClientHandler> clientHandlers;
private final ServerSocket serverSocket; private final ServerSocket serverSocket;
private final DataTransceiver dataTransceiver;
private final World world; private final World world;
private final WorldUpdater worldUpdater; private final WorldUpdater worldUpdater;
private final ServerCli cli; private final ServerCli cli;
private final ChatManager chatManager;
private volatile boolean running; private volatile boolean running;
public Server(int port) throws IOException { public Server(int port) throws IOException {
this.clientHandlers = new CopyOnWriteArrayList<>(); this.clientHandlers = new CopyOnWriteArrayList<>();
this.serverSocket = new ServerSocket(port); this.serverSocket = new ServerSocket(port);
this.dataTransceiver = new DataTransceiver(this, port);
this.cli = new ServerCli(this); this.cli = new ServerCli(this);
this.world = new World(new Vec2(50, 70)); this.world = new World(new Vec2(50, 70));
this.initWorld(); this.initWorld();
this.worldUpdater = new WorldUpdater(this, this.world); this.worldUpdater = new WorldUpdater(this, this.world);
this.chatManager = new ChatManager(this);
} }
private void initWorld() { private void initWorld() {
@ -68,6 +72,10 @@ public class Server {
return world; return world;
} }
public ChatManager getChatManager() {
return chatManager;
}
public void acceptClientConnection() { public void acceptClientConnection() {
try { try {
Socket socket = this.serverSocket.accept(); 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); int id = ThreadLocalRandom.current().nextInt(1, Integer.MAX_VALUE);
Team team = null; Team team = null;
for (Team t : this.world.getTeams()) { for (Team t : this.world.getTeams()) {
@ -94,11 +102,10 @@ public class Server {
} }
Player p = new Player(id, name, team); Player p = new Player(id, name, team);
this.world.getPlayers().put(p.getId(), p); this.world.getPlayers().put(p.getId(), p);
handler.send(new PlayerRegisteredMessage(id));
String message = p.getName() + " connected."; String message = p.getName() + " connected.";
this.broadcastMessage(new SystemChatMessage(SystemChatMessage.Level.INFO, message)); this.broadcastMessage(new SystemChatMessage(SystemChatMessage.Level.INFO, message));
System.out.println(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) { if (team != null) {
team.getPlayers().add(p); team.getPlayers().add(p);
p.setPosition(team.getSpawnPoint()); p.setPosition(team.getSpawnPoint());
@ -106,11 +113,12 @@ public class Server {
message = name + " joined team " + team.getName() + "."; message = name + " joined team " + team.getName() + ".";
this.broadcastMessage(new SystemChatMessage(SystemChatMessage.Level.INFO, message)); 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) { public void clientDisconnected(ClientHandler clientHandler) {
Player player = this.world.getPlayers().get(clientHandler.getPlayerId()); Player player = clientHandler.getPlayer();
this.clientHandlers.remove(clientHandler); this.clientHandlers.remove(clientHandler);
clientHandler.shutdown(); clientHandler.shutdown();
this.world.getPlayers().remove(player.getId()); this.world.getPlayers().remove(player.getId());
@ -120,25 +128,37 @@ public class Server {
String message = player.getName() + " disconnected."; String message = player.getName() + " disconnected.";
this.broadcastMessage(new SystemChatMessage(SystemChatMessage.Level.INFO, message)); this.broadcastMessage(new SystemChatMessage(SystemChatMessage.Level.INFO, message));
System.out.println(message); System.out.println(message);
this.broadcastMessage(new PlayerUpdateMessage(Type.PLAYER_LEFT, player));
} }
public void kickPlayer(Player player) { public void kickPlayer(Player player) {
for (ClientHandler handler : this.clientHandlers) { for (ClientHandler handler : this.clientHandlers) {
if (handler.getPlayerId() == player.getId()) { if (handler.getPlayer().getId() == player.getId()) {
handler.shutdown(); handler.shutdown();
return; return;
} }
} }
} }
public void sendWorldToClients() { 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) { for (ClientHandler handler : this.clientHandlers) {
handler.send(new WorldUpdateMessage(this.world)); 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) { public void updatePlayerState(int playerId, PlayerControlState state) {
Player p = this.world.getPlayers().get(state.getPlayerId()); Player p = this.world.getPlayers().get(playerId);
if (p != null) { if (p != null) {
p.setState(state); p.setState(state);
} }
@ -151,6 +171,7 @@ public class Server {
p.respawn(); p.respawn();
} }
} }
broadcastMessage(new SystemChatMessage(SystemChatMessage.Level.INFO, "Game has been reset."));
} }
public void broadcastMessage(Message message) { 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() { public void shutdown() {
this.running = false; this.running = false;
try { try {
@ -207,17 +194,21 @@ public class Server {
public void run() { public void run() {
this.running = true; this.running = true;
this.dataTransceiver.start();
this.worldUpdater.start(); this.worldUpdater.start();
this.cli.start(); this.cli.start();
System.out.println("Started AOS-Server TCP on port " + this.serverSocket.getLocalPort() + "; now accepting connections."); System.out.println("Started AOS-Server TCP on port " + this.serverSocket.getLocalPort() + "; now accepting connections.");
while (this.running) { while (this.running) {
this.acceptClientConnection(); this.acceptClientConnection();
} }
this.shutdown();
System.out.println("Stopped accepting new client connections."); System.out.println("Stopped accepting new client connections.");
this.worldUpdater.shutdown(); this.worldUpdater.shutdown();
System.out.println("Stopped world updater."); System.out.println("Stopped world updater.");
this.cli.shutdown(); this.cli.shutdown();
System.out.println("Stopped CLI interface."); System.out.println("Stopped CLI interface.");
this.dataTransceiver.shutdown();
System.out.println("Stopped data transceiver.");
} }

View File

@ -4,6 +4,9 @@ import nl.andrewlalis.aos_core.geom.Vec2;
import nl.andrewlalis.aos_core.model.*; import nl.andrewlalis.aos_core.model.*;
import nl.andrewlalis.aos_core.model.tools.GunType; import nl.andrewlalis.aos_core.model.tools.GunType;
import nl.andrewlalis.aos_core.net.chat.SystemChatMessage; 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.ArrayList;
import java.util.List; import java.util.List;
@ -15,11 +18,15 @@ public class WorldUpdater extends Thread {
private final Server server; private final Server server;
private final World world; private final World world;
private final WorldUpdate worldUpdate;
private volatile boolean running = true; private volatile boolean running = true;
public WorldUpdater(Server server, World world) { public WorldUpdater(Server server, World world) {
this.server = server; this.server = server;
this.world = world; this.world = world;
this.worldUpdate = new WorldUpdate();
} }
public void shutdown() { public void shutdown() {
@ -33,7 +40,7 @@ public class WorldUpdater extends Thread {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
long msSinceLastTick = now - lastTick; long msSinceLastTick = now - lastTick;
if (msSinceLastTick >= MS_PER_TICK) { if (msSinceLastTick >= MS_PER_TICK) {
double elapsedSeconds = msSinceLastTick / 1000.0; float elapsedSeconds = msSinceLastTick / 1000.0f;
this.tick(elapsedSeconds); this.tick(elapsedSeconds);
lastTick = now; lastTick = now;
} }
@ -48,21 +55,22 @@ public class WorldUpdater extends Thread {
} }
} }
private void tick(double t) { private void tick(float t) {
world.getSoundsToPlay().clear(); this.worldUpdate.clear();
this.updateBullets(t); this.updateBullets(t);
this.updatePlayers(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()) { for (Player p : this.world.getPlayers().values()) {
this.updatePlayerMovement(p, t); this.updatePlayerMovement(p, t);
this.updatePlayerShooting(p); 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) { if (p.getState().getMouseLocation() != null && p.getState().getMouseLocation().mag() > 0) {
Vec2 newOrientation = p.getState().getMouseLocation().unit(); Vec2 newOrientation = p.getState().getMouseLocation().unit();
if (p.getTeam() != null) { if (p.getTeam() != null) {
@ -71,8 +79,8 @@ public class WorldUpdater extends Thread {
} }
p.setOrientation(newOrientation); p.setOrientation(newOrientation);
} }
double vx = 0; float vx = 0;
double vy = 0; float vy = 0;
if (p.getState().isMovingForward()) vy += Player.MOVEMENT_SPEED; if (p.getState().isMovingForward()) vy += Player.MOVEMENT_SPEED;
if (p.getState().isMovingBackward()) vy -= Player.MOVEMENT_SPEED; if (p.getState().isMovingBackward()) vy -= Player.MOVEMENT_SPEED;
if (p.getState().isMovingLeft()) vx -= 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 leftVector = forwardVector.perp();
Vec2 newPos = p.getPosition().add(forwardVector.mul(vy * t)).add(leftVector.mul(vx * t)); Vec2 newPos = p.getPosition().add(forwardVector.mul(vy * t)).add(leftVector.mul(vx * t));
double nx = newPos.x(); float nx = newPos.x();
double ny = newPos.y(); float ny = newPos.y();
for (Barricade b : world.getBarricades()) { for (Barricade b : world.getBarricades()) {
// TODO: Improve barricade collision smoothness. // TODO: Improve barricade collision smoothness.
double x1 = b.getPosition().x(); float x1 = b.getPosition().x();
double x2 = x1 + b.getSize().x(); float x2 = x1 + b.getSize().x();
double y1 = b.getPosition().y(); float y1 = b.getPosition().y();
double y2 = y1 + b.getSize().y(); float y2 = y1 + b.getSize().y();
if (nx + Player.RADIUS > x1 && nx - Player.RADIUS < x2 && ny + Player.RADIUS > y1 && ny - Player.RADIUS < y2) { if (nx + Player.RADIUS > x1 && nx - Player.RADIUS < x2 && ny + Player.RADIUS > y1 && ny - Player.RADIUS < y2) {
double distanceLeft = Math.abs(nx - x1); double distanceLeft = Math.abs(nx - x1);
double distanceRight = Math.abs(nx - x2); double distanceRight = Math.abs(nx - x2);
@ -123,15 +131,17 @@ public class WorldUpdater extends Thread {
private void updatePlayerShooting(Player p) { private void updatePlayerShooting(Player p) {
if (p.canUseWeapon()) { if (p.canUseWeapon()) {
for (int i = 0; i < p.getGun().getBulletsPerRound(); i++) { 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) { if (p.getGun().getType() == GunType.RIFLE) {
sound = "m1garand-shot1.wav"; soundType = SoundType.SHOT_RIFLE;
} else if (p.getGun().getType() == GunType.SHOTGUN) { } 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(); p.useWeapon();
} }
if (p.getState().isReloading() && !p.isReloading() && p.getGun().canReload()) { if (p.getState().isReloading() && !p.isReloading() && p.getGun().canReload()) {
@ -139,11 +149,11 @@ public class WorldUpdater extends Thread {
} }
if (p.isReloading() && p.isReloadingComplete()) { if (p.isReloading() && p.isReloadingComplete()) {
p.finishReloading(); 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<Bullet> bulletsToRemove = new ArrayList<>(); List<Bullet> bulletsToRemove = new ArrayList<>();
for (Bullet b : this.world.getBullets()) { for (Bullet b : this.world.getBullets()) {
Vec2 oldPos = b.getPosition(); Vec2 oldPos = b.getPosition();
@ -151,26 +161,30 @@ public class WorldUpdater extends Thread {
Vec2 pos = b.getPosition(); Vec2 pos = b.getPosition();
if (pos.x() < 0 || pos.y() < 0 || pos.x() > this.world.getSize().x() || pos.y() > this.world.getSize().y()) { if (pos.x() < 0 || pos.y() < 0 || pos.x() > this.world.getSize().x() || pos.y() > this.world.getSize().y()) {
bulletsToRemove.add(b); bulletsToRemove.add(b);
continue;
} }
boolean removed = false;
for (Barricade bar : this.world.getBarricades()) { for (Barricade bar : this.world.getBarricades()) {
if ( if (
pos.x() > bar.getPosition().x() && pos.x() < bar.getPosition().x() + bar.getSize().x() && 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() pos.y() > bar.getPosition().y() && pos.y() < bar.getPosition().y() + bar.getSize().y()
) { ) {
int n = ThreadLocalRandom.current().nextInt(1, 6); int code = ThreadLocalRandom.current().nextInt(SoundType.BULLET_IMPACT_1.getCode(), SoundType.BULLET_IMPACT_5.getCode() + 1);
this.world.getSoundsToPlay().add("bullet_impact_" + n + ".wav"); this.worldUpdate.addSound(new Sound(b.getPosition(), 1.0f, SoundType.get((byte) code)));
bulletsToRemove.add(b); bulletsToRemove.add(b);
removed = true;
break; break;
} }
} }
if (removed) continue;
double x1 = oldPos.x(); float x1 = oldPos.x();
double x2 = b.getPosition().x(); float x2 = b.getPosition().x();
double y1 = oldPos.y(); float y1 = oldPos.y();
double y2 = b.getPosition().y(); float y2 = b.getPosition().y();
double lineDist = oldPos.dist(b.getPosition()); float lineDist = oldPos.dist(b.getPosition());
for (Player p : this.world.getPlayers().values()) { 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); n = Math.max(Math.min(n, 1), 0);
double dist = p.getPosition().dist(new Vec2(x1 + n * (x2 - x1), y1 + n * (y2 - y1))); 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)) { 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) { if (p.getHealth() == 0.0f) {
Player shooter = this.world.getPlayers().get(b.getPlayerId()); Player shooter = this.world.getPlayers().get(b.getPlayerId());
this.server.broadcastMessage(new SystemChatMessage(SystemChatMessage.Level.SEVERE, p.getName() + " was shot by " + shooter.getName() + ".")); 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) { if (shooter.getTeam() != null) {
shooter.getTeam().incrementScore(); shooter.getTeam().incrementScore();
} }
@ -189,6 +203,7 @@ public class WorldUpdater extends Thread {
} }
} }
} }
this.worldUpdate.addBullet(b);
} }
this.world.getBullets().removeAll(bulletsToRemove); this.world.getBullets().removeAll(bulletsToRemove);
} }

View File

@ -1,8 +1,11 @@
package nl.andrewlalis.aos_server.command; 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.Server;
import nl.andrewlalis.aos_server.command.chat.ChatCommand;
public class ResetCommand implements Command { public class ResetCommand implements Command, ChatCommand {
private final Server server; private final Server server;
public ResetCommand(Server server) { public ResetCommand(Server server) {
@ -14,4 +17,9 @@ public class ResetCommand implements Command {
this.server.resetGame(); this.server.resetGame();
System.out.println("Reset the game."); System.out.println("Reset the game.");
} }
@Override
public void execute(ClientHandler handler, Player player, String[] args) {
this.server.resetGame();
}
} }

View File

@ -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);
}

View File

@ -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() + "."));
}
}