diff --git a/Projekte/battleship/client/src/main/java/pp/battleship/client/BattleshipServer.java b/Projekte/battleship/client/src/main/java/pp/battleship/client/BattleshipServer.java new file mode 100644 index 00000000..7162bc20 --- /dev/null +++ b/Projekte/battleship/client/src/main/java/pp/battleship/client/BattleshipServer.java @@ -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, 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 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 + } +} \ No newline at end of file diff --git a/Projekte/battleship/client/src/main/java/pp/battleship/client/NetworkDialog.java b/Projekte/battleship/client/src/main/java/pp/battleship/client/NetworkDialog.java index 3f2f5a65..7b182cda 100644 --- a/Projekte/battleship/client/src/main/java/pp/battleship/client/NetworkDialog.java +++ b/Projekte/battleship/client/src/main/java/pp/battleship/client/NetworkDialog.java @@ -7,6 +7,7 @@ package pp.battleship.client; +import com.simsilica.lemur.Checkbox; import com.simsilica.lemur.Container; import com.simsilica.lemur.Label; 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 String LOCALHOST = "localhost"; //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 TextField host = new TextField(LOCALHOST); private final TextField port = new TextField(DEFAULT_PORT); @@ -37,6 +39,7 @@ class NetworkDialog extends SimpleDialog { private int portNumber; private Future connectionFuture; private Dialog progressDialog; + private boolean hostServer = false; /** * Constructs a new NetworkDialog. @@ -50,21 +53,26 @@ class NetworkDialog extends SimpleDialog { host.setPreferredWidth(400f); 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 Container input = new Container(new SpringGridLayout()); input.addChild(new Label(lookup("host.name") + ": ")); input.addChild(host, 1); input.addChild(new Label(lookup("port.number") + ": ")); input.addChild(port, 1); + input.addChild(hostCheckbox); DialogBuilder.simple(app.getDialogManager()) - .setTitle(lookup("server.dialog")) - .setExtension(d -> d.addChild(input)) - .setOkButton(lookup("button.connect"), d -> connect()) - .setNoButton(lookup("button.cancel"), app::closeApp) - .setOkClose(false) - .setNoClose(false) - .build(this); + .setTitle(lookup("server.dialog")) + .setExtension(d -> d.addChild(input)) + .setOkButton(lookup("button.connect"), d -> connectHostServer()) + .setNoButton(lookup("button.cancel"), app::closeApp) + .setOkClose(false) + .setNoClose(false) + .build(this); } /** @@ -78,19 +86,54 @@ private void connect() { portNumber = Integer.parseInt(port.getText()); openProgressDialog(); connectionFuture = network.getApp().getExecutor().submit(this::initNetwork); - } - catch (NumberFormatException e) { + } catch (NumberFormatException e) { 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. */ private void openProgressDialog() { progressDialog = DialogBuilder.simple(network.getApp().getDialogManager()) - .setText(lookup("label.connecting")) - .build(); + .setText(lookup("label.connecting")) + .build(); progressDialog.open(); } @@ -103,8 +146,7 @@ private Object initNetwork() { try { network.initNetwork(hostname, portNumber); return null; - } - catch (Exception e) { + } catch (Exception e) { throw new RuntimeException(e); } } @@ -119,11 +161,9 @@ public void update(float delta) { try { connectionFuture.get(); success(); - } - catch (ExecutionException e) { + } catch (ExecutionException e) { failure(e.getCause()); - } - catch (InterruptedException e) { + } catch (InterruptedException e) { LOGGER.log(Level.WARNING, "Interrupted!", e); //NON-NLS Thread.currentThread().interrupt(); } diff --git a/Projekte/battleship/client/src/main/java/pp/battleship/client/ReceivedMessages.java b/Projekte/battleship/client/src/main/java/pp/battleship/client/ReceivedMessages.java new file mode 100644 index 00000000..e1355cc6 --- /dev/null +++ b/Projekte/battleship/client/src/main/java/pp/battleship/client/ReceivedMessages.java @@ -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); + } +} diff --git a/Projekte/battleship/model/src/main/resources/battleship.properties b/Projekte/battleship/model/src/main/resources/battleship.properties index 7df389c1..aa8fe6fd 100644 --- a/Projekte/battleship/model/src/main/resources/battleship.properties +++ b/Projekte/battleship/model/src/main/resources/battleship.properties @@ -24,6 +24,7 @@ button.connect=Connect button.cancel=Cancel server.dialog=Server host.name=Host +host.own-server=Host own server port.number=Port wait.its.not.your.turn=Wait, it's not your turn!! menu.quit=Quit game diff --git a/Projekte/battleship/model/src/main/resources/battleship_de.properties b/Projekte/battleship/model/src/main/resources/battleship_de.properties index 018f48a0..2fe69a18 100644 --- a/Projekte/battleship/model/src/main/resources/battleship_de.properties +++ b/Projekte/battleship/model/src/main/resources/battleship_de.properties @@ -25,6 +25,7 @@ button.cancel=Abbruch server.dialog=Server host.name=Host port.number=Port +host.own-server=Server hosten wait.its.not.your.turn=Warte, Du bist nicht dran!! menu.quit=Spiel beenden menu.return-to-game=Zurück zum Spiel