Added more flexible dependency injection to MusicManager for unittesting. Moved to event driven design for less complex code and lower runtime complexity.

This commit is contained in:
Bas de Jong
2025-10-11 20:45:57 +02:00
parent 1ecdb9a555
commit 9749d3eee8
8 changed files with 109 additions and 60 deletions

View File

@@ -1,17 +1,12 @@
package org.toop; package org.toop;
import javafx.scene.media.MediaPlayer;
import org.toop.app.App; import org.toop.app.App;
import org.toop.framework.audio.AudioEventListener; import org.toop.framework.audio.*;
import org.toop.framework.audio.AudioVolumeManager;
import org.toop.framework.audio.MusicManager;
import org.toop.framework.audio.SoundEffectManager;
import org.toop.framework.networking.NetworkingClientManager; import org.toop.framework.networking.NetworkingClientManager;
import org.toop.framework.networking.NetworkingInitializationException; import org.toop.framework.networking.NetworkingInitializationException;
import org.toop.framework.resource.ResourceLoader; import org.toop.framework.resource.ResourceLoader;
import org.toop.framework.resource.ResourceManager; import org.toop.framework.resource.ResourceManager;
import org.toop.framework.resource.resources.MusicAsset; import org.toop.framework.resource.resources.MusicAsset;
import org.toop.framework.resource.resources.SoundEffectAsset;
public final class Main { public final class Main {
static void main(String[] args) { static void main(String[] args) {

View File

@@ -0,0 +1,12 @@
package org.toop.framework.audio;
import javafx.application.Platform;
import org.toop.framework.audio.interfaces.Dispatcher;
// TODO isn't specific to audio
public class JavaFXDispatcher implements Dispatcher {
@Override
public void run(Runnable task) {
Platform.runLater(task);
}
}

View File

@@ -1,31 +1,40 @@
package org.toop.framework.audio; package org.toop.framework.audio;
import javafx.application.Platform;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.toop.framework.audio.interfaces.Dispatcher;
import org.toop.framework.resource.ResourceManager; import org.toop.framework.resource.ResourceManager;
import org.toop.framework.resource.resources.BaseResource; import org.toop.framework.resource.resources.BaseResource;
import org.toop.framework.resource.types.AudioResource; import org.toop.framework.resource.types.AudioResource;
import java.util.*; import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;
public class MusicManager<T extends AudioResource> implements org.toop.framework.audio.interfaces.MusicManager<T> { public class MusicManager<T extends AudioResource> implements org.toop.framework.audio.interfaces.MusicManager<T> {
private static final Logger logger = LogManager.getLogger(MusicManager.class); private static final Logger logger = LogManager.getLogger(MusicManager.class);
private final Class<T> type;
private final List<T> backgroundMusic = new LinkedList<>(); private final List<T> backgroundMusic = new LinkedList<>();
private final Dispatcher dispatcher;
private final List<T> resources;
private int playingIndex = 0; private int playingIndex = 0;
private ScheduledExecutorService scheduler; private boolean playing = false;
public MusicManager(Class<T> type) { public MusicManager(Class<T> type) {
this.type = type; this.dispatcher = new JavaFXDispatcher();
this.resources = new ArrayList<>(ResourceManager.getAllOfType((Class<? extends BaseResource>) type)
Runtime.getRuntime().addShutdownHook(new Thread(this::shutdownScheduler)); .stream()
.map(e -> (T) e.getResource())
.toList());
createShuffled();
} }
private void increasePlayingIndex() { // Used in unit testing
playingIndex = (playingIndex + 1) % backgroundMusic.size(); MusicManager(Class<T> type, Dispatcher dispatcher, ResourceManager resourceManager) {
this.dispatcher = dispatcher;
this.resources = new ArrayList<>(resourceManager.getAllOfType((Class<? extends BaseResource>) type)
.stream()
.map(e -> (T) e.getResource())
.toList());
createShuffled();
} }
@Override @Override
@@ -37,52 +46,61 @@ public class MusicManager<T extends AudioResource> implements org.toop.framework
backgroundMusic.add(musicAsset); backgroundMusic.add(musicAsset);
} }
private void shutdownScheduler() { private void createShuffled() {
if (scheduler != null && !scheduler.isShutdown()) { backgroundMusic.clear();
scheduler.shutdownNow(); Collections.shuffle(resources);
scheduler = null; backgroundMusic.addAll(resources);
logger.debug("MusicManager scheduler shut down.");
}
}
@Override
public void stop() {
shutdownScheduler();
Platform.runLater(() -> backgroundMusic.forEach(T::stop));
} }
public void play() { public void play() {
backgroundMusic.clear(); if (playing) {
@SuppressWarnings("unchecked") logger.warn("MusicManager is already playing.");
List<T> resources = new ArrayList<>(ResourceManager.getAllOfType((Class<? extends BaseResource>) type) return;
.stream() }
.map(e -> (T) e.getResource())
.toList());
Collections.shuffle(resources);
backgroundMusic.addAll(resources);
if (backgroundMusic.isEmpty()) return; if (backgroundMusic.isEmpty()) return;
shutdownScheduler(); playingIndex = 0;
scheduler = Executors.newSingleThreadScheduledExecutor(); playing = true;
playCurrentTrack();
}
AtomicReference<T> current = new AtomicReference<>(backgroundMusic.get(playingIndex)); private void playCurrentTrack() {
if (playingIndex >= backgroundMusic.size()) {
playingIndex = 0;
}
Platform.runLater(() -> { T current = backgroundMusic.get(playingIndex);
T first = current.get();
if (!first.isPlaying()) first.play(); if (current == null) {
logger.error("Current track is null!");
return;
}
dispatcher.run(() -> {
current.play();
current.setOnEnd(() -> {
playingIndex++;
playCurrentTrack();
});
current.setOnError(() -> {
logger.error("Error playing track: {}", current);
backgroundMusic.remove(current);
if (!backgroundMusic.isEmpty()) {
playCurrentTrack();
} else {
playing = false;
}
});
}); });
}
scheduler.scheduleAtFixedRate(() -> { public void stop() {
T track = current.get(); if (!playing) return;
if (!track.isPlaying()) {
increasePlayingIndex(); playing = false;
T next = backgroundMusic.get(playingIndex); dispatcher.run(() -> backgroundMusic.forEach(T::stop));
current.set(next);
Platform.runLater(() -> {
if (!next.isPlaying()) next.play();
});
}
}, 500, 500, TimeUnit.MILLISECONDS);
} }
} }

View File

@@ -0,0 +1,6 @@
package org.toop.framework.audio.interfaces;
// TODO isn't specific to audio
public interface Dispatcher {
void run(Runnable task);
}

View File

@@ -7,7 +7,6 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.toop.framework.resource.exceptions.ResourceNotFoundException; import org.toop.framework.resource.exceptions.ResourceNotFoundException;
import org.toop.framework.resource.resources.*; import org.toop.framework.resource.resources.*;
import org.toop.framework.resource.types.AudioResource;
/** /**
* Centralized manager for all loaded assets in the application. * Centralized manager for all loaded assets in the application.
@@ -52,12 +51,20 @@ import org.toop.framework.resource.types.AudioResource;
* </ul> * </ul>
*/ */
public class ResourceManager { public class ResourceManager {
private static final Logger logger = LogManager.getLogger(ResourceManager.class); private static final Logger logger = LogManager.getLogger(ResourceManager.class);
private static final Map<String, ResourceMeta<? extends BaseResource>> assets = private static final Map<String, ResourceMeta<? extends BaseResource>> assets =
new ConcurrentHashMap<>(); new ConcurrentHashMap<>();
private static ResourceManager instance;
private ResourceManager() {} private ResourceManager() {}
public static ResourceManager getInstance() {
if (instance == null) {
instance = new ResourceManager();
}
return instance;
}
/** /**
* Loads all assets from a given {@link ResourceLoader} into the manager. * Loads all assets from a given {@link ResourceLoader} into the manager.
* *

View File

@@ -62,8 +62,13 @@ public class MusicAsset extends BaseResource implements LoadableResource, AudioR
} }
@Override @Override
public boolean isPlaying() { public void setOnEnd(Runnable run) {
return isPlaying; mediaPlayer.setOnEndOfMedia(run);
}
@Override
public void setOnError(Runnable run) {
mediaPlayer.setOnError(run);
} }
@Override @Override

View File

@@ -101,9 +101,13 @@ public class SoundEffectAsset extends BaseResource implements LoadableResource,
} }
@Override @Override
public boolean isPlaying() { public void setOnEnd(Runnable run) {
// TODO
}
@Override
public void setOnError(Runnable run) {
// TODO // TODO
return false;
} }
@Override @Override

View File

@@ -2,7 +2,9 @@ package org.toop.framework.resource.types;
public interface AudioResource { public interface AudioResource {
void updateVolume(double volume); void updateVolume(double volume);
boolean isPlaying(); // boolean isPlaying();
void play(); void play();
void stop(); void stop();
void setOnEnd(Runnable run);
void setOnError(Runnable run);
} }