diff --git a/Projekte/battleship/client/src/main/java/pp/battleship/client/BattleshipApp.java b/Projekte/battleship/client/src/main/java/pp/battleship/client/BattleshipApp.java index 1f04c63..25ef74b 100644 --- a/Projekte/battleship/client/src/main/java/pp/battleship/client/BattleshipApp.java +++ b/Projekte/battleship/client/src/main/java/pp/battleship/client/BattleshipApp.java @@ -22,8 +22,8 @@ import com.simsilica.lemur.GuiGlobals; import com.simsilica.lemur.style.BaseStyles; import pp.battleship.client.gui.BattleAppState; import pp.battleship.client.gui.EditorAppState; +import pp.battleship.client.gui.GameMusic; import pp.battleship.client.gui.SeaAppState; -import pp.battleship.client.GameMusic; import pp.battleship.game.client.BattleshipClient; import pp.battleship.game.client.ClientGameLogic; import pp.battleship.game.client.ServerConnection; @@ -269,7 +269,6 @@ public class BattleshipApp extends SimpleApplication implements BattleshipClient attachGameSound(); attachGameMusic(); - stateManager.attachAll(new EditorAppState(), new BattleAppState(), new SeaAppState()); } @@ -284,12 +283,12 @@ public class BattleshipApp extends SimpleApplication implements BattleshipClient } /** - * Attaches the music state and sets its initial enabled state. + * Attaches the background music state and sets its initial enabled state. */ private void attachGameMusic() { - final GameMusic gameMusic = new GameMusic(); - gameMusic.setEnabled(GameMusic.enabledInPreferences()); - stateManager.attach(gameMusic); + final GameMusic gameSound = new GameMusic(); + gameSound.setEnabled(GameMusic.enabledInPreferences()); + stateManager.attach(gameSound); } /** @@ -439,4 +438,3 @@ public class BattleshipApp extends SimpleApplication implements BattleshipClient .open(); } } - 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 574c97a..60c4af8 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 @@ -1,5 +1,11 @@ -package pp.battleship.client; +//////////////////////////////////////// +// 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.app.Application; import com.jme3.app.state.AbstractAppState; @@ -8,7 +14,8 @@ 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; import java.lang.System.Logger; import java.lang.System.Logger.Level; @@ -17,17 +24,16 @@ import java.util.prefs.Preferences; import static pp.util.PreferencesUtils.getPreferences; /** - * An application state that plays music. + * An application state that plays sounds. */ -public class GameMusic extends AbstractAppState { - private static final Logger LOGGER = System.getLogger(GameMusic.class.getName()); - private static final Preferences PREFERENCES = getPreferences(GameMusic.class); +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 static final String VOLUME_PREF = "volume"; //NON-NLS - private AudioNode battleMusic; - private AudioNode menuMusicModern; - private AudioNode menuMusicTraditional; + private AudioNode splashSound; + private AudioNode shipDestroyedSound; + private AudioNode explosionSound; /** * Checks if sound is enabled in the preferences. @@ -37,17 +43,6 @@ public class GameMusic extends AbstractAppState { public static boolean enabledInPreferences() { return PREFERENCES.getBoolean(ENABLED_PREF, true); } - /** - * Checks enabled volume in preferences. - * - * @return {@code float} if a volumePreference is set, the volume is set to the Value in PREFERENCES, - * {@code 0.25f} if no volumePreference is set in PREFERENCES, the Volume is set to a default of 0.25f - * - */ - - public static float volumePreference() { - return PREFERENCES.getFloat(VOLUME_PREF, 0.25f); - } /** * Toggles the game sound on or off. @@ -65,20 +60,14 @@ public class GameMusic extends AbstractAppState { @Override public void setEnabled(boolean enabled) { if (isEnabled() == enabled) return; - else if (!isEnabled() && enabled) { - if (menuMusicModern != null) menuMusicModern.play(); - } else if (isEnabled() && !enabled) { - if (menuMusicModern != null) menuMusicModern.stop(); - } - super.setEnabled(enabled); - LOGGER.log(Level.INFO, "Music enabled: {0}", enabled); //NON-NLS + LOGGER.log(Level.INFO, "Sound enabled: {0}", enabled); //NON-NLS PREFERENCES.putBoolean(ENABLED_PREF, enabled); } /** - * Initializes the music. - * Overrides {@link AbstractAppState#initialize(AppStateManager, Application)} + * Initializes the sound effects for the game. + * Overrides {@link com.jme3.app.state.AbstractAppState#initialize(com.jme3.app.state.AppStateManager, com.jme3.app.Application)} * * @param stateManager The state manager * @param app The application @@ -86,17 +75,13 @@ public class GameMusic extends AbstractAppState { @Override public void initialize(AppStateManager stateManager, Application app) { super.initialize(stateManager, app); - menuMusicModern =loadSound(app, "Sound/BackgroundMusic/menu-music-modern.ogg"); - setVolume(volumePreference()); - menuMusicModern.setLooping(true); - if (isEnabled() && menuMusicModern != null) { - menuMusicModern.play(); - } - + shipDestroyedSound = loadSound(app, "Sound/Effects/sunken.wav"); //NON-NLS + splashSound = loadSound(app, "Sound/Effects/splash.wav"); //NON-NLS + explosionSound = loadSound(app, "Sound/Effects/explosion.wav"); //NON-NLS } /** - * Loads the music from the specified file. + * Loads a sound from the specified file. * * @param app The application * @param name The name of the sound file. @@ -116,18 +101,35 @@ public class GameMusic extends AbstractAppState { } /** - * Sets the vol param to the level set in PREFERENCES - * - * @param vol Volume level of the music as indicated by the Volume control Slider - * + * Plays the splash sound effect. */ - public void setVolume(float vol){ - menuMusicModern.setVolume(vol); - PREFERENCES.putFloat(VOLUME_PREF, vol); + public void splash() { + if (isEnabled() && splashSound != null) + splashSound.playInstance(); } + /** + * Plays the explosion sound effect. + */ + public void explosion() { + if (isEnabled() && explosionSound != null) + explosionSound.playInstance(); + } + + /** + * Plays sound effect when a ship has been destroyed. + */ + public void shipDestroyed() { + if (isEnabled() && shipDestroyedSound != null) + shipDestroyedSound.playInstance(); + } + + @Override + public void receivedEvent(SoundEvent event) { + switch (event.sound()) { + case EXPLOSION -> explosion(); + case SPLASH -> splash(); + case DESTROYED_SHIP -> shipDestroyed(); + } + } } - - - - diff --git a/Projekte/battleship/client/src/main/java/pp/battleship/client/Menu.java b/Projekte/battleship/client/src/main/java/pp/battleship/client/Menu.java index 0e100e8..6c2a299 100644 --- a/Projekte/battleship/client/src/main/java/pp/battleship/client/Menu.java +++ b/Projekte/battleship/client/src/main/java/pp/battleship/client/Menu.java @@ -11,6 +11,8 @@ import com.simsilica.lemur.Button; import com.simsilica.lemur.Checkbox; import com.simsilica.lemur.Label; import com.simsilica.lemur.style.ElementId; +import pp.battleship.client.gui.GameMusic; +import pp.battleship.client.gui.VolumeSlider; import pp.dialog.Dialog; import pp.dialog.StateCheckboxModel; import pp.dialog.TextInputDialog; @@ -33,6 +35,7 @@ class Menu extends Dialog { private final BattleshipApp app; private final Button loadButton = new Button(lookup("menu.map.load")); private final Button saveButton = new Button(lookup("menu.map.save")); + private final VolumeSlider slider; /** * Constructs the Menu dialog for the Battleship application. @@ -42,17 +45,19 @@ class Menu extends Dialog { public Menu(BattleshipApp app) { super(app.getDialogManager()); this.app = app; + slider = new VolumeSlider(app.getStateManager().getState(GameMusic.class)); addChild(new Label(lookup("battleship.name"), new ElementId("header"))); //NON-NLS - addChild(new Checkbox(lookup("menu.sound-enabled"), - new StateCheckboxModel(app, GameSound.class))); - addChild(loadButton) - .addClickCommands(s -> ifTopDialog(this::loadDialog)); - addChild(saveButton) - .addClickCommands(s -> ifTopDialog(this::saveDialog)); - addChild(new Button(lookup("menu.return-to-game"))) - .addClickCommands(s -> ifTopDialog(this::close)); - addChild(new Button(lookup("menu.quit"))) - .addClickCommands(s -> ifTopDialog(app::closeApp)); + addChild(new Checkbox(lookup("menu.sound-enabled"), new StateCheckboxModel(app, GameSound.class))); + + addChild(new Checkbox(lookup("menu.background-sound-enabled"), new StateCheckboxModel(app, GameMusic.class))); + + addChild(slider); + + addChild(loadButton).addClickCommands(s -> ifTopDialog(this::loadDialog)); + addChild(saveButton).addClickCommands(s -> ifTopDialog(this::saveDialog)); + addChild(new Button(lookup("menu.return-to-game"))).addClickCommands(s -> ifTopDialog(this::close)); + addChild(new Button(lookup("menu.quit"))).addClickCommands(s -> ifTopDialog(app::closeApp)); + update(); } @@ -65,6 +70,11 @@ class Menu extends Dialog { saveButton.setEnabled(app.getGameLogic().maySaveMap()); } + @Override + public void update(float delta) { + slider.update(); + } + /** * As an escape action, this method closes the menu if it is the top dialog. */ @@ -82,7 +92,7 @@ class Menu extends Dialog { * Executes a file action. * * @param file the file to be processed - * @throws IOException if an I/O error occurs + * @throws java.io.IOException if an I/O error occurs */ void run(File file) throws IOException; } diff --git a/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/GameMusic.java b/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/GameMusic.java new file mode 100644 index 0000000..975177a --- /dev/null +++ b/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/GameMusic.java @@ -0,0 +1,119 @@ +package pp.battleship.client.gui; + +import static pp.util.PreferencesUtils.getPreferences; + +import java.lang.System.Logger; +import java.lang.System.Logger.Level; +import java.util.prefs.Preferences; + +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; + +public class GameMusic extends AbstractAppState{ + 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"; //NON-NLS + private static final String VOLUME_PREF = "volume"; //NON-NLS + + private AudioNode music; + + /** + * Checks if sound is enabled in the preferences. + * + * @return {@code true} if sound is enabled, {@code false} otherwise. + */ + public static boolean enabledInPreferences() { + return PREFERENCES.getBoolean(ENABLED_PREF, true); + } + + /** + * Checks if sound is enabled in the preferences. + * + * @return float to which the volume is set + */ + public static float volumeInPreferences() { + return PREFERENCES.getFloat(VOLUME_PREF, 0.5f); + } + + /** + * Initializes the sound effects for the game. + * Overrides {@link AbstractAppState#initialize(AppStateManager, Application)} + * + * @param stateManager The state manager + * @param app The application + */ + @Override + public void initialize(AppStateManager stateManager, Application app) { + super.initialize(stateManager, app); + music = loadSound(app, "Sound/background.ogg"); + setVolume(volumeInPreferences()); + music.setLooping(true); + if (isEnabled() && music != null) { + music.play(); + } + } + + /** + * 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; + } + + /** + * 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 (isEnabled() == enabled) return; + + if (music != null) { + if (enabled) { + music.play(); + } else { + music.stop(); + } + } + + super.setEnabled(enabled); + LOGGER.log(Level.INFO, "Sound enabled: {0}", enabled); //NON-NLS + PREFERENCES.putBoolean(ENABLED_PREF, enabled); + } + + /** + * Toggles the game sound on or off. + */ + public void toggleSound() { + setEnabled(!isEnabled()); + } + + /** + * Sets the volume of music + * @param vol the volume to which the music should be set + */ + public void setVolume(float vol){ + music.setVolume(vol); + PREFERENCES.putFloat(VOLUME_PREF, vol); + } +} diff --git a/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/VolumeSlider.java b/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/VolumeSlider.java new file mode 100644 index 0000000..df8a4b3 --- /dev/null +++ b/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/VolumeSlider.java @@ -0,0 +1,35 @@ +package pp.battleship.client.gui; + +import com.simsilica.lemur.Slider; +/** + * The VolumeSlider class represents the Volume Slider in the Menu. + * It extends the Slider class and provides functionalities for setting the music volume, + * with the help of the Slider in the GUI + */ +public class VolumeSlider extends Slider { + + private final GameMusic music; + private double vol; + + /** + * Constructs the Volume Slider for the Menu dialog + * @param music the music instance + */ + public VolumeSlider(GameMusic music) { + super(); + this.music = music; + vol = GameMusic.volumeInPreferences(); + getModel().setPercent(vol); + } + + /** + * when triggered it updates the volume to the value set with the slider + */ + public void update() { + if (vol != getModel().getPercent()) { + vol = getModel().getPercent(); + music.setVolume( (float) vol); + } + } + +} diff --git a/Projekte/battleship/client/src/main/java/pp/battleship/client/server/BattleshipServer.java b/Projekte/battleship/client/src/main/java/pp/battleship/client/server/BattleshipServer.java new file mode 100644 index 0000000..045822f --- /dev/null +++ b/Projekte/battleship/client/src/main/java/pp/battleship/client/server/BattleshipServer.java @@ -0,0 +1,205 @@ +//////////////////////////////////////// +// 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 BattleshipServer implements MessageListener, ConnectionListener, ServerSender { + private static final Logger LOGGER = System.getLogger(BattleshipServer.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<>(); + private boolean running = true; // Condition for stopping the server + + 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()); + } + } + + /** + * Starts the Battleships server. + */ + public static void main(String[] args) { + new BattleshipServer().run(); + } + + /** + * Creates the server. + */ + BattleshipServer() { + config.readFromIfExists(CONFIG_FILE); + LOGGER.log(Level.INFO, "Configuration: {0}", config); //NON-NLS + logic = new ServerGameLogic(this, config); + } + + public void run() { + startServer(); + try { + while (running) { + processNextMessage(); + } + } finally { + stopServer(); // Stop the server when the loop exits + } + } + + /** + * Starts the server and initializes required settings. + */ + 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); + } + } + + /** + * Stops the server and closes all connections. + */ + private void stopServer() { + LOGGER.log(Level.INFO, "Stopping server..."); //NON-NLS + if (myServer != null) { + for (HostedConnection client : myServer.getConnections()) { + if (client != null) client.close("Game over"); + } + myServer.close(); + LOGGER.log(Level.INFO, "Server stopped."); //NON-NLS + } + } + + /** + * Gracefully stops the server loop by setting the running flag to false. + */ + public void stop() { + running = false; + } + + 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 { + LOGGER.log(Level.INFO, "closed connection belongs to {0}", player); //NON-NLS + exit(0); + } + } + + private void exit(int exitValue) { + LOGGER.log(Level.INFO, "close request"); //NON-NLS + stop(); + 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 0000000..a688fe9 --- /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); + } +}