Added start of a launcher frame, and other improvements to communication protocol.

This commit is contained in:
Andrew Lalis 2021-06-23 19:00:46 +02:00
parent 09f5630a0c
commit 4e01673bf8
12 changed files with 287 additions and 52 deletions

View File

@ -22,15 +22,14 @@ public class Client {
private Player myPlayer;
private final GameRenderer renderer;
private final GamePanel gamePanel;
private final SoundManager soundManager;
private final ChatManager chatManager;
private final GameFrame frame;
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);
this.messageTransceiver = new MessageTransceiver(this, serverHost, serverPort, username);
this.messageTransceiver.start();
this.chatManager.bindTransceiver(this.messageTransceiver);
@ -45,8 +44,10 @@ public class Client {
}
System.out.println("Player and world data initialized.");
GameFrame g = new GameFrame("Ace of Shades - " + serverHost + ":" + serverPort, this, this.gamePanel);
g.setVisible(true);
GamePanel gamePanel = new GamePanel(this);
this.renderer = new GameRenderer(this, gamePanel);
this.frame = new GameFrame("Ace of Shades - " + serverHost + ":" + serverPort, this, gamePanel);
this.frame.setVisible(true);
this.renderer.start();
}
@ -119,6 +120,7 @@ public class Client {
System.out.println("Renderer shutdown.");
this.soundManager.close();
System.out.println("Sound manager closed.");
this.frame.dispose();
}

View File

