From 5edd4ebe0b4424a338b893ddaaaaa92252cb2f4d Mon Sep 17 00:00:00 2001 From: Cedric Beck Date: Sun, 6 Oct 2024 14:29:31 +0200 Subject: [PATCH] Added Ex. 10 added client hosts a server added javadoc comments --- .../java/pp/battleship/client/GameMusic.java | 52 +++-- .../java/pp/battleship/client/GameSound.java | 24 +-- .../pp/battleship/client/NetworkDialog.java | 46 +++- .../pp/battleship/client/VolumeSlider.java | 18 +- .../clienthost/BattleshipServerClient.java | 202 ++++++++++++++++++ .../clienthost/ReceivedMessageClient.java | 17 ++ .../pp/battleship/client/gui/ShipModel.java | 38 ++++ .../game/server/ServerGameLogic.java | 1 - .../pp/battleship/model/dto/ShipMapDTO.java | 10 +- .../common/src/main/java/pp/util/Util.java | 30 +++ 10 files changed, 392 insertions(+), 46 deletions(-) create mode 100644 Projekte/battleship/client/src/main/java/pp/battleship/client/clienthost/BattleshipServerClient.java create mode 100644 Projekte/battleship/client/src/main/java/pp/battleship/client/clienthost/ReceivedMessageClient.java diff --git a/Projekte/battleship/client/src/main/java/pp/battleship/client/GameMusic.java b/Projekte/battleship/client/src/main/java/pp/battleship/client/GameMusic.java index 44d93896..69bd3461 100644 --- a/Projekte/battleship/client/src/main/java/pp/battleship/client/GameMusic.java +++ b/Projekte/battleship/client/src/main/java/pp/battleship/client/GameMusic.java @@ -14,9 +14,12 @@ import java.util.prefs.Preferences; import static pp.util.PreferencesUtils.getPreferences; +import static pp.util.Util.loadSound; +/** + * An application state that plays sounds. + */ public class GameMusic extends AbstractAppState implements GameEventListener { - private static final Logger LOGGER = System.getLogger(GameMusic.class.getName()); private static final Preferences PREFERENCES = getPreferences(GameMusic.class); private static final String ENABLED_PREF = "enabled"; private static final String VOLUME_PREF = "volume"; @@ -25,16 +28,30 @@ public class GameMusic extends AbstractAppState implements GameEventListener { private AudioNode backgroundMusic; + /** + * Returns whether the music is enabled in the user preferences. + * + * @return true if music is enabled, false otherwise + */ public static boolean enabledInPreferences(){ return PREFERENCES.getBoolean(ENABLED_PREF, true); } + /** + * Returns the music volume level stored in the user preferences. + * + * @return the volume level as a float (default is 0.5f) + */ public static float volumeInPreferences(){ return PREFERENCES.getFloat(VOLUME_PREF, 0.5f); } - - + /** + * Initializes the game music system + * + * @param stateManager the state manager of the game + * @param app the main application + */ @Override public void initialize(AppStateManager stateManager, Application app) { super.initialize(stateManager, app); @@ -43,19 +60,12 @@ public void initialize(AppStateManager stateManager, Application app) { if (isEnabled()) playMusic(); } - private AudioNode loadSound(Application app, String name) { - try { - final AudioNode sound = new AudioNode(app.getAssetManager(), name, AudioData.DataType.Buffer); - sound.setLooping(false); - sound.setPositional(false); - return sound; - } - catch (AssetLoadException | AssetNotFoundException ex) { - LOGGER.log(Level.ERROR, ex.getMessage(), ex); - } - return null; - } - + /** + * Sets the enabled state of this AppState. + * Overrides {@link com.jme3.app.state.AbstractAppState#setEnabled(boolean)} + * + * @param enabled {@code true} to enable the AppState, {@code false} to disable it. + */ @Override public void setEnabled(boolean enabled) { if(enabled && !isEnabled()){ @@ -68,6 +78,9 @@ else if(!enabled && isEnabled()){ PREFERENCES.putBoolean(ENABLED_PREF, enabled); } + /** + * Plays the background music. + */ public void playMusic(){ if (backgroundMusic != null){ backgroundMusic.play(); @@ -75,7 +88,7 @@ public void playMusic(){ } /** - * Stops background music + * Stops background music. */ public void stopMusic(){ if (backgroundMusic != null){ @@ -83,6 +96,11 @@ public void stopMusic(){ } } + /** + * Sets the volume of the background music and saves the volume setting in user preferences. + * + * @param volume the volume level to set (0.0f to 1.0f) + */ public void setMusicVolume(float volume){ if(backgroundMusic != null){ backgroundMusic.setVolume(volume); 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 4fbc69cf..8adcb432 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 @@ -10,9 +10,6 @@ import com.jme3.app.Application; import com.jme3.app.state.AbstractAppState; import com.jme3.app.state.AppStateManager; -import com.jme3.asset.AssetLoadException; -import com.jme3.asset.AssetNotFoundException; -import com.jme3.audio.AudioData; import com.jme3.audio.AudioNode; import pp.battleship.notification.GameEventListener; import pp.battleship.notification.SoundEvent; @@ -22,6 +19,7 @@ import java.util.prefs.Preferences; import static pp.util.PreferencesUtils.getPreferences; +import static pp.util.Util.loadSound; /** * An application state that plays sounds. @@ -81,26 +79,6 @@ public void initialize(AppStateManager stateManager, Application app) { explosionSound = loadSound(app, "Sound/Effects/explosion.wav"); //NON-NLS } - /** - * Loads a sound from the specified file. - * - * @param app The application - * @param name The name of the sound file. - * @return The loaded AudioNode. - */ - private AudioNode loadSound(Application app, String name) { - try { - final AudioNode sound = new AudioNode(app.getAssetManager(), name, AudioData.DataType.Buffer); - sound.setLooping(false); - sound.setPositional(false); - return sound; - } - catch (AssetLoadException | AssetNotFoundException ex) { - LOGGER.log(Level.ERROR, ex.getMessage(), ex); - } - return null; - } - /** * Plays the splash sound effect. */ 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..996f4d02 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,12 @@ 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.clienthost.BattleshipServerClient; import pp.dialog.Dialog; import pp.dialog.DialogBuilder; import pp.dialog.SimpleDialog; @@ -36,7 +38,10 @@ class NetworkDialog extends SimpleDialog { private String hostname; private int portNumber; private Future connectionFuture; + private Future serverFuture; private Dialog progressDialog; + private BattleshipServerClient server; + private final Checkbox clientHostCheckbox; /** * Constructs a new NetworkDialog. @@ -56,7 +61,8 @@ class NetworkDialog extends SimpleDialog { input.addChild(host, 1); input.addChild(new Label(lookup("port.number") + ": ")); input.addChild(port, 1); - + clientHostCheckbox = new Checkbox("Host Server"); + input.addChild(clientHostCheckbox); DialogBuilder.simple(app.getDialogManager()) .setTitle(lookup("server.dialog")) .setExtension(d -> d.addChild(input)) @@ -77,11 +83,22 @@ private void connect() { hostname = host.getText().trim().isEmpty() ? LOCALHOST : host.getText(); portNumber = Integer.parseInt(port.getText()); openProgressDialog(); + + if (clientHostCheckbox.isChecked()){ + serverFuture = network.getApp().getExecutor().submit(this::initServer); + + while (server == null || !server.isReady()){ + Thread.sleep(100); + } + } connectionFuture = network.getApp().getExecutor().submit(this::initNetwork); } catch (NumberFormatException e) { network.getApp().errorDialog(lookup("port.must.be.integer")); } + catch (InterruptedException e) { + throw new RuntimeException(e); + } } /** @@ -109,6 +126,22 @@ private Object initNetwork() { } } + /** + * Tries to initialize the server hosted by the client. + * + * @throws RuntimeException If an error occurs when starting the server. + */ + private Object initServer(){ + try { + server = new BattleshipServerClient(); + server.run(); + return null; + } catch (Exception e) { + LOGGER.log(Level.ERROR, "Error while starting server", e); + throw new RuntimeException(e); + } + } + /** * This method is called by {@linkplain pp.dialog.DialogManager#update(float)} for periodically * updating this dialog. T @@ -127,6 +160,17 @@ public void update(float delta) { LOGGER.log(Level.WARNING, "Interrupted!", e); //NON-NLS Thread.currentThread().interrupt(); } + + if (serverFuture != null && serverFuture.isDone()) { + try { + serverFuture.get(); + } catch (ExecutionException e) { + LOGGER.log(Level.ERROR, "Failed to start server", e.getCause()); + } catch (InterruptedException e) { + LOGGER.log(Level.WARNING, "Server thread was interrupted", e); + Thread.currentThread().interrupt(); + } + } } /** diff --git a/Projekte/battleship/client/src/main/java/pp/battleship/client/VolumeSlider.java b/Projekte/battleship/client/src/main/java/pp/battleship/client/VolumeSlider.java index ba2d1662..4b28dc41 100644 --- a/Projekte/battleship/client/src/main/java/pp/battleship/client/VolumeSlider.java +++ b/Projekte/battleship/client/src/main/java/pp/battleship/client/VolumeSlider.java @@ -2,10 +2,21 @@ import com.simsilica.lemur.Slider; +/** + * Represents a volume slider for controlling the background music volume in the Battleship game. + * This class extends the {@link Slider} class and interfaces with the {@link GameMusic} instance + * to adjust the volume settings based on user input. + */ public class VolumeSlider extends Slider { - private GameMusic gameMusic; + private final GameMusic gameMusic; private float volume; + /** + * Constructs a new VolumeSlider instance and initializes it with the current volume level + * from the game music preferences. + * + * @param gameMusic the instance of {@link GameMusic} to control music volume + */ public VolumeSlider(GameMusic gameMusic){ super(); this.gameMusic = gameMusic; @@ -13,6 +24,11 @@ public VolumeSlider(GameMusic gameMusic){ getModel().setPercent(volume); } + /** + * Updates the volume setting based on the current slider position. + * If the slider's percent value has changed, it updates the music volume + * in the associated {@link GameMusic} instance. + */ public void update(){ if(getModel().getPercent() != volume){ this.volume = (float) getModel().getPercent(); diff --git a/Projekte/battleship/client/src/main/java/pp/battleship/client/clienthost/BattleshipServerClient.java b/Projekte/battleship/client/src/main/java/pp/battleship/client/clienthost/BattleshipServerClient.java new file mode 100644 index 00000000..c8e364ae --- /dev/null +++ b/Projekte/battleship/client/src/main/java/pp/battleship/client/clienthost/BattleshipServerClient.java @@ -0,0 +1,202 @@ +//////////////////////////////////////// +// Programming project code +// UniBw M, 2022, 2023, 2024 +// www.unibw.de/inf2 +// (c) Mark Minas (mark.minas@unibw.de) +//////////////////////////////////////// + +package pp.battleship.client.clienthost; + +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 BattleshipServerClient implements MessageListener, ConnectionListener, ServerSender { + private static final Logger LOGGER = System.getLogger(BattleshipServerClient.class.getName()); + private static final File CONFIG_FILE = new File("server.properties"); + + private final BattleshipConfig config = new BattleshipConfig(); + private Server myServer; + 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()); + } + } + + /** + * Creates the server and reads the configuration from the specified file. + * Initializes the game logic and sets up logging. + */ + public BattleshipServerClient() { + config.readFromIfExists(CONFIG_FILE); + LOGGER.log(Level.INFO, "Configuration: {0}", config); //NON-NLS + logic = new ServerGameLogic(this, config); + } + + /** + * Checks if the server is ready. + * + * @return true if the server is running, false otherwise + */ + public boolean isReady() { + return myServer != null && myServer.isRunning(); + } + + /** + * Starts the server and continuously processes incoming messages. + */ + public void run() { + startServer(); + while (true) + processNextMessage(); + } + + /** + * Starts the server by creating a network server on the specified port. + */ + private void startServer() { + try { + LOGGER.log(Level.INFO, "Starting server..."); //NON-NLS + myServer = Network.createServer(config.getPort()); + 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. + */ + 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 all serializable message classes for network transmission. + */ + 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 the message and connection listeners for the server. + */ + 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 ReceivedMessageClient(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); + } + } + + /** + * Shuts down the server and closes all active connections. + * + * @param exitValue the exit code to terminate the program with + */ + 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/clienthost/ReceivedMessageClient.java b/Projekte/battleship/client/src/main/java/pp/battleship/client/clienthost/ReceivedMessageClient.java new file mode 100644 index 00000000..ebff95a3 --- /dev/null +++ b/Projekte/battleship/client/src/main/java/pp/battleship/client/clienthost/ReceivedMessageClient.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.clienthost; + +import pp.battleship.message.client.ClientInterpreter; +import pp.battleship.message.client.ClientMessage; + +record ReceivedMessageClient(ClientMessage message, int from) { + void process(ClientInterpreter interpreter) { + message.accept(interpreter, from); + } +} diff --git a/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/ShipModel.java b/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/ShipModel.java index c313fb29..5fbdf108 100644 --- a/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/ShipModel.java +++ b/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/ShipModel.java @@ -2,6 +2,10 @@ import com.jme3.math.Vector3f; +/** + * Enum representing different ship models for the Battleship game. + * Each ship model has a corresponding 3D model path, scale, translation, color texture, and optional bump texture. + */ public enum ShipModel { SHIP1("Models/Ships/1/ship1.j3o",0.15f, new Vector3f(0f, 0f, 0f), "Models/Ships/1/ship1_color.png", null), SHIP2("Models/Ships/2/ship2.j3o",0.03f, new Vector3f(0f, 0.2f, 0f), "Models/Ships/2/ship2.jpg", null), @@ -14,6 +18,15 @@ public enum ShipModel { private final String colorPath; private final String bumpPath; + /** + * Constructs a new ShipModel with the specified parameters. + * + * @param modelPath the path to the 3D model of the ship + * @param modelScale the scale factor to be applied to the model + * @param translation the translation to be applied to the model + * @param colorPath the path to the color texture of the model + * @param bumpPath the optional path to the bump texture of the model (may be null) + */ ShipModel(String modelPath, float modelScale, Vector3f translation, String colorPath, String bumpPath){ this.modelPath = modelPath; this.modelScale = modelScale; @@ -22,22 +35,47 @@ public enum ShipModel { this.bumpPath = bumpPath; } + /** + * Returns the path to the bump texture of the ship model. + * + * @return the bump texture path, or null if no bump texture is defined + */ public String getBumpPath() { return bumpPath; } + /** + * Returns the path to the color texture of the ship model. + * + * @return the color texture path + */ public String getColorPath() { return colorPath; } + /** + * Returns the scale factor of the ship model. + * + * @return the scale factor + */ public float getScale() { return modelScale; } + /** + * Returns the path to the 3D model of the ship. + * + * @return the model path + */ public String getPath() { return modelPath; } + /** + * Returns the translation to be applied to the ship model. + * + * @return the translation vector + */ public Vector3f getTranslation() { return translation; } diff --git a/Projekte/battleship/model/src/main/java/pp/battleship/game/server/ServerGameLogic.java b/Projekte/battleship/model/src/main/java/pp/battleship/game/server/ServerGameLogic.java index dd8f578d..4f6ec347 100644 --- a/Projekte/battleship/model/src/main/java/pp/battleship/game/server/ServerGameLogic.java +++ b/Projekte/battleship/model/src/main/java/pp/battleship/game/server/ServerGameLogic.java @@ -155,7 +155,6 @@ public void received(MapMessage msg, int from) { /** * Validates the placement of battleships on the map. - * * Ensures that: *
    *
  • The number of ships matches the configuration.
  • diff --git a/Projekte/battleship/model/src/main/java/pp/battleship/model/dto/ShipMapDTO.java b/Projekte/battleship/model/src/main/java/pp/battleship/model/dto/ShipMapDTO.java index 70034511..242bbf99 100644 --- a/Projekte/battleship/model/src/main/java/pp/battleship/model/dto/ShipMapDTO.java +++ b/Projekte/battleship/model/src/main/java/pp/battleship/model/dto/ShipMapDTO.java @@ -52,10 +52,14 @@ public ShipMapDTO(ShipMap map) { } /** - * Checks if the current ship map fits the game details provided. + * Checks if the ship map is compatible with the provided game details. * - * @param details the game details to be matched against - * @return true if the ship map fits the game details, false otherwise + * This method verifies that the ship map's dimensions match the game details, + * that the total number of ships is correct, and that the ship placements + * do not overlap and stay within the map boundaries. + * + * @param details the game details containing required dimensions and ship configurations + * @return true if the ship map fits the game details; false otherwise */ public boolean fits(GameDetails details) { if (width != details.getWidth() || height != details.getHeight()) diff --git a/Projekte/common/src/main/java/pp/util/Util.java b/Projekte/common/src/main/java/pp/util/Util.java index acebd346..2523cb19 100644 --- a/Projekte/common/src/main/java/pp/util/Util.java +++ b/Projekte/common/src/main/java/pp/util/Util.java @@ -7,6 +7,14 @@ package pp.util; +import com.jme3.app.Application; +import com.jme3.asset.AssetLoadException; +import com.jme3.asset.AssetNotFoundException; +import com.jme3.audio.AudioData; +import com.jme3.audio.AudioNode; + +import java.lang.System.Logger; +import java.lang.System.Logger.Level; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -17,6 +25,8 @@ * A class with auxiliary functions. */ public class Util { + private static final Logger LOGGER = System.getLogger(Util.class.getName()); + private Util() { /* do not instantiate */ } /** @@ -87,4 +97,24 @@ public static Set add(Set set, E element) { newSet.add(element); return newSet; } + + /** + * Loads a sound from the specified file. + * + * @param app The application + * @param name The name of the sound file. + * @return The loaded AudioNode. + */ + public static AudioNode loadSound(Application app, String name) { + try { + final AudioNode sound = new AudioNode(app.getAssetManager(), name, AudioData.DataType.Buffer); + sound.setLooping(false); + sound.setPositional(false); + return sound; + } + catch (AssetLoadException | AssetNotFoundException ex) { + LOGGER.log(Level.ERROR, ex.getMessage(), ex); + } + return null; + } }