Added start of a launcher frame, and other improvements to communication protocol.
This commit is contained in:
parent
09f5630a0c
commit
4e01673bf8
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue