diff --git a/Projekte/battleship/client/src/main/java/pp/battleship/client/ClientServer.java b/Projekte/battleship/client/src/main/java/pp/battleship/client/ClientServer.java deleted file mode 100644 index 3fb21586..00000000 --- a/Projekte/battleship/client/src/main/java/pp/battleship/client/ClientServer.java +++ /dev/null @@ -1,7 +0,0 @@ -package pp.battleship.client; - -/** - *this class is used for the server, when one player decides to host the server locally - */ -public class ClientServer { -} diff --git a/Projekte/battleship/client/src/main/java/pp/battleship/client/GameSound.java b/Projekte/battleship/client/src/main/java/pp/battleship/client/GameSound.java index 0fdcef68..821f6245 100644 --- a/Projekte/battleship/client/src/main/java/pp/battleship/client/GameSound.java +++ b/Projekte/battleship/client/src/main/java/pp/battleship/client/GameSound.java @@ -30,7 +30,6 @@ public class GameSound extends AbstractAppState implements GameEventListener { private static final Logger LOGGER = System.getLogger(GameSound.class.getName()); private static final Preferences PREFERENCES = getPreferences(GameSound.class); private static final String ENABLED_PREF = "enabled"; //NON-NLS - private AudioNode splashSound; private AudioNode shipDestroyedSound; private AudioNode explosionSound; 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..57e88835 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,10 +7,13 @@ package pp.battleship.client; +import com.simsilica.lemur.Checkbox; import com.simsilica.lemur.Container; import com.simsilica.lemur.Label; import com.simsilica.lemur.TextField; import com.simsilica.lemur.component.SpringGridLayout; +import pp.battleship.client.server.BattleshipLocalServer; +import pp.battleship.client.server.BattleshipLocalServer; import pp.dialog.Dialog; import pp.dialog.DialogBuilder; import pp.dialog.SimpleDialog; @@ -37,6 +40,7 @@ class NetworkDialog extends SimpleDialog { private int portNumber; private Future connectionFuture; private Dialog progressDialog; + private boolean localServer = false; /** * Constructs a new NetworkDialog. @@ -57,6 +61,13 @@ class NetworkDialog extends SimpleDialog { input.addChild(new Label(lookup("port.number") + ": ")); input.addChild(port, 1); + /* + * this checkbox controls, whether it will connect to an existing server or create a separate server + */ + Checkbox localServerCb = new Checkbox(lookup("local.server")); + localServerCb.setChecked(false); + localServerCb.addClickCommands(s -> localServer = !localServer); + DialogBuilder.simple(app.getDialogManager()) .setTitle(lookup("server.dialog")) .setExtension(d -> d.addChild(input)) @@ -65,13 +76,14 @@ class NetworkDialog extends SimpleDialog { .setOkClose(false) .setNoClose(false) .build(this); + addChild(localServerCb); // adds the checkbox to the network dialog at the bottom } /** * Handles the action for the connect button in the connection dialog. * Tries to parse the port number and initiate connection to the server. */ - private void connect() { + private void connectServer() { LOGGER.log(Level.INFO, "connect to host={0}, port={1}", host, port); //NON-NLS try { hostname = host.getText().trim().isEmpty() ? LOCALHOST : host.getText(); @@ -84,6 +96,40 @@ private void connect() { } } + /** + * connects to the server, creates one, if no server is up + */ + public void connect() { + if (localServer) { + startLocalServer(); //starts a local server + try { + Thread.sleep(1000); // waits, to ensure, that the server in the separate Thread has enough time to start properly + } + catch (Exception e) { + LOGGER.log(Level.WARNING, e.getMessage(), e); + } + connectServer(); // connects to the server, that was just created + } + else { + connectServer(); // connects to existing server, when the server si not running, or not started by one of the clients + } + } + + /** + *starts a local server, in another thread + */ + private void startLocalServer() { + new Thread(() -> { //creates new Thread + try { + BattleshipLocalServer battleshipLocalServer = new BattleshipLocalServer(Integer.parseInt(port.getText())); // creates local server, with the given port numbers + battleshipLocalServer.run(); // runs the local server + } + catch (Exception e) { + LOGGER.log(Level.ERROR, e.getMessage(), e); + } + }).start(); // starts the thread + } + /** * Creates a dialog indicating that the connection is in progress. */ diff --git a/Projekte/battleship/client/src/main/java/pp/battleship/client/server/BattleshipLocalServer.java b/Projekte/battleship/client/src/main/java/pp/battleship/client/server/BattleshipLocalServer.java new file mode 100644 index 00000000..21aea371 --- /dev/null +++ b/Projekte/battleship/client/src/main/java/pp/battleship/client/server/BattleshipLocalServer.java @@ -0,0 +1,178 @@ +//////////////////////////////////////// +// Programming project code +// UniBw M, 2022, 2023, 2024 +// www.unibw.de/inf2 +// (c) Mark Minas (mark.minas@unibw.de) +//////////////////////////////////////// + +package pp.battleship.client.server; + +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 BattleshipLocalServer implements MessageListener, ConnectionListener, ServerSender { + private static final Logger LOGGER = System.getLogger(BattleshipLocalServer.class.getName()); + private static final File CONFIG_FILE = new File("server.properties"); + private final BattleshipConfig config = new BattleshipConfig(); + private Server myServer; + private static int PORT_NUMBER; + private final ServerGameLogic logic; + 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()); + } + } + + /** + * this constructs the local battleship server + * + * @param portNumber the port number, the local-host should be accessed at + */ + public BattleshipLocalServer(int portNumber) { + config.readFromIfExists(CONFIG_FILE); + PORT_NUMBER = portNumber; + LOGGER.log(Level.INFO, "Configuration: {0}", config); //NON-NLS + logic = new ServerGameLogic(this, config); + } + + public void run() { + startServer(); + while (true) + processNextMessage(); + } + + /** + * this method is used to start a server + */ + private void startServer() { + try { + LOGGER.log(Level.INFO, "Starting server..."); //NON-NLS + myServer = Network.createServer(PORT_NUMBER); // creates new server, with the port given before + 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); + } + } + + 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(); + } + } + + 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); + } + + private void registerListeners() { + myServer.addMessageListener(this, MapMessage.class); + myServer.addMessageListener(this, ShootMessage.class); + myServer.addConnectionListener(this); + } + + @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())); + } + + @Override + public void connectionAdded(Server server, HostedConnection hostedConnection) { + LOGGER.log(Level.INFO, "new connection {0}", hostedConnection); //NON-NLS + logic.addPlayer(hostedConnection.getId()); + } + + @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); + } + } + + 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/server/ReceivedMessage.java b/Projekte/battleship/client/src/main/java/pp/battleship/client/server/ReceivedMessage.java new file mode 100644 index 00000000..a688fe98 --- /dev/null +++ b/Projekte/battleship/client/src/main/java/pp/battleship/client/server/ReceivedMessage.java @@ -0,0 +1,17 @@ +//////////////////////////////////////// +// Programming project code +// UniBw M, 2022, 2023, 2024 +// www.unibw.de/inf2 +// (c) Mark Minas (mark.minas@unibw.de) +//////////////////////////////////////// + +package pp.battleship.client.server; + +import pp.battleship.message.client.ClientInterpreter; +import pp.battleship.message.client.ClientMessage; + +record ReceivedMessage(ClientMessage message, int from) { + void process(ClientInterpreter interpreter) { + message.accept(interpreter, from); + } +} diff --git a/Projekte/battleship/client/src/main/resources/Sound/Background/README.txt b/Projekte/battleship/client/src/main/resources/Sound/Background/README.txt new file mode 100644 index 00000000..56ebf7df --- /dev/null +++ b/Projekte/battleship/client/src/main/resources/Sound/Background/README.txt @@ -0,0 +1,3 @@ +The soundtrack for the background-music is from: +https://pixabay.com/music/future-bass-nightfall-future-bass-music-228100/ +the soundtrack is free to use \ No newline at end of file diff --git a/Projekte/battleship/model/src/main/resources/battleship.properties b/Projekte/battleship/model/src/main/resources/battleship.properties index 0aee4da3..8964446a 100644 --- a/Projekte/battleship/model/src/main/resources/battleship.properties +++ b/Projekte/battleship/model/src/main/resources/battleship.properties @@ -40,3 +40,4 @@ map.doesnt.fit=The map doesn't fit to this game background.music.checkbox= Background music on/off background.music.volume= Background volume player.submitted.invalid.map= invalid map submitted +local.server= instantiate local server diff --git a/Projekte/battleship/model/src/main/resources/battleship_de.properties b/Projekte/battleship/model/src/main/resources/battleship_de.properties index da5e4af6..c80c2516 100644 --- a/Projekte/battleship/model/src/main/resources/battleship_de.properties +++ b/Projekte/battleship/model/src/main/resources/battleship_de.properties @@ -40,4 +40,5 @@ map.doesnt.fit=Diese Karte passt nicht zu diesem Spiel background.music.checkbox= Musik an/aus background.music.volume= Musiklautstärke player.submitted.invalid.map= Invalide karte gegeben +local.server=Lokalen Server erstellen