@ -11,24 +11,68 @@ import java.net.InetAddress;
import java.net.SocketException;
import java.nio.ByteBuffer;
/**
* This thread is responsible for managing incoming datagram packets, and also
* offers functionality to send packets back to the server.
*/
public class DataTransceiver extends Thread {
private final Client client;
private final DatagramSocket socket;
private final InetAddress serverAddress;
private final int serverPort;
private volatile boolean running;
public DataTransceiver(Client client) throws SocketException {
/**
* Constructs a new data transceiver thread, and immediately initiates a
* connection to the server to ensure we've got an "outbound" connection.
* @param client A reference to the client this thread is for.
* @param serverAddress The server's address to connect to.
* @param serverPort The server's port to connect to.
* @throws IOException If we could not open the socket and initialize the
* connection.
*/
public DataTransceiver(Client client, InetAddress serverAddress, int serverPort) throws IOException {
this.client = client;
this.serverAddress = serverAddress;
this.serverPort = serverPort;
this.socket = new DatagramSocket();
this.initiateConnection();
}
/**
* Initiates the "connection" to the server's UDP socket. This is so that
* the router knows that this is an "outbound" connection which the client
* initiated. Otherwise, we can't receive UDP packets from the server.
* @throws IOException If we couldn't connect.
*/
private void initiateConnection() throws IOException {
boolean established = false;
int attempts = 0;
while (!established && attempts < 100) {
this.send(new byte[]{DataTypes.INIT});
byte[] buffer = new byte[1400];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
this.socket.receive(packet);
if (packet.getLength() == 1 && packet.getData()[0] == DataTypes.INIT) {
established = true;
}
attempts++;
}
if (!established) {
throw new IOException("Could not initiate UDP connection after " + attempts + " attempts.");
}
System.out.println("Initiated UDP connection with server.");
}
public int getLocalPort() {
return this.socket.getLocalPort();
}
public void send(byte[] bytes, InetAddress address, int port) throws IOException {
public void send(byte[] bytes) throws IOException {
if (this.socket.isClosed()) return;
var packet = new DatagramPacket(bytes, bytes.length, address, port);
var packet = new DatagramPacket(bytes, bytes.length, this.serverAddress, this.serverPort);
this.socket.send(packet);
}
@ -42,7 +86,6 @@ public class DataTransceiver extends Thread {
@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) {

View File

@ -3,8 +3,8 @@ package nl.andrewlalis.aos_client;
import nl.andrewlalis.aos_core.net.*;
import nl.andrewlalis.aos_core.net.chat.ChatMessage;
import javax.swing.*;
import java.io.*;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketException;
import java.nio.ByteBuffer;
@ -29,11 +29,40 @@ public class MessageTransceiver extends Thread {
public MessageTransceiver(Client client, String serverHost, int serverPort, String username) throws IOException {
this.client = client;
this.socket = new Socket(serverHost, serverPort);
this.dataTransceiver = new DataTransceiver(client);
this.dataTransceiver = new DataTransceiver(client, this.socket.getInetAddress(), this.socket.getPort());
this.out = new ObjectOutputStream(this.socket.getOutputStream());
this.in = new ObjectInputStream(this.socket.getInputStream());
this.initializeConnection(username);
}
/**
* Initializes the TCP connection to the server. This involves sending an
* IDENT packet containing some data the server needs about this client, and
* waiting for a {@link PlayerRegisteredMessage} response from the server,
* which contains the basic data we need to start the game.
* @param username The username for this client.
* @throws IOException If the connection could not be initialized.
*/
private void initializeConnection(String username) throws IOException {
boolean established = false;
int attempts = 0;
while (!established && attempts < 100) {
this.send(new IdentMessage(username, this.dataTransceiver.getLocalPort()));
System.out.println("Sent identification packet.");
try {
Object obj = this.in.readObject();
if (obj instanceof PlayerRegisteredMessage msg) {
this.client.setPlayer(msg.getPlayer());
this.client.setWorld(msg.getWorld());
established = true;
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
attempts++;
}
if (!established) {
throw new IOException("Could not initialize connection to server in " + attempts + " attempts.");
}
}
public void shutdown() {
@ -66,7 +95,7 @@ public class MessageTransceiver extends Thread {
buffer.put(type);
buffer.putInt(playerId);
buffer.put(data);
this.dataTransceiver.send(buffer.array(), this.socket.getInetAddress(), this.socket.getPort());
this.dataTransceiver.send(buffer.array());
}
@Override
@ -75,12 +104,7 @@ public class MessageTransceiver extends Thread {
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.setPlayer(prm.getPlayer());
this.client.setWorld(prm.getWorld());
} else if (msg.getType() == Type.CHAT) {
if (msg.getType() == Type.CHAT) {
this.client.getChatManager().addChatMessage((ChatMessage) msg);
} else if (msg.getType() == Type.PLAYER_JOINED && this.client.getWorld() != null) {
PlayerUpdateMessage pum = (PlayerUpdateMessage) msg;
@ -88,6 +112,9 @@ public class MessageTransceiver extends Thread {
} else if (msg.getType() == Type.PLAYER_LEFT && this.client.getWorld() != null) {
PlayerUpdateMessage pum = (PlayerUpdateMessage) msg;
this.client.getWorld().getPlayers().remove(pum.getPlayer().getId());
} else if (msg.getType() == Type.SERVER_SHUTDOWN) {
this.client.shutdown();
JOptionPane.showMessageDialog(null, "Server has been shut down.", "Server Shutdown", JOptionPane.WARNING_MESSAGE);
}
} catch (StreamCorruptedException | EOFException e) {
this.shutdown();

View File

@ -9,7 +9,7 @@ public class Tester {
};
public static void main(String[] args) {
for (int i = 0; i < 6; i++) {
for (int i = 0; i < 1; i++) {
try {
new Client("localhost", 8035, names[ThreadLocalRandom.current().nextInt(names.length)]);
} catch (IOException e) {

View File

@ -9,7 +9,7 @@ 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 float MOUSE_UPDATES_PER_SECOND = 60.0f;
private static final long MS_PER_MOUSE_UPDATE = (long) (1000.0f / MOUSE_UPDATES_PER_SECOND);
private final Client client;

View File

@ -0,0 +1,132 @@
package nl.andrewlalis.aos_client.launcher;
import nl.andrewlalis.aos_client.Client;
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
public class Launcher extends JFrame {
private static final Pattern addressPattern = Pattern.compile("(.+):(\\d+)");
public Launcher() throws HeadlessException {
super("Ace of Shades - Launcher");
this.setDefaultCloseOperation(EXIT_ON_CLOSE);
this.setContentPane(this.buildContent());
this.pack();
this.setLocationRelativeTo(null);
}
private Container buildContent() {
JTabbedPane mainPanel = new JTabbedPane(SwingConstants.TOP, JTabbedPane.SCROLL_TAB_LAYOUT);
mainPanel.addTab("Connect", null, this.getConnectPanel(), "Connect to a server and play.");
JPanel serversPanel = new JPanel();
mainPanel.addTab("Servers", null, serversPanel, "View a list of available servers.");
JPanel settingsPanel = new JPanel();
mainPanel.addTab("Settings", null, settingsPanel, "Change game settings.");
return mainPanel;
}
private Container getConnectPanel() {
JPanel inputPanel = new JPanel(new GridBagLayout());
GridBagConstraints c = new GridBagConstraints();
c.insets = new Insets(5, 5, 5, 5);
c.gridx = 0;
c.gridy = 0;
inputPanel.add(new JLabel("Address"), c);
JTextField addressField = new JTextField(20);
c.gridx = 1;
inputPanel.add(addressField, c);
c.gridy = 1;
c.gridx = 0;
inputPanel.add(new JLabel("Username"), c);
JTextField usernameField = new JTextField(20);
c.gridx = 1;
inputPanel.add(usernameField, c);
var enterListener = new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
if (validateInput(addressField, usernameField)) {
connect(addressField, usernameField);
}
}
}
};
addressField.addKeyListener(enterListener);
usernameField.addKeyListener(enterListener);
JPanel buttonPanel = new JPanel(new FlowLayout());
JButton cancelButton = new JButton("Cancel");
cancelButton.addActionListener(e -> this.dispose());
JButton connectButton = new JButton("Connect");
connectButton.addActionListener(e -> {
if (validateInput(addressField, usernameField)) {
connect(addressField, usernameField);
}
});
buttonPanel.add(cancelButton);
buttonPanel.add(connectButton);
JPanel mainPanel = new JPanel(new BorderLayout());
mainPanel.add(inputPanel, BorderLayout.CENTER);
mainPanel.add(buttonPanel, BorderLayout.SOUTH);
return mainPanel;
}
private boolean validateInput(JTextField addressField, JTextField usernameField) {
List<String> warnings = new ArrayList<>();
if (addressField.getText() == null || addressField.getText().isBlank()) {
warnings.add("Address must not be empty.");
}
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.");
}
if (!warnings.isEmpty()) {
JOptionPane.showMessageDialog(
this,
String.join("\n", warnings),
"Invalid Input",
JOptionPane.WARNING_MESSAGE
);
}
return warnings.isEmpty();
}
private void connect(JTextField addressField, JTextField usernameField) {
String hostAndPort = addressField.getText();
String[] parts = hostAndPort.split(":");
String host = parts[0].trim();
int port = Integer.parseInt(parts[1]);
String username = usernameField.getText();
try {
new Client(host, port, username);
} catch (IOException ex) {
ex.printStackTrace();
JOptionPane.showMessageDialog(null, "Could not connect:\n" + ex.getMessage(), "Connection Error", JOptionPane.WARNING_MESSAGE);
}
}
public static void main(String[] args) {
Launcher launcher = new Launcher();
launcher.setVisible(true);
}
}

View File

@ -1,16 +0,0 @@
package nl.andrewlalis.aos_core.net;
import nl.andrewlalis.aos_core.model.PlayerControlState;
public class PlayerControlStateMessage extends Message {
private final PlayerControlState playerControlState;
public PlayerControlStateMessage(PlayerControlState pcs) {
super(Type.PLAYER_CONTROL_STATE);
this.playerControlState = pcs;
}
public PlayerControlState getPlayerControlState() {
return playerControlState;
}
}

View File

@ -4,8 +4,8 @@ public enum Type {
IDENT,
PLAYER_REGISTERED,
CHAT,
PLAYER_CONTROL_STATE,
PLAYER_JOINED,
PLAYER_LEFT,
PLAYER_TEAM_CHANGE
PLAYER_TEAM_CHANGE,
SERVER_SHUTDOWN
}

View File

@ -1,6 +1,7 @@
package nl.andrewlalis.aos_core.net.data;
public class DataTypes {
public static final byte INIT = 0;
public static final byte PLAYER_CONTROL_STATE = 1;
public static final byte WORLD_DATA = 2;
public static final byte PLAYER_DETAIL = 3;

View File

@ -16,6 +16,7 @@ import java.net.Socket;
import java.net.SocketException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* Thread which handles communicating with a single client socket connection.
@ -41,6 +42,38 @@ public class ClientHandler extends Thread {
this.in = new ObjectInputStream(socket.getInputStream());
}
/**
* Initializes the TCP connection to a client, by waiting for the client to
* send an IDENT packet containing the client's username and other details
* that are needed to start.
* <p>
* Once we receive a valid IDENT packet, the player is registered into
* the server, and receives a {@link PlayerRegisteredMessage} response.
* </p>
* @throws IOException If initialization could not be completed.
*/
private void initializeConnection() throws IOException {
boolean connectionEstablished = false;
int attempts = 0;
while (!connectionEstablished && attempts < 100) {
try {
Object obj = this.in.readObject();
if (obj instanceof IdentMessage msg) {
this.player = this.server.registerNewPlayer(msg.getName());
this.clientUdpPort = msg.getUdpPort();
this.send(new PlayerRegisteredMessage(this.player, this.server.getWorld()));
connectionEstablished = true;
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
attempts++;
}
if (!connectionEstablished) {
throw new IOException("Could not establish connection after " + attempts + " attempts.");
}
}
public Player getPlayer() {
return player;
}
@ -55,12 +88,21 @@ public class ClientHandler extends Thread {
public void shutdown() {
this.running = false;
if (!this.socket.isClosed()) {
this.send(new Message(Type.SERVER_SHUTDOWN));
}
this.sendingQueue.shutdown();
try {
boolean terminated = false;
while (!terminated) {
terminated = this.sendingQueue.awaitTermination(1000, TimeUnit.MILLISECONDS);
}
this.in.close();
this.out.close();
if (!this.socket.isClosed()) {
this.socket.close();
} catch (IOException e) {
}
} catch (IOException | InterruptedException e) {
System.err.println("Could not close streams when shutting down client handler for player " + this.player.getId() + ": " + e.getMessage());
}
}
@ -72,22 +114,24 @@ public class ClientHandler extends Thread {
this.out.reset();
this.out.writeObject(message);
} catch (IOException e) {
e.printStackTrace();
System.err.println("Could not send message " + message.getClass().getName() + ": " + e.getMessage());
}
});
}
@Override
public void run() {
try {
this.initializeConnection();
} catch (IOException e) {
System.err.println("Could not initialize connection to the client: " + e.getMessage());
this.shutdown();
return;
}
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) {
if (msg.getType() == Type.CHAT) {
this.server.getChatManager().handlePlayerChat(this, this.player, (ChatMessage) msg);
}
} catch (SocketException e) {

View File

@ -47,8 +47,10 @@ public class DataTransceiver extends Thread {
this.socket.receive(packet);
ByteBuffer b = ByteBuffer.wrap(packet.getData(), 0, packet.getLength());
byte type = b.get();
if (type == DataTypes.INIT) { // New client is trying to initiate an outbound connection so simply echo packet.
this.send(new byte[]{DataTypes.INIT}, packet.getAddress(), packet.getPort());
} else if (type == DataTypes.PLAYER_CONTROL_STATE) {
int playerId = b.getInt();
if (type == DataTypes.PLAYER_CONTROL_STATE) {
if (playerId < 1) continue;
byte[] stateBuffer = new byte[b.remaining()];
b.get(stateBuffer);

View File

@ -197,7 +197,7 @@ public class Server {
this.dataTransceiver.start();
this.worldUpdater.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/UDP on port " + this.serverSocket.getLocalPort() + "; now accepting connections.");
while (this.running) {
this.acceptClientConnection();
}