added the feature that a client can host a server
- added a class BattleshipServer (a client host a local server) and ReceivedMessage - edited the NetworkDialog, that a client has a checkbox to select to host a server -
This commit is contained in:
@@ -0,0 +1,245 @@
|
|||||||
|
////////////////////////////////////////
|
||||||
|
// Programming project code
|
||||||
|
// UniBw M, 2022, 2023, 2024
|
||||||
|
// www.unibw.de/inf2
|
||||||
|
// (c) Mark Minas (mark.minas@unibw.de)
|
||||||
|
////////////////////////////////////////
|
||||||
|
|
||||||
|
package pp.battleship.client;
|
||||||
|
|
||||||
|
import com.jme3.network.ConnectionListener;
|
||||||
|
import com.jme3.network.HostedConnection;
|
||||||
|
import com.jme3.network.Message;
|
||||||
|
import com.jme3.network.MessageListener;
|
||||||
|
import com.jme3.network.Network;
|
||||||
|
import com.jme3.network.Server;
|
||||||
|
import com.jme3.network.serializing.Serializer;
|
||||||
|
import pp.battleship.BattleshipConfig;
|
||||||
|
import pp.battleship.game.server.Player;
|
||||||
|
import pp.battleship.game.server.ServerGameLogic;
|
||||||
|
import pp.battleship.game.server.ServerSender;
|
||||||
|
import pp.battleship.message.client.ClientMessage;
|
||||||
|
import pp.battleship.message.client.MapMessage;
|
||||||
|
import pp.battleship.message.client.ShootMessage;
|
||||||
|
import pp.battleship.message.server.EffectMessage;
|
||||||
|
import pp.battleship.message.server.GameDetails;
|
||||||
|
import pp.battleship.message.server.ServerMessage;
|
||||||
|
import pp.battleship.message.server.StartBattleMessage;
|
||||||
|
import pp.battleship.model.Battleship;
|
||||||
|
import pp.battleship.model.IntPoint;
|
||||||
|
import pp.battleship.model.Shot;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.System.Logger;
|
||||||
|
import java.lang.System.Logger.Level;
|
||||||
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
import java.util.logging.LogManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server implementing the visitor pattern as MessageReceiver for ClientMessages
|
||||||
|
*/
|
||||||
|
public class BattleshipServer implements MessageListener<HostedConnection>, ConnectionListener, ServerSender {
|
||||||
|
/**
|
||||||
|
* Logger for the BattleshipServer class.
|
||||||
|
*/
|
||||||
|
private static final Logger LOGGER = System.getLogger(BattleshipServer.class.getName());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration file for the server.
|
||||||
|
*/
|
||||||
|
private static final File CONFIG_FILE = new File("server.properties");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Port number for the server.
|
||||||
|
*/
|
||||||
|
private final int PORT_NUMBER;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration settings for the Battleship server.
|
||||||
|
*/
|
||||||
|
private final BattleshipConfig config = new BattleshipConfig();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The server instance.
|
||||||
|
*/
|
||||||
|
private Server myServer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Game logic for the server.
|
||||||
|
*/
|
||||||
|
private final ServerGameLogic logic;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queue for pending messages to be processed by the server.
|
||||||
|
*/
|
||||||
|
private final BlockingQueue<ReceivedMessage> pendingMessages = new LinkedBlockingQueue<>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
// Configure logging
|
||||||
|
LogManager manager = LogManager.getLogManager();
|
||||||
|
try {
|
||||||
|
manager.readConfiguration(new FileInputStream("logging.properties"));
|
||||||
|
LOGGER.log(Level.INFO, "Successfully read logging properties"); //NON-NLS
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.log(Level.INFO, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the server.
|
||||||
|
*/
|
||||||
|
public BattleshipServer(int PORT_NUMBER) {
|
||||||
|
config.readFromIfExists(CONFIG_FILE);
|
||||||
|
this.PORT_NUMBER = PORT_NUMBER;
|
||||||
|
LOGGER.log(Level.INFO, "Configuration: {0}", config); //NON-NLS
|
||||||
|
logic = new ServerGameLogic(this, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the server and processes incoming messages indefinitely.
|
||||||
|
*/
|
||||||
|
public void run() {
|
||||||
|
startServer();
|
||||||
|
while (true)
|
||||||
|
processNextMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the server and initializes necessary components.
|
||||||
|
* This method sets up the server, registers serializable classes,
|
||||||
|
* starts the server, and registers listeners for incoming connections and messages.
|
||||||
|
* If the server fails to start, it logs an error and exits the application.
|
||||||
|
*/
|
||||||
|
private void startServer() {
|
||||||
|
try {
|
||||||
|
LOGGER.log(Level.INFO, "Starting server..."); //NON-NLS
|
||||||
|
myServer = Network.createServer(PORT_NUMBER);
|
||||||
|
initializeSerializables();
|
||||||
|
myServer.start();
|
||||||
|
registerListeners();
|
||||||
|
LOGGER.log(Level.INFO, "Server started: {0}", myServer.isRunning()); //NON-NLS
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.log(Level.ERROR, "Couldn't start server: {0}", e.getMessage()); //NON-NLS
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes the next message in the queue.
|
||||||
|
* This method blocks until a message is available, then processes it using the server logic.
|
||||||
|
* If interrupted while waiting, it logs the interruption and re-interrupts the thread.
|
||||||
|
*/
|
||||||
|
private void processNextMessage() {
|
||||||
|
try {
|
||||||
|
pendingMessages.take().process(logic);
|
||||||
|
} catch (InterruptedException ex) {
|
||||||
|
LOGGER.log(Level.INFO, "Interrupted while waiting for messages"); //NON-NLS
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers the serializable classes used by the server.
|
||||||
|
* This method ensures that the necessary classes are registered with the serializer
|
||||||
|
* so that they can be correctly transmitted over the network.
|
||||||
|
*/
|
||||||
|
private void initializeSerializables() {
|
||||||
|
Serializer.registerClass(GameDetails.class);
|
||||||
|
Serializer.registerClass(StartBattleMessage.class);
|
||||||
|
Serializer.registerClass(MapMessage.class);
|
||||||
|
Serializer.registerClass(ShootMessage.class);
|
||||||
|
Serializer.registerClass(EffectMessage.class);
|
||||||
|
Serializer.registerClass(Battleship.class);
|
||||||
|
Serializer.registerClass(IntPoint.class);
|
||||||
|
Serializer.registerClass(Shot.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers listeners for incoming connections and messages.
|
||||||
|
* This method adds message listeners for `MapMessage` and `ShootMessage` classes,
|
||||||
|
* and a connection listener for handling connection events.
|
||||||
|
*/
|
||||||
|
private void registerListeners() {
|
||||||
|
myServer.addMessageListener(this, MapMessage.class);
|
||||||
|
myServer.addMessageListener(this, ShootMessage.class);
|
||||||
|
myServer.addConnectionListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the reception of messages from clients.
|
||||||
|
*
|
||||||
|
* @param source the connection from which the message was received
|
||||||
|
* @param message the message received from the client
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void messageReceived(HostedConnection source, Message message) {
|
||||||
|
LOGGER.log(Level.INFO, "message received from {0}: {1}", source.getId(), message); //NON-NLS
|
||||||
|
if (message instanceof ClientMessage clientMessage)
|
||||||
|
pendingMessages.add(new ReceivedMessage(clientMessage, source.getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a new connection is added to the server.
|
||||||
|
*
|
||||||
|
* @param server the server to which the connection was added
|
||||||
|
* @param hostedConnection the connection that was added
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void connectionAdded(Server server, HostedConnection hostedConnection) {
|
||||||
|
LOGGER.log(Level.INFO, "new connection {0}", hostedConnection); //NON-NLS
|
||||||
|
logic.addPlayer(hostedConnection.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a connection is removed from the server.
|
||||||
|
*
|
||||||
|
* @param server the server from which the connection was removed
|
||||||
|
* @param hostedConnection the connection that was removed
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void connectionRemoved(Server server, HostedConnection hostedConnection) {
|
||||||
|
LOGGER.log(Level.INFO, "connection closed: {0}", hostedConnection); //NON-NLS
|
||||||
|
final Player player = logic.getPlayerById(hostedConnection.getId());
|
||||||
|
if (player == null)
|
||||||
|
LOGGER.log(Level.INFO, "closed connection does not belong to an active player"); //NON-NLS
|
||||||
|
else { //NON-NLS
|
||||||
|
LOGGER.log(Level.INFO, "closed connection belongs to {0}", player); //NON-NLS
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exits the application with the specified exit value.
|
||||||
|
* Closes all client connections and logs the close request.
|
||||||
|
*
|
||||||
|
* @param exitValue the exit value to be used when exiting the application
|
||||||
|
*/
|
||||||
|
private void exit(int exitValue) { //NON-NLS
|
||||||
|
LOGGER.log(Level.INFO, "close request"); //NON-NLS
|
||||||
|
if (myServer != null)
|
||||||
|
for (HostedConnection client : myServer.getConnections()) //NON-NLS
|
||||||
|
if (client != null) client.close("Game over"); //NON-NLS
|
||||||
|
System.exit(exitValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the specified message to the specified connection.
|
||||||
|
*
|
||||||
|
* @param id the connection id
|
||||||
|
* @param message the message
|
||||||
|
*/
|
||||||
|
public void send(int id, ServerMessage message) {
|
||||||
|
if (myServer == null || !myServer.isRunning()) {
|
||||||
|
LOGGER.log(Level.ERROR, "no server running when trying to send {0}", message); //NON-NLS
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final HostedConnection connection = myServer.getConnection(id);
|
||||||
|
if (connection != null)
|
||||||
|
connection.send(message);
|
||||||
|
else
|
||||||
|
LOGGER.log(Level.ERROR, "there is no connection with id={0}", id); //NON-NLS
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
package pp.battleship.client;
|
package pp.battleship.client;
|
||||||
|
|
||||||
|
import com.simsilica.lemur.Checkbox;
|
||||||
import com.simsilica.lemur.Container;
|
import com.simsilica.lemur.Container;
|
||||||
import com.simsilica.lemur.Label;
|
import com.simsilica.lemur.Label;
|
||||||
import com.simsilica.lemur.TextField;
|
import com.simsilica.lemur.TextField;
|
||||||
@@ -30,6 +31,7 @@ class NetworkDialog extends SimpleDialog {
|
|||||||
private static final Logger LOGGER = System.getLogger(NetworkDialog.class.getName());
|
private static final Logger LOGGER = System.getLogger(NetworkDialog.class.getName());
|
||||||
private static final String LOCALHOST = "localhost"; //NON-NLS
|
private static final String LOCALHOST = "localhost"; //NON-NLS
|
||||||
private static final String DEFAULT_PORT = "1234"; //NON-NLS
|
private static final String DEFAULT_PORT = "1234"; //NON-NLS
|
||||||
|
private static final int START_SERVER_DELAY = 2000;
|
||||||
private final NetworkSupport network;
|
private final NetworkSupport network;
|
||||||
private final TextField host = new TextField(LOCALHOST);
|
private final TextField host = new TextField(LOCALHOST);
|
||||||
private final TextField port = new TextField(DEFAULT_PORT);
|
private final TextField port = new TextField(DEFAULT_PORT);
|
||||||
@@ -37,6 +39,7 @@ class NetworkDialog extends SimpleDialog {
|
|||||||
private int portNumber;
|
private int portNumber;
|
||||||
private Future<Object> connectionFuture;
|
private Future<Object> connectionFuture;
|
||||||
private Dialog progressDialog;
|
private Dialog progressDialog;
|
||||||
|
private boolean hostServer = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a new NetworkDialog.
|
* Constructs a new NetworkDialog.
|
||||||
@@ -50,21 +53,26 @@ class NetworkDialog extends SimpleDialog {
|
|||||||
host.setPreferredWidth(400f);
|
host.setPreferredWidth(400f);
|
||||||
port.setSingleLine(true);
|
port.setSingleLine(true);
|
||||||
|
|
||||||
|
Checkbox hostCheckbox = new Checkbox(lookup("host.own-server"));
|
||||||
|
hostCheckbox.setChecked(false);
|
||||||
|
hostCheckbox.addClickCommands(s -> hostServer = !hostServer);
|
||||||
|
|
||||||
final BattleshipApp app = network.getApp();
|
final BattleshipApp app = network.getApp();
|
||||||
final Container input = new Container(new SpringGridLayout());
|
final Container input = new Container(new SpringGridLayout());
|
||||||
input.addChild(new Label(lookup("host.name") + ": "));
|
input.addChild(new Label(lookup("host.name") + ": "));
|
||||||
input.addChild(host, 1);
|
input.addChild(host, 1);
|
||||||
input.addChild(new Label(lookup("port.number") + ": "));
|
input.addChild(new Label(lookup("port.number") + ": "));
|
||||||
input.addChild(port, 1);
|
input.addChild(port, 1);
|
||||||
|
input.addChild(hostCheckbox);
|
||||||
|
|
||||||
DialogBuilder.simple(app.getDialogManager())
|
DialogBuilder.simple(app.getDialogManager())
|
||||||
.setTitle(lookup("server.dialog"))
|
.setTitle(lookup("server.dialog"))
|
||||||
.setExtension(d -> d.addChild(input))
|
.setExtension(d -> d.addChild(input))
|
||||||
.setOkButton(lookup("button.connect"), d -> connect())
|
.setOkButton(lookup("button.connect"), d -> connectHostServer())
|
||||||
.setNoButton(lookup("button.cancel"), app::closeApp)
|
.setNoButton(lookup("button.cancel"), app::closeApp)
|
||||||
.setOkClose(false)
|
.setOkClose(false)
|
||||||
.setNoClose(false)
|
.setNoClose(false)
|
||||||
.build(this);
|
.build(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,19 +86,54 @@ private void connect() {
|
|||||||
portNumber = Integer.parseInt(port.getText());
|
portNumber = Integer.parseInt(port.getText());
|
||||||
openProgressDialog();
|
openProgressDialog();
|
||||||
connectionFuture = network.getApp().getExecutor().submit(this::initNetwork);
|
connectionFuture = network.getApp().getExecutor().submit(this::initNetwork);
|
||||||
}
|
} catch (NumberFormatException e) {
|
||||||
catch (NumberFormatException e) {
|
|
||||||
network.getApp().errorDialog(lookup("port.must.be.integer"));
|
network.getApp().errorDialog(lookup("port.must.be.integer"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connects to the host server. If the `hostServer` flag is set, it starts the server
|
||||||
|
* before attempting to connect. If the server fails to start, logs an error.
|
||||||
|
*/
|
||||||
|
private void connectHostServer() {
|
||||||
|
if (hostServer) {
|
||||||
|
startServer();
|
||||||
|
try {
|
||||||
|
Thread.sleep(START_SERVER_DELAY);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.log(Level.ERROR, "Server start failed", e); //NON-NLS
|
||||||
|
}
|
||||||
|
connect();
|
||||||
|
} else {
|
||||||
|
connect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the game server in a new thread.
|
||||||
|
* Logs an error if the server fails to start.
|
||||||
|
*/
|
||||||
|
private void startServer() {
|
||||||
|
LOGGER.log(Level.INFO, "start server"); //NON-NLS
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
LOGGER.log(Level.INFO, "Starting server..."); //NON-NLS
|
||||||
|
BattleshipServer server = new BattleshipServer(Integer.parseInt(port.getText()));
|
||||||
|
LOGGER.log(Level.INFO, "Server started"); //NON-NLS
|
||||||
|
server.run();
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.log(Level.ERROR, "Server start failed", e); //NON-NLS
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a dialog indicating that the connection is in progress.
|
* Creates a dialog indicating that the connection is in progress.
|
||||||
*/
|
*/
|
||||||
private void openProgressDialog() {
|
private void openProgressDialog() {
|
||||||
progressDialog = DialogBuilder.simple(network.getApp().getDialogManager())
|
progressDialog = DialogBuilder.simple(network.getApp().getDialogManager())
|
||||||
.setText(lookup("label.connecting"))
|
.setText(lookup("label.connecting"))
|
||||||
.build();
|
.build();
|
||||||
progressDialog.open();
|
progressDialog.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,8 +146,7 @@ private Object initNetwork() {
|
|||||||
try {
|
try {
|
||||||
network.initNetwork(hostname, portNumber);
|
network.initNetwork(hostname, portNumber);
|
||||||
return null;
|
return null;
|
||||||
}
|
} catch (Exception e) {
|
||||||
catch (Exception e) {
|
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,11 +161,9 @@ public void update(float delta) {
|
|||||||
try {
|
try {
|
||||||
connectionFuture.get();
|
connectionFuture.get();
|
||||||
success();
|
success();
|
||||||
}
|
} catch (ExecutionException e) {
|
||||||
catch (ExecutionException e) {
|
|
||||||
failure(e.getCause());
|
failure(e.getCause());
|
||||||
}
|
} catch (InterruptedException e) {
|
||||||
catch (InterruptedException e) {
|
|
||||||
LOGGER.log(Level.WARNING, "Interrupted!", e); //NON-NLS
|
LOGGER.log(Level.WARNING, "Interrupted!", e); //NON-NLS
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
////////////////////////////////////////
|
||||||
|
// Programming project code
|
||||||
|
// UniBw M, 2022, 2023, 2024
|
||||||
|
// www.unibw.de/inf2
|
||||||
|
// (c) Mark Minas (mark.minas@unibw.de)
|
||||||
|
////////////////////////////////////////
|
||||||
|
|
||||||
|
package pp.battleship.client;
|
||||||
|
|
||||||
|
import pp.battleship.message.client.ClientInterpreter;
|
||||||
|
import pp.battleship.message.client.ClientMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a received message from a client.
|
||||||
|
*
|
||||||
|
* @param message the client message
|
||||||
|
* @param from the ID of the sender
|
||||||
|
*/
|
||||||
|
record ReceivedMessage(ClientMessage message, int from) {
|
||||||
|
/**
|
||||||
|
* Processes the received message using the specified interpreter.
|
||||||
|
*
|
||||||
|
* @param interpreter the client interpreter
|
||||||
|
*/
|
||||||
|
void process(ClientInterpreter interpreter) {
|
||||||
|
message.accept(interpreter, from);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ button.connect=Connect
|
|||||||
button.cancel=Cancel
|
button.cancel=Cancel
|
||||||
server.dialog=Server
|
server.dialog=Server
|
||||||
host.name=Host
|
host.name=Host
|
||||||
|
host.own-server=Host own server
|
||||||
port.number=Port
|
port.number=Port
|
||||||
wait.its.not.your.turn=Wait, it's not your turn!!
|
wait.its.not.your.turn=Wait, it's not your turn!!
|
||||||
menu.quit=Quit game
|
menu.quit=Quit game
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ button.cancel=Abbruch
|
|||||||
server.dialog=Server
|
server.dialog=Server
|
||||||
host.name=Host
|
host.name=Host
|
||||||
port.number=Port
|
port.number=Port
|
||||||
|
host.own-server=Server hosten
|
||||||
wait.its.not.your.turn=Warte, Du bist nicht dran!!
|
wait.its.not.your.turn=Warte, Du bist nicht dran!!
|
||||||
menu.quit=Spiel beenden
|
menu.quit=Spiel beenden
|
||||||
menu.return-to-game=Zurück zum Spiel
|
menu.return-to-game=Zurück zum Spiel
|
||||||
|
|||||||
Reference in New Issue
Block a user