com.diffplug.spotless
diff --git a/framework/src/main/java/org/toop/framework/Logging.java b/framework/src/main/java/org/toop/framework/Logging.java
index abd13cc..186f186 100644
--- a/framework/src/main/java/org/toop/framework/Logging.java
+++ b/framework/src/main/java/org/toop/framework/Logging.java
@@ -6,197 +6,198 @@ import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.LoggerConfig;
+import java.util.Locale;
+
/**
* Utility class for configuring logging levels dynamically at runtime using Log4j 2.
*
* Provides methods to enable or disable logs globally or per class, with support for specifying
* log levels either via {@link Level} enums or string names.
*/
-// Todo: refactor
public final class Logging {
- /** Disables all logging globally by setting the root logger level to {@link Level#OFF}. */
- public static void disableAllLogs() {
- LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
- Configuration config = ctx.getConfiguration();
- LoggerConfig rootLoggerConfig = config.getRootLogger();
- rootLoggerConfig.setLevel(Level.OFF);
- ctx.updateLoggers();
- }
+ /** Disables all logging globally by setting the root logger level to {@link Level#OFF}. */
+ public static void disableAllLogs() {
+ LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
+ Configuration config = ctx.getConfiguration();
+ LoggerConfig rootLoggerConfig = config.getRootLogger();
+ rootLoggerConfig.setLevel(Level.OFF);
+ ctx.updateLoggers();
+ }
- /** Enables all logging globally by setting the root logger level to {@link Level#ALL}. */
- public static void enableAllLogs() {
- LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
- Configuration config = ctx.getConfiguration();
- LoggerConfig rootLoggerConfig = config.getRootLogger();
- rootLoggerConfig.setLevel(Level.ALL);
- ctx.updateLoggers();
- }
+ /** Enables all logging globally by setting the root logger level to {@link Level#ALL}. */
+ public static void enableAllLogs() {
+ LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
+ Configuration config = ctx.getConfiguration();
+ LoggerConfig rootLoggerConfig = config.getRootLogger();
+ rootLoggerConfig.setLevel(Level.ALL);
+ ctx.updateLoggers();
+ }
- /**
- * Enables global logging at a specific level by setting the root logger.
- *
- * @param level the logging level to enable for all logs
- */
- public static void enableAllLogs(Level level) {
- LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
- Configuration config = ctx.getConfiguration();
- LoggerConfig rootLoggerConfig = config.getRootLogger();
- rootLoggerConfig.setLevel(level);
- ctx.updateLoggers();
- }
+ /**
+ * Enables global logging at a specific level by setting the root logger.
+ *
+ * @param level the logging level to enable for all logs
+ */
+ public static void enableAllLogs(Level level) {
+ LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
+ Configuration config = ctx.getConfiguration();
+ LoggerConfig rootLoggerConfig = config.getRootLogger();
+ rootLoggerConfig.setLevel(level);
+ ctx.updateLoggers();
+ }
- /**
- * Verifies whether the provided string corresponds to a valid class name.
- *
- * @param className fully-qualified class name to check
- * @return true if the class exists, false otherwise
- */
- private static boolean verifyStringIsActualClass(String className) {
- try {
- Class.forName(className);
- return true;
- } catch (ClassNotFoundException e) {
- return false;
- }
- }
+ /**
+ * Verifies whether the provided string corresponds to a valid class name.
+ *
+ * @param className fully-qualified class name to check
+ * @return true if the class exists, false otherwise
+ */
+ private static boolean verifyStringIsActualClass(String className) {
+ try {
+ Class.forName(className);
+ return true;
+ } catch (ClassNotFoundException e) {
+ return false;
+ }
+ }
- /**
- * Internal helper to disable logs for a specific class by name.
- *
- * @param className fully-qualified class name
- */
- private static void disableLogsForClassInternal(String className) {
- LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
- Configuration config = ctx.getConfiguration();
- config.removeLogger(className);
- LoggerConfig specificConfig = new LoggerConfig(className, Level.OFF, false);
- config.addLogger(className, specificConfig);
- ctx.updateLoggers();
- }
+ /**
+ * Internal helper to disable logs for a specific class by name.
+ *
+ * @param className fully-qualified class name
+ */
+ private static void disableLogsForClassInternal(String className) {
+ LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
+ Configuration config = ctx.getConfiguration();
+ config.removeLogger(className);
+ LoggerConfig specificConfig = new LoggerConfig(className, Level.OFF, false);
+ config.addLogger(className, specificConfig);
+ ctx.updateLoggers();
+ }
- /**
- * Disables logs for a specific class.
- *
- * @param class_ the class for which logs should be disabled
- * @param type of the class
- */
- public static void disableLogsForClass(Class class_) {
- disableLogsForClassInternal(class_.getName());
- }
+ /**
+ * Disables logs for a specific class.
+ *
+ * @param class_ the class for which logs should be disabled
+ * @param type of the class
+ */
+ public static void disableLogsForClass(Class class_) {
+ disableLogsForClassInternal(class_.getName());
+ }
- /**
- * Disables logs for a class specified by fully-qualified name, if the class exists.
- *
- * @param className fully-qualified class name
- */
- public static void disableLogsForClass(String className) {
- if (verifyStringIsActualClass(className)) {
- disableLogsForClassInternal(className);
- }
- }
+ /**
+ * Disables logs for a class specified by fully-qualified name, if the class exists.
+ *
+ * @param className fully-qualified class name
+ */
+ public static void disableLogsForClass(String className) {
+ if (verifyStringIsActualClass(className)) {
+ disableLogsForClassInternal(className);
+ }
+ }
- /**
- * Internal helper to enable logs for a specific class at a specific level.
- *
- * @param className fully-qualified class name
- * @param level logging level to set
- */
- private static void enableLogsForClassInternal(String className, Level level) {
- LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
- Configuration config = ctx.getConfiguration();
- LoggerConfig loggerConfig = config.getLoggers().get(className);
- if (loggerConfig == null) {
- loggerConfig = new LoggerConfig(className, level, false);
- config.addLogger(className, loggerConfig);
- } else {
- loggerConfig.setLevel(level);
- }
- ctx.updateLoggers();
- }
+ /**
+ * Internal helper to enable logs for a specific class at a specific level.
+ *
+ * @param className fully-qualified class name
+ * @param level logging level to set
+ */
+ private static void enableLogsForClassInternal(String className, Level level) {
+ LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
+ Configuration config = ctx.getConfiguration();
+ LoggerConfig loggerConfig = config.getLoggers().get(className);
+ if (loggerConfig == null) {
+ loggerConfig = new LoggerConfig(className, level, false);
+ config.addLogger(className, loggerConfig);
+ } else {
+ loggerConfig.setLevel(level);
+ }
+ ctx.updateLoggers();
+ }
- /**
- * Enables logging for a class at a specific level.
- *
- * @param class_ class to configure
- * @param levelToLog the logging level to set
- * @param type of the class
- */
- public static void enableLogsForClass(Class class_, Level levelToLog) {
- enableLogsForClassInternal(class_.getName(), levelToLog);
- }
+ /**
+ * Enables logging for a class at a specific level.
+ *
+ * @param class_ class to configure
+ * @param levelToLog the logging level to set
+ * @param type of the class
+ */
+ public static void enableLogsForClass(Class class_, Level levelToLog) {
+ enableLogsForClassInternal(class_.getName(), levelToLog);
+ }
- /**
- * Enables logging for a class specified by name at a specific level, if the class exists.
- *
- * @param className fully-qualified class name
- * @param levelToLog the logging level to set
- */
- public static void enableLogsForClass(String className, Level levelToLog) {
- if (verifyStringIsActualClass(className)) {
- enableLogsForClassInternal(className, levelToLog);
- }
- }
+ /**
+ * Enables logging for a class specified by name at a specific level, if the class exists.
+ *
+ * @param className fully-qualified class name
+ * @param levelToLog the logging level to set
+ */
+ public static void enableLogsForClass(String className, Level levelToLog) {
+ if (verifyStringIsActualClass(className)) {
+ enableLogsForClassInternal(className, levelToLog);
+ }
+ }
- /**
- * Enables logging for a class specified by name at a specific level using a string.
- *
- * @param className fully-qualified class name
- * @param levelToLog name of the logging level (e.g., "DEBUG", "INFO")
- */
- public static void enableLogsForClass(String className, String levelToLog) {
- Level level = Level.valueOf(levelToLog.trim().toUpperCase());
- if (level != null && verifyStringIsActualClass(className)) {
- enableLogsForClassInternal(className, level);
- }
- }
+ /**
+ * Enables logging for a class specified by name at a specific level using a string.
+ *
+ * @param className fully-qualified class name
+ * @param levelToLog name of the logging level (e.g., "DEBUG", "INFO")
+ */
+ public static void enableLogsForClass(String className, String levelToLog) {
+ Level level = Level.valueOf(levelToLog.trim().toUpperCase(Locale.ROOT));
+ if (level != null && verifyStringIsActualClass(className)) {
+ enableLogsForClassInternal(className, level);
+ }
+ }
- /** Convenience methods for enabling logs at specific levels for classes. */
- public static void enableAllLogsForClass(Class class_) {
- enableLogsForClass(class_, Level.ALL);
- }
+ /** Convenience methods for enabling logs at specific levels for classes. */
+ public static void enableAllLogsForClass(Class class_) {
+ enableLogsForClass(class_, Level.ALL);
+ }
- public static void enableAllLogsForClass(String className) {
- enableLogsForClass(className, Level.ALL);
- }
+ public static void enableAllLogsForClass(String className) {
+ enableLogsForClass(className, Level.ALL);
+ }
- public static void enableDebugLogsForClass(Class class_) {
- enableLogsForClass(class_, Level.DEBUG);
- }
+ public static void enableDebugLogsForClass(Class class_) {
+ enableLogsForClass(class_, Level.DEBUG);
+ }
- public static void enableDebugLogsForClass(String className) {
- enableLogsForClass(className, Level.DEBUG);
- }
+ public static void enableDebugLogsForClass(String className) {
+ enableLogsForClass(className, Level.DEBUG);
+ }
- public static void enableErrorLogsForClass(Class class_) {
- enableLogsForClass(class_, Level.ERROR);
- }
+ public static void enableErrorLogsForClass(Class class_) {
+ enableLogsForClass(class_, Level.ERROR);
+ }
- public static void enableErrorLogsForClass(String className) {
- enableLogsForClass(className, Level.ERROR);
- }
+ public static void enableErrorLogsForClass(String className) {
+ enableLogsForClass(className, Level.ERROR);
+ }
- public static void enableFatalLogsForClass(Class class_) {
- enableLogsForClass(class_, Level.FATAL);
- }
+ public static void enableFatalLogsForClass(Class class_) {
+ enableLogsForClass(class_, Level.FATAL);
+ }
- public static void enableFatalLogsForClass(String className) {
- enableLogsForClass(className, Level.FATAL);
- }
+ public static void enableFatalLogsForClass(String className) {
+ enableLogsForClass(className, Level.FATAL);
+ }
- public static void enableInfoLogsForClass(Class class_) {
- enableLogsForClass(class_, Level.INFO);
- }
+ public static void enableInfoLogsForClass(Class class_) {
+ enableLogsForClass(class_, Level.INFO);
+ }
- public static void enableInfoLogsForClass(String className) {
- enableLogsForClass(className, Level.INFO);
- }
+ public static void enableInfoLogsForClass(String className) {
+ enableLogsForClass(className, Level.INFO);
+ }
- public static void enableTraceLogsForClass(Class class_) {
- enableLogsForClass(class_, Level.TRACE);
- }
+ public static void enableTraceLogsForClass(Class class_) {
+ enableLogsForClass(class_, Level.TRACE);
+ }
- public static void enableTraceLogsForClass(String className) {
- enableLogsForClass(className, Level.TRACE);
- }
-}
\ No newline at end of file
+ public static void enableTraceLogsForClass(String className) {
+ enableLogsForClass(className, Level.TRACE);
+ }
+}
diff --git a/framework/src/main/java/org/toop/framework/SnowflakeGenerator.java b/framework/src/main/java/org/toop/framework/SnowflakeGenerator.java
index a6d9ab3..fb17d5f 100644
--- a/framework/src/main/java/org/toop/framework/SnowflakeGenerator.java
+++ b/framework/src/main/java/org/toop/framework/SnowflakeGenerator.java
@@ -9,31 +9,16 @@ import java.util.concurrent.atomic.AtomicLong;
* A thread-safe, distributed unique ID generator following the Snowflake pattern.
*
* Each generated 64-bit ID encodes:
- *
*
* - 41-bit timestamp (milliseconds since custom epoch)
*
- 10-bit machine identifier
*
- 12-bit sequence number for IDs generated in the same millisecond
*
*
- * This implementation ensures:
- *
- *
- * - IDs are unique per machine.
- *
- Monotonicity within the same machine.
- *
- Safe concurrent generation via synchronized {@link #nextId()}.
- *
- *
- * Custom epoch is set to {@code 2025-01-01T00:00:00Z}.
- *
- *
Usage example:
- *
- *
{@code
- * SnowflakeGenerator generator = new SnowflakeGenerator();
- * long id = generator.nextId();
- * }
+ * This static implementation ensures global uniqueness per JVM process
+ * and can be accessed via {@link SnowflakeGenerator#nextId()}.
*/
-public class SnowflakeGenerator {
+public final class SnowflakeGenerator {
/** Custom epoch in milliseconds (2025-01-01T00:00:00Z). */
private static final long EPOCH = Instant.parse("2025-01-01T00:00:00Z").toEpochMilli();
@@ -43,25 +28,26 @@ public class SnowflakeGenerator {
private static final long MACHINE_BITS = 10;
private static final long SEQUENCE_BITS = 12;
- // Maximum values for each component
+ // Maximum values
private static final long MAX_MACHINE_ID = (1L << MACHINE_BITS) - 1;
private static final long MAX_SEQUENCE = (1L << SEQUENCE_BITS) - 1;
private static final long MAX_TIMESTAMP = (1L << TIMESTAMP_BITS) - 1;
- // Bit shifts for composing the ID
+ // Bit shifts
private static final long MACHINE_SHIFT = SEQUENCE_BITS;
private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + MACHINE_BITS;
- /** Unique machine identifier derived from network interfaces (10 bits). */
- private static final long machineId = SnowflakeGenerator.genMachineId();
+ /** Unique machine identifier derived from MAC addresses. */
+ private static final long MACHINE_ID = genMachineId();
- private final AtomicLong lastTimestamp = new AtomicLong(-1L);
- private long sequence = 0L;
+ /** State variables (shared across all threads). */
+ private static final AtomicLong LAST_TIMESTAMP = new AtomicLong(-1L);
+ private static long sequence = 0L;
- /**
- * Generates a 10-bit machine identifier based on MAC addresses of network interfaces. Falls
- * back to a random value if MAC cannot be determined.
- */
+ // Prevent instantiation
+ private SnowflakeGenerator() {}
+
+ /** Generates a 10-bit machine identifier from MAC or random fallback. */
private static long genMachineId() {
try {
StringBuilder sb = new StringBuilder();
@@ -77,48 +63,19 @@ public class SnowflakeGenerator {
}
}
- /**
- * For testing: manually set the last generated timestamp.
- *
- * @param l timestamp in milliseconds
- */
- void setTime(long l) {
- this.lastTimestamp.set(l);
- }
-
- /**
- * Constructs a SnowflakeGenerator. Validates that the machine ID is within allowed range.
- *
- * @throws IllegalArgumentException if machine ID is invalid
- */
- public SnowflakeGenerator() {
- if (machineId < 0 || machineId > MAX_MACHINE_ID) {
- throw new IllegalArgumentException(
- "Machine ID must be between 0 and " + MAX_MACHINE_ID);
- }
- }
-
- /**
- * Generates the next unique ID.
- *
- *
If multiple IDs are generated in the same millisecond, a sequence number is incremented.
- * If the sequence overflows, waits until the next millisecond.
- *
- * @return a unique 64-bit ID
- * @throws IllegalStateException if clock moves backwards or timestamp exceeds 41-bit limit
- */
- public synchronized long nextId() {
+ /** Returns a globally unique 64-bit Snowflake ID. */
+ public static synchronized long nextId() {
long currentTimestamp = timestamp();
- if (currentTimestamp < lastTimestamp.get()) {
- throw new IllegalStateException("Clock moved backwards. Refusing to generate id.");
+ if (currentTimestamp < LAST_TIMESTAMP.get()) {
+ throw new IllegalStateException("Clock moved backwards. Refusing to generate ID.");
}
if (currentTimestamp > MAX_TIMESTAMP) {
- throw new IllegalStateException("Timestamp bits overflow, Snowflake expired.");
+ throw new IllegalStateException("Timestamp bits overflow ā Snowflake expired.");
}
- if (currentTimestamp == lastTimestamp.get()) {
+ if (currentTimestamp == LAST_TIMESTAMP.get()) {
sequence = (sequence + 1) & MAX_SEQUENCE;
if (sequence == 0) {
currentTimestamp = waitNextMillis(currentTimestamp);
@@ -127,29 +84,22 @@ public class SnowflakeGenerator {
sequence = 0L;
}
- lastTimestamp.set(currentTimestamp);
+ LAST_TIMESTAMP.set(currentTimestamp);
return ((currentTimestamp - EPOCH) << TIMESTAMP_SHIFT)
- | (machineId << MACHINE_SHIFT)
+ | (MACHINE_ID << MACHINE_SHIFT)
| sequence;
}
- /**
- * Waits until the next millisecond if sequence overflows.
- *
- * @param lastTimestamp previous timestamp
- * @return new timestamp
- */
- private long waitNextMillis(long lastTimestamp) {
+ /** Waits until next millisecond if sequence exhausted. */
+ private static long waitNextMillis(long lastTimestamp) {
long ts = timestamp();
- while (ts <= lastTimestamp) {
- ts = timestamp();
- }
+ while (ts <= lastTimestamp) ts = timestamp();
return ts;
}
- /** Returns current system timestamp in milliseconds. */
- private long timestamp() {
+ /** Returns current timestamp in milliseconds. */
+ private static long timestamp() {
return System.currentTimeMillis();
}
}
diff --git a/framework/src/main/java/org/toop/framework/asset/events/AssetLoaderEvents.java b/framework/src/main/java/org/toop/framework/asset/events/AssetLoaderEvents.java
deleted file mode 100644
index b19709c..0000000
--- a/framework/src/main/java/org/toop/framework/asset/events/AssetLoaderEvents.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package org.toop.framework.asset.events;
-
-import org.toop.framework.eventbus.events.EventWithoutSnowflake;
-
-public class AssetLoaderEvents {
- public record LoadingProgressUpdate(int hasLoadedAmount, int isLoadingAmount)
- implements EventWithoutSnowflake {}
-}
diff --git a/framework/src/main/java/org/toop/framework/asset/resources/MusicAsset.java b/framework/src/main/java/org/toop/framework/asset/resources/MusicAsset.java
deleted file mode 100644
index 1d79c88..0000000
--- a/framework/src/main/java/org/toop/framework/asset/resources/MusicAsset.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package org.toop.framework.asset.resources;
-
-import java.io.*;
-import javafx.scene.media.Media;
-import org.toop.framework.asset.types.FileExtension;
-import org.toop.framework.asset.types.LoadableResource;
-
-@FileExtension({"mp3"})
-public class MusicAsset extends BaseResource implements LoadableResource {
- private Media media;
-
- public MusicAsset(final File audioFile) {
- super(audioFile);
- }
-
- public Media getMedia() {
- if (media == null) {
- media = new Media(file.toURI().toString());
- }
- return media;
- }
-
- @Override
- public void load() {
- if (media == null) media = new Media(file.toURI().toString());
- this.isLoaded = true;
- }
-
- @Override
- public void unload() {
- media = null;
- isLoaded = false;
- }
-
- @Override
- public boolean isLoaded() {
- return isLoaded;
- }
-}
diff --git a/framework/src/main/java/org/toop/framework/asset/resources/SoundEffectAsset.java b/framework/src/main/java/org/toop/framework/asset/resources/SoundEffectAsset.java
deleted file mode 100644
index b85951b..0000000
--- a/framework/src/main/java/org/toop/framework/asset/resources/SoundEffectAsset.java
+++ /dev/null
@@ -1,80 +0,0 @@
-package org.toop.framework.asset.resources;
-
-import java.io.*;
-import java.nio.file.Files;
-import javax.sound.sampled.*;
-import org.toop.framework.asset.types.FileExtension;
-import org.toop.framework.asset.types.LoadableResource;
-
-@FileExtension({"wav"})
-public class SoundEffectAsset extends BaseResource implements LoadableResource {
- private byte[] rawData;
-
- public SoundEffectAsset(final File audioFile) {
- super(audioFile);
- }
-
- // Gets a new clip to play
- public Clip getNewClip()
- throws LineUnavailableException, UnsupportedAudioFileException, IOException {
- // Get a new clip from audio system
- Clip clip = AudioSystem.getClip();
-
- // Insert a new audio stream into the clip
- AudioInputStream inputStream = this.getAudioStream();
- AudioFormat baseFormat = inputStream.getFormat();
- if (baseFormat.getSampleSizeInBits() > 16)
- inputStream = downSampleAudio(inputStream, baseFormat);
- clip.open(
- inputStream); // ^ Clip can only run 16 bit and lower, thus downsampling necessary.
- return clip;
- }
-
- // Generates a new audio stream from byte array
- private AudioInputStream getAudioStream() throws UnsupportedAudioFileException, IOException {
- // Check if raw data is loaded into memory
- if (!this.isLoaded()) {
- this.load();
- }
-
- // Turn rawData into an input stream and turn that into an audio input stream;
- return AudioSystem.getAudioInputStream(new ByteArrayInputStream(this.rawData));
- }
-
- private AudioInputStream downSampleAudio(
- AudioInputStream audioInputStream, AudioFormat baseFormat) {
- AudioFormat decodedFormat =
- new AudioFormat(
- AudioFormat.Encoding.PCM_SIGNED,
- baseFormat.getSampleRate(),
- 16, // force 16-bit
- baseFormat.getChannels(),
- baseFormat.getChannels() * 2,
- baseFormat.getSampleRate(),
- false // little-endian
- );
-
- return AudioSystem.getAudioInputStream(decodedFormat, audioInputStream);
- }
-
- @Override
- public void load() {
- try {
- this.rawData = Files.readAllBytes(file.toPath());
- this.isLoaded = true;
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
-
- @Override
- public void unload() {
- this.rawData = null;
- this.isLoaded = false;
- }
-
- @Override
- public boolean isLoaded() {
- return this.isLoaded;
- }
-}
diff --git a/framework/src/main/java/org/toop/framework/audio/AudioEventListener.java b/framework/src/main/java/org/toop/framework/audio/AudioEventListener.java
new file mode 100644
index 0000000..8cc718c
--- /dev/null
+++ b/framework/src/main/java/org/toop/framework/audio/AudioEventListener.java
@@ -0,0 +1,84 @@
+package org.toop.framework.audio;
+
+import org.toop.framework.audio.events.AudioEvents;
+import org.toop.framework.audio.interfaces.MusicManager;
+import org.toop.framework.audio.interfaces.SoundEffectManager;
+import org.toop.framework.audio.interfaces.VolumeManager;
+import org.toop.framework.eventbus.EventFlow;
+import org.toop.framework.eventbus.bus.EventBus;
+import org.toop.framework.resource.types.AudioResource;
+
+public class AudioEventListener {
+ private final EventBus eventBus;
+ private final MusicManager musicManager;
+ private final SoundEffectManager soundEffectManager;
+ private final VolumeManager audioVolumeManager;
+
+ public AudioEventListener(
+ EventBus eventBus,
+ MusicManager musicManager,
+ SoundEffectManager soundEffectManager,
+ VolumeManager audioVolumeManager
+ ) {
+ this.eventBus = eventBus;
+ this.musicManager = musicManager;
+ this.soundEffectManager = soundEffectManager;
+ this.audioVolumeManager = audioVolumeManager;
+ }
+
+ public AudioEventListener, ?> initListeners(String buttonSoundToPlay) {
+ new EventFlow(eventBus)
+ .listen(AudioEvents.StopAudioManager.class, this::handleStopMusicManager, false)
+ .listen(AudioEvents.PlayEffect.class, this::handlePlaySound, false)
+ .listen(AudioEvents.SkipMusic.class, this::handleSkipSong, false)
+ .listen(AudioEvents.PauseMusic.class, this::handlePauseSong, false)
+ .listen(AudioEvents.PreviousMusic.class, this::handlePreviousSong, false)
+ .listen(AudioEvents.StopEffect.class, this::handleStopSound, false)
+ .listen(AudioEvents.StartBackgroundMusic.class, this::handleMusicStart, false)
+ .listen(AudioEvents.ChangeVolume.class, this::handleVolumeChange, false)
+ .listen(AudioEvents.GetVolume.class, this::handleGetVolume,false)
+ .listen(AudioEvents.ClickButton.class, _ -> soundEffectManager.play(buttonSoundToPlay, false), false);
+
+ return this;
+ }
+
+ private void handleStopMusicManager(AudioEvents.StopAudioManager event) {
+ this.musicManager.stop();
+ }
+
+ private void handlePlaySound(AudioEvents.PlayEffect event) {
+ this.soundEffectManager.play(event.fileName(), event.loop());
+ }
+
+ private void handleSkipSong(AudioEvents.SkipMusic event) {
+ this.musicManager.skip();
+ }
+
+ private void handlePauseSong(AudioEvents.PauseMusic event) {
+ this.musicManager.pause();
+
+ }
+
+ private void handlePreviousSong(AudioEvents.PreviousMusic event) {
+ this.musicManager.previous();
+ }
+
+ private void handleStopSound(AudioEvents.StopEffect event) {
+ this.soundEffectManager.stop(event.fileName());
+ }
+
+ private void handleMusicStart(AudioEvents.StartBackgroundMusic event) {
+ this.musicManager.play();
+ }
+
+ private void handleVolumeChange(AudioEvents.ChangeVolume event) {
+ this.audioVolumeManager.setVolume(event.newVolume() / 100, event.controlType());
+ }
+
+ private void handleGetVolume(AudioEvents.GetVolume event) {
+ eventBus.post(new AudioEvents.GetVolumeResponse(
+ audioVolumeManager.getVolume(event.controlType()),
+ event.identifier()));
+ }
+
+}
diff --git a/framework/src/main/java/org/toop/framework/audio/AudioVolumeManager.java b/framework/src/main/java/org/toop/framework/audio/AudioVolumeManager.java
index add826b..05095ef 100644
--- a/framework/src/main/java/org/toop/framework/audio/AudioVolumeManager.java
+++ b/framework/src/main/java/org/toop/framework/audio/AudioVolumeManager.java
@@ -1,100 +1,84 @@
package org.toop.framework.audio;
-import javafx.scene.media.MediaPlayer;
-import javax.sound.sampled.Clip;
-import javax.sound.sampled.FloatControl;
-import org.toop.framework.audio.events.AudioEvents;
-import org.toop.framework.eventbus.EventFlow;
+import org.toop.framework.audio.interfaces.AudioManager;
+import org.toop.framework.audio.interfaces.VolumeManager;
+import org.toop.framework.resource.types.AudioResource;
-public class AudioVolumeManager {
- private final SoundManager sM;
+/**
+ * Concrete implementation of {@link VolumeManager} that delegates volume control
+ * to the {@link VolumeControl} enum.
+ *
+ * This class acts as a central point for updating volume levels for different
+ * audio categories (MASTER, FX, MUSIC) and for registering audio managers
+ * to the appropriate volume types.
+ *
+ *
+ * Key responsibilities:
+ *
+ * - Set and get volume levels for each {@link VolumeControl} category.
+ * - Register {@link AudioManager} instances to specific volume types so
+ * that their active audio resources receive volume updates automatically.
+ * - Automatically scales non-master volumes according to the current master volume.
+ *
+ *
+ * Example usage:
+ * {@code
+ * AudioVolumeManager volumeManager = new AudioVolumeManager();
+ *
+ * // Register music manager to MUSIC volume type
+ * volumeManager.registerManager(VolumeControl.MUSIC, musicManager);
+ *
+ * // Set master volume to 80%
+ * volumeManager.setVolume(0.8, VolumeControl.MASTERVOLUME);
+ *
+ * // Set FX volume to 50% of master
+ * volumeManager.setVolume(0.5, VolumeControl.FX);
+ *
+ * // Retrieve current MUSIC volume
+ * double musicVol = volumeManager.getVolume(VolumeControl.MUSIC);
+ * }
+ */
+public class AudioVolumeManager implements VolumeManager {
- private double volume = 1.0;
- private double fxVolume = 1.0;
- private double musicVolume = 1.0;
-
- public AudioVolumeManager(SoundManager soundManager) {
- this.sM = soundManager;
-
- new EventFlow()
- .listen(this::handleVolumeChange)
- .listen(this::handleFxVolumeChange)
- .listen(this::handleMusicVolumeChange)
- .listen(this::handleGetCurrentVolume)
- .listen(this::handleGetCurrentFxVolume)
- .listen(this::handleGetCurrentMusicVolume);
+ /**
+ * Sets the volume for a specific volume type.
+ *
+ * This method automatically takes into account the master volume
+ * for non-master types.
+ *
+ * @param newVolume the desired volume level (0.0 to 1.0)
+ * @param type the {@link VolumeControl} category to update
+ */
+ @Override
+ public void setVolume(double newVolume, VolumeControl type) {
+ type.setVolume(newVolume, VolumeControl.MASTERVOLUME.getVolume());
}
- public void updateMusicVolume(MediaPlayer mediaPlayer) {
- mediaPlayer.setVolume(this.musicVolume * this.volume);
+ /**
+ * Returns the current volume for the specified {@link VolumeControl} category.
+ *
+ * @param type the volume category
+ * @return the current volume (0.0 to 1.0)
+ */
+ @Override
+ public double getVolume(VolumeControl type) {
+ return type.getVolume();
}
- public void updateSoundEffectVolume(Clip clip) {
- if (clip.isControlSupported(FloatControl.Type.MASTER_GAIN)) {
- FloatControl volumeControl =
- (FloatControl) clip.getControl(FloatControl.Type.MASTER_GAIN);
- float min = volumeControl.getMinimum();
- float max = volumeControl.getMaximum();
- float dB =
- (float)
- (Math.log10(Math.max(this.fxVolume * this.volume, 0.0001))
- * 20.0); // convert linear to dB
- dB = Math.max(min, Math.min(max, dB));
- volumeControl.setValue(dB);
+ /**
+ * Registers an {@link AudioManager} with the specified {@link VolumeControl} category.
+ *
+ * All active audio resources managed by the given {@link AudioManager} will
+ * automatically receive volume updates when the volume type changes.
+ *
+ * @param type the volume type to register the manager under
+ * @param manager the audio manager to register
+ * @return the current {@link AudioVolumeManager} instance (for method chaining)
+ */
+ public AudioVolumeManager registerManager(VolumeControl type, AudioManager extends AudioResource> manager) {
+ if (manager != null) {
+ type.addManager(manager);
}
- }
-
- private double limitVolume(double volume) {
- if (volume > 1.0) return 1.0;
- else return Math.max(volume, 0.0);
- }
-
- private void handleFxVolumeChange(AudioEvents.ChangeFxVolume event) {
- this.fxVolume = limitVolume(event.newVolume() / 100);
- for (Clip clip : sM.getActiveSoundEffects().values()) {
- updateSoundEffectVolume(clip);
- }
- }
-
- private void handleVolumeChange(AudioEvents.ChangeVolume event) {
- this.volume = limitVolume(event.newVolume() / 100);
- for (MediaPlayer mediaPlayer : sM.getActiveMusic()) {
- this.updateMusicVolume(mediaPlayer);
- }
- for (Clip clip : sM.getActiveSoundEffects().values()) {
- updateSoundEffectVolume(clip);
- }
- }
-
- private void handleMusicVolumeChange(AudioEvents.ChangeMusicVolume event) {
- this.musicVolume = limitVolume(event.newVolume() / 100);
- System.out.println(this.musicVolume);
- System.out.println(this.volume);
- for (MediaPlayer mediaPlayer : sM.getActiveMusic()) {
- this.updateMusicVolume(mediaPlayer);
- }
- }
-
- private void handleGetCurrentVolume(AudioEvents.GetCurrentVolume event) {
- new EventFlow()
- .addPostEvent(
- new AudioEvents.GetCurrentVolumeResponse(volume * 100, event.snowflakeId()))
- .asyncPostEvent();
- }
-
- private void handleGetCurrentFxVolume(AudioEvents.GetCurrentFxVolume event) {
- new EventFlow()
- .addPostEvent(
- new AudioEvents.GetCurrentFxVolumeResponse(
- fxVolume * 100, event.snowflakeId()))
- .asyncPostEvent();
- }
-
- private void handleGetCurrentMusicVolume(AudioEvents.GetCurrentMusicVolume event) {
- new EventFlow()
- .addPostEvent(
- new AudioEvents.GetCurrentMusicVolumeResponse(
- musicVolume * 100, event.snowflakeId()))
- .asyncPostEvent();
+ return this;
}
}
diff --git a/framework/src/main/java/org/toop/framework/audio/MusicManager.java b/framework/src/main/java/org/toop/framework/audio/MusicManager.java
new file mode 100644
index 0000000..7476311
--- /dev/null
+++ b/framework/src/main/java/org/toop/framework/audio/MusicManager.java
@@ -0,0 +1,189 @@
+package org.toop.framework.audio;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.toop.framework.audio.events.AudioEvents;
+import org.toop.framework.dispatch.interfaces.Dispatcher;
+import org.toop.framework.dispatch.JavaFXDispatcher;
+import org.toop.annotations.TestsOnly;
+import org.toop.framework.eventbus.bus.EventBus;
+import org.toop.framework.resource.types.AudioResource;
+
+import java.util.*;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+public class MusicManager implements org.toop.framework.audio.interfaces.MusicManager {
+ private static final Logger logger = LogManager.getLogger(MusicManager.class);
+
+ private final EventBus eventBus;
+ private final List backgroundMusic = new ArrayList<>();
+ private final Dispatcher dispatcher;
+ private final List resources;
+ private int playingIndex = 0;
+ private boolean playing = false;
+ private long pausedPosition = 0;
+ private ScheduledExecutorService scheduler;
+
+
+ public MusicManager(EventBus eventbus, List resources, boolean shuffleMusic) {
+ this.eventBus = eventbus;
+ this.dispatcher = new JavaFXDispatcher();
+ this.resources = resources;
+ // Shuffle if wanting to shuffle
+ if (shuffleMusic) createShuffled();
+ else backgroundMusic.addAll(resources);
+ // ------------------------------
+ }
+
+ /**
+ * {@code @TestsOnly} DO NOT USE
+ */
+ @TestsOnly
+ public MusicManager(EventBus eventBus, List resources, Dispatcher dispatcher) {
+ this.eventBus = eventBus;
+ this.dispatcher = dispatcher;
+ this.resources = new ArrayList<>(resources);
+ backgroundMusic.addAll(resources);
+ }
+
+ @Override
+ public Collection getActiveAudio() {
+ return backgroundMusic;
+ }
+
+ void addBackgroundMusic(T musicAsset) {
+ backgroundMusic.add(musicAsset);
+ }
+
+ private void createShuffled() {
+ backgroundMusic.clear();
+ Collections.shuffle(resources);
+ backgroundMusic.addAll(resources);
+ }
+
+ @Override
+ public void play() {
+ if (playing) {
+ logger.warn("MusicManager is already playing.");
+ return;
+ }
+
+ if (backgroundMusic.isEmpty()) return;
+
+ playingIndex = 0;
+ playing = true;
+ playCurrentTrack();
+ }
+
+ public void skip() {
+ if (backgroundMusic.isEmpty()) return;
+ stop();
+ scheduler.shutdownNow();
+ playingIndex = playingIndex + 1;
+ playing = true;
+ playCurrentTrack();
+ }
+
+ // Used in testing
+ void play(int index) {
+ if (playing) {
+ logger.warn("MusicManager is already playing.");
+ return;
+ }
+
+ if (backgroundMusic.isEmpty()) return;
+
+ playingIndex = index;
+ playing = true;
+ playCurrentTrack();
+ }
+
+ private void playCurrentTrack() {
+ if (playingIndex >= backgroundMusic.size()) {
+ playingIndex = 0;
+ }
+
+ T current = backgroundMusic.get(playingIndex);
+
+ if (current == null) {
+ logger.error("Current track is null!");
+ return;
+ }
+
+ dispatcher.run(() -> {
+ current.play();
+
+ setTrackRunnable(current);
+ });
+ }
+
+ private void setTrackRunnable(T track) {
+
+ scheduler = Executors.newSingleThreadScheduledExecutor();
+
+ Runnable currentMusicTask = new Runnable() {
+ @Override
+ public void run() {
+ eventBus.post(new AudioEvents.PlayingMusic(track.getName(), track.currentPosition(), track.duration()));
+ scheduler.schedule(this, 1, TimeUnit.SECONDS);
+ }
+ };
+
+ track.setOnEnd(() -> {
+ scheduler.shutdownNow();
+ playingIndex++;
+ playCurrentTrack();
+ });
+
+ track.setOnError(() -> {
+ scheduler.shutdownNow();
+ logger.error("Error playing track: {}", track);
+ backgroundMusic.remove(track);
+
+ if (!backgroundMusic.isEmpty()) {
+ playCurrentTrack();
+ } else {
+ playing = false;
+ }
+ });
+
+ scheduler.schedule(currentMusicTask, 1, TimeUnit.MILLISECONDS);
+ }
+
+ @Override
+ public void stop() {
+ if (!playing) return;
+
+ playing = false;
+ dispatcher.run(() -> backgroundMusic.forEach(T::stop));
+ }
+
+ public void pause() {
+ T current = backgroundMusic.get(playingIndex);
+ if (this.playing) {
+ current.pause();
+ playing = false;
+ }
+ else {
+ this.playing = true;
+ dispatcher.run(() -> {
+ current.play();
+ setTrackRunnable(current);
+ });
+ }
+ }
+
+ public void previous() {
+ if (backgroundMusic.isEmpty()) return;
+ if (playingIndex == 0) {
+ playingIndex = backgroundMusic.size();
+ }
+ stop();
+ scheduler.shutdownNow();
+ playingIndex = playingIndex - 1;
+ playing = true;
+ playCurrentTrack();
+ }
+}
diff --git a/framework/src/main/java/org/toop/framework/audio/SoundEffectManager.java b/framework/src/main/java/org/toop/framework/audio/SoundEffectManager.java
new file mode 100644
index 0000000..c34c5a9
--- /dev/null
+++ b/framework/src/main/java/org/toop/framework/audio/SoundEffectManager.java
@@ -0,0 +1,57 @@
+package org.toop.framework.audio;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.toop.framework.resource.ResourceMeta;
+import org.toop.framework.resource.resources.BaseResource;
+import org.toop.framework.resource.types.AudioResource;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+public class SoundEffectManager implements org.toop.framework.audio.interfaces.SoundEffectManager {
+ private static final Logger logger = LogManager.getLogger(SoundEffectManager.class);
+ private final HashMap soundEffectResources;
+
+ public SoundEffectManager(List> resources) {
+ // If there are duplicates, takes discards the first
+ this.soundEffectResources = (HashMap) resources
+ .stream()
+ .collect(Collectors.
+ toMap(ResourceMeta::getName, ResourceMeta::getResource, (a, b) -> b, HashMap::new));
+
+ }
+
+ @Override
+ public Collection getActiveAudio() {
+ return this.soundEffectResources.values();
+ }
+
+ @Override
+ public void play(String name, boolean loop) {
+ T asset = soundEffectResources.get(name);
+
+ if (asset == null) {
+ logger.warn("Unable to load audio asset: {}", name);
+ return;
+ }
+
+ asset.play();
+
+ logger.debug("Playing sound: {}", asset.getName());
+ }
+
+ @Override
+ public void stop(String name){
+ T asset = soundEffectResources.get(name);
+
+ if (asset == null) {
+ logger.warn("Unable to load audio asset: {}", name);
+ return;
+ }
+
+ asset.stop();
+
+ logger.debug("Stopped sound: {}", asset.getName());
+ }
+}
diff --git a/framework/src/main/java/org/toop/framework/audio/SoundManager.java b/framework/src/main/java/org/toop/framework/audio/SoundManager.java
deleted file mode 100644
index f6608ba..0000000
--- a/framework/src/main/java/org/toop/framework/audio/SoundManager.java
+++ /dev/null
@@ -1,197 +0,0 @@
-package org.toop.framework.audio;
-
-import java.io.*;
-import java.util.*;
-import javafx.scene.media.MediaPlayer;
-import javax.sound.sampled.*;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-import org.toop.framework.SnowflakeGenerator;
-import org.toop.framework.asset.ResourceManager;
-import org.toop.framework.asset.ResourceMeta;
-import org.toop.framework.asset.resources.MusicAsset;
-import org.toop.framework.asset.resources.SoundEffectAsset;
-import org.toop.framework.audio.events.AudioEvents;
-import org.toop.framework.eventbus.EventFlow;
-
-public class SoundManager {
- private static final Logger logger = LogManager.getLogger(SoundManager.class);
- private final List activeMusic = new ArrayList<>();
- private final Queue backgroundMusicQueue = new LinkedList<>();
- private final Map activeSoundEffects = new HashMap<>();
- private final HashMap audioResources = new HashMap<>();
- private final SnowflakeGenerator idGenerator =
- new SnowflakeGenerator(); // TODO: Don't create a new generator
- private final AudioVolumeManager audioVolumeManager = new AudioVolumeManager(this);
-
- public SoundManager() {
- // Get all Audio Resources and add them to a list.
- for (ResourceMeta asset :
- ResourceManager.getAllOfType(SoundEffectAsset.class)) {
- try {
- this.addAudioResource(asset);
- } catch (IOException | LineUnavailableException | UnsupportedAudioFileException e) {
- throw new RuntimeException(e);
- }
- }
- new EventFlow()
- .listen(this::handlePlaySound)
- .listen(this::handleStopSound)
- .listen(this::handleMusicStart)
- .listen(
- AudioEvents.ClickButton.class,
- _ -> {
- try {
- playSound("medium-button-click.wav", false);
- } catch (UnsupportedAudioFileException
- | LineUnavailableException
- | IOException e) {
- logger.error(e);
- }
- });
- }
-
- private void handlePlaySound(AudioEvents.PlayEffect event) {
- try {
- this.playSound(event.fileName(), event.loop());
- } catch (UnsupportedAudioFileException | LineUnavailableException | IOException e) {
- throw new RuntimeException(e);
- }
- }
-
- private void handleStopSound(AudioEvents.StopEffect event) {
- this.stopSound(event.clipId());
- }
-
- private void addAudioResource(ResourceMeta audioAsset)
- throws IOException, UnsupportedAudioFileException, LineUnavailableException {
-
- this.audioResources.put(audioAsset.getName(), audioAsset.getResource());
- }
-
- private void handleMusicStart(AudioEvents.StartBackgroundMusic e) {
- backgroundMusicQueue.clear();
- List shuffledArray =
- new ArrayList<>(
- ResourceManager.getAllOfType(MusicAsset.class).stream()
- .map(ResourceMeta::getResource)
- .toList());
- Collections.shuffle(shuffledArray);
- backgroundMusicQueue.addAll(shuffledArray);
- backgroundMusicPlayer();
- }
-
- private void addBackgroundMusic(MusicAsset musicAsset) {
- backgroundMusicQueue.add(musicAsset);
- }
-
- private void backgroundMusicPlayer() {
- MusicAsset ma = backgroundMusicQueue.poll();
- if (ma == null) return;
-
- MediaPlayer mediaPlayer = new MediaPlayer(ma.getMedia());
-
- mediaPlayer.setOnEndOfMedia(
- () -> {
- addBackgroundMusic(ma);
- activeMusic.remove(mediaPlayer);
- mediaPlayer.dispose();
- ma.unload();
- backgroundMusicPlayer(); // play next
- });
-
- mediaPlayer.setOnStopped(
- () -> {
- addBackgroundMusic(ma);
- activeMusic.remove(mediaPlayer);
- ma.unload();
- });
-
- mediaPlayer.setOnError(
- () -> {
- addBackgroundMusic(ma);
- activeMusic.remove(mediaPlayer);
- ma.unload();
- });
-
- audioVolumeManager.updateMusicVolume(mediaPlayer);
- mediaPlayer.play();
- activeMusic.add(mediaPlayer);
- logger.info("Playing background music: {}", ma.getFile().getName());
- logger.info(
- "Background music next in line: {}",
- backgroundMusicQueue.peek().getFile().getName());
- }
-
- private long playSound(String audioFileName, boolean loop)
- throws UnsupportedAudioFileException, LineUnavailableException, IOException {
- SoundEffectAsset asset = audioResources.get(audioFileName);
-
- // Return -1 which indicates resource wasn't available
- if (asset == null) {
- logger.warn("Unable to load audio asset: {}", audioFileName);
- return -1;
- }
-
- // Get a new clip from resource
- Clip clip = asset.getNewClip();
-
- // Set volume of clip
- audioVolumeManager.updateSoundEffectVolume(clip);
-
- // If supposed to loop make it loop, else just start it once
- if (loop) {
- clip.loop(Clip.LOOP_CONTINUOUSLY);
- } else {
- clip.start();
- }
-
- logger.debug("Playing sound: {}", asset.getFile().getName());
-
- // Generate id for clip
- long clipId = idGenerator.nextId();
-
- // store it so we can stop it later
- activeSoundEffects.put(clipId, clip); // TODO: Do on snowflake for specific sound to stop
-
- // remove when finished (only for non-looping sounds)
- clip.addLineListener(
- event -> {
- if (event.getType() == LineEvent.Type.STOP && !clip.isRunning()) {
- activeSoundEffects.remove(clipId);
- clip.close();
- }
- });
-
- // Return id so it can be stopped
- return clipId;
- }
-
- public void stopSound(long clipId) {
- Clip clip = activeSoundEffects.get(clipId);
-
- if (clip == null) {
- return;
- }
-
- clip.stop();
- clip.close();
- activeSoundEffects.remove(clipId);
- }
-
- public void stopAllSounds() {
- for (Clip clip : activeSoundEffects.values()) {
- clip.stop();
- clip.close();
- }
- activeSoundEffects.clear();
- }
-
- public Map getActiveSoundEffects() {
- return this.activeSoundEffects;
- }
-
- public List getActiveMusic() {
- return activeMusic;
- }
-}
diff --git a/framework/src/main/java/org/toop/framework/audio/VolumeControl.java b/framework/src/main/java/org/toop/framework/audio/VolumeControl.java
new file mode 100644
index 0000000..31bbc57
--- /dev/null
+++ b/framework/src/main/java/org/toop/framework/audio/VolumeControl.java
@@ -0,0 +1,162 @@
+package org.toop.framework.audio;
+
+import org.toop.framework.audio.interfaces.AudioManager;
+import org.toop.framework.resource.types.AudioResource;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * Enum representing different categories of audio volume in the application.
+ *
+ * Each volume type maintains its own volume level and a list of {@link AudioManager}s
+ * that manage audio resources of that type. The enum provides methods to set, get,
+ * and propagate volume changes, including master volume adjustments that automatically
+ * update dependent volume types (FX and MUSIC).
+ *
+ *
+ * Volume types:
+ *
+ * - {@link #MASTERVOLUME}: The global/master volume that scales all other volume types.
+ * - {@link #FX}: Volume for sound effects, scaled by the master volume.
+ * - {@link #MUSIC}: Volume for music tracks, scaled by the master volume.
+ *
+ *
+ * Key features:
+ *
+ * - Thread-safe management of audio managers using {@link CopyOnWriteArrayList}.
+ * - Automatic propagation of master volume changes to dependent volume types.
+ * - Clamping volume values between 0.0 and 1.0 to ensure valid audio levels.
+ * - Dynamic registration and removal of audio managers for each volume type.
+ *
+ *
+ * Example usage:
+ * {@code
+ * // Add a music manager to the MUSIC volume type
+ * VolumeControl.MUSIC.addManager(musicManager);
+ *
+ * // Set master volume to 80%
+ * VolumeControl.MASTERVOLUME.setVolume(0.8, 0);
+ *
+ * // Set FX volume to 50% of master
+ * VolumeControl.FX.setVolume(0.5, VolumeControl.MASTERVOLUME.getVolume());
+ *
+ * // Retrieve current music volume
+ * double musicVol = VolumeControl.MUSIC.getVolume();
+ * }
+ */
+public enum VolumeControl {
+ MASTERVOLUME(),
+ FX(),
+ MUSIC();
+
+ @SuppressWarnings("ImmutableEnumChecker")
+ private final List> managers = new CopyOnWriteArrayList<>();
+ @SuppressWarnings("ImmutableEnumChecker")
+ private double volume = 1.0;
+ @SuppressWarnings("ImmutableEnumChecker")
+ private double masterVolume = 1.0;
+
+ /**
+ * Sets the volume for this volume type.
+ *
+ * If this type is {@link #MASTERVOLUME}, all dependent volume types
+ * (FX, MUSIC, etc.) are automatically updated to reflect the new master volume.
+ * Otherwise, the volume is scaled by the provided master volume.
+ *
+ * @param newVolume the new volume level (0.0 to 1.0)
+ * @param currentMasterVolume the current master volume for scaling non-master types
+ */
+ public void setVolume(double newVolume, double currentMasterVolume) {
+ this.volume = clamp(newVolume);
+
+ if (this == MASTERVOLUME) {
+ for (VolumeControl type : VolumeControl.values()) {
+ if (type != MASTERVOLUME) {
+ type.masterVolume = this.volume;
+ type.broadcastVolume(type.computeEffectiveVolume());
+ }
+ }
+ } else {
+ this.masterVolume = clamp(currentMasterVolume);
+ broadcastVolume(computeEffectiveVolume());
+ }
+ }
+
+ /**
+ * Computes the effective volume for this type, taking into account
+ * the master volume if this is not {@link #MASTERVOLUME}.
+ *
+ * @return the effective volume (0.0 to 1.0)
+ */
+ private double computeEffectiveVolume() {
+ return (this == MASTERVOLUME) ? volume : volume * masterVolume;
+ }
+
+ /**
+ * Updates all registered audio managers with the given effective volume.
+ *
+ * @param effectiveVolume the volume to apply to all active audio resources
+ */
+ private void broadcastVolume(double effectiveVolume) {
+ managers.stream()
+ .filter(Objects::nonNull)
+ .forEach(manager -> manager.getActiveAudio()
+ .forEach(aud -> aud.updateVolume(effectiveVolume)));
+ }
+
+ /**
+ * Clamps a volume value to the valid range [0.0, 1.0].
+ *
+ * @param vol the volume to clamp
+ * @return the clamped volume
+ */
+ private double clamp(double vol) {
+ return Math.max(0, Math.min(vol, 1.0));
+ }
+
+ /**
+ * Gets the current volume for this type.
+ *
+ * @return the current volume (0.0 to 1.0)
+ */
+ public double getVolume() {
+ return volume;
+ }
+
+ /**
+ * Registers an {@link AudioManager} to this volume type.
+ *
+ * Duplicate managers are ignored. Managers will receive volume updates
+ * when this type's volume changes.
+ *
+ * @param manager the audio manager to register
+ */
+ public void addManager(AudioManager extends AudioResource> manager) {
+ if (manager != null && !managers.contains(manager)) {
+ managers.add(manager);
+ }
+ }
+
+ /**
+ * Removes a previously registered {@link AudioManager} from this type.
+ *
+ * @param manager the audio manager to remove
+ */
+ public void removeManager(AudioManager extends AudioResource> manager) {
+ if (manager != null) {
+ managers.remove(manager);
+ }
+ }
+
+ /**
+ * Returns an unmodifiable view of all registered audio managers for this type.
+ *
+ * @return a list of registered audio managers
+ */
+ public List> getManagers() {
+ return Collections.unmodifiableList(managers);
+ }
+}
\ No newline at end of file
diff --git a/framework/src/main/java/org/toop/framework/audio/events/AudioEvents.java b/framework/src/main/java/org/toop/framework/audio/events/AudioEvents.java
index aed23ee..0636db5 100644
--- a/framework/src/main/java/org/toop/framework/audio/events/AudioEvents.java
+++ b/framework/src/main/java/org/toop/framework/audio/events/AudioEvents.java
@@ -1,98 +1,40 @@
package org.toop.framework.audio.events;
-import java.util.Map;
-import org.toop.framework.eventbus.events.EventWithSnowflake;
-import org.toop.framework.eventbus.events.EventWithoutSnowflake;
-import org.toop.framework.eventbus.events.EventsBase;
+import org.toop.framework.audio.VolumeControl;
+import org.toop.framework.eventbus.events.*;
public class AudioEvents extends EventsBase {
- /** Starts playing a sound. */
- public record PlayEffect(String fileName, boolean loop) implements EventWithoutSnowflake {}
+ /** Stops the audio manager. */
+ public record StopAudioManager() implements GenericEvent {}
- public record StopEffect(long clipId) implements EventWithoutSnowflake {}
+ /** Start playing a sound effect. */
+ public record PlayEffect(String fileName, boolean loop) implements GenericEvent {}
- public record StartBackgroundMusic() implements EventWithoutSnowflake {}
+ /** Stop playing a sound effect. */
+ public record StopEffect(String fileName) implements GenericEvent {}
- public record ChangeVolume(double newVolume) implements EventWithoutSnowflake {}
+ /** Start background music. */
+ public record StartBackgroundMusic() implements GenericEvent {}
- public record ChangeFxVolume(double newVolume) implements EventWithoutSnowflake {}
+ /** Gives back the name of the song, the position its currently at (in seconds) and how long it takes (in seconds) */
+ public record PlayingMusic(String name, long currentPosition, long duration) implements GenericEvent {}
- public record ChangeMusicVolume(double newVolume) implements EventWithoutSnowflake {}
+ /** Skips the song to the last second of the song resulting in a skip effect */
+ public record SkipMusic() implements GenericEvent {}
- public record GetCurrentVolume(long snowflakeId) implements EventWithSnowflake {
- @Override
- public Map result() {
- return Map.of();
- }
+ public record PreviousMusic() implements GenericEvent {}
- @Override
- public long eventSnowflake() {
- return snowflakeId;
- }
- }
+ public record PauseMusic() implements GenericEvent {}
- public record GetCurrentVolumeResponse(double currentVolume, long snowflakeId)
- implements EventWithSnowflake {
- @Override
- public Map result() {
- return Map.of();
- }
+ /** Change volume, choose type with {@link VolumeControl}. */
+ public record ChangeVolume(double newVolume, VolumeControl controlType) implements GenericEvent {}
- @Override
- public long eventSnowflake() {
- return snowflakeId;
- }
- }
+ /** Requests the desired volume by selecting it with {@link VolumeControl}. */
+ public record GetVolume(VolumeControl controlType, long identifier) implements UniqueEvent {}
- public record GetCurrentFxVolume(long snowflakeId) implements EventWithSnowflake {
- @Override
- public Map result() {
- return Map.of();
- }
+ /** Response to GetVolume. */
+ public record GetVolumeResponse(double currentVolume, long identifier) implements ResponseToUniqueEvent {}
- @Override
- public long eventSnowflake() {
- return this.snowflakeId;
- }
- }
-
- public record GetCurrentMusicVolume(long snowflakeId) implements EventWithSnowflake {
- @Override
- public Map result() {
- return Map.of();
- }
-
- @Override
- public long eventSnowflake() {
- return this.snowflakeId;
- }
- }
-
- public record GetCurrentFxVolumeResponse(double currentVolume, long snowflakeId)
- implements EventWithSnowflake {
- @Override
- public Map result() {
- return Map.of();
- }
-
- @Override
- public long eventSnowflake() {
- return this.snowflakeId;
- }
- }
-
- public record GetCurrentMusicVolumeResponse(double currentVolume, long snowflakeId)
- implements EventWithSnowflake {
- @Override
- public Map result() {
- return Map.of();
- }
-
- @Override
- public long eventSnowflake() {
- return this.snowflakeId;
- }
- }
-
- public record ClickButton() implements EventWithoutSnowflake {}
+ /** Plays the predetermined sound for pressing a button. */
+ public record ClickButton() implements GenericEvent {}
}
diff --git a/framework/src/main/java/org/toop/framework/audio/interfaces/AudioManager.java b/framework/src/main/java/org/toop/framework/audio/interfaces/AudioManager.java
new file mode 100644
index 0000000..9ba7777
--- /dev/null
+++ b/framework/src/main/java/org/toop/framework/audio/interfaces/AudioManager.java
@@ -0,0 +1,7 @@
+package org.toop.framework.audio.interfaces;
+
+import java.util.Collection;
+
+public interface AudioManager {
+ Collection getActiveAudio();
+}
diff --git a/framework/src/main/java/org/toop/framework/audio/interfaces/MusicManager.java b/framework/src/main/java/org/toop/framework/audio/interfaces/MusicManager.java
new file mode 100644
index 0000000..8b59179
--- /dev/null
+++ b/framework/src/main/java/org/toop/framework/audio/interfaces/MusicManager.java
@@ -0,0 +1,11 @@
+package org.toop.framework.audio.interfaces;
+
+import org.toop.framework.resource.types.AudioResource;
+
+public interface MusicManager extends AudioManager {
+ void play();
+ void stop();
+ void skip();
+ void previous();
+ void pause();
+}
diff --git a/framework/src/main/java/org/toop/framework/audio/interfaces/SoundEffectManager.java b/framework/src/main/java/org/toop/framework/audio/interfaces/SoundEffectManager.java
new file mode 100644
index 0000000..faba4ca
--- /dev/null
+++ b/framework/src/main/java/org/toop/framework/audio/interfaces/SoundEffectManager.java
@@ -0,0 +1,8 @@
+package org.toop.framework.audio.interfaces;
+
+import org.toop.framework.resource.types.AudioResource;
+
+public interface SoundEffectManager extends AudioManager {
+ void play(String name, boolean loop);
+ void stop(String name);
+}
diff --git a/framework/src/main/java/org/toop/framework/audio/interfaces/VolumeManager.java b/framework/src/main/java/org/toop/framework/audio/interfaces/VolumeManager.java
new file mode 100644
index 0000000..d5e03ab
--- /dev/null
+++ b/framework/src/main/java/org/toop/framework/audio/interfaces/VolumeManager.java
@@ -0,0 +1,53 @@
+package org.toop.framework.audio.interfaces;
+
+import org.toop.framework.audio.VolumeControl;
+
+
+/**
+ * Interface for managing audio volumes in the application.
+ *
+ * Implementations of this interface are responsible for controlling the volume levels
+ * of different categories of audio (e.g., master volume, music, sound effects) and
+ * updating the associated audio managers or resources accordingly.
+ *
+ *
+ * Typical responsibilities include:
+ *
+ * - Setting the volume for a specific category (master, music, FX).
+ * - Retrieving the current volume of a category.
+ * - Ensuring that changes in master volume propagate to dependent audio categories.
+ * - Interfacing with {@link org.toop.framework.audio.interfaces.AudioManager} to update active audio resources.
+ *
+ *
+ * Example usage:
+ * {@code
+ * VolumeManager volumeManager = ...;
+ * // Set master volume to 80%
+ * volumeManager.setVolume(0.8, VolumeControl.MASTERVOLUME);
+ *
+ * // Set music volume to 50% of master
+ * volumeManager.setVolume(0.5, VolumeControl.MUSIC);
+ *
+ * // Retrieve current FX volume
+ * double fxVolume = volumeManager.getVolume(VolumeControl.FX);
+ * }
+ */
+public interface VolumeManager {
+
+ /**
+ *
+ * Sets the volume to for the specified {@link VolumeControl}.
+ *
+ * @param newVolume The volume to be set to.
+ * @param type The type of volume to change.
+ */
+ void setVolume(double newVolume, VolumeControl type);
+
+ /**
+ * Gets the current volume for the specified {@link VolumeControl}.
+ *
+ * @param type the type of volume to get.
+ * @return The volume as a {@link Double}
+ */
+ double getVolume(VolumeControl type);
+}
diff --git a/framework/src/main/java/org/toop/framework/dispatch/JavaFXDispatcher.java b/framework/src/main/java/org/toop/framework/dispatch/JavaFXDispatcher.java
new file mode 100644
index 0000000..6fd4d87
--- /dev/null
+++ b/framework/src/main/java/org/toop/framework/dispatch/JavaFXDispatcher.java
@@ -0,0 +1,11 @@
+package org.toop.framework.dispatch;
+
+import javafx.application.Platform;
+import org.toop.framework.dispatch.interfaces.Dispatcher;
+
+public class JavaFXDispatcher implements Dispatcher {
+ @Override
+ public void run(Runnable task) {
+ Platform.runLater(task);
+ }
+}
\ No newline at end of file
diff --git a/framework/src/main/java/org/toop/framework/dispatch/interfaces/Dispatcher.java b/framework/src/main/java/org/toop/framework/dispatch/interfaces/Dispatcher.java
new file mode 100644
index 0000000..17306d6
--- /dev/null
+++ b/framework/src/main/java/org/toop/framework/dispatch/interfaces/Dispatcher.java
@@ -0,0 +1,5 @@
+package org.toop.framework.dispatch.interfaces;
+
+public interface Dispatcher {
+ void run(Runnable task);
+}
diff --git a/framework/src/main/java/org/toop/framework/eventbus/EventFlow.java b/framework/src/main/java/org/toop/framework/eventbus/EventFlow.java
index 4c4a8de..a9ffd9d 100644
--- a/framework/src/main/java/org/toop/framework/eventbus/EventFlow.java
+++ b/framework/src/main/java/org/toop/framework/eventbus/EventFlow.java
@@ -11,13 +11,18 @@ import java.util.function.Consumer;
import java.util.function.Supplier;
import org.toop.framework.SnowflakeGenerator;
import org.toop.framework.eventbus.events.EventType;
-import org.toop.framework.eventbus.events.EventWithSnowflake;
+import org.toop.framework.eventbus.events.ResponseToUniqueEvent;
+import org.toop.framework.eventbus.events.UniqueEvent;
+import org.toop.framework.eventbus.bus.EventBus;
+import org.toop.framework.eventbus.subscriber.DefaultNamedSubscriber;
+import org.toop.framework.eventbus.subscriber.NamedSubscriber;
+import org.toop.framework.eventbus.subscriber.Subscriber;
/**
* EventFlow is a utility class for creating, posting, and optionally subscribing to events in a
- * type-safe and chainable manner. It is designed to work with the {@link GlobalEventBus}.
+ * type-safe and chainable manner. It is designed to work with the {@link EventBus}.
*
- * This class supports automatic UUID assignment for {@link EventWithSnowflake} events, and
+ *
This class supports automatic UUID assignment for {@link UniqueEvent} events, and
* allows filtering subscribers so they only respond to events with a specific UUID. All
* subscription methods are chainable, and you can configure automatic unsubscription after an event
* has been successfully handled.
@@ -30,37 +35,41 @@ public class EventFlow {
/** Cache of constructor handles for event classes to avoid repeated reflection lookups. */
private static final Map, MethodHandle> CONSTRUCTOR_CACHE = new ConcurrentHashMap<>();
- /** Automatically assigned UUID for {@link EventWithSnowflake} events. */
+ private final EventBus eventBus;
+
+ /** Automatically assigned UUID for {@link UniqueEvent} events. */
private long eventSnowflake = -1;
/** The event instance created by this publisher. */
private EventType event = null;
/** The listener returned by GlobalEventBus subscription. Used for unsubscription. */
- private final List listeners = new ArrayList<>();
+ private final List> listeners = new ArrayList<>();
/** Holds the results returned from the subscribed event, if any. */
- private Map result = null;
+ private Map result = null;
/** Empty constructor (event must be added via {@link #addPostEvent(Class, Object...)}). */
- public EventFlow() {}
-
- // New: accept an event instance directly
- public EventFlow addPostEvent(EventType event) {
- this.event = event;
- return this;
+ public EventFlow(EventBus eventBus) {
+ this.eventBus = eventBus;
}
- // Optional: accept a Supplier to defer construction
- public EventFlow addPostEvent(Supplier extends EventType> eventSupplier) {
- this.event = eventSupplier.get();
- return this;
+ public EventFlow() {
+ this.eventBus = GlobalEventBus.get();
}
- // Keep the old class+args version if needed
+ /**
+ *
+ * Add an event that will be triggered when {@link #postEvent()} or {@link #asyncPostEvent()} is called.
+ *
+ * @param eventClass The event that will be posted.
+ * @param args The event arguments, see the added event record for more information.
+ * @return {@link #EventFlow}
+ *
+ */
public EventFlow addPostEvent(Class eventClass, Object... args) {
try {
- boolean isUuidEvent = EventWithSnowflake.class.isAssignableFrom(eventClass);
+ boolean isUuidEvent = UniqueEvent.class.isAssignableFrom(eventClass);
MethodHandle ctorHandle =
CONSTRUCTOR_CACHE.computeIfAbsent(
@@ -81,7 +90,7 @@ public class EventFlow {
int expectedParamCount = ctorHandle.type().parameterCount();
if (isUuidEvent && args.length < expectedParamCount) {
- this.eventSnowflake = new SnowflakeGenerator().nextId();
+ this.eventSnowflake = SnowflakeGenerator.nextId();
finalArgs = new Object[args.length + 1];
System.arraycopy(args, 0, finalArgs, 0, args.length);
finalArgs[args.length] = this.eventSnowflake;
@@ -100,155 +109,403 @@ public class EventFlow {
}
}
- // public EventFlow addSnowflake() {
- // this.eventSnowflake = new SnowflakeGenerator(1).nextId();
- // return this;
- // }
-
- /** Subscribe by ID: only fires if UUID matches this publisher's eventId. */
- public EventFlow onResponse(
- Class eventClass, Consumer action, boolean unsubscribeAfterSuccess) {
- ListenerHandler[] listenerHolder = new ListenerHandler[1];
- listenerHolder[0] =
- new ListenerHandler(
- GlobalEventBus.subscribe(
- eventClass,
- event -> {
- if (event.eventSnowflake() != this.eventSnowflake) return;
-
- action.accept(event);
-
- if (unsubscribeAfterSuccess && listenerHolder[0] != null) {
- GlobalEventBus.unsubscribe(listenerHolder[0]);
- this.listeners.remove(listenerHolder[0]);
- }
-
- this.result = event.result();
- }));
- this.listeners.add(listenerHolder[0]);
+ /**
+ *
+ * Add an event that will be triggered when {@link #postEvent()} or {@link #asyncPostEvent()} is called.
+ *
+ * @param event The event to be posted.
+ * @return {@link #EventFlow}
+ *
+ */
+ public EventFlow addPostEvent(EventType event) {
+ this.event = event;
return this;
}
- /** Subscribe by ID: only fires if UUID matches this publisher's eventId. */
- public EventFlow onResponse(
- Class eventClass, Consumer action) {
- return this.onResponse(eventClass, action, true);
+ /**
+ *
+ * Add an event that will be triggered when {@link #postEvent()} or {@link #asyncPostEvent()} is called.
+ *
+ * @param eventSupplier The event that will be posted through a Supplier.
+ * @return {@link #EventFlow}
+ *
+ */
+ public EventFlow addPostEvent(Supplier extends EventType> eventSupplier) {
+ this.event = eventSupplier.get();
+ return this;
}
- /** Subscribe by ID without explicit class. */
+ /**
+ *
+ * Start listening for an event and trigger when ID correlates.
+ *
+ * @param event The {@link ResponseToUniqueEvent} to trigger the lambda.
+ * @param action The lambda to run when triggered.
+ * @param unsubscribeAfterSuccess Enable/disable auto unsubscribing to event after being triggered.
+ * @param name A name given to the event, can later be used to unsubscribe.
+ * @return {@link #EventFlow}
+ *
+ */
+ public EventFlow onResponse(
+ Class event, Consumer action, boolean unsubscribeAfterSuccess, String name
+ ) {
+
+ final long id = SnowflakeGenerator.nextId();
+
+ Consumer newAction = eventClass -> {
+ if (eventClass.getIdentifier() != this.eventSnowflake) return;
+
+ action.accept(eventClass);
+
+ if (unsubscribeAfterSuccess) unsubscribe(String.valueOf(id));
+
+ this.result = eventClass.result();
+ };
+
+ var subscriber = new DefaultNamedSubscriber<>(
+ name,
+ event,
+ newAction
+ );
+
+ eventBus.subscribe(subscriber);
+ this.listeners.add(subscriber);
+ return this;
+ }
+
+ /**
+ *
+ * Start listening for an event and trigger when ID correlates, auto unsubscribes after being triggered and adds an empty name.
+ *
+ * @param event The {@link ResponseToUniqueEvent} to trigger the lambda.
+ * @param action The lambda to run when triggered.
+ * @return {@link #EventFlow}
+ *
+ */
+ public EventFlow onResponse(Class event, Consumer action) {
+ return this.onResponse(event, action, true, "");
+ }
+
+ /**
+ *
+ * Start listening for an event and trigger when ID correlates, auto adds an empty name.
+ *
+ * @param event The {@link ResponseToUniqueEvent} to trigger the lambda.
+ * @param action The lambda to run when triggered.
+ * @param unsubscribeAfterSuccess Enable/disable auto unsubscribing to event after being triggered.
+ * @return {@link #EventFlow}
+ *
+ */
+ public EventFlow onResponse(Class event, Consumer action, boolean unsubscribeAfterSuccess) {
+ return this.onResponse(event, action, unsubscribeAfterSuccess, "");
+ }
+
+ /**
+ *
+ * Start listening for an event and trigger when ID correlates, auto unsubscribes after being triggered.
+ *
+ * @param event The {@link ResponseToUniqueEvent} to trigger the lambda.
+ * @param action The lambda to run when triggered.
+ * @param name A name given to the event, can later be used to unsubscribe.
+ * @return {@link #EventFlow}
+ *
+ */
+ public EventFlow onResponse(Class event, Consumer action, String name) {
+ return this.onResponse(event, action, true, name);
+ }
+
+ /**
+ *
+ * Subscribe by ID without explicit class.
+ *
+ * @param action The lambda to run when triggered.
+ * @return {@link #EventFlow}
+ *
+ * @deprecated use {@link #onResponse(Class, Consumer, boolean, String)} instead.
+ */
+ @Deprecated
@SuppressWarnings("unchecked")
- public EventFlow onResponse(
- Consumer action, boolean unsubscribeAfterSuccess) {
- ListenerHandler[] listenerHolder = new ListenerHandler[1];
- listenerHolder[0] =
- new ListenerHandler(
- GlobalEventBus.subscribe(
- event -> {
- if (!(event instanceof EventWithSnowflake uuidEvent)) return;
- if (uuidEvent.eventSnowflake() == this.eventSnowflake) {
- try {
- TT typedEvent = (TT) uuidEvent;
- action.accept(typedEvent);
- if (unsubscribeAfterSuccess
- && listenerHolder[0] != null) {
- GlobalEventBus.unsubscribe(listenerHolder[0]);
- this.listeners.remove(listenerHolder[0]);
- }
- this.result = typedEvent.result();
- } catch (ClassCastException _) {
- throw new ClassCastException(
- "Cannot cast "
- + event.getClass().getName()
- + " to EventWithSnowflake");
- }
- }
- }));
- this.listeners.add(listenerHolder[0]);
+ public EventFlow onResponse(
+ Consumer action, boolean unsubscribeAfterSuccess, String name) {
+
+ final long id = SnowflakeGenerator.nextId();
+
+ Consumer newAction = event -> {
+ if (!(event instanceof UniqueEvent uuidEvent)) return;
+ if (uuidEvent.getIdentifier() == this.eventSnowflake) {
+ try {
+ TT typedEvent = (TT) uuidEvent;
+ action.accept(typedEvent);
+
+ if (unsubscribeAfterSuccess) unsubscribe(String.valueOf(id));
+
+ this.result = typedEvent.result();
+ } catch (ClassCastException _) {
+ throw new ClassCastException(
+ "Cannot cast "
+ + event.getClass().getName()
+ + " to UniqueEvent");
+ }
+ }
+ };
+
+ var listener = new DefaultNamedSubscriber<>(
+ name,
+ (Class) action.getClass().getDeclaredMethods()[0].getParameterTypes()[0],
+ newAction
+ );
+
+ eventBus.subscribe(listener);
+ this.listeners.add(listener);
return this;
}
- public EventFlow onResponse(Consumer action) {
- return this.onResponse(action, true);
+ /**
+ *
+ * Subscribe by ID without explicit class.
+ *
+ * @param action The lambda to run when triggered.
+ * @return {@link #EventFlow}
+ *
+ * @deprecated use {@link #onResponse(Class, Consumer)} instead.
+ */
+ @Deprecated
+ public EventFlow onResponse(Consumer action) {
+ return this.onResponse(action, true, "");
}
+ /**
+ *
+ * Start listening for an event, and run a lambda when triggered.
+ *
+ * @param event The {@link EventType} to trigger the lambda.
+ * @param action The lambda to run when triggered.
+ * @param unsubscribeAfterSuccess Enable/disable auto unsubscribing to event after being triggered.
+ * @param name A name given to the event, can later be used to unsubscribe.
+ * @return {@link #EventFlow}
+ *
+ */
public EventFlow listen(
- Class eventClass, Consumer action, boolean unsubscribeAfterSuccess) {
- ListenerHandler[] listenerHolder = new ListenerHandler[1];
- listenerHolder[0] =
- new ListenerHandler(
- GlobalEventBus.subscribe(
- eventClass,
- event -> {
- action.accept(event);
+ Class event, Consumer action, boolean unsubscribeAfterSuccess, String name) {
- if (unsubscribeAfterSuccess && listenerHolder[0] != null) {
- GlobalEventBus.unsubscribe(listenerHolder[0]);
- this.listeners.remove(listenerHolder[0]);
- }
- }));
- this.listeners.add(listenerHolder[0]);
+ long id = SnowflakeGenerator.nextId();
+
+ Consumer newAction = eventc -> {
+ action.accept(eventc);
+
+ if (unsubscribeAfterSuccess) unsubscribe(String.valueOf(id));
+ };
+
+ var listener = new DefaultNamedSubscriber<>(
+ name,
+ event,
+ newAction
+ );
+
+ eventBus.subscribe(listener);
+ this.listeners.add(listener);
return this;
}
- public EventFlow listen(Class eventClass, Consumer action) {
- return this.listen(eventClass, action, true);
+ /**
+ *
+ * Start listening for an event, and run a lambda when triggered, auto unsubscribes.
+ *
+ * @param event The {@link EventType} to trigger the lambda.
+ * @param action The lambda to run when triggered.
+ * @param name A name given to the event, can later be used to unsubscribe.
+ * @return {@link #EventFlow}
+ *
+ */
+ public EventFlow listen(Class event, Consumer action, String name) {
+ return this.listen(event, action, true, name);
}
+ /**
+ *
+ * Start listening for an event, and run a lambda when triggered, auto unsubscribe and gives it an empty name.
+ *
+ * @param event The {@link EventType} to trigger the lambda.
+ * @param action The lambda to run when triggered.
+ * @return {@link #EventFlow}
+ *
+ */
+ public EventFlow listen(Class event, Consumer action) {
+ return this.listen(event, action, true, "");
+ }
+
+ /**
+ *
+ * Start listening for an event, and run a lambda when triggered, adds an empty name.
+ *
+ * @param event The {@link EventType} to trigger the lambda.
+ * @param action The lambda to run when triggered.
+ * @param unsubscribeAfterSuccess Enable/disable auto unsubscribing to event after being triggered.
+ * @return {@link #EventFlow}
+ *
+ */
+ public EventFlow listen(Class event, Consumer action, boolean unsubscribeAfterSuccess) {
+ return this.listen(event, action, unsubscribeAfterSuccess, "");
+ }
+
+ /**
+ *
+ * Start listening to an event.
+ *
+ * @param action The lambda to run when triggered.
+ * @return {@link EventFlow}
+ *
+ * @deprecated use {@link #listen(Class, Consumer, boolean, String)} instead.
+ */
+ @Deprecated
@SuppressWarnings("unchecked")
public EventFlow listen(
- Consumer action, boolean unsubscribeAfterSuccess) {
- ListenerHandler[] listenerHolder = new ListenerHandler[1];
- listenerHolder[0] =
- new ListenerHandler(
- GlobalEventBus.subscribe(
- event -> {
- if (!(event instanceof EventType nonUuidEvent)) return;
- try {
- TT typedEvent = (TT) nonUuidEvent;
- action.accept(typedEvent);
- if (unsubscribeAfterSuccess && listenerHolder[0] != null) {
- GlobalEventBus.unsubscribe(listenerHolder[0]);
- this.listeners.remove(listenerHolder[0]);
- }
- } catch (ClassCastException _) {
- throw new ClassCastException(
- "Cannot cast "
- + event.getClass().getName()
- + " to EventWithSnowflake");
- }
- }));
- this.listeners.add(listenerHolder[0]);
+ Consumer action, boolean unsubscribeAfterSuccess, String name) {
+ long id = SnowflakeGenerator.nextId();
+
+ Class eventClass = (Class) action.getClass().getDeclaredMethods()[0].getParameterTypes()[0];
+
+ Consumer newAction = event -> {
+ if (!(event instanceof EventType nonUuidEvent)) return;
+ try {
+ TT typedEvent = (TT) nonUuidEvent;
+ action.accept(typedEvent);
+ if (unsubscribeAfterSuccess) unsubscribe(String.valueOf(id));
+ } catch (ClassCastException _) {
+ throw new ClassCastException(
+ "Cannot cast "
+ + event.getClass().getName()
+ + " to UniqueEvent");
+ }
+ };
+
+ var listener = new DefaultNamedSubscriber<>(
+ name,
+ eventClass,
+ newAction
+ );
+
+ eventBus.subscribe(listener);
+ this.listeners.add(listener);
return this;
}
+ /**
+ *
+ * Start listening to an event.
+ *
+ * @param action The lambda to run when triggered.
+ * @return {@link EventFlow}
+ *
+ * @deprecated use {@link #listen(Class, Consumer)} instead.
+ */
+ @Deprecated
public EventFlow listen(Consumer action) {
- return this.listen(action, true);
+ return this.listen(action, true, "");
}
- /** Post synchronously */
+ /**
+ * Posts the event added through {@link #addPostEvent}.
+ */
public EventFlow postEvent() {
- GlobalEventBus.post(this.event);
+ eventBus.post(this.event);
return this;
}
- /** Post asynchronously */
+ /**
+ * Posts the event added through {@link #addPostEvent} asynchronously.
+ *
+ * @deprecated use {@link #postEvent()} instead.
+ */
+ @Deprecated
public EventFlow asyncPostEvent() {
- GlobalEventBus.postAsync(this.event);
+ eventBus.post(this.event);
return this;
}
- public Map