diff --git a/.gitignore b/.gitignore
index 592a652..bfad6e0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -48,6 +48,8 @@ shelf/
*.ipr
*.iws
misc.xml
+uiDesigner.xml
+
##############################
## Eclipse
@@ -76,6 +78,8 @@ dist/
nbdist/
nbactions.xml
nb-configuration.xml
+misc.xml
+compiler.xml
##############################
## Visual Studio Code
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
deleted file mode 100644
index dcffce8..0000000
--- a/.idea/compiler.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml
index 46f4d3b..e638523 100644
--- a/.idea/dictionaries/project.xml
+++ b/.idea/dictionaries/project.xml
@@ -8,11 +8,13 @@
flushnlgaafgamelist
+ pismplayerlisttictactoetoopvmoptionsxplugin
+ yourturn
\ No newline at end of file
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
index 895ac80..3b3a142 100644
--- a/.idea/encodings.xml
+++ b/.idea/encodings.xml
@@ -1,12 +1,16 @@
+
+
+
+
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index c168b80..e917175 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -2,7 +2,8 @@
-
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index 64c32f6..0000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/pom.xml b/app/pom.xml
index eb93b3b..a4da233 100644
--- a/app/pom.xml
+++ b/app/pom.xml
@@ -1,8 +1,13 @@
4.0.0
- org.toop
- pism_app
+
+ org.toop
+ pism
+ 0.1
+
+
+ app0.1
@@ -24,24 +29,36 @@
gson2.10.1
-
- org.toop
- pism_framework
- 0.1
- compile
-
-
- org.toop
- pism_game
- 0.1
- compile
- org.openjfxjavafx-controls25
+
+
+ com.google.errorprone
+ error_prone_core
+ 2.42.0
+
+
+ com.google.errorprone
+ error_prone_annotations
+ 2.42.0
+
+
+ org.toop
+ framework
+ 0.1
+ compile
+
+
+ org.toop
+ game
+ 0.1
+ compile
+
+
@@ -112,14 +129,56 @@
-
- org.apache.maven.plugins
- maven-compiler-plugin
-
- 25
- 25
-
-
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.14.1
+
+ true
+ true
+ ${java.home}/bin/javac
+ 25
+ 25
+ 25
+ UTF-8
+
+ -J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
+ -J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED
+ -J--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED
+ -J--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED
+ -J--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED
+ -J--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED
+ -J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED
+ -J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
+ -J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
+ -J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED
+
+ -Xplugin:ErrorProne \
+
+
+
+
+
+
+ -XDcompilePolicy=simple
+ --should-stop=ifError=FLOW
+
+
+
+ com.google.errorprone
+ error_prone_core
+ 2.42.0
+
+
+
+
+
+ com.google.errorprone
+ error_prone_core
+ 2.42.0
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/org/toop/Main.java b/app/src/main/java/org/toop/Main.java
index 05a34a4..69b1b86 100644
--- a/app/src/main/java/org/toop/Main.java
+++ b/app/src/main/java/org/toop/Main.java
@@ -1,21 +1,42 @@
package org.toop;
import org.toop.app.App;
-import org.toop.framework.asset.ResourceLoader;
-import org.toop.framework.asset.ResourceManager;
-import org.toop.framework.audio.SoundManager;
+import org.toop.framework.audio.*;
import org.toop.framework.networking.NetworkingClientManager;
import org.toop.framework.networking.NetworkingInitializationException;
+import org.toop.framework.resource.ResourceLoader;
+import org.toop.framework.resource.ResourceManager;
+import org.toop.framework.resource.ResourceMeta;
+import org.toop.framework.resource.resources.MusicAsset;
+import org.toop.framework.resource.resources.SoundEffectAsset;
+
+import java.util.Arrays;
+import java.util.List;
public final class Main {
- public static void main(String[] args) {
- initSystems();
- App.run(args);
- }
+ static void main(String[] args) {
+ initSystems();
+ App.run(args);
+ }
- private static void initSystems() throws NetworkingInitializationException {
- ResourceManager.loadAssets(new ResourceLoader("app/src/main/resources/assets"));
- new Thread(NetworkingClientManager::new).start();
- new Thread(SoundManager::new).start();
- }
-}
\ No newline at end of file
+ private static void initSystems() throws NetworkingInitializationException {
+ ResourceManager.loadAssets(new ResourceLoader("app/src/main/resources/assets"));
+ new Thread(NetworkingClientManager::new).start();
+ new Thread(() -> {
+ MusicManager musicManager = new MusicManager<>(ResourceManager.getAllOfTypeAndRemoveWrapper(MusicAsset.class));
+ SoundEffectManager soundEffectManager = new SoundEffectManager<>(ResourceManager.getAllOfType(SoundEffectAsset.class));
+ AudioVolumeManager audioVolumeManager = new AudioVolumeManager()
+ .registerManager(VolumeControl.MASTERVOLUME, musicManager)
+ .registerManager(VolumeControl.MASTERVOLUME, soundEffectManager)
+ .registerManager(VolumeControl.FX, soundEffectManager)
+ .registerManager(VolumeControl.MUSIC, musicManager);
+
+ new AudioEventListener<>(
+ musicManager,
+ soundEffectManager,
+ audioVolumeManager
+ ).initListeners("medium-button-click.wav");
+
+ }).start();
+ }
+}
diff --git a/app/src/main/java/org/toop/app/App.java b/app/src/main/java/org/toop/app/App.java
index 38a4873..fe06b87 100644
--- a/app/src/main/java/org/toop/app/App.java
+++ b/app/src/main/java/org/toop/app/App.java
@@ -1,163 +1,170 @@
package org.toop.app;
-import javafx.application.Platform;
-import org.toop.app.layer.Layer;
-import org.toop.app.layer.layers.MainLayer;
-import org.toop.app.layer.layers.QuitPopup;
-import org.toop.framework.asset.ResourceManager;
-import org.toop.framework.asset.resources.CssAsset;
-import org.toop.framework.audio.events.AudioEvents;
-import org.toop.framework.eventbus.EventFlow;
-import org.toop.local.AppContext;
-
+import java.util.Stack;
import javafx.application.Application;
+import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
+import org.toop.app.layer.Layer;
+import org.toop.app.layer.layers.MainLayer;
+import org.toop.app.layer.layers.QuitPopup;
+import org.toop.framework.audio.VolumeControl;
+import org.toop.framework.audio.events.AudioEvents;
+import org.toop.framework.eventbus.EventFlow;
+import org.toop.framework.resource.ResourceManager;
+import org.toop.framework.resource.resources.CssAsset;
+import org.toop.local.AppContext;
import org.toop.local.AppSettings;
-import java.util.Stack;
-
public final class App extends Application {
- private static Stage stage;
- private static Scene scene;
- private static StackPane root;
+ private static Stage stage;
+ private static Scene scene;
+ private static StackPane root;
- private static Stack stack;
+ private static Stack stack;
private static int height;
private static int width;
- private static boolean isQuitting;
+ private static boolean isQuitting;
- public static void run(String[] args) {
- launch(args);
- }
+ public static void run(String[] args) {
+ launch(args);
+ }
- @Override
- public void start(Stage stage) throws Exception {
+ @Override
+ public void start(Stage stage) throws Exception {
final StackPane root = new StackPane();
- final Scene scene = new Scene(root);
+ final Scene scene = new Scene(root);
- stage.setTitle(AppContext.getString("appTitle"));
- stage.setWidth(1080);
- stage.setHeight(720);
+ stage.setTitle(AppContext.getString("appTitle"));
+ stage.setWidth(1080);
+ stage.setHeight(720);
- stage.setOnCloseRequest(event -> {
- event.consume();
+ stage.setOnCloseRequest(
+ event -> {
+ event.consume();
- if (!isQuitting) {
- quitPopup();
- }
- });
+ if (!isQuitting) {
+ quitPopup();
+ }
+ });
- stage.setScene(scene);
- stage.setResizable(false);
+ stage.setScene(scene);
+ stage.setResizable(false);
- stage.show();
+ stage.show();
- App.stage = stage;
- App.scene = scene;
- App.root = root;
+ App.stage = stage;
+ App.scene = scene;
+ App.root = root;
- App.stack = new Stack<>();
+ App.stack = new Stack<>();
- App.width = (int) stage.getWidth();
- App.height = (int) stage.getHeight();
+ App.width = (int) stage.getWidth();
+ App.height = (int) stage.getHeight();
- App.isQuitting = false;
+ App.isQuitting = false;
- final AppSettings settings = new AppSettings();
- settings.applySettings();
+ new EventFlow().addPostEvent(new AudioEvents.StartBackgroundMusic()).postEvent();
- new EventFlow().addPostEvent(new AudioEvents.StartBackgroundMusic()).asyncPostEvent();
- activate(new MainLayer());
- }
+ final AppSettings settings = new AppSettings();
+ settings.applySettings();
- public static void activate(Layer layer) {
- Platform.runLater(() -> {
- popAll();
- push(layer);
- });
- }
+ activate(new MainLayer());
+ }
- public static void push(Layer layer) {
- Platform.runLater(() -> {
- root.getChildren().addLast(layer.getLayer());
- stack.push(layer);
- });
- }
+ public static void activate(Layer layer) {
+ Platform.runLater(
+ () -> {
+ popAll();
+ push(layer);
+ });
+ }
- public static void pop() {
- Platform.runLater(() -> {
- root.getChildren().removeLast();
- stack.pop();
+ public static void push(Layer layer) {
+ Platform.runLater(
+ () -> {
+ root.getChildren().addLast(layer.getLayer());
+ stack.push(layer);
+ });
+ }
- isQuitting = false;
- });
- }
+ public static void pop() {
+ Platform.runLater(
+ () -> {
+ root.getChildren().removeLast();
+ stack.pop();
- public static void popAll() {
- Platform.runLater(() -> {
- final int childrenCount = root.getChildren().size();
+ isQuitting = false;
+ });
+ }
- for (int i = 0; i < childrenCount; i++) {
- try {
- root.getChildren().removeLast();
- } catch (Exception e) {
- IO.println(e);
- }
- }
+ public static void popAll() {
+ Platform.runLater(
+ () -> {
+ final int childrenCount = root.getChildren().size();
- stack.removeAllElements();
- });
- }
+ for (int i = 0; i < childrenCount; i++) {
+ try {
+ root.getChildren().removeLast();
+ } catch (Exception e) {
+ IO.println(e); // TODO: Use logger
+ }
+ }
- public static void quitPopup() {
- Platform.runLater(() -> {
- push(new QuitPopup());
- isQuitting = true;
- });
- }
+ stack.removeAllElements();
+ });
+ }
- public static void quit() {
- stage.close();
- }
+ public static void quitPopup() {
+ Platform.runLater(
+ () -> {
+ push(new QuitPopup());
+ isQuitting = true;
+ });
+ }
- public static void reloadAll() {
- stage.setTitle(AppContext.getString("appTitle"));
+ public static void quit() {
+ new EventFlow().addPostEvent(new AudioEvents.StopAudioManager()).postEvent();
+ stage.close();
+ }
- for (final Layer layer : stack) {
- layer.reload();
- }
- }
+ public static void reloadAll() {
+ stage.setTitle(AppContext.getString("appTitle"));
- public static void setFullscreen(boolean fullscreen) {
- stage.setFullScreen(fullscreen);
+ for (final Layer layer : stack) {
+ layer.reload();
+ }
+ }
- width = (int) stage.getWidth();
- height = (int) stage.getHeight();
+ public static void setFullscreen(boolean fullscreen) {
+ stage.setFullScreen(fullscreen);
- reloadAll();
- }
+ width = (int) stage.getWidth();
+ height = (int) stage.getHeight();
- public static void setStyle(String theme, String layoutSize) {
- final int stylesCount = scene.getStylesheets().size();
+ reloadAll();
+ }
- for (int i = 0; i < stylesCount; i++) {
- scene.getStylesheets().removeLast();
- }
+ public static void setStyle(String theme, String layoutSize) {
+ final int stylesCount = scene.getStylesheets().size();
- scene.getStylesheets().add(ResourceManager.get(theme + ".css").getUrl());
- scene.getStylesheets().add(ResourceManager.get(layoutSize + ".css").getUrl());
+ for (int i = 0; i < stylesCount; i++) {
+ scene.getStylesheets().removeLast();
+ }
- reloadAll();
- }
+ scene.getStylesheets().add(ResourceManager.get(theme + ".css").getUrl());
+ scene.getStylesheets().add(ResourceManager.get(layoutSize + ".css").getUrl());
- public static int getWidth() {
- return width;
- }
+ reloadAll();
+ }
- public static int getHeight() {
- return height;
- }
-}
\ No newline at end of file
+ public static int getWidth() {
+ return width;
+ }
+
+ public static int getHeight() {
+ return height;
+ }
+}
diff --git a/app/src/main/java/org/toop/app/GameInformation.java b/app/src/main/java/org/toop/app/GameInformation.java
index fd7c56e..2a3e56f 100644
--- a/app/src/main/java/org/toop/app/GameInformation.java
+++ b/app/src/main/java/org/toop/app/GameInformation.java
@@ -1,6 +1,10 @@
package org.toop.app;
-public record GameInformation(String[] playerName, boolean[] isPlayerHuman,
- int[] computerDifficulty, int[] computerThinkTime,
- boolean isConnectionLocal, String serverIP, String serverPort) {
-}
+public record GameInformation(
+ String[] playerName,
+ boolean[] isPlayerHuman,
+ int[] computerDifficulty,
+ int[] computerThinkTime,
+ boolean isConnectionLocal,
+ String serverIP,
+ String serverPort) {}
diff --git a/app/src/main/java/org/toop/app/canvas/GameCanvas.java b/app/src/main/java/org/toop/app/canvas/GameCanvas.java
index 048d184..c7c1ed4 100644
--- a/app/src/main/java/org/toop/app/canvas/GameCanvas.java
+++ b/app/src/main/java/org/toop/app/canvas/GameCanvas.java
@@ -1,99 +1,106 @@
package org.toop.app.canvas;
+import java.util.function.Consumer;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.input.MouseButton;
import javafx.scene.paint.Color;
-import java.util.function.Consumer;
-
public abstract class GameCanvas {
- protected record Cell(float x, float y, float width, float height) {
- }
+ protected record Cell(float x, float y, float width, float height) {}
- protected final Canvas canvas;
- protected final GraphicsContext graphics;
+ protected final Canvas canvas;
+ protected final GraphicsContext graphics;
- protected final Color color;
+ protected final Color color;
- protected int width;
- protected int height;
+ protected int width;
+ protected int height;
- protected final int rows;
- protected final int columns;
+ protected final int rows;
+ protected final int columns;
- protected final int gapSize;
- protected final boolean edges;
+ protected final int gapSize;
+ protected final boolean edges;
- protected final Cell[] cells;
+ protected final Cell[] cells;
- protected GameCanvas(Color color, int width, int height, int rows, int columns, int gapSize, boolean edges, Consumer onCellClicked) {
- canvas = new Canvas(width, height);
- graphics = canvas.getGraphicsContext2D();
+ protected GameCanvas(
+ Color color,
+ int width,
+ int height,
+ int rows,
+ int columns,
+ int gapSize,
+ boolean edges,
+ Consumer onCellClicked) {
+ canvas = new Canvas(width, height);
+ graphics = canvas.getGraphicsContext2D();
- this.color = color;
+ this.color = color;
- this.width = width;
- this.height = height;
+ this.width = width;
+ this.height = height;
- this.rows = rows;
- this.columns = columns;
+ this.rows = rows;
+ this.columns = columns;
- this.gapSize = gapSize;
- this.edges = edges;
+ this.gapSize = gapSize;
+ this.edges = edges;
- cells = new Cell[rows * columns];
+ cells = new Cell[rows * columns];
- final float cellWidth = ((float) width - (rows - 1) * gapSize) / rows;
- final float cellHeight = ((float) height - (columns - 1) * gapSize) / columns;
+ final float cellWidth = ((float) width - (rows - 1) * gapSize) / rows;
+ final float cellHeight = ((float) height - (columns - 1) * gapSize) / columns;
- for (int y = 0; y < columns; y++) {
- final float startY = y * cellHeight + y * gapSize;
+ for (int y = 0; y < columns; y++) {
+ final float startY = y * cellHeight + y * gapSize;
- for (int x = 0; x < rows; x++) {
- final float startX = x * cellWidth + x * gapSize;
- cells[y * rows + x] = new Cell(startX, startY, cellWidth, cellHeight);
- }
- }
+ for (int x = 0; x < rows; x++) {
+ final float startX = x * cellWidth + x * gapSize;
+ cells[y * rows + x] = new Cell(startX, startY, cellWidth, cellHeight);
+ }
+ }
- canvas.setOnMouseClicked(event -> {
- if (event.getButton() != MouseButton.PRIMARY) {
- return;
- }
+ canvas.setOnMouseClicked(
+ event -> {
+ if (event.getButton() != MouseButton.PRIMARY) {
+ return;
+ }
- final int column = (int) ((event.getX() / width) * rows);
- final int row = (int) ((event.getY() / height) * columns);
+ final int column = (int) ((event.getX() / width) * rows);
+ final int row = (int) ((event.getY() / height) * columns);
- event.consume();
- onCellClicked.accept(row * rows + column);
- });
+ event.consume();
+ onCellClicked.accept(row * rows + column);
+ });
- render();
- }
+ render();
+ }
- public void clear() {
- graphics.clearRect(0, 0, width, height);
- }
+ public void clear() {
+ graphics.clearRect(0, 0, width, height);
+ }
- public void render() {
- graphics.setFill(color);
+ public void render() {
+ graphics.setFill(color);
- for (int x = 1; x < rows; x++) {
- graphics.fillRect(cells[x].x() - gapSize, 0, gapSize, height);
- }
+ for (int x = 1; x < rows; x++) {
+ graphics.fillRect(cells[x].x() - gapSize, 0, gapSize, height);
+ }
- for (int y = 1; y < columns; y++) {
- graphics.fillRect(0, cells[y * rows].y() - gapSize, width, gapSize);
- }
+ for (int y = 1; y < columns; y++) {
+ graphics.fillRect(0, cells[y * rows].y() - gapSize, width, gapSize);
+ }
- if (edges) {
- graphics.fillRect(-gapSize, 0, gapSize, height);
- graphics.fillRect(0, -gapSize, width, gapSize);
+ if (edges) {
+ graphics.fillRect(-gapSize, 0, gapSize, height);
+ graphics.fillRect(0, -gapSize, width, gapSize);
- graphics.fillRect(width - gapSize, 0, gapSize, height);
- graphics.fillRect(0, height - gapSize, width, gapSize);
- }
- }
+ graphics.fillRect(width - gapSize, 0, gapSize, height);
+ graphics.fillRect(0, height - gapSize, width, gapSize);
+ }
+ }
public void drawDot(Color color, int cell) {
final float x = cells[cell].x() + gapSize;
@@ -106,18 +113,29 @@ public abstract class GameCanvas {
graphics.fillOval(x, y, width, height);
}
- public void resize(int width, int height) {
- canvas.setWidth(width);
- canvas.setHeight(height);
+ public void draw(Color color, int cell) {
+ final float x = cells[cell].x() + gapSize;
+ final float y = cells[cell].y() + gapSize;
- this.width = width;
- this.height = height;
+ final float width = cells[cell].width() - gapSize * 2;
+ final float height = cells[cell].height() - gapSize * 2;
- clear();
- render();
- }
+ graphics.setFill(color);
+ graphics.fillRect(x, y, width, height);
+ }
- public Canvas getCanvas() {
- return canvas;
- }
-}
\ No newline at end of file
+ public void resize(int width, int height) {
+ canvas.setWidth(width);
+ canvas.setHeight(height);
+
+ this.width = width;
+ this.height = height;
+
+ clear();
+ render();
+ }
+
+ public Canvas getCanvas() {
+ return canvas;
+ }
+}
diff --git a/app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java b/app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java
index 0f7cbb9..1838335 100644
--- a/app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java
+++ b/app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java
@@ -1,38 +1,37 @@
package org.toop.app.canvas;
+import java.util.function.Consumer;
import javafx.scene.paint.Color;
-import java.util.function.Consumer;
-
public class TicTacToeCanvas extends GameCanvas {
- public TicTacToeCanvas(Color color, int width, int height, Consumer onCellClicked) {
- super(color, width, height, 3, 3, 10, false, onCellClicked);
- }
+ public TicTacToeCanvas(Color color, int width, int height, Consumer onCellClicked) {
+ super(color, width, height, 3, 3, 10, false, onCellClicked);
+ }
- public void drawX(Color color, int cell) {
- graphics.setStroke(color);
- graphics.setLineWidth(gapSize);
+ public void drawX(Color color, int cell) {
+ graphics.setStroke(color);
+ graphics.setLineWidth(gapSize);
- final float x = cells[cell].x() + gapSize;
- final float y = cells[cell].y() + gapSize;
+ final float x = cells[cell].x() + gapSize;
+ final float y = cells[cell].y() + gapSize;
- final float width = cells[cell].width() - gapSize * 2;
- final float height = cells[cell].height() - gapSize * 2;
+ final float width = cells[cell].width() - gapSize * 2;
+ final float height = cells[cell].height() - gapSize * 2;
- graphics.strokeLine(x, y, x + width, y + height);
- graphics.strokeLine(x + width, y, x, y + height);
- }
+ graphics.strokeLine(x, y, x + width, y + height);
+ graphics.strokeLine(x + width, y, x, y + height);
+ }
- public void drawO(Color color, int cell) {
- graphics.setStroke(color);
- graphics.setLineWidth(gapSize);
+ public void drawO(Color color, int cell) {
+ graphics.setStroke(color);
+ graphics.setLineWidth(gapSize);
- final float x = cells[cell].x() + gapSize;
- final float y = cells[cell].y() + gapSize;
+ final float x = cells[cell].x() + gapSize;
+ final float y = cells[cell].y() + gapSize;
- final float width = cells[cell].width() - gapSize * 2;
- final float height = cells[cell].height() - gapSize * 2;
+ final float width = cells[cell].width() - gapSize * 2;
+ final float height = cells[cell].height() - gapSize * 2;
- graphics.strokeOval(x, y, width, height);
- }
-}
\ No newline at end of file
+ graphics.strokeOval(x, y, width, height);
+ }
+}
diff --git a/app/src/main/java/org/toop/app/layer/Container.java b/app/src/main/java/org/toop/app/layer/Container.java
index 409c0eb..89e6436 100644
--- a/app/src/main/java/org/toop/app/layer/Container.java
+++ b/app/src/main/java/org/toop/app/layer/Container.java
@@ -4,8 +4,9 @@ import javafx.scene.Node;
import javafx.scene.layout.Region;
public abstract class Container {
- public abstract Region getContainer();
+ public abstract Region getContainer();
- public abstract void addNodes(Node... nodes);
- public abstract void addContainer(Container container, boolean fill);
-}
\ No newline at end of file
+ public abstract void addNodes(Node... nodes);
+
+ public abstract void addContainer(Container container, boolean fill);
+}
diff --git a/app/src/main/java/org/toop/app/layer/Layer.java b/app/src/main/java/org/toop/app/layer/Layer.java
index 35034c9..d357200 100644
--- a/app/src/main/java/org/toop/app/layer/Layer.java
+++ b/app/src/main/java/org/toop/app/layer/Layer.java
@@ -1,81 +1,86 @@
package org.toop.app.layer;
-import org.toop.app.App;
-import org.toop.app.canvas.GameCanvas;
-
import javafx.geometry.Pos;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
+import org.toop.app.App;
+import org.toop.app.canvas.GameCanvas;
public abstract class Layer {
- protected StackPane layer;
- protected Region background;
+ protected StackPane layer;
+ protected Region background;
- protected Layer(String... backgroundStyles) {
- layer = new StackPane();
+ protected Layer(String... backgroundStyles) {
+ layer = new StackPane();
- background = new Region();
- background.getStyleClass().addAll(backgroundStyles);
- background.setPrefSize(Double.MAX_VALUE, Double.MAX_VALUE);
+ background = new Region();
+ background.getStyleClass().addAll(backgroundStyles);
+ background.setPrefSize(Double.MAX_VALUE, Double.MAX_VALUE);
- layer.getChildren().addLast(background);
- }
+ layer.getChildren().addLast(background);
+ }
- protected void addContainer(Container container, Pos position, int xOffset, int yOffset, int widthPercent, int heightPercent) {
- StackPane.setAlignment(container.getContainer(), position);
+ protected void addContainer(
+ Container container,
+ Pos position,
+ int xOffset,
+ int yOffset,
+ int widthPercent,
+ int heightPercent) {
+ StackPane.setAlignment(container.getContainer(), position);
- final double widthUnit = App.getWidth() / 100.0;
- final double heightUnit = App.getHeight() / 100.0;
+ final double widthUnit = App.getWidth() / 100.0;
+ final double heightUnit = App.getHeight() / 100.0;
- if (widthPercent > 0) {
- container.getContainer().setMaxWidth(widthPercent * widthUnit);
- } else {
- container.getContainer().setMaxWidth(Region.USE_PREF_SIZE);
- }
+ if (widthPercent > 0) {
+ container.getContainer().setMaxWidth(widthPercent * widthUnit);
+ } else {
+ container.getContainer().setMaxWidth(Region.USE_PREF_SIZE);
+ }
- if (heightPercent > 0) {
- container.getContainer().setMaxHeight(heightPercent * heightUnit);
- } else {
- container.getContainer().setMaxHeight(Region.USE_PREF_SIZE);
- }
+ if (heightPercent > 0) {
+ container.getContainer().setMaxHeight(heightPercent * heightUnit);
+ } else {
+ container.getContainer().setMaxHeight(Region.USE_PREF_SIZE);
+ }
- container.getContainer().setTranslateX(xOffset * widthUnit);
- container.getContainer().setTranslateY(yOffset * heightUnit);
+ container.getContainer().setTranslateX(xOffset * widthUnit);
+ container.getContainer().setTranslateY(yOffset * heightUnit);
- layer.getChildren().addLast(container.getContainer());
- }
+ layer.getChildren().addLast(container.getContainer());
+ }
- protected void addGameCanvas(GameCanvas canvas, Pos position, int xOffset, int yOffset) {
- StackPane.setAlignment(canvas.getCanvas(), position);
+ protected void addGameCanvas(GameCanvas canvas, Pos position, int xOffset, int yOffset) {
+ StackPane.setAlignment(canvas.getCanvas(), position);
- final double widthUnit = App.getWidth() / 100.0;
- final double heightUnit = App.getHeight() / 100.0;
+ final double widthUnit = App.getWidth() / 100.0;
+ final double heightUnit = App.getHeight() / 100.0;
- canvas.getCanvas().setTranslateX(xOffset * widthUnit);
- canvas.getCanvas().setTranslateY(yOffset * heightUnit);
+ canvas.getCanvas().setTranslateX(xOffset * widthUnit);
+ canvas.getCanvas().setTranslateY(yOffset * heightUnit);
- layer.getChildren().addLast(canvas.getCanvas());
- }
+ layer.getChildren().addLast(canvas.getCanvas());
+ }
- protected void pop() {
- if (layer.getChildren().size() <= 1) {
- return;
- }
+ protected void pop() {
+ if (layer.getChildren().size() <= 1) {
+ return;
+ }
- layer.getChildren().removeLast();
- }
+ layer.getChildren().removeLast();
+ }
- protected void popAll() {
- final int containers = layer.getChildren().size();
+ protected void popAll() {
+ final int containers = layer.getChildren().size();
- for (int i = 1; i < containers; i++) {
- layer.getChildren().removeLast();
- }
- }
+ for (int i = 1; i < containers; i++) {
+ layer.getChildren().removeLast();
+ }
+ }
- public StackPane getLayer() {
- return layer;
- }
+ public StackPane getLayer() {
+ return layer;
+ }
- public abstract void reload();
-}
\ No newline at end of file
+ public abstract void reload();
+}
diff --git a/app/src/main/java/org/toop/app/layer/NodeBuilder.java b/app/src/main/java/org/toop/app/layer/NodeBuilder.java
index a0f2996..b55a70d 100644
--- a/app/src/main/java/org/toop/app/layer/NodeBuilder.java
+++ b/app/src/main/java/org/toop/app/layer/NodeBuilder.java
@@ -1,131 +1,140 @@
package org.toop.app.layer;
-import org.toop.framework.audio.events.AudioEvents;
-import org.toop.framework.eventbus.EventFlow;
-
+import java.util.function.Consumer;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Orientation;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.text.Text;
-
-import java.util.function.Consumer;
+import org.toop.framework.audio.events.AudioEvents;
+import org.toop.framework.eventbus.EventFlow;
public final class NodeBuilder {
- public static void addCss(Node node, String... cssClasses) {
- node.getStyleClass().addAll(cssClasses);
- }
+ public static void addCss(Node node, String... cssClasses) {
+ node.getStyleClass().addAll(cssClasses);
+ }
- public static void setCss(Node node, String... cssClasses) {
- node.getStyleClass().removeAll();
- node.getStyleClass().addAll(cssClasses);
- }
+ public static void setCss(Node node, String... cssClasses) {
+ node.getStyleClass().removeAll();
+ node.getStyleClass().addAll(cssClasses);
+ }
- public static Text header(String x) {
- final Text element = new Text(x);
- setCss(element, "text-primary", "text-header");
+ public static Text header(String x) {
+ final Text element = new Text(x);
+ setCss(element, "text-primary", "text-header");
- return element;
- }
+ return element;
+ }
- public static Text text(String x) {
- final Text element = new Text(x);
- setCss(element, "text-secondary", "text-normal");
+ public static Text text(String x) {
+ final Text element = new Text(x);
+ setCss(element, "text-secondary", "text-normal");
- return element;
- }
+ return element;
+ }
- public static Label button(String x, Runnable runnable) {
- final Label element = new Label(x);
- setCss(element, "button", "text-normal");
+ public static Label button(String x, Runnable runnable) {
+ final Label element = new Label(x);
+ setCss(element, "button", "text-normal");
- element.setOnMouseClicked(_ -> {
- new EventFlow().addPostEvent(new AudioEvents.ClickButton()).asyncPostEvent();
- runnable.run();
- });
+ element.setOnMouseClicked(
+ _ -> {
+ new EventFlow().addPostEvent(new AudioEvents.ClickButton()).asyncPostEvent();
+ runnable.run();
+ });
- return element;
- }
+ return element;
+ }
- public static Label toggle(String x1, String x2, boolean toggled, Consumer consumer) {
- final Label element = new Label(toggled ? x2 : x1);
- setCss(element, "toggle", "text-normal");
+ public static Label toggle(String x1, String x2, boolean toggled, Consumer consumer) {
+ final Label element = new Label(toggled ? x2 : x1);
+ setCss(element, "toggle", "text-normal");
- final BooleanProperty checked = new SimpleBooleanProperty(toggled);
+ final BooleanProperty checked = new SimpleBooleanProperty(toggled);
- element.setOnMouseClicked(_ -> {
- new EventFlow().addPostEvent(new AudioEvents.ClickButton()).asyncPostEvent();
- checked.set(!checked.get());
+ element.setOnMouseClicked(
+ _ -> {
+ new EventFlow().addPostEvent(new AudioEvents.ClickButton()).asyncPostEvent();
+ checked.set(!checked.get());
- if (checked.get()) {
- element.setText(x1);
- } else {
- element.setText(x2);
- }
+ if (checked.get()) {
+ element.setText(x1);
+ } else {
+ element.setText(x2);
+ }
- consumer.accept(checked.get());
- });
+ consumer.accept(checked.get());
+ });
- return element;
- }
+ return element;
+ }
- public static Slider slider(int max, int initial, Consumer consumer) {
- final Slider element = new Slider(0, max, initial);
- setCss(element, "bg-slider-track");
+ public static Slider slider(int max, int initial, Consumer consumer) {
+ final Slider element = new Slider(0, max, initial);
+ setCss(element, "bg-slider-track");
- element.setMinorTickCount(0);
- element.setMajorTickUnit(1);
- element.setBlockIncrement(1);
+ element.setMinorTickCount(0);
+ element.setMajorTickUnit(1);
+ element.setBlockIncrement(1);
- element.setSnapToTicks(true);
- element.setShowTickLabels(true);
+ element.setSnapToTicks(true);
+ element.setShowTickLabels(true);
- element.setOnMouseClicked(_ -> {
- new EventFlow().addPostEvent(new AudioEvents.ClickButton()).asyncPostEvent();
- });
+ element.setOnMouseClicked(
+ _ -> {
+ new EventFlow().addPostEvent(new AudioEvents.ClickButton()).asyncPostEvent();
+ });
- element.valueProperty().addListener((_, _, newValue) -> {
- consumer.accept(newValue.intValue());
- });
+ element.valueProperty()
+ .addListener(
+ (_, _, newValue) -> {
+ consumer.accept(newValue.intValue());
+ });
- return element;
- }
+ return element;
+ }
- public static TextField input(String x, Consumer consumer) {
- final TextField element = new TextField(x);
- setCss(element, "input", "text-normal");
+ public static TextField input(String x, Consumer consumer) {
+ final TextField element = new TextField(x);
+ setCss(element, "input", "text-normal");
- element.setOnMouseClicked(_ -> {
- new EventFlow().addPostEvent(new AudioEvents.ClickButton()).asyncPostEvent();
- });
+ element.setOnMouseClicked(
+ _ -> {
+ new EventFlow().addPostEvent(new AudioEvents.ClickButton()).asyncPostEvent();
+ });
- element.textProperty().addListener((_, _, newValue) -> {
- consumer.accept(newValue);
- });
+ element.textProperty()
+ .addListener(
+ (_, _, newValue) -> {
+ consumer.accept(newValue);
+ });
- return element;
- }
+ return element;
+ }
- public static ChoiceBox choiceBox(Consumer consumer) {
- final ChoiceBox element = new ChoiceBox<>();
- setCss(element, "choice-box", "text-normal");
+ public static ChoiceBox choiceBox(Consumer consumer) {
+ final ChoiceBox element = new ChoiceBox<>();
+ setCss(element, "choice-box", "text-normal");
- element.setOnMouseClicked(_ -> {
- new EventFlow().addPostEvent(new AudioEvents.ClickButton()).asyncPostEvent();
- });
+ element.setOnMouseClicked(
+ _ -> {
+ new EventFlow().addPostEvent(new AudioEvents.ClickButton()).asyncPostEvent();
+ });
- element.valueProperty().addListener((_, _, newValue) -> {
- consumer.accept(newValue);
- });
+ element.valueProperty()
+ .addListener(
+ (_, _, newValue) -> {
+ consumer.accept(newValue);
+ });
- return element;
- }
+ return element;
+ }
- public static Separator separator() {
- final Separator element = new Separator(Orientation.HORIZONTAL);
- setCss(element, "separator");
+ public static Separator separator() {
+ final Separator element = new Separator(Orientation.HORIZONTAL);
+ setCss(element, "separator");
- return element;
- }
-}
\ No newline at end of file
+ return element;
+ }
+}
diff --git a/app/src/main/java/org/toop/app/layer/Popup.java b/app/src/main/java/org/toop/app/layer/Popup.java
index 6a54bec..7e498df 100644
--- a/app/src/main/java/org/toop/app/layer/Popup.java
+++ b/app/src/main/java/org/toop/app/layer/Popup.java
@@ -3,17 +3,18 @@ package org.toop.app.layer;
import org.toop.app.App;
public abstract class Popup extends Layer {
- protected Popup(boolean popOnBackground, String... backgroundStyles) {
- super(backgroundStyles);
+ protected Popup(boolean popOnBackground, String... backgroundStyles) {
+ super(backgroundStyles);
- if (popOnBackground) {
- background.setOnMouseClicked(_ -> {
- App.pop();
- });
- }
- }
+ if (popOnBackground) {
+ background.setOnMouseClicked(
+ _ -> {
+ App.pop();
+ });
+ }
+ }
- protected Popup(boolean popOnBackground) {
- this(popOnBackground, "bg-popup");
- }
-}
\ No newline at end of file
+ protected Popup(boolean popOnBackground) {
+ this(popOnBackground, "bg-popup");
+ }
+}
diff --git a/app/src/main/java/org/toop/app/layer/containers/HorizontalContainer.java b/app/src/main/java/org/toop/app/layer/containers/HorizontalContainer.java
index b3f00a5..2350216 100644
--- a/app/src/main/java/org/toop/app/layer/containers/HorizontalContainer.java
+++ b/app/src/main/java/org/toop/app/layer/containers/HorizontalContainer.java
@@ -1,60 +1,59 @@
package org.toop.app.layer.containers;
-import org.toop.app.layer.Container;
-
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
+import org.toop.app.layer.Container;
public final class HorizontalContainer extends Container {
- private final HBox container;
+ private final HBox container;
- public HorizontalContainer(int spacing, String... cssClasses) {
- container = new HBox(spacing);
- container.getStyleClass().addAll(cssClasses);
- }
+ public HorizontalContainer(int spacing, String... cssClasses) {
+ container = new HBox(spacing);
+ container.getStyleClass().addAll(cssClasses);
+ }
- public HorizontalContainer(int spacing) {
- this(spacing, "container");
- }
+ public HorizontalContainer(int spacing) {
+ this(spacing, "container");
+ }
- @Override
- public Region getContainer() {
- return container;
- }
+ @Override
+ public Region getContainer() {
+ return container;
+ }
- @Override
- public void addNodes(Node... nodes) {
- container.getChildren().addAll(nodes);
- }
+ @Override
+ public void addNodes(Node... nodes) {
+ container.getChildren().addAll(nodes);
+ }
- @Override
- public void addContainer(Container container, boolean fill) {
- if (fill) {
- container.getContainer().setMinSize(0, 0);
- container.getContainer().setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
- HBox.setHgrow(container.getContainer(), Priority.ALWAYS);
- } else {
- container.getContainer().setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
- }
+ @Override
+ public void addContainer(Container container, boolean fill) {
+ if (fill) {
+ container.getContainer().setMinSize(0, 0);
+ container.getContainer().setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
+ HBox.setHgrow(container.getContainer(), Priority.ALWAYS);
+ } else {
+ container.getContainer().setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
+ }
- this.container.getChildren().add(container.getContainer());
+ this.container.getChildren().add(container.getContainer());
- if (fill) {
- balanceChildWidths();
- }
- }
+ if (fill) {
+ balanceChildWidths();
+ }
+ }
- private void balanceChildWidths() {
- final ObservableList children = container.getChildren();
- final double widthPerChild = container.getWidth() / children.size();
+ private void balanceChildWidths() {
+ final ObservableList children = container.getChildren();
+ final double widthPerChild = container.getWidth() / children.size();
- for (final Node child : children) {
- if (child instanceof Region) {
- ((Region) child).setPrefWidth(widthPerChild);
- }
- }
- }
-}
\ No newline at end of file
+ for (final Node child : children) {
+ if (child instanceof Region) {
+ ((Region) child).setPrefWidth(widthPerChild);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/toop/app/layer/containers/VerticalContainer.java b/app/src/main/java/org/toop/app/layer/containers/VerticalContainer.java
index a8fb74d..56d610c 100644
--- a/app/src/main/java/org/toop/app/layer/containers/VerticalContainer.java
+++ b/app/src/main/java/org/toop/app/layer/containers/VerticalContainer.java
@@ -1,60 +1,59 @@
package org.toop.app.layer.containers;
-import org.toop.app.layer.Container;
-
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
+import org.toop.app.layer.Container;
public final class VerticalContainer extends Container {
- private final VBox container;
+ private final VBox container;
- public VerticalContainer(int spacing, String... cssClasses) {
- container = new VBox(spacing);
- container.getStyleClass().addAll(cssClasses);
- }
+ public VerticalContainer(int spacing, String... cssClasses) {
+ container = new VBox(spacing);
+ container.getStyleClass().addAll(cssClasses);
+ }
- public VerticalContainer(int spacing) {
- this(spacing, "container");
- }
+ public VerticalContainer(int spacing) {
+ this(spacing, "container");
+ }
- @Override
- public Region getContainer() {
- return container;
- }
+ @Override
+ public Region getContainer() {
+ return container;
+ }
- @Override
- public void addNodes(Node... nodes) {
- container.getChildren().addAll(nodes);
- }
+ @Override
+ public void addNodes(Node... nodes) {
+ container.getChildren().addAll(nodes);
+ }
- @Override
- public void addContainer(Container container, boolean fill) {
- if (fill) {
- container.getContainer().setMinSize(0, 0);
- container.getContainer().setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
- VBox.setVgrow(container.getContainer(), Priority.ALWAYS);
- } else {
- container.getContainer().setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
- }
+ @Override
+ public void addContainer(Container container, boolean fill) {
+ if (fill) {
+ container.getContainer().setMinSize(0, 0);
+ container.getContainer().setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
+ VBox.setVgrow(container.getContainer(), Priority.ALWAYS);
+ } else {
+ container.getContainer().setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
+ }
- this.container.getChildren().add(container.getContainer());
+ this.container.getChildren().add(container.getContainer());
- if (fill) {
- balanceChildHeights();
- }
- }
+ if (fill) {
+ balanceChildHeights();
+ }
+ }
- private void balanceChildHeights() {
- final ObservableList children = container.getChildren();
- final double heightPerChild = container.getHeight() / children.size();
+ private void balanceChildHeights() {
+ final ObservableList children = container.getChildren();
+ final double heightPerChild = container.getHeight() / children.size();
- for (final Node child : children) {
- if (child instanceof Region) {
- ((Region) child).setPrefHeight(heightPerChild);
- }
- }
- }
-}
\ No newline at end of file
+ for (final Node child : children) {
+ if (child instanceof Region) {
+ ((Region) child).setPrefHeight(heightPerChild);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/toop/app/layer/layers/ConnectedLayer.java b/app/src/main/java/org/toop/app/layer/layers/ConnectedLayer.java
index 8873f4c..b255c3d 100644
--- a/app/src/main/java/org/toop/app/layer/layers/ConnectedLayer.java
+++ b/app/src/main/java/org/toop/app/layer/layers/ConnectedLayer.java
@@ -1,6 +1,14 @@
package org.toop.app.layer.layers;
+import java.util.List;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicInteger;
import javafx.application.Platform;
+import javafx.geometry.Pos;
+import javafx.scene.control.Label;
+import javafx.scene.control.ListView;
import org.toop.app.App;
import org.toop.app.GameInformation;
import org.toop.app.layer.Container;
@@ -12,171 +20,216 @@ import org.toop.app.layer.containers.VerticalContainer;
import org.toop.app.layer.layers.game.TicTacToeLayer;
import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.networking.events.NetworkEvents;
-
-import javafx.geometry.Pos;
-import javafx.scene.control.Label;
-import javafx.scene.control.ListView;
import org.toop.local.AppContext;
-import java.util.List;
-import java.util.Timer;
-import java.util.TimerTask;
-import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.concurrent.atomic.AtomicInteger;
-
public final class ConnectedLayer extends Layer {
- private static Timer pollTimer = new Timer();
+ private static Timer pollTimer = new Timer();
- private static class ChallengePopup extends Popup {
- private final GameInformation information;
+ private static class ChallengePopup extends Popup {
+ private final GameInformation information;
- private final String challenger;
- private final String game;
+ private final String challenger;
+ private final String game;
- private final long clientID;
- private final int challengeID;
+ private final long clientID;
+ private final int challengeID;
- public ChallengePopup(GameInformation information, String challenger, String game, long clientID, String challengeID) {
- super(false, "bg-popup");
+ public ChallengePopup(
+ GameInformation information,
+ String challenger,
+ String game,
+ long clientID,
+ String challengeID) {
+ super(false, "bg-popup");
- this.information = information;
+ this.information = information;
- this.challenger = challenger;
- this.game = game;
+ this.challenger = challenger;
+ this.game = game;
- this.clientID = clientID;
- this.challengeID = Integer.parseInt(challengeID.substring(18, challengeID.length() - 2));
+ this.clientID = clientID;
+ this.challengeID =
+ Integer.parseInt(challengeID.substring(18, challengeID.length() - 2));
- reload();
- }
+ reload();
+ }
- @Override
- public void reload() {
- popAll();
+ @Override
+ public void reload() {
+ popAll();
- final var challengeText = NodeBuilder.header(AppContext.getString("challengeText"));
- final var challengerNameText = NodeBuilder.header(challenger);
+ final var challengeText = NodeBuilder.header(AppContext.getString("challengeText"));
+ final var challengerNameText = NodeBuilder.header(challenger);
- final var gameText = NodeBuilder.text(AppContext.getString("gameIsText"));
- final var gameNameText = NodeBuilder.text(game);
+ final var gameText = NodeBuilder.text(AppContext.getString("gameIsText"));
+ final var gameNameText = NodeBuilder.text(game);
- final var acceptButton = NodeBuilder.button(AppContext.getString("accept"), () -> {
- pollTimer.cancel();
+ final var acceptButton =
+ NodeBuilder.button(
+ AppContext.getString("accept"),
+ () -> {
+ pollTimer.cancel();
- new EventFlow().addPostEvent(new NetworkEvents.SendAcceptChallenge(clientID, challengeID)).postEvent();
- App.activate(new TicTacToeLayer(information, clientID));
- });
+ new EventFlow()
+ .addPostEvent(
+ new NetworkEvents.SendAcceptChallenge(
+ clientID, challengeID))
+ .postEvent();
+ App.activate(new TicTacToeLayer(information, clientID));
+ });
- final var denyButton = NodeBuilder.button(AppContext.getString("deny"), () -> {
- App.pop();
- });
+ final var denyButton =
+ NodeBuilder.button(
+ AppContext.getString("deny"),
+ () -> {
+ App.pop();
+ });
- final Container controlContainer = new HorizontalContainer(30);
- controlContainer.addNodes(acceptButton, denyButton);
+ final Container controlContainer = new HorizontalContainer(30);
+ controlContainer.addNodes(acceptButton, denyButton);
- final Container mainContainer = new VerticalContainer(30);
- mainContainer.addNodes(challengeText, challengerNameText);
- mainContainer.addNodes(gameText, gameNameText);
+ final Container mainContainer = new VerticalContainer(30);
+ mainContainer.addNodes(challengeText, challengerNameText);
+ mainContainer.addNodes(gameText, gameNameText);
- mainContainer.addContainer(controlContainer, false);
+ mainContainer.addContainer(controlContainer, false);
- addContainer(mainContainer, Pos.CENTER, 0, 0, 30, 30);
- }
- }
+ addContainer(mainContainer, Pos.CENTER, 0, 0, 30, 30);
+ }
+ }
- GameInformation information;
- long clientId;
- String user;
- List onlinePlayers = new CopyOnWriteArrayList<>();
+ GameInformation information;
+ long clientId;
+ String user;
+ List onlinePlayers = new CopyOnWriteArrayList<>();
- public ConnectedLayer(GameInformation information) {
- super("bg-primary");
+ public ConnectedLayer(GameInformation information) {
+ super("bg-primary");
- this.information = information;
+ this.information = information;
- new EventFlow()
- .addPostEvent(NetworkEvents.StartClient.class, information.serverIP(), Integer.parseInt(information.serverPort()))
- .onResponse(NetworkEvents.StartClientResponse.class, e -> {
- clientId = e.clientId();
- user = information.playerName()[0].replaceAll("\\s+", "");
+ new EventFlow()
+ .addPostEvent(
+ NetworkEvents.StartClient.class,
+ information.serverIP(),
+ Integer.parseInt(information.serverPort()))
+ .onResponse(
+ NetworkEvents.StartClientResponse.class,
+ e -> {
+ clientId = e.clientId();
+ user = information.playerName()[0].replaceAll("\\s+", "");
- new EventFlow().addPostEvent(new NetworkEvents.SendLogin(this.clientId, this.user)).postEvent();
+ new EventFlow()
+ .addPostEvent(
+ new NetworkEvents.SendLogin(this.clientId, this.user))
+ .postEvent();
- Thread popThread = new Thread(this::populatePlayerList);
- popThread.setDaemon(false);
- popThread.start();
- }).postEvent();
+ Thread popThread = new Thread(this::populatePlayerList);
+ popThread.setDaemon(false);
+ popThread.start();
+ })
+ .postEvent();
- new EventFlow().listen(this::handleReceivedChallenge);
+ new EventFlow().listen(this::handleReceivedChallenge);
reload();
- }
+ }
- private void populatePlayerList() {
- EventFlow sendGetPlayerList = new EventFlow().addPostEvent(new NetworkEvents.SendGetPlayerlist(this.clientId));
- new EventFlow().listen(NetworkEvents.PlayerlistResponse.class, e -> {
- if (e.clientId() == this.clientId) {
- List playerList = new java.util.ArrayList<>(List.of(e.playerlist())); // TODO: Garbage, but works
- playerList.removeIf(name -> name.equalsIgnoreCase(user));
- if (this.onlinePlayers != playerList) {
- this.onlinePlayers.clear();
- this.onlinePlayers.addAll(playerList);
- }
- }
- });
+ private void populatePlayerList() {
+ EventFlow sendGetPlayerList =
+ new EventFlow().addPostEvent(new NetworkEvents.SendGetPlayerlist(this.clientId));
+ new EventFlow()
+ .listen(
+ NetworkEvents.PlayerlistResponse.class,
+ e -> {
+ if (e.clientId() == this.clientId) {
+ List playerList =
+ new java.util.ArrayList<>(
+ List.of(e.playerlist())); // TODO: Garbage,
+ // but works
+ playerList.removeIf(name -> name.equalsIgnoreCase(user));
+ if (this.onlinePlayers != playerList) {
+ this.onlinePlayers.clear();
+ this.onlinePlayers.addAll(playerList);
+ }
+ }
+ });
- TimerTask task = new TimerTask() {
- public void run() {
- sendGetPlayerList.postEvent();
- Platform.runLater(() -> reload());
- }
- };
+ TimerTask task =
+ new TimerTask() {
+ public void run() {
+ sendGetPlayerList.postEvent();
+ Platform.runLater(() -> reload());
+ }
+ };
- pollTimer.schedule(task, 0L, 5000L); // TODO: Block app exit, fix later
- }
+ pollTimer.schedule(task, 0L, 5000L); // TODO: Block app exit, fix later
+ }
- private void sendChallenge(String oppUsername, String gameType) {
- final AtomicInteger challengeId = new AtomicInteger(-1);
+ private void sendChallenge(String oppUsername, String gameType) {
+ final AtomicInteger challengeId = new AtomicInteger(-1);
- if (onlinePlayers.contains(oppUsername)) {
- new EventFlow().addPostEvent(new NetworkEvents.SendChallenge(this.clientId, oppUsername, gameType))
- .listen(NetworkEvents.ChallengeResponse.class, e -> {
- challengeId.set(Integer.parseInt(e.challengeId().substring(18, e.challengeId().length() - 2)));
- })
- .listen(NetworkEvents.GameMatchResponse.class, e -> {
- if (e.clientId() == this.clientId) {
- pollTimer.cancel();
- App.activate(new TicTacToeLayer(information, this.clientId));
- }
- }, false).postEvent();
- // ^
- // |
- // |
- // |
- }
- }
+ if (onlinePlayers.contains(oppUsername)) {
+ new EventFlow()
+ .addPostEvent(
+ new NetworkEvents.SendChallenge(this.clientId, oppUsername, gameType))
+ .listen(
+ NetworkEvents.ChallengeResponse.class,
+ e -> {
+ challengeId.set(
+ Integer.parseInt(
+ e.challengeId()
+ .substring(
+ 18, e.challengeId().length() - 2)));
+ })
+ .listen(
+ NetworkEvents.GameMatchResponse.class,
+ e -> {
+ if (e.clientId() == this.clientId) {
+ pollTimer.cancel();
+ App.activate(new TicTacToeLayer(information, this.clientId));
+ }
+ },
+ false)
+ .postEvent();
+ // ^
+ // |
+ // |
+ // |
+ }
+ }
- private void handleReceivedChallenge(NetworkEvents.ChallengeResponse response) {
- App.push(new ChallengePopup(information, response.challengerName(), response.gameType(), clientId, response.challengeId()));
- }
+ private void handleReceivedChallenge(NetworkEvents.ChallengeResponse response) {
+ App.push(
+ new ChallengePopup(
+ information,
+ response.challengerName(),
+ response.gameType(),
+ clientId,
+ response.challengeId()));
+ }
- @Override
- public void reload() {
- popAll();
+ @Override
+ public void reload() {
+ popAll();
- ListView
- *
- *
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).
- */
+ /** Custom epoch in milliseconds (2025-01-01T00:00:00Z). */
private static final long EPOCH = Instant.parse("2025-01-01T00:00:00Z").toEpochMilli();
// Bit allocations
@@ -44,27 +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();
@@ -80,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);
@@ -130,30 +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 91a296e..0000000
--- a/framework/src/main/java/org/toop/framework/asset/events/AssetLoaderEvents.java
+++ /dev/null
@@ -1,7 +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 fc6eec7..0000000
--- a/framework/src/main/java/org/toop/framework/asset/resources/MusicAsset.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package org.toop.framework.asset.resources;
-
-import javafx.scene.media.Media;
-import org.toop.framework.asset.types.FileExtension;
-import org.toop.framework.asset.types.LoadableResource;
-
-import java.io.*;
-
-@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;
- }
-}
\ No newline at end of file
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 d207077..0000000
--- a/framework/src/main/java/org/toop/framework/asset/resources/SoundEffectAsset.java
+++ /dev/null
@@ -1,76 +0,0 @@
-package org.toop.framework.asset.resources;
-
-import org.toop.framework.asset.types.FileExtension;
-import org.toop.framework.asset.types.LoadableResource;
-
-import javax.sound.sampled.*;
-import java.io.*;
-import java.nio.file.Files;
-
-@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/asset/types/BundledResource.java b/framework/src/main/java/org/toop/framework/asset/types/BundledResource.java
deleted file mode 100644
index ceb0f5f..0000000
--- a/framework/src/main/java/org/toop/framework/asset/types/BundledResource.java
+++ /dev/null
@@ -1,74 +0,0 @@
-package org.toop.framework.asset.types;
-
-import org.toop.framework.asset.ResourceLoader;
-
-import java.io.File;
-
-/**
- * Represents a resource that can be composed of multiple files, or "bundled" together
- * under a common base name.
- *
- *
Implementing classes allow an {@link ResourceLoader}
- * to automatically merge multiple related files into a single resource instance.
- *
- *
Typical use cases include:
- *
- *
Localization assets, where multiple `.properties` files (e.g., `messages_en.properties`,
- * `messages_nl.properties`) are grouped under the same logical resource.
- *
Sprite sheets, tile sets, or other multi-file resources that logically belong together.
- *
- *
- *
Implementing classes must provide:
- *
- *
{@link #loadFile(File)}: Logic to load or merge an individual file into the resource.
- *
{@link #getBaseName()}: A consistent base name used to group multiple files into this resource.
When used with an asset loader, all files sharing the same base name are
- * automatically merged into a single resource instance.
- */
-public interface BundledResource {
-
- /**
- * Load or merge an additional file into this resource.
- *
- * @param file the file to load or merge
- */
- void loadFile(File file);
-
- /**
- * Return a base name for grouping multiple files into this single resource.
- * Files with the same base name are automatically merged by the loader.
- *
- * @return the base name used to identify this bundled resource
- */
- String getBaseName();
-
-// /**
-// Returns the name
-// */
-// String getDefaultName();
-}
\ No newline at end of file
diff --git a/framework/src/main/java/org/toop/framework/asset/types/FileExtension.java b/framework/src/main/java/org/toop/framework/asset/types/FileExtension.java
deleted file mode 100644
index b3c42d5..0000000
--- a/framework/src/main/java/org/toop/framework/asset/types/FileExtension.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package org.toop.framework.asset.types;
-
-import org.toop.framework.asset.ResourceLoader;
-import org.toop.framework.asset.resources.BaseResource;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-import java.lang.annotation.ElementType;
-
-/**
- * Annotation to declare which file extensions a {@link BaseResource} subclass
- * can handle.
- *
- *
This annotation is processed by the {@link ResourceLoader}
- * to automatically register resource types for specific file extensions.
- * Each extension listed will be mapped to the annotated resource class,
- * allowing the loader to instantiate the correct type when scanning files.
The annotation is retained at runtime for reflection-based registration.
- *
Can only be applied to types (classes) that extend {@link BaseResource}.
- *
Multiple extensions can be specified in the {@code value()} array.
- *
- */
-@Retention(RetentionPolicy.RUNTIME)
-@Target(ElementType.TYPE)
-public @interface FileExtension {
- /**
- * The list of file extensions (without leading dot) that the annotated resource class can handle.
- *
- * @return array of file extensions
- */
- String[] value();
-}
diff --git a/framework/src/main/java/org/toop/framework/asset/types/LoadableResource.java b/framework/src/main/java/org/toop/framework/asset/types/LoadableResource.java
deleted file mode 100644
index d25ba9e..0000000
--- a/framework/src/main/java/org/toop/framework/asset/types/LoadableResource.java
+++ /dev/null
@@ -1,66 +0,0 @@
-package org.toop.framework.asset.types;
-
-import org.toop.framework.asset.ResourceLoader;
-
-/**
- * Represents a resource that can be explicitly loaded and unloaded.
- *
- * Any class implementing {@code LoadableResource} is responsible for managing its own
- * loading and unloading logic, such as reading files, initializing data structures,
- * or allocating external resources.
- *
- *
- *
Implementing classes must define the following behaviors:
- *
- *
{@link #load()}: Load the resource into memory or perform necessary initialization.
- *
{@link #unload()}: Release any held resources or memory when the resource is no longer needed.
- *
{@link #isLoaded()}: Return {@code true} if the resource has been successfully loaded and is ready for use, {@code false} otherwise.
This interface is commonly used with {@link PreloadResource} to allow automatic
- * loading by an {@link ResourceLoader} if desired.
- */
-public interface LoadableResource {
- /**
- * Load the resource into memory or initialize it.
- * This method may throw runtime exceptions if loading fails.
- */
- void load();
-
- /**
- * Unload the resource and free any associated resources.
- * After this call, {@link #isLoaded()} should return false.
- */
- void unload();
-
- /**
- * Check whether the resource has been successfully loaded.
- *
- * @return true if the resource is loaded and ready for use, false otherwise
- */
- boolean 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..6245249
--- /dev/null
+++ b/framework/src/main/java/org/toop/framework/audio/AudioEventListener.java
@@ -0,0 +1,68 @@
+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.resource.types.AudioResource;
+
+public class AudioEventListener {
+ private final MusicManager musicManager;
+ private final SoundEffectManager soundEffectManager;
+ private final VolumeManager audioVolumeManager;
+
+ public AudioEventListener(
+ MusicManager musicManager,
+ SoundEffectManager soundEffectManager,
+ VolumeManager audioVolumeManager
+ ) {
+ this.musicManager = musicManager;
+ this.soundEffectManager = soundEffectManager;
+ this.audioVolumeManager = audioVolumeManager;
+ }
+
+ public AudioEventListener, ?> initListeners(String buttonSoundToPlay) {
+ new EventFlow()
+ .listen(this::handleStopMusicManager)
+ .listen(this::handlePlaySound)
+ .listen(this::handleStopSound)
+ .listen(this::handleMusicStart)
+ .listen(this::handleVolumeChange)
+ .listen(this::handleGetVolume)
+ .listen(AudioEvents.ClickButton.class, _ ->
+ soundEffectManager.play(buttonSoundToPlay, 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 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) {
+ new EventFlow()
+ .addPostEvent(
+ new AudioEvents.GetVolumeResponse(
+ audioVolumeManager.getVolume(event.controlType()),
+ event.identifier()))
+ .asyncPostEvent();
+ }
+
+}
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 7d256cc..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,91 +1,84 @@
package org.toop.framework.audio;
-import com.sun.scenario.Settings;
-import javafx.scene.media.MediaPlayer;
-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;
-import javax.sound.sampled.Clip;
-import javax.sound.sampled.FloatControl;
-
-public class AudioVolumeManager {
- private final SoundManager sM;
-
- 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);
+/**
+ * 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 {
+ /**
+ * 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..ea20e8e
--- /dev/null
+++ b/framework/src/main/java/org/toop/framework/audio/MusicManager.java
@@ -0,0 +1,125 @@
+package org.toop.framework.audio;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.toop.framework.dispatch.interfaces.Dispatcher;
+import org.toop.framework.dispatch.JavaFXDispatcher;
+import org.toop.annotations.TestsOnly;
+import org.toop.framework.resource.types.AudioResource;
+
+import java.util.*;
+
+public class MusicManager implements org.toop.framework.audio.interfaces.MusicManager {
+ private static final Logger logger = LogManager.getLogger(MusicManager.class);
+
+ private final List backgroundMusic = new ArrayList<>();
+ private final Dispatcher dispatcher;
+ private final List resources;
+ private int playingIndex = 0;
+ private boolean playing = false;
+
+ public MusicManager(List resources) {
+ this.dispatcher = new JavaFXDispatcher();
+ this.resources = resources;
+ createShuffled();
+ }
+
+ /**
+ * {@code @TestsOnly} DO NOT USE
+ */
+ @TestsOnly
+ public MusicManager(List resources, Dispatcher dispatcher) {
+ 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();
+ }
+
+ // 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) {
+ track.setOnEnd(() -> {
+ playingIndex++;
+ playCurrentTrack();
+ });
+
+ track.setOnError(() -> {
+ logger.error("Error playing track: {}", track);
+ backgroundMusic.remove(track);
+
+ if (!backgroundMusic.isEmpty()) {
+ playCurrentTrack();
+ } else {
+ playing = false;
+ }
+ });
+ }
+
+ @Override
+ public void stop() {
+ if (!playing) return;
+
+ playing = false;
+ dispatcher.run(() -> backgroundMusic.forEach(T::stop));
+ }
+}
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..e357aee
--- /dev/null
+++ b/framework/src/main/java/org/toop/framework/audio/SoundEffectManager.java
@@ -0,0 +1,65 @@
+package org.toop.framework.audio;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.toop.framework.resource.ResourceManager;
+import org.toop.framework.resource.ResourceMeta;
+import org.toop.framework.resource.resources.BaseResource;
+import org.toop.framework.resource.resources.MusicAsset;
+import org.toop.framework.resource.resources.SoundEffectAsset;
+import org.toop.framework.resource.types.AudioResource;
+
+import javax.sound.sampled.Clip;
+import javax.sound.sampled.LineEvent;
+import javax.sound.sampled.LineUnavailableException;
+import javax.sound.sampled.UnsupportedAudioFileException;
+import java.io.IOException;
+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 51c31ad..0000000
--- a/framework/src/main/java/org/toop/framework/audio/SoundManager.java
+++ /dev/null
@@ -1,185 +0,0 @@
-package org.toop.framework.audio;
-
-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;
-
-import javafx.scene.media.MediaPlayer;
-
-import java.io.*;
-import java.util.*;
-import javax.sound.sampled.*;
-
-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();
+ * }
+ * 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 5cb596d..530b5be 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,94 +1,32 @@
package org.toop.framework.audio.events;
-import org.toop.framework.eventbus.events.EventWithSnowflake;
-import org.toop.framework.eventbus.events.EventWithoutSnowflake;
-import org.toop.framework.eventbus.events.EventsBase;
-
-import java.util.Map;
+import org.toop.framework.audio.VolumeControl;
+import org.toop.framework.eventbus.events.*;
+import org.toop.framework.eventbus.events.ResponseToUniqueEvent;
+import org.toop.framework.eventbus.events.UniqueEvent;
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 {}
- public record ChangeVolume(double newVolume) implements EventWithoutSnowflake {}
- public record ChangeFxVolume(double newVolume) implements EventWithoutSnowflake {}
- public record ChangeMusicVolume(double newVolume) implements EventWithoutSnowflake {}
+ /** Stop playing a sound effect. */
+ public record StopEffect(String fileName) implements GenericEvent {}
- public record GetCurrentVolume(long snowflakeId) implements EventWithSnowflake {
- @Override
- public Map result() {
- return Map.of();
- }
+ /** Start background music. */
+ public record StartBackgroundMusic() implements GenericEvent {}
- @Override
- public long eventSnowflake() {
- return snowflakeId;
- }
- }
- 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..21c495b
--- /dev/null
+++ b/framework/src/main/java/org/toop/framework/audio/interfaces/MusicManager.java
@@ -0,0 +1,8 @@
+package org.toop.framework.audio.interfaces;
+
+import org.toop.framework.resource.types.AudioResource;
+
+public interface MusicManager extends AudioManager {
+ void play();
+ void stop();
+}
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..2a72297
--- /dev/null
+++ b/framework/src/main/java/org/toop/framework/audio/interfaces/SoundEffectManager.java
@@ -0,0 +1,13 @@
+package org.toop.framework.audio.interfaces;
+
+import org.toop.framework.resource.resources.SoundEffectAsset;
+import org.toop.framework.resource.types.AudioResource;
+
+import javax.sound.sampled.LineUnavailableException;
+import javax.sound.sampled.UnsupportedAudioFileException;
+import java.io.IOException;
+
+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..d9cc8a4 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,14 @@ 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;
/**
* 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}.
*
- *
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,7 +31,7 @@ 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. */
+ /** Automatically assigned UUID for {@link UniqueEvent} events. */
private long eventSnowflake = -1;
/** The event instance created by this publisher. */
@@ -40,7 +41,7 @@ public class EventFlow {
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() {}
@@ -60,7 +61,7 @@ public class EventFlow {
// Keep the old class+args version if needed
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 +82,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,13 +101,8 @@ 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(
+ public EventFlow onResponse(
Class eventClass, Consumer action, boolean unsubscribeAfterSuccess) {
ListenerHandler[] listenerHolder = new ListenerHandler[1];
listenerHolder[0] =
@@ -114,7 +110,7 @@ public class EventFlow {
GlobalEventBus.subscribe(
eventClass,
event -> {
- if (event.eventSnowflake() != this.eventSnowflake) return;
+ if (event.getIdentifier() != this.eventSnowflake) return;
action.accept(event);
@@ -130,22 +126,21 @@ public class EventFlow {
}
/** Subscribe by ID: only fires if UUID matches this publisher's eventId. */
- public EventFlow onResponse(
- Class eventClass, Consumer action) {
+ public EventFlow onResponse(Class eventClass, Consumer action) {
return this.onResponse(eventClass, action, true);
}
/** Subscribe by ID without explicit class. */
@SuppressWarnings("unchecked")
- public EventFlow onResponse(
+ 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) {
+ if (!(event instanceof UniqueEvent uuidEvent)) return;
+ if (uuidEvent.getIdentifier() == this.eventSnowflake) {
try {
TT typedEvent = (TT) uuidEvent;
action.accept(typedEvent);
@@ -159,7 +154,7 @@ public class EventFlow {
throw new ClassCastException(
"Cannot cast "
+ event.getClass().getName()
- + " to EventWithSnowflake");
+ + " to UniqueEvent");
}
}
}));
@@ -167,7 +162,7 @@ public class EventFlow {
return this;
}
- public EventFlow onResponse(Consumer action) {
+ public EventFlow onResponse(Consumer action) {
return this.onResponse(action, true);
}
@@ -214,7 +209,7 @@ public class EventFlow {
throw new ClassCastException(
"Cannot cast "
+ event.getClass().getName()
- + " to EventWithSnowflake");
+ + " to UniqueEvent");
}
}));
this.listeners.add(listenerHolder[0]);
@@ -237,7 +232,13 @@ public class EventFlow {
return this;
}
- public Map getResult() {
+ private void clean() {
+ this.listeners.clear();
+ this.event = null;
+ this.result = null;
+ } // TODO
+
+ public Map getResult() {
return this.result;
}
diff --git a/framework/src/main/java/org/toop/framework/eventbus/GlobalEventBus.java b/framework/src/main/java/org/toop/framework/eventbus/GlobalEventBus.java
index 41386bf..6c8745f 100644
--- a/framework/src/main/java/org/toop/framework/eventbus/GlobalEventBus.java
+++ b/framework/src/main/java/org/toop/framework/eventbus/GlobalEventBus.java
@@ -7,7 +7,7 @@ import java.util.Map;
import java.util.concurrent.*;
import java.util.function.Consumer;
import org.toop.framework.eventbus.events.EventType;
-import org.toop.framework.eventbus.events.EventWithSnowflake;
+import org.toop.framework.eventbus.events.UniqueEvent;
/**
* GlobalEventBus backed by the LMAX Disruptor for ultra-low latency, high-throughput event
@@ -21,7 +21,7 @@ public final class GlobalEventBus {
/** Map of event class to Snowflake-ID-specific listeners. */
private static final Map<
- Class>, ConcurrentHashMap>>
+ Class>, ConcurrentHashMap>>
UUID_LISTENERS = new ConcurrentHashMap<>();
/** Disruptor ring buffer size (must be power of two). */
@@ -90,7 +90,7 @@ public final class GlobalEventBus {
return wrapper;
}
- public static void subscribeById(
+ public static void subscribeById(
Class eventClass, long eventId, Consumer listener) {
UUID_LISTENERS
.computeIfAbsent(eventClass, _ -> new ConcurrentHashMap<>())
@@ -101,9 +101,9 @@ public final class GlobalEventBus {
LISTENERS.values().forEach(list -> list.remove(listener));
}
- public static void unsubscribeById(
+ public static void unsubscribeById(
Class eventClass, long eventId) {
- Map> map = UUID_LISTENERS.get(eventClass);
+ Map> map = UUID_LISTENERS.get(eventClass);
if (map != null) map.remove(eventId);
}
@@ -152,11 +152,11 @@ public final class GlobalEventBus {
}
// snowflake listeners
- if (event instanceof EventWithSnowflake snowflakeEvent) {
- Map> map = UUID_LISTENERS.get(clazz);
+ if (event instanceof UniqueEvent snowflakeEvent) {
+ Map> map = UUID_LISTENERS.get(clazz);
if (map != null) {
- Consumer listener =
- (Consumer) map.remove(snowflakeEvent.eventSnowflake());
+ Consumer listener =
+ (Consumer) map.remove(snowflakeEvent.getIdentifier());
if (listener != null) {
try {
listener.accept(snowflakeEvent);
diff --git a/framework/src/main/java/org/toop/framework/eventbus/ListenerHandler.java b/framework/src/main/java/org/toop/framework/eventbus/ListenerHandler.java
index 8daa274..cc5fbc4 100644
--- a/framework/src/main/java/org/toop/framework/eventbus/ListenerHandler.java
+++ b/framework/src/main/java/org/toop/framework/eventbus/ListenerHandler.java
@@ -1,7 +1,7 @@
package org.toop.framework.eventbus;
public class ListenerHandler {
- private Object listener = null;
+ private Object listener;
// private boolean unsubscribeAfterSuccess = true;
diff --git a/framework/src/main/java/org/toop/framework/eventbus/events/EventWithSnowflake.java b/framework/src/main/java/org/toop/framework/eventbus/events/EventWithSnowflake.java
deleted file mode 100644
index 80a1708..0000000
--- a/framework/src/main/java/org/toop/framework/eventbus/events/EventWithSnowflake.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package org.toop.framework.eventbus.events;
-
-import java.util.Map;
-
-public interface EventWithSnowflake extends EventType {
- Map result();
- long eventSnowflake();
-}
diff --git a/framework/src/main/java/org/toop/framework/eventbus/events/EventWithoutSnowflake.java b/framework/src/main/java/org/toop/framework/eventbus/events/EventWithoutSnowflake.java
deleted file mode 100644
index 08593a6..0000000
--- a/framework/src/main/java/org/toop/framework/eventbus/events/EventWithoutSnowflake.java
+++ /dev/null
@@ -1,3 +0,0 @@
-package org.toop.framework.eventbus.events;
-
-public interface EventWithoutSnowflake extends EventType {}
diff --git a/framework/src/main/java/org/toop/framework/eventbus/events/EventsBase.java b/framework/src/main/java/org/toop/framework/eventbus/events/EventsBase.java
index 18b86d2..cc5d589 100644
--- a/framework/src/main/java/org/toop/framework/eventbus/events/EventsBase.java
+++ b/framework/src/main/java/org/toop/framework/eventbus/events/EventsBase.java
@@ -1,69 +1,4 @@
package org.toop.framework.eventbus.events;
-import java.lang.reflect.Constructor;
-import java.util.Arrays;
-
/** Events that are used in the GlobalEventBus class. */
-public class EventsBase {
-
- /**
- * WIP, DO NOT USE!
- *
- * @param eventName
- * @param args
- * @return
- * @throws Exception
- */
- public static Object get(String eventName, Object... args) throws Exception {
- Class> clazz = Class.forName("org.toop.framework.eventbus.events.Events$ServerEvents$" + eventName);
- Class>[] paramTypes = Arrays.stream(args).map(Object::getClass).toArray(Class>[]::new);
- Constructor> constructor = clazz.getConstructor(paramTypes);
- return constructor.newInstance(args);
- }
-
- /**
- * WIP, DO NOT USE!
- *
- * @param eventCategory
- * @param eventName
- * @param args
- * @return
- * @throws Exception
- */
- public static Object get(String eventCategory, String eventName, Object... args)
- throws Exception {
- Class> clazz =
- Class.forName("org.toop.framework.eventbus.events.Events$" + eventCategory + "$" + eventName);
- Class>[] paramTypes = Arrays.stream(args).map(Object::getClass).toArray(Class>[]::new);
- Constructor> constructor = clazz.getConstructor(paramTypes);
- return constructor.newInstance(args);
- }
-
- /**
- * WIP, DO NOT USE!
- *
- * @param eventName
- * @param args
- * @return
- * @throws Exception
- */
- public static Object get2(String eventName, Object... args) throws Exception {
- // Fully qualified class name
- String className = "org.toop.server.backend.Events$ServerEvents$" + eventName;
-
- // Load the class
- Class> clazz = Class.forName(className);
-
- // Build array of argument types
- Class>[] paramTypes = new Class[args.length];
- for (int i = 0; i < args.length; i++) {
- paramTypes[i] = args[i].getClass();
- }
-
- // Get the constructor
- Constructor> constructor = clazz.getConstructor(paramTypes);
-
- // Create a new instance
- return constructor.newInstance(args);
- }
-}
+public class EventsBase {}
diff --git a/framework/src/main/java/org/toop/framework/eventbus/events/GenericEvent.java b/framework/src/main/java/org/toop/framework/eventbus/events/GenericEvent.java
new file mode 100644
index 0000000..9ec47c5
--- /dev/null
+++ b/framework/src/main/java/org/toop/framework/eventbus/events/GenericEvent.java
@@ -0,0 +1,3 @@
+package org.toop.framework.eventbus.events;
+
+public interface GenericEvent extends EventType {}
diff --git a/framework/src/main/java/org/toop/framework/eventbus/events/ResponseToUniqueEvent.java b/framework/src/main/java/org/toop/framework/eventbus/events/ResponseToUniqueEvent.java
new file mode 100644
index 0000000..30328ce
--- /dev/null
+++ b/framework/src/main/java/org/toop/framework/eventbus/events/ResponseToUniqueEvent.java
@@ -0,0 +1,20 @@
+package org.toop.framework.eventbus.events;
+
+import java.lang.reflect.RecordComponent;
+import java.util.HashMap;
+import java.util.Map;
+
+public interface ResponseToUniqueEvent extends UniqueEvent {
+ default Map result() {
+ Map map = new HashMap<>();
+ try {
+ for (RecordComponent component : this.getClass().getRecordComponents()) {
+ Object value = component.getAccessor().invoke(this);
+ map.put(component.getName(), value);
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to build result map via reflection", e);
+ }
+ return Map.copyOf(map);
+ }
+}
diff --git a/framework/src/main/java/org/toop/framework/eventbus/events/UniqueEvent.java b/framework/src/main/java/org/toop/framework/eventbus/events/UniqueEvent.java
new file mode 100644
index 0000000..bb68f61
--- /dev/null
+++ b/framework/src/main/java/org/toop/framework/eventbus/events/UniqueEvent.java
@@ -0,0 +1,12 @@
+package org.toop.framework.eventbus.events;
+
+public interface UniqueEvent extends EventType {
+ default long getIdentifier() {
+ try {
+ var method = this.getClass().getMethod("identifier");
+ return (long) method.invoke(this);
+ } catch (Exception e) {
+ throw new RuntimeException("No identifier accessor found", e);
+ }
+ }
+}
diff --git a/framework/src/main/java/org/toop/framework/networking/NetworkingClient.java b/framework/src/main/java/org/toop/framework/networking/NetworkingClient.java
index 74f8a9f..fd99bf7 100644
--- a/framework/src/main/java/org/toop/framework/networking/NetworkingClient.java
+++ b/framework/src/main/java/org/toop/framework/networking/NetworkingClient.java
@@ -84,9 +84,9 @@ public class NetworkingClient {
if (isChannelActive()) {
this.channel.writeAndFlush(msg);
logger.info(
- "Connection {} sent message: '{}'", this.channel.remoteAddress(), literalMsg);
+ "Connection {} sent message: '{}' ", this.channel.remoteAddress(), literalMsg);
} else {
- logger.warn("Cannot send message: '{}', connection inactive.", literalMsg);
+ logger.warn("Cannot send message: '{}', connection inactive. ", literalMsg);
}
}
diff --git a/framework/src/main/java/org/toop/framework/networking/NetworkingClientManager.java b/framework/src/main/java/org/toop/framework/networking/NetworkingClientManager.java
index 9c313d1..d42ed9b 100644
--- a/framework/src/main/java/org/toop/framework/networking/NetworkingClientManager.java
+++ b/framework/src/main/java/org/toop/framework/networking/NetworkingClientManager.java
@@ -45,8 +45,8 @@ public class NetworkingClientManager {
}
long startClientRequest(String ip, int port) {
- long connectionId = new SnowflakeGenerator().nextId(); // TODO: Maybe use the one generated
- try { // With EventFlow
+ long connectionId = SnowflakeGenerator.nextId();
+ try {
NetworkingClient client =
new NetworkingClient(
() -> new NetworkingGameClientHandler(connectionId),
@@ -81,19 +81,13 @@ public class NetworkingClientManager {
void handleStartClient(NetworkEvents.StartClient event) {
long id = this.startClientRequest(event.ip(), event.port());
new Thread(
- () -> {
- try {
- Thread.sleep(100); // TODO: Is this a good idea?
+ () ->
new EventFlow()
.addPostEvent(
NetworkEvents.StartClientResponse.class,
id,
event.eventSnowflake())
- .asyncPostEvent();
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
- })
+ .asyncPostEvent())
.start();
}
@@ -185,7 +179,7 @@ public class NetworkingClientManager {
void handleCloseClient(NetworkEvents.CloseClient event) {
NetworkingClient client = this.networkClients.get(event.clientId());
- client.closeConnection(); // TODO: Check if not blocking, what if error, mb not remove?
+ client.closeConnection();
this.networkClients.remove(event.clientId());
logger.info("Client {} closed successfully.", event.clientId());
}
diff --git a/framework/src/main/java/org/toop/framework/networking/NetworkingGameClientHandler.java b/framework/src/main/java/org/toop/framework/networking/NetworkingGameClientHandler.java
index 0b6dcaa..388e420 100644
--- a/framework/src/main/java/org/toop/framework/networking/NetworkingGameClientHandler.java
+++ b/framework/src/main/java/org/toop/framework/networking/NetworkingGameClientHandler.java
@@ -74,7 +74,7 @@ public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
gameWinConditionHandler(recSrvRemoved);
return;
default:
- return;
+ // return
}
} else {
@@ -93,10 +93,10 @@ public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
helpHandler(recSrvRemoved);
return;
default:
- return;
+ // return
}
} else {
- return; // TODO: Should be an error.
+ logger.error("Could not parse: {}", rec);
}
}
}
@@ -119,6 +119,7 @@ public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
}
private void gameWinConditionHandler(String rec) {
+ @SuppressWarnings("StreamToString")
String condition =
Pattern.compile("\\b(win|draw|lose)\\b", Pattern.CASE_INSENSITIVE)
.matcher(rec)
@@ -136,7 +137,7 @@ public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
try {
String[] msg =
Pattern.compile(
- "(?:CHALLENGER|GAMETYPE|CHALLENGENUMBER):\\s*\"?(.*?)\"?\\s*(?:,|})")
+ "(?:CHALLENGER|GAMETYPE|CHALLENGENUMBER):\\s*\"?(.*?)\"?\\s*[,}]")
.matcher(rec)
.results()
.map(m -> m.group().trim())
@@ -180,6 +181,7 @@ public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
}
private void gameYourTurnHandler(String rec) {
+ @SuppressWarnings("StreamToString")
String msg =
Pattern.compile("TURNMESSAGE:\\s*\"([^\"]*)\"")
.matcher(rec)
diff --git a/framework/src/main/java/org/toop/framework/networking/events/NetworkEvents.java b/framework/src/main/java/org/toop/framework/networking/events/NetworkEvents.java
index 49583af..6574016 100644
--- a/framework/src/main/java/org/toop/framework/networking/events/NetworkEvents.java
+++ b/framework/src/main/java/org/toop/framework/networking/events/NetworkEvents.java
@@ -1,13 +1,13 @@
package org.toop.framework.networking.events;
-import java.lang.reflect.RecordComponent;
import java.util.*;
import java.util.concurrent.CompletableFuture;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-import org.toop.framework.eventbus.events.EventWithSnowflake;
-import org.toop.framework.eventbus.events.EventWithoutSnowflake;
+
+import org.toop.framework.eventbus.events.GenericEvent;
+import org.toop.framework.eventbus.events.ResponseToUniqueEvent;
+import org.toop.framework.eventbus.events.UniqueEvent;
import org.toop.framework.eventbus.events.EventsBase;
+import org.toop.annotations.AutoResponseResult;
import org.toop.framework.networking.NetworkingClient;
/**
@@ -15,8 +15,8 @@ import org.toop.framework.networking.NetworkingClient;
* org.toop.framework.eventbus.GlobalEventBus}.
*
*
This class defines all the events that can be posted or listened to in the networking
- * subsystem. Events are separated into those with unique IDs (EventWithSnowflake) and those without
- * (EventWithoutSnowflake).
+ * subsystem. Events are separated into those with unique IDs (UniqueEvent) and those without
+ * (GenericEvent).
*/
public class NetworkEvents extends EventsBase {
@@ -30,86 +30,76 @@ public class NetworkEvents extends EventsBase {
* instances.
*/
public record RequestsAllClients(CompletableFuture> future)
- implements EventWithoutSnowflake {}
+ implements GenericEvent {}
/** Forces all active client connections to close immediately. */
- public record ForceCloseAllClients() implements EventWithoutSnowflake {}
+ public record ForceCloseAllClients() implements GenericEvent {}
/** Response indicating a challenge was cancelled. */
- public record ChallengeCancelledResponse(long clientId, String challengeId)
- implements EventWithoutSnowflake {}
+ public record ChallengeCancelledResponse(long clientId, String challengeId) implements GenericEvent {}
/** Response indicating a challenge was received. */
- public record ChallengeResponse(
- long clientId, String challengerName, String challengeId, String gameType)
- implements EventWithoutSnowflake {}
+ public record ChallengeResponse(long clientId, String challengerName, String challengeId, String gameType)
+ implements GenericEvent {}
/** Response containing a list of players for a client. */
- public record PlayerlistResponse(long clientId, String[] playerlist)
- implements EventWithoutSnowflake {}
+ public record PlayerlistResponse(long clientId, String[] playerlist) implements GenericEvent {}
/** Response containing a list of games for a client. */
- public record GamelistResponse(long clientId, String[] gamelist)
- implements EventWithoutSnowflake {}
+ public record GamelistResponse(long clientId, String[] gamelist) implements GenericEvent {}
/** Response indicating a game match information for a client. */
- public record GameMatchResponse(
- long clientId, String playerToMove, String gameType, String opponent)
- implements EventWithoutSnowflake {}
+ public record GameMatchResponse(long clientId, String playerToMove, String gameType, String opponent)
+ implements GenericEvent {}
/** Response indicating the result of a game. */
- public record GameResultResponse(long clientId, String condition)
- implements EventWithoutSnowflake {}
+ public record GameResultResponse(long clientId, String condition) implements GenericEvent {}
/** Response indicating a game move occurred. */
- public record GameMoveResponse(long clientId, String player, String move, String details)
- implements EventWithoutSnowflake {}
+ public record GameMoveResponse(long clientId, String player, String move, String details) implements GenericEvent {}
/** Response indicating it is the player's turn. */
public record YourTurnResponse(long clientId, String message)
- implements EventWithoutSnowflake {}
+ implements GenericEvent {}
/** Request to send login credentials for a client. */
- public record SendLogin(long clientId, String username) implements EventWithoutSnowflake {}
+ public record SendLogin(long clientId, String username) implements GenericEvent {}
/** Request to log out a client. */
- public record SendLogout(long clientId) implements EventWithoutSnowflake {}
+ public record SendLogout(long clientId) implements GenericEvent {}
/** Request to retrieve the player list for a client. */
- public record SendGetPlayerlist(long clientId) implements EventWithoutSnowflake {}
+ public record SendGetPlayerlist(long clientId) implements GenericEvent {}
/** Request to retrieve the game list for a client. */
- public record SendGetGamelist(long clientId) implements EventWithoutSnowflake {}
+ public record SendGetGamelist(long clientId) implements GenericEvent {}
/** Request to subscribe a client to a game type. */
- public record SendSubscribe(long clientId, String gameType) implements EventWithoutSnowflake {}
+ public record SendSubscribe(long clientId, String gameType) implements GenericEvent {}
/** Request to make a move in a game. */
- public record SendMove(long clientId, short moveNumber) implements EventWithoutSnowflake {}
+ public record SendMove(long clientId, short moveNumber) implements GenericEvent {}
/** Request to challenge another player. */
- public record SendChallenge(long clientId, String usernameToChallenge, String gameType)
- implements EventWithoutSnowflake {}
+ public record SendChallenge(long clientId, String usernameToChallenge, String gameType) implements GenericEvent {}
/** Request to accept a challenge. */
- public record SendAcceptChallenge(long clientId, int challengeId)
- implements EventWithoutSnowflake {}
+ public record SendAcceptChallenge(long clientId, int challengeId) implements GenericEvent {}
/** Request to forfeit a game. */
- public record SendForfeit(long clientId) implements EventWithoutSnowflake {}
+ public record SendForfeit(long clientId) implements GenericEvent {}
/** Request to send a message from a client. */
- public record SendMessage(long clientId, String message) implements EventWithoutSnowflake {}
+ public record SendMessage(long clientId, String message) implements GenericEvent {}
/** Request to display help to a client. */
- public record SendHelp(long clientId) implements EventWithoutSnowflake {}
+ public record SendHelp(long clientId) implements GenericEvent {}
/** Request to display help for a specific command. */
- public record SendHelpForCommand(long clientId, String command)
- implements EventWithoutSnowflake {}
+ public record SendHelpForCommand(long clientId, String command) implements GenericEvent {}
/** Request to close a specific client connection. */
- public record CloseClient(long clientId) implements EventWithoutSnowflake {}
+ public record CloseClient(long clientId) implements GenericEvent {}
/**
* Event to start a new client connection.
@@ -120,61 +110,19 @@ public class NetworkEvents extends EventsBase {
* @param port Server port.
* @param eventSnowflake Unique event identifier for correlation.
*/
- public record StartClient(String ip, int port, long eventSnowflake)
- implements EventWithSnowflake {
-
- @Override
- public Map result() {
- return Stream.of(this.getClass().getRecordComponents())
- .collect(
- Collectors.toMap(
- RecordComponent::getName,
- rc -> {
- try {
- return rc.getAccessor().invoke(this);
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- }));
- }
-
- @Override
- public long eventSnowflake() {
- return this.eventSnowflake;
- }
- }
+ public record StartClient(String ip, int port, long eventSnowflake) implements UniqueEvent {}
/**
* Response confirming a client was started.
*
* @param clientId The client ID assigned to the new connection.
- * @param eventSnowflake Event ID used for correlation.
+ * @param identifier Event ID used for correlation.
*/
- public record StartClientResponse(long clientId, long eventSnowflake)
- implements EventWithSnowflake {
- @Override
- public Map result() {
- return Stream.of(this.getClass().getRecordComponents())
- .collect(
- Collectors.toMap(
- RecordComponent::getName,
- rc -> {
- try {
- return rc.getAccessor().invoke(this);
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- }));
- }
-
- @Override
- public long eventSnowflake() {
- return this.eventSnowflake;
- }
- }
+ @AutoResponseResult
+ public record StartClientResponse(long clientId, long identifier) implements ResponseToUniqueEvent {}
/** Generic server response. */
- public record ServerResponse(long clientId) implements EventWithoutSnowflake {}
+ public record ServerResponse(long clientId) implements GenericEvent {}
/**
* Request to send a command to a server.
@@ -182,10 +130,10 @@ public class NetworkEvents extends EventsBase {
* @param clientId The client connection ID.
* @param args The command arguments.
*/
- public record SendCommand(long clientId, String... args) implements EventWithoutSnowflake {}
+ public record SendCommand(long clientId, String... args) implements GenericEvent {}
/** WIP (Not working) Request to reconnect a client to a previous address. */
- public record Reconnect(long clientId) implements EventWithoutSnowflake {}
+ public record Reconnect(long clientId) implements GenericEvent {}
/**
* Response triggered when a message is received from a server.
@@ -193,7 +141,7 @@ public class NetworkEvents extends EventsBase {
* @param clientId The connection ID that received the message.
* @param message The message content.
*/
- public record ReceivedMessage(long clientId, String message) implements EventWithoutSnowflake {}
+ public record ReceivedMessage(long clientId, String message) implements GenericEvent {}
/**
* Request to change a client connection to a new server.
@@ -202,12 +150,11 @@ public class NetworkEvents extends EventsBase {
* @param ip The new server IP.
* @param port The new server port.
*/
- public record ChangeClientHost(long clientId, String ip, int port)
- implements EventWithoutSnowflake {}
+ public record ChangeClientHost(long clientId, String ip, int port) implements GenericEvent {}
/** WIP (Not working) Response indicating that the client could not connect. */
- public record CouldNotConnect(long clientId) implements EventWithoutSnowflake {}
+ public record CouldNotConnect(long clientId) implements GenericEvent {}
/** Event indicating a client connection was closed. */
- public record ClosedConnection(long clientId) implements EventWithoutSnowflake {}
+ public record ClosedConnection(long clientId) implements GenericEvent {}
}
diff --git a/framework/src/main/java/org/toop/framework/asset/ResourceLoader.java b/framework/src/main/java/org/toop/framework/resource/ResourceLoader.java
similarity index 61%
rename from framework/src/main/java/org/toop/framework/asset/ResourceLoader.java
rename to framework/src/main/java/org/toop/framework/resource/ResourceLoader.java
index 4e69a50..dcada62 100644
--- a/framework/src/main/java/org/toop/framework/asset/ResourceLoader.java
+++ b/framework/src/main/java/org/toop/framework/resource/ResourceLoader.java
@@ -1,14 +1,4 @@
-package org.toop.framework.asset;
-
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-import org.toop.framework.asset.events.AssetLoaderEvents;
-import org.toop.framework.asset.resources.*;
-import org.toop.framework.asset.types.BundledResource;
-import org.toop.framework.asset.types.FileExtension;
-import org.toop.framework.asset.types.PreloadResource;
-import org.toop.framework.eventbus.EventFlow;
-import org.reflections.Reflections;
+package org.toop.framework.resource;
import java.io.File;
import java.util.*;
@@ -16,30 +6,40 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.reflections.Reflections;
+import org.toop.framework.eventbus.EventFlow;
+import org.toop.framework.resource.events.AssetLoaderEvents;
+import org.toop.framework.resource.exceptions.CouldNotCreateResourceFactoryException;
+import org.toop.framework.resource.resources.*;
+import org.toop.framework.resource.types.BundledResource;
+import org.toop.framework.resource.types.FileExtension;
+import org.toop.framework.resource.types.PreloadResource;
/**
* Responsible for loading assets from a file system directory into memory.
- *
- * The {@code ResourceLoader} scans a root folder recursively, identifies files,
- * and maps them to registered resource types based on file extensions and
- * {@link FileExtension} annotations.
- * It supports multiple resource types including {@link PreloadResource} (automatically loaded)
- * and {@link BundledResource} (merged across multiple files).
- *
*
- *
Assets are stored in a static, thread-safe list and can be retrieved
- * through {@link ResourceManager}.
+ *
The {@code ResourceLoader} scans a root folder recursively, identifies files, and maps them to
+ * registered resource types based on file extensions and {@link FileExtension} annotations. It
+ * supports multiple resource types including {@link PreloadResource} (automatically loaded) and
+ * {@link BundledResource} (merged across multiple files).
+ *
+ *
Assets are stored in a static, thread-safe list and can be retrieved through {@link
+ * ResourceManager}.
+ *
+ *
Features:
*
- *
Features:
*
- *
Recursive directory scanning for assets.
- *
Automatic registration of resource classes via reflection.
- *
Bundled resource support: multiple files merged into a single resource instance.
Progress tracking via {@link AssetLoaderEvents.LoadingProgressUpdate} events.
*
*
- *
Usage example:
+ *
Usage example:
+ *
*
{@code
* ResourceLoader loader = new ResourceLoader("assets");
* double progress = loader.getProgress();
@@ -48,14 +48,17 @@ import java.util.function.Function;
*/
public class ResourceLoader {
private static final Logger logger = LogManager.getLogger(ResourceLoader.class);
- private static final List> assets = new CopyOnWriteArrayList<>();
- private final Map> registry = new ConcurrentHashMap<>();
+ private static final List> assets =
+ new CopyOnWriteArrayList<>();
+ private final Map> registry =
+ new ConcurrentHashMap<>();
private final AtomicInteger loadedCount = new AtomicInteger(0);
private int totalCount = 0;
/**
* Constructs an ResourceLoader and loads assets from the given root folder.
+ *
* @param rootFolder the folder containing asset files
*/
public ResourceLoader(File rootFolder) {
@@ -84,6 +87,7 @@ public class ResourceLoader {
/**
* Constructs an ResourceLoader from a folder path.
+ *
* @param rootFolder the folder path containing assets
*/
public ResourceLoader(String rootFolder) {
@@ -92,6 +96,7 @@ public class ResourceLoader {
/**
* Returns the current progress of loading assets (0.0 to 1.0).
+ *
* @return progress as a double
*/
public double getProgress() {
@@ -100,6 +105,7 @@ public class ResourceLoader {
/**
* Returns the number of assets loaded so far.
+ *
* @return loaded count
*/
public int getLoadedCount() {
@@ -108,6 +114,7 @@ public class ResourceLoader {
/**
* Returns the total number of files found to load.
+ *
* @return total asset count
*/
public int getTotalCount() {
@@ -116,6 +123,7 @@ public class ResourceLoader {
/**
* Returns a snapshot list of all assets loaded by this loader.
+ *
* @return list of loaded assets
*/
public List> getAssets() {
@@ -124,6 +132,7 @@ public class ResourceLoader {
/**
* Registers a factory for a specific file extension.
+ *
* @param extension the file extension (without dot)
* @param factory a function mapping a File to a resource instance
* @param the type of resource
@@ -132,69 +141,79 @@ public class ResourceLoader {
this.registry.put(extension, factory);
}
- /**
- * Maps a file to a resource instance based on its extension and registered factories.
- */
- private T resourceMapper(File file, Class type) {
+ /** Maps a file to a resource instance based on its extension and registered factories. */
+ private T resourceMapper(File file)
+ throws CouldNotCreateResourceFactoryException, IllegalArgumentException {
String ext = getExtension(file.getName());
Function factory = registry.get(ext);
- if (factory == null) return null;
+ if (factory == null)
+ throw new CouldNotCreateResourceFactoryException(registry, file.getName());
BaseResource resource = factory.apply(file);
- if (!type.isInstance(resource)) {
+ if (resource == null) {
throw new IllegalArgumentException(
- "File " + file.getName() + " is not of type " + type.getSimpleName()
- );
+ "File "
+ + file.getName()
+ + " is not of type "
+ + BaseResource.class.getSimpleName());
}
- return type.cast(resource);
+ return ((Class) BaseResource.class).cast(resource);
}
- /**
- * Loads the given list of files into assets, handling bundled and preload resources.
- */
+ /** Loads the given list of files into assets, handling bundled and preload resources. */
private void loader(List files) {
Map bundledResources = new HashMap<>();
for (File file : files) {
boolean skipAdd = false;
- BaseResource resource = resourceMapper(file, BaseResource.class);
+ BaseResource resource = null;
+ try {
+ resource = resourceMapper(file);
+ } catch (CouldNotCreateResourceFactoryException _) {
+ logger.warn("Could not create resource for: {}", file);
+ } catch (IllegalArgumentException e) {
+ logger.error(e);
+ }
switch (resource) {
case null -> {
continue;
}
case BundledResource br -> {
String key = resource.getClass().getName() + ":" + br.getBaseName();
- if (!bundledResources.containsKey(key)) {bundledResources.put(key, br);}
+ if (!bundledResources.containsKey(key)) {
+ bundledResources.put(key, br);
+ }
bundledResources.get(key).loadFile(file);
resource = (BaseResource) bundledResources.get(key);
assets.add(new ResourceMeta<>(br.getBaseName(), resource));
skipAdd = true;
}
case PreloadResource pr -> pr.load();
- default -> {
- }
+ default -> {}
}
BaseResource finalResource = resource;
- boolean alreadyAdded = assets.stream()
- .anyMatch(a -> a.getResource() == finalResource);
+ boolean alreadyAdded = assets.stream().anyMatch(a -> a.getResource() == finalResource);
if (!alreadyAdded && !skipAdd) {
assets.add(new ResourceMeta<>(file.getName(), resource));
}
- logger.info("Loaded {} from {}", resource.getClass().getSimpleName(), file.getAbsolutePath());
+ logger.info(
+ "Loaded {} from {}",
+ resource.getClass().getSimpleName(),
+ file.getAbsolutePath());
loadedCount.incrementAndGet();
new EventFlow()
- .addPostEvent(new AssetLoaderEvents.LoadingProgressUpdate(loadedCount.get(), totalCount))
+ .addPostEvent(
+ new AssetLoaderEvents.LoadingProgressUpdate(
+ loadedCount.get(), totalCount))
.postEvent();
}
}
- /**
- * Recursively searches a folder and adds all files to the foundFiles list.
- */
+ /** Recursively searches a folder and adds all files to the foundFiles list. */
private void fileSearcher(final File folder, List foundFiles) {
for (File fileEntry : Objects.requireNonNull(folder.listFiles())) {
if (fileEntry.isDirectory()) {
@@ -206,31 +225,31 @@ public class ResourceLoader {
}
/**
- * Uses reflection to automatically register all {@link BaseResource} subclasses
- * annotated with {@link FileExtension}.
+ * Uses reflection to automatically register all {@link BaseResource} subclasses annotated with
+ * {@link FileExtension}.
*/
private void autoRegisterResources() {
- Reflections reflections = new Reflections("org.toop.framework.asset.resources");
+ Reflections reflections = new Reflections("org.toop.framework.resource.resources");
Set> classes = reflections.getSubTypesOf(BaseResource.class);
for (Class extends BaseResource> cls : classes) {
if (!cls.isAnnotationPresent(FileExtension.class)) continue;
FileExtension annotation = cls.getAnnotation(FileExtension.class);
for (String ext : annotation.value()) {
- registry.put(ext, file -> {
- try {
- return cls.getConstructor(File.class).newInstance(file);
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- });
+ registry.put(
+ ext,
+ file -> {
+ try {
+ return cls.getConstructor(File.class).newInstance(file);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ });
}
}
}
- /**
- * Extracts the base name from a file name, used for bundling multiple files.
- */
+ /** Extracts the base name from a file name, used for bundling multiple files. */
private static String getBaseName(String fileName) {
int underscoreIndex = fileName.indexOf('_');
int dotIndex = fileName.lastIndexOf('.');
@@ -238,9 +257,7 @@ public class ResourceLoader {
return fileName.substring(0, dotIndex);
}
- /**
- * Returns the file extension of a given file name (without dot).
- */
+ /** Returns the file extension of a given file name (without dot). */
public static String getExtension(String name) {
int i = name.lastIndexOf('.');
return (i > 0) ? name.substring(i + 1) : "";
diff --git a/framework/src/main/java/org/toop/framework/asset/ResourceManager.java b/framework/src/main/java/org/toop/framework/resource/ResourceManager.java
similarity index 51%
rename from framework/src/main/java/org/toop/framework/asset/ResourceManager.java
rename to framework/src/main/java/org/toop/framework/resource/ResourceManager.java
index 983dc02..0a9668a 100644
--- a/framework/src/main/java/org/toop/framework/asset/ResourceManager.java
+++ b/framework/src/main/java/org/toop/framework/resource/ResourceManager.java
@@ -1,30 +1,31 @@
-package org.toop.framework.asset;
-
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-import org.toop.framework.asset.resources.*;
+package org.toop.framework.resource;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.toop.framework.resource.exceptions.ResourceNotFoundException;
+import org.toop.framework.resource.resources.*;
+
/**
* Centralized manager for all loaded assets in the application.
- *
- * {@code ResourceManager} maintains a thread-safe registry of {@link Asset} objects
- * and provides utility methods to retrieve assets by name, ID, or type.
- * It works together with {@link ResourceLoader} to register assets automatically
- * when they are loaded from the file system.
- *
*
- *
Key responsibilities:
+ *
{@code ResourceManager} maintains a thread-safe registry of {@link ResourceMeta} objects and
+ * provides utility methods to retrieve assets by name, ID, or type. It works together with {@link
+ * ResourceLoader} to register assets automatically when they are loaded from the file system.
+ *
+ *
Key responsibilities:
+ *
*
- *
Storing all loaded assets in a concurrent map.
- *
Providing typed access to asset resources.
- *
Allowing lookup by asset name or ID.
- *
Supporting retrieval of all assets of a specific {@link BaseResource} subclass.
+ *
Storing all loaded assets in a concurrent map.
+ *
Providing typed access to asset resources.
+ *
Allowing lookup by asset name or ID.
+ *
Supporting retrieval of all assets of a specific {@link BaseResource} subclass.
*
*
- *
Example usage:
+ *
Example usage:
+ *
*
{@code
* // Load assets from a loader
* ResourceLoader loader = new ResourceLoader(new File("RootFolder"));
@@ -40,27 +41,28 @@ import java.util.concurrent.ConcurrentHashMap;
* Optional> maybeAsset = ResourceManager.findByName("menu.css");
* }
*
- *
Notes:
+ *
Notes:
+ *
*
- *
All retrieval methods are static and thread-safe.
- *
The {@link #get(String)} method may require casting if the asset type is not known at compile time.
- *
Assets should be loaded via {@link ResourceLoader} before retrieval.
+ *
All retrieval methods are static and thread-safe.
+ *
The {@link #get(String)} method may require casting if the asset type is not known at
+ * compile time.
+ *
Assets should be loaded via {@link ResourceLoader} before retrieval.
*
*/
public class ResourceManager {
private static final Logger logger = LogManager.getLogger(ResourceManager.class);
- private static final ResourceManager INSTANCE = new ResourceManager();
- private static final Map> assets = new ConcurrentHashMap<>();
+ private static final Map> assets =
+ new ConcurrentHashMap<>();
+ private static ResourceManager instance;
private ResourceManager() {}
- /**
- * Returns the singleton instance of {@code ResourceManager}.
- *
- * @return the shared instance
- */
public static ResourceManager getInstance() {
- return INSTANCE;
+ if (instance == null) {
+ instance = new ResourceManager();
+ }
+ return instance;
}
/**
@@ -68,8 +70,8 @@ public class ResourceManager {
*
* @param loader the loader that has already loaded assets
*/
- public synchronized static void loadAssets(ResourceLoader loader) {
- for (var asset : loader.getAssets()) {
+ public static synchronized void loadAssets(ResourceLoader loader) {
+ for (ResourceMeta extends BaseResource> asset : loader.getAssets()) {
assets.put(asset.getName(), asset);
}
}
@@ -85,15 +87,15 @@ public class ResourceManager {
public static T get(String name) {
ResourceMeta asset = (ResourceMeta) assets.get(name);
if (asset == null) {
- throw new TypeNotPresentException(name, new RuntimeException(String.format("Type %s not present", name))); // TODO: Create own exception, BAM
+ throw new ResourceNotFoundException(name);
}
return asset.getResource();
}
-// @SuppressWarnings("unchecked")
-// public static ArrayList> getAllOfType() {
-// return (ArrayList>) (ArrayList>) new ArrayList<>(assets.values());
-// }
+ // @SuppressWarnings("unchecked")
+ // public static ArrayList> getAllOfType() {
+ // return (ArrayList>) (ArrayList>) new ArrayList<>(assets.values());
+ // }
/**
* Retrieve all assets of a specific resource type.
@@ -102,16 +104,32 @@ public class ResourceManager {
* @param the resource type
* @return a list of assets matching the type
*/
- public static ArrayList> getAllOfType(Class type) {
- ArrayList> list = new ArrayList<>();
- for (ResourceMeta extends BaseResource> asset : assets.values()) {
- if (type.isInstance(asset.getResource())) {
+ public static List> getAllOfType(Class type) {
+ List> result = new ArrayList<>();
+
+ for (ResourceMeta extends BaseResource> meta : assets.values()) {
+ BaseResource res = meta.getResource();
+ if (type.isInstance(res)) {
@SuppressWarnings("unchecked")
- ResourceMeta typed = (ResourceMeta) asset;
- list.add(typed);
+ ResourceMeta typed = (ResourceMeta) meta;
+ result.add(typed);
}
}
- return list;
+
+ return result;
+ }
+
+ public static List getAllOfTypeAndRemoveWrapper(Class type) {
+ List result = new ArrayList<>();
+
+ for (ResourceMeta extends BaseResource> meta : assets.values()) {
+ BaseResource res = meta.getResource();
+ if (type.isInstance(res)) {
+ result.add((T) res);
+ }
+ }
+
+ return result;
}
/**
@@ -136,5 +154,6 @@ public class ResourceManager {
*/
public static void addAsset(ResourceMeta extends BaseResource> asset) {
assets.put(asset.getName(), asset);
+ logger.info("Successfully added asset: {}, to the asset list", asset.getName());
}
}
diff --git a/framework/src/main/java/org/toop/framework/asset/ResourceMeta.java b/framework/src/main/java/org/toop/framework/resource/ResourceMeta.java
similarity index 77%
rename from framework/src/main/java/org/toop/framework/asset/ResourceMeta.java
rename to framework/src/main/java/org/toop/framework/resource/ResourceMeta.java
index 972b635..0e47863 100644
--- a/framework/src/main/java/org/toop/framework/asset/ResourceMeta.java
+++ b/framework/src/main/java/org/toop/framework/resource/ResourceMeta.java
@@ -1,7 +1,7 @@
-package org.toop.framework.asset;
+package org.toop.framework.resource;
import org.toop.framework.SnowflakeGenerator;
-import org.toop.framework.asset.resources.BaseResource;
+import org.toop.framework.resource.resources.BaseResource;
public class ResourceMeta {
private final Long id;
@@ -9,7 +9,7 @@ public class ResourceMeta {
private final T resource;
public ResourceMeta(String name, T resource) {
- this.id = new SnowflakeGenerator().nextId();
+ this.id = SnowflakeGenerator.nextId();
this.name = name;
this.resource = resource;
}
@@ -25,5 +25,4 @@ public class ResourceMeta {
public T getResource() {
return this.resource;
}
-
}
diff --git a/framework/src/main/java/org/toop/framework/resource/events/AssetLoaderEvents.java b/framework/src/main/java/org/toop/framework/resource/events/AssetLoaderEvents.java
new file mode 100644
index 0000000..066dd96
--- /dev/null
+++ b/framework/src/main/java/org/toop/framework/resource/events/AssetLoaderEvents.java
@@ -0,0 +1,8 @@
+package org.toop.framework.resource.events;
+
+import org.toop.framework.eventbus.events.GenericEvent;
+
+public class AssetLoaderEvents {
+ public record LoadingProgressUpdate(int hasLoadedAmount, int isLoadingAmount)
+ implements GenericEvent {}
+}
diff --git a/framework/src/main/java/org/toop/framework/resource/exceptions/CouldNotCreateResourceFactoryException.java b/framework/src/main/java/org/toop/framework/resource/exceptions/CouldNotCreateResourceFactoryException.java
new file mode 100644
index 0000000..aa7b3c0
--- /dev/null
+++ b/framework/src/main/java/org/toop/framework/resource/exceptions/CouldNotCreateResourceFactoryException.java
@@ -0,0 +1,12 @@
+package org.toop.framework.resource.exceptions;
+
+import java.util.Map;
+
+public class CouldNotCreateResourceFactoryException extends RuntimeException {
+ public CouldNotCreateResourceFactoryException(Map, ?> registry, String fileName) {
+ super(
+ String.format(
+ "Could not create resource factory for: %s, isRegistryEmpty: %b",
+ fileName, registry.isEmpty()));
+ }
+}
diff --git a/framework/src/main/java/org/toop/framework/resource/exceptions/IsNotAResourceException.java b/framework/src/main/java/org/toop/framework/resource/exceptions/IsNotAResourceException.java
new file mode 100644
index 0000000..41abbde
--- /dev/null
+++ b/framework/src/main/java/org/toop/framework/resource/exceptions/IsNotAResourceException.java
@@ -0,0 +1,7 @@
+package org.toop.framework.resource.exceptions;
+
+public class IsNotAResourceException extends RuntimeException {
+ public IsNotAResourceException(Class clazz, String message) {
+ super(clazz.getName() + " does not implement BaseResource");
+ }
+}
diff --git a/framework/src/main/java/org/toop/framework/resource/exceptions/ResourceNotFoundException.java b/framework/src/main/java/org/toop/framework/resource/exceptions/ResourceNotFoundException.java
new file mode 100644
index 0000000..c75de1e
--- /dev/null
+++ b/framework/src/main/java/org/toop/framework/resource/exceptions/ResourceNotFoundException.java
@@ -0,0 +1,7 @@
+package org.toop.framework.resource.exceptions;
+
+public class ResourceNotFoundException extends RuntimeException {
+ public ResourceNotFoundException(String name) {
+ super("Could not find resource: " + name);
+ }
+}
diff --git a/framework/src/main/java/org/toop/framework/asset/resources/BaseResource.java b/framework/src/main/java/org/toop/framework/resource/resources/BaseResource.java
similarity index 84%
rename from framework/src/main/java/org/toop/framework/asset/resources/BaseResource.java
rename to framework/src/main/java/org/toop/framework/resource/resources/BaseResource.java
index c1aa040..72da47c 100644
--- a/framework/src/main/java/org/toop/framework/asset/resources/BaseResource.java
+++ b/framework/src/main/java/org/toop/framework/resource/resources/BaseResource.java
@@ -1,4 +1,4 @@
-package org.toop.framework.asset.resources;
+package org.toop.framework.resource.resources;
import java.io.*;
@@ -14,5 +14,4 @@ public abstract class BaseResource {
public File getFile() {
return this.file;
}
-
}
diff --git a/framework/src/main/java/org/toop/framework/asset/resources/CssAsset.java b/framework/src/main/java/org/toop/framework/resource/resources/CssAsset.java
similarity index 73%
rename from framework/src/main/java/org/toop/framework/asset/resources/CssAsset.java
rename to framework/src/main/java/org/toop/framework/resource/resources/CssAsset.java
index 6f8cd19..0d056ae 100644
--- a/framework/src/main/java/org/toop/framework/asset/resources/CssAsset.java
+++ b/framework/src/main/java/org/toop/framework/resource/resources/CssAsset.java
@@ -1,8 +1,7 @@
-package org.toop.framework.asset.resources;
-
-import org.toop.framework.asset.types.FileExtension;
+package org.toop.framework.resource.resources;
import java.io.File;
+import org.toop.framework.resource.types.FileExtension;
@FileExtension({"css"})
public class CssAsset extends BaseResource {
diff --git a/framework/src/main/java/org/toop/framework/asset/resources/FontAsset.java b/framework/src/main/java/org/toop/framework/resource/resources/FontAsset.java
similarity index 91%
rename from framework/src/main/java/org/toop/framework/asset/resources/FontAsset.java
rename to framework/src/main/java/org/toop/framework/resource/resources/FontAsset.java
index 1054d03..da9c709 100644
--- a/framework/src/main/java/org/toop/framework/asset/resources/FontAsset.java
+++ b/framework/src/main/java/org/toop/framework/resource/resources/FontAsset.java
@@ -1,12 +1,11 @@
-package org.toop.framework.asset.resources;
-
-import javafx.scene.text.Font;
-import org.toop.framework.asset.types.FileExtension;
-import org.toop.framework.asset.types.PreloadResource;
+package org.toop.framework.resource.resources;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
+import javafx.scene.text.Font;
+import org.toop.framework.resource.types.FileExtension;
+import org.toop.framework.resource.types.PreloadResource;
@FileExtension({"ttf", "otf"})
public class FontAsset extends BaseResource implements PreloadResource {
@@ -60,4 +59,4 @@ public class FontAsset extends BaseResource implements PreloadResource {
}
return this.family;
}
-}
\ No newline at end of file
+}
diff --git a/framework/src/main/java/org/toop/framework/asset/resources/ImageAsset.java b/framework/src/main/java/org/toop/framework/resource/resources/ImageAsset.java
similarity index 86%
rename from framework/src/main/java/org/toop/framework/asset/resources/ImageAsset.java
rename to framework/src/main/java/org/toop/framework/resource/resources/ImageAsset.java
index ed2c87e..2e6b417 100644
--- a/framework/src/main/java/org/toop/framework/asset/resources/ImageAsset.java
+++ b/framework/src/main/java/org/toop/framework/resource/resources/ImageAsset.java
@@ -1,11 +1,10 @@
-package org.toop.framework.asset.resources;
-
-import javafx.scene.image.Image;
-import org.toop.framework.asset.types.FileExtension;
-import org.toop.framework.asset.types.LoadableResource;
+package org.toop.framework.resource.resources;
import java.io.File;
import java.io.FileInputStream;
+import javafx.scene.image.Image;
+import org.toop.framework.resource.types.FileExtension;
+import org.toop.framework.resource.types.LoadableResource;
@FileExtension({"png", "jpg", "jpeg"})
public class ImageAsset extends BaseResource implements LoadableResource {
@@ -45,4 +44,4 @@ public class ImageAsset extends BaseResource implements LoadableResource {
}
return null;
}
-}
\ No newline at end of file
+}
diff --git a/framework/src/main/java/org/toop/framework/asset/resources/JsonAsset.java b/framework/src/main/java/org/toop/framework/resource/resources/JsonAsset.java
similarity index 83%
rename from framework/src/main/java/org/toop/framework/asset/resources/JsonAsset.java
rename to framework/src/main/java/org/toop/framework/resource/resources/JsonAsset.java
index 5f9e1ba..c13849c 100644
--- a/framework/src/main/java/org/toop/framework/asset/resources/JsonAsset.java
+++ b/framework/src/main/java/org/toop/framework/resource/resources/JsonAsset.java
@@ -1,12 +1,13 @@
-package org.toop.framework.asset.resources;
+package org.toop.framework.resource.resources;
+
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
-import org.toop.framework.asset.types.FileExtension;
-import org.toop.framework.asset.types.LoadableResource;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
+import org.toop.framework.resource.types.FileExtension;
+import org.toop.framework.resource.types.LoadableResource;
@FileExtension({"json"})
public class JsonAsset extends BaseResource implements LoadableResource {
@@ -25,7 +26,8 @@ public class JsonAsset extends BaseResource implements LoadableResource {
File file = getFile();
if (!file.exists()) {
try {
- // make a new file with the declared constructor (example: settings) if it doesn't exist
+ // make a new file with the declared constructor (example: settings) if it doesn't
+ // exist
content = type.getDeclaredConstructor().newInstance();
save();
} catch (Exception e) {
@@ -36,7 +38,7 @@ public class JsonAsset extends BaseResource implements LoadableResource {
try (FileReader reader = new FileReader(file)) {
content = gson.fromJson(reader, type);
this.isLoaded = true;
- } catch(Exception e) {
+ } catch (Exception e) {
throw new RuntimeException("Failed to load JSON asset" + getFile(), e);
}
}
@@ -59,10 +61,11 @@ public class JsonAsset extends BaseResource implements LoadableResource {
File file = getFile();
File parent = file.getParentFile();
if (parent != null && !parent.exists()) {
- parent.mkdirs();
+ boolean isDirectoryMade = parent.mkdirs();
+ assert isDirectoryMade;
}
try (FileWriter writer = new FileWriter(file)) {
- gson.toJson(content, writer);
+ gson.toJson(content, writer);
} catch (IOException e) {
throw new RuntimeException("Failed to save JSON asset" + getFile(), e);
}
diff --git a/framework/src/main/java/org/toop/framework/asset/resources/LocalizationAsset.java b/framework/src/main/java/org/toop/framework/resource/resources/LocalizationAsset.java
similarity index 62%
rename from framework/src/main/java/org/toop/framework/asset/resources/LocalizationAsset.java
rename to framework/src/main/java/org/toop/framework/resource/resources/LocalizationAsset.java
index cf18f14..3ec3328 100644
--- a/framework/src/main/java/org/toop/framework/asset/resources/LocalizationAsset.java
+++ b/framework/src/main/java/org/toop/framework/resource/resources/LocalizationAsset.java
@@ -1,34 +1,29 @@
-package org.toop.framework.asset.resources;
-
-import org.toop.framework.asset.types.BundledResource;
-import org.toop.framework.asset.types.FileExtension;
-import org.toop.framework.asset.types.LoadableResource;
+package org.toop.framework.resource.resources;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
+import org.toop.framework.resource.types.BundledResource;
+import org.toop.framework.resource.types.FileExtension;
+import org.toop.framework.resource.types.LoadableResource;
/**
- * Represents a localization resource asset that loads and manages property files
- * containing key-value pairs for different locales.
- *
- * This class implements {@link LoadableResource} to support loading/unloading
- * and {@link BundledResource} to represent resources that can contain multiple
- * localized bundles.
- *
- *
- * Files handled by this class must have the {@code .properties} extension,
- * optionally with a locale suffix, e.g., {@code messages_en_US.properties}.
- *
+ * Represents a localization resource asset that loads and manages property files containing
+ * key-value pairs for different locales.
+ *
+ *
This class implements {@link LoadableResource} to support loading/unloading and {@link
+ * BundledResource} to represent resources that can contain multiple localized bundles.
+ *
+ *
Files handled by this class must have the {@code .properties} extension, optionally with a
+ * locale suffix, e.g., {@code messages_en_US.properties}.
+ *
+ *
*/
@FileExtension({"properties"})
public class LocalizationAsset extends BaseResource implements LoadableResource, BundledResource {
@@ -37,13 +32,13 @@ public class LocalizationAsset extends BaseResource implements LoadableResource,
private final Map bundles = new HashMap<>();
/** Flag indicating whether this asset has been loaded. */
- private boolean isLoaded = false;
+ private boolean loaded = false;
/** Basename of the given asset */
private final String baseName = "localization";
/** Fallback locale used when no matching locale is found. */
- private final Locale fallback = Locale.forLanguageTag("en_US");
+ private final Locale fallback = Locale.forLanguageTag("en");
/**
* Constructs a new LocalizationAsset for the specified file.
@@ -54,22 +49,18 @@ public class LocalizationAsset extends BaseResource implements LoadableResource,
super(file);
}
- /**
- * Loads the resource file into memory and prepares localized bundles.
- */
+ /** Loads the resource file into memory and prepares localized bundles. */
@Override
public void load() {
loadFile(getFile());
- isLoaded = true;
+ loaded = true;
}
- /**
- * Unloads all loaded resource bundles, freeing memory.
- */
+ /** Unloads all loaded resource bundles, freeing memory. */
@Override
public void unload() {
bundles.clear();
- isLoaded = false;
+ loaded = false;
}
/**
@@ -79,15 +70,14 @@ public class LocalizationAsset extends BaseResource implements LoadableResource,
*/
@Override
public boolean isLoaded() {
- return isLoaded;
+ return loaded;
}
/**
- * Retrieves a localized string for the given key and locale.
- * If an exact match for the locale is not found, a fallback
- * matching the language or the default locale will be used.
+ * Retrieves a localized string for the given key and locale. If an exact match for the locale
+ * is not found, a fallback matching the language or the default locale will be used.
*
- * @param key the key of the string
+ * @param key the key of the string
* @param locale the desired locale
* @return the localized string
* @throws MissingResourceException if no resource bundle is available for the locale
@@ -95,14 +85,15 @@ public class LocalizationAsset extends BaseResource implements LoadableResource,
public String getString(String key, Locale locale) {
Locale target = findBestLocale(locale);
ResourceBundle bundle = bundles.get(target);
- if (bundle == null) throw new MissingResourceException(
- "No bundle for locale: " + target, getClass().getName(), key);
+ if (bundle == null)
+ throw new MissingResourceException(
+ "No bundle for locale: " + target, getClass().getName(), key);
return bundle.getString(key);
}
/**
- * Finds the best matching locale among loaded bundles.
- * Prefers an exact match, then language-only match, then fallback.
+ * Finds the best matching locale among loaded bundles. Prefers an exact match, then
+ * language-only match, then fallback.
*
* @param locale the desired locale
* @return the best matching locale
@@ -125,8 +116,8 @@ public class LocalizationAsset extends BaseResource implements LoadableResource,
}
/**
- * Loads a specific property file as a resource bundle.
- * The locale is extracted from the file name if present.
+ * Loads a specific property file as a resource bundle. The locale is extracted from the file
+ * name if present.
*
* @param file the property file to load
* @throws RuntimeException if the file cannot be read
@@ -134,13 +125,13 @@ public class LocalizationAsset extends BaseResource implements LoadableResource,
@Override
public void loadFile(File file) {
try (InputStreamReader reader =
- new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)) {
+ new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)) {
Locale locale = extractLocale(file.getName(), baseName);
bundles.put(locale, new PropertyResourceBundle(reader));
} catch (IOException e) {
throw new RuntimeException("Failed to load localization file: " + file, e);
}
- isLoaded = true;
+ loaded = true;
}
/**
@@ -153,22 +144,23 @@ public class LocalizationAsset extends BaseResource implements LoadableResource,
return this.baseName;
}
-// /**
-// * Extracts the base name from a file name.
-// *
-// * @param fileName the file name
-// * @return base name without locale or extension
-// */
-// private String getBaseName(String fileName) {
-// int dotIndex = fileName.lastIndexOf('.');
-// String nameWithoutExtension = (dotIndex > 0) ? fileName.substring(0, dotIndex) : fileName;
-//
-// int underscoreIndex = nameWithoutExtension.indexOf('_');
-// if (underscoreIndex > 0) {
-// return nameWithoutExtension.substring(0, underscoreIndex);
-// }
-// return nameWithoutExtension;
-// }
+ // /**
+ // * Extracts the base name from a file name.
+ // *
+ // * @param fileName the file name
+ // * @return base name without locale or extension
+ // */
+ // private String getBaseName(String fileName) {
+ // int dotIndex = fileName.lastIndexOf('.');
+ // String nameWithoutExtension = (dotIndex > 0) ? fileName.substring(0, dotIndex) :
+ // fileName;
+ //
+ // int underscoreIndex = nameWithoutExtension.indexOf('_');
+ // if (underscoreIndex > 0) {
+ // return nameWithoutExtension.substring(0, underscoreIndex);
+ // }
+ // return nameWithoutExtension;
+ // }
/**
* Extracts a locale from a file name based on the pattern "base_LOCALE.properties".
diff --git a/framework/src/main/java/org/toop/framework/resource/resources/MusicAsset.java b/framework/src/main/java/org/toop/framework/resource/resources/MusicAsset.java
new file mode 100644
index 0000000..31e580f
--- /dev/null
+++ b/framework/src/main/java/org/toop/framework/resource/resources/MusicAsset.java
@@ -0,0 +1,85 @@
+package org.toop.framework.resource.resources;
+
+import java.io.*;
+import javafx.scene.media.Media;
+import javafx.scene.media.MediaPlayer;
+import org.toop.framework.resource.types.AudioResource;
+import org.toop.framework.resource.types.FileExtension;
+import org.toop.framework.resource.types.LoadableResource;
+
+@FileExtension({"mp3"})
+public class MusicAsset extends BaseResource implements LoadableResource, AudioResource {
+ private MediaPlayer mediaPlayer;
+ private double volume;
+
+ public MusicAsset(final File audioFile) {
+ super(audioFile);
+ }
+
+ public MediaPlayer getMediaPlayer() {
+ load();
+ return mediaPlayer;
+ }
+
+ private void initPlayer() {
+ mediaPlayer.setOnEndOfMedia(this::stop);
+ mediaPlayer.setOnError(this::stop);
+ mediaPlayer.setOnStopped(null);
+ }
+
+ @Override
+ public void load() {
+ if (mediaPlayer == null) {
+ mediaPlayer = new MediaPlayer(new Media(file.toURI().toString()));
+ initPlayer();
+ mediaPlayer.setVolume(volume);
+ }
+ this.isLoaded = true;
+ }
+
+ @Override
+ public void unload() {
+ if (mediaPlayer != null) {
+ mediaPlayer.stop();
+ mediaPlayer.dispose();
+ mediaPlayer = null;
+ }
+ isLoaded = false;
+ }
+
+ @Override
+ public boolean isLoaded() {
+ return isLoaded;
+ }
+
+ @Override
+ public void updateVolume(double volume) {
+ if (mediaPlayer != null) {
+ mediaPlayer.setVolume(volume);
+ }
+ this.volume = volume;
+ }
+
+ @Override
+ public String getName() { return super.getFile().getName(); }
+
+ @Override
+ public void setOnEnd(Runnable run) {
+ mediaPlayer.setOnEndOfMedia(run);
+ }
+
+ @Override
+ public void setOnError(Runnable run) {
+ mediaPlayer.setOnError(run);
+ }
+
+ @Override
+ public void play() {
+ getMediaPlayer().play();
+ }
+
+ @Override
+ public void stop() {
+ getMediaPlayer().stop();
+ }
+}
diff --git a/framework/src/main/java/org/toop/framework/asset/resources/SettingsAsset.java b/framework/src/main/java/org/toop/framework/resource/resources/SettingsAsset.java
similarity index 74%
rename from framework/src/main/java/org/toop/framework/asset/resources/SettingsAsset.java
rename to framework/src/main/java/org/toop/framework/resource/resources/SettingsAsset.java
index c496213..7728c9a 100644
--- a/framework/src/main/java/org/toop/framework/asset/resources/SettingsAsset.java
+++ b/framework/src/main/java/org/toop/framework/resource/resources/SettingsAsset.java
@@ -1,10 +1,8 @@
-package org.toop.framework.asset.resources;
-
-
-import org.toop.framework.settings.Settings;
+package org.toop.framework.resource.resources;
import java.io.File;
import java.util.Locale;
+import org.toop.framework.settings.Settings;
public class SettingsAsset extends JsonAsset {
@@ -32,13 +30,13 @@ public class SettingsAsset extends JsonAsset {
return getContent().fullScreen;
}
- public String getTheme() {
- return getContent().theme;
- }
+ public String getTheme() {
+ return getContent().theme;
+ }
- public String getLayoutSize() {
- return getContent().layoutSize;
- }
+ public String getLayoutSize() {
+ return getContent().layoutSize;
+ }
public void setVolume(int volume) {
getContent().volume = volume;
@@ -65,13 +63,13 @@ public class SettingsAsset extends JsonAsset {
save();
}
- public void setTheme(String theme) {
- getContent().theme = theme;
- save();
- }
+ public void setTheme(String theme) {
+ getContent().theme = theme;
+ save();
+ }
- public void setLayoutSize(String layoutSize) {
- getContent().layoutSize = layoutSize;
- save();
- }
-}
\ No newline at end of file
+ public void setLayoutSize(String layoutSize) {
+ getContent().layoutSize = layoutSize;
+ save();
+ }
+}
diff --git a/framework/src/main/java/org/toop/framework/resource/resources/SoundEffectAsset.java b/framework/src/main/java/org/toop/framework/resource/resources/SoundEffectAsset.java
new file mode 100644
index 0000000..df1f637
--- /dev/null
+++ b/framework/src/main/java/org/toop/framework/resource/resources/SoundEffectAsset.java
@@ -0,0 +1,147 @@
+package org.toop.framework.resource.resources;
+
+import java.io.*;
+import javax.sound.sampled.*;
+
+import org.toop.framework.resource.types.AudioResource;
+import org.toop.framework.resource.types.FileExtension;
+import org.toop.framework.resource.types.LoadableResource;
+
+@FileExtension({"wav"})
+public class SoundEffectAsset extends BaseResource implements LoadableResource, AudioResource {
+ private final Clip clip = AudioSystem.getClip();
+
+ private LineListener onEnd = null;
+ private LineListener onError = null;
+
+ private double volume = 100; // TODO: Find a better way to set volume on clip load
+
+ public SoundEffectAsset(final File audioFile) throws LineUnavailableException {
+ super(audioFile);
+ }
+
+ // Gets a new clip to play
+ public Clip getClip() {
+ if (!this.isLoaded()) {this.load();} return this.clip;
+ }
+
+ 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 {
+ if (this.isLoaded){
+ return; // Return if it is already loaded
+ }
+
+ // Insert a new audio stream into the clip
+ AudioInputStream inputStream = AudioSystem.getAudioInputStream(new BufferedInputStream(new FileInputStream(this.getFile())));
+ AudioFormat baseFormat = inputStream.getFormat();
+ if (baseFormat.getSampleSizeInBits() > 16)
+ inputStream = downSampleAudio(inputStream, baseFormat);
+ this.clip.open(inputStream); // ^ Clip can only run 16 bit and lower, thus downsampling necessary.
+ this.updateVolume(this.volume);
+ this.isLoaded = true;
+ } catch (LineUnavailableException | UnsupportedAudioFileException | IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void unload() {
+ if (!this.isLoaded) return; // Return if already unloaded
+
+ if (clip.isRunning()) clip.stop(); // Stops playback of the clip
+
+ clip.close(); // Releases native resources (empties buffer)
+
+ this.getClip().removeLineListener(this.onEnd);
+ this.getClip().removeLineListener(this.onError);
+
+ this.onEnd = null;
+ this.onError = null;
+
+ this.isLoaded = false;
+ }
+
+ @Override
+ public boolean isLoaded() {
+ return this.isLoaded;
+ }
+
+ @Override
+ public void updateVolume(double volume) {
+ {
+ this.volume = volume;
+ 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(volume, 0.0001))
+ * 20.0); // convert linear to dB
+ dB = Math.max(min, Math.min(max, dB));
+ volumeControl.setValue(dB);
+ }
+ }
+ }
+
+ @Override
+ public String getName() {
+ return this.getFile().getName();
+ }
+
+ @Override
+ public void setOnEnd(Runnable run) {
+ this.onEnd = event -> {
+ if (event.getType() == LineEvent.Type.STOP) {
+ run.run();
+ }
+ };
+
+ this.getClip().addLineListener(this.onEnd);
+ }
+
+ @Override
+ public void setOnError(Runnable run) {
+// this.onError = event -> {
+// if (event.getType() == LineEvent.Type.STOP) {
+// run.run();
+// }
+// }; TODO
+//
+// this.getClip().addLineListener(this.onEnd);
+
+ }
+
+ @Override
+ public void play() {
+ if (!isLoaded()) load();
+
+ this.clip.setFramePosition(0); // rewind to the start
+ this.clip.start();
+ }
+
+ @Override
+ public void stop() {
+ if (this.clip.isRunning()) this.clip.stop();
+ }
+
+}
diff --git a/framework/src/main/java/org/toop/framework/asset/resources/TextAsset.java b/framework/src/main/java/org/toop/framework/resource/resources/TextAsset.java
similarity index 85%
rename from framework/src/main/java/org/toop/framework/asset/resources/TextAsset.java
rename to framework/src/main/java/org/toop/framework/resource/resources/TextAsset.java
index e6acc3c..9d91405 100644
--- a/framework/src/main/java/org/toop/framework/asset/resources/TextAsset.java
+++ b/framework/src/main/java/org/toop/framework/resource/resources/TextAsset.java
@@ -1,12 +1,11 @@
-package org.toop.framework.asset.resources;
-
-import org.toop.framework.asset.types.FileExtension;
-import org.toop.framework.asset.types.LoadableResource;
+package org.toop.framework.resource.resources;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
+import org.toop.framework.resource.types.FileExtension;
+import org.toop.framework.resource.types.LoadableResource;
@FileExtension({"txt", "json", "xml"})
public class TextAsset extends BaseResource implements LoadableResource {
@@ -41,4 +40,4 @@ public class TextAsset extends BaseResource implements LoadableResource {
public String getContent() {
return this.content;
}
-}
\ No newline at end of file
+}
diff --git a/framework/src/main/java/org/toop/framework/resource/types/AudioResource.java b/framework/src/main/java/org/toop/framework/resource/types/AudioResource.java
new file mode 100644
index 0000000..d5d4e89
--- /dev/null
+++ b/framework/src/main/java/org/toop/framework/resource/types/AudioResource.java
@@ -0,0 +1,10 @@
+package org.toop.framework.resource.types;
+
+public interface AudioResource {
+ String getName();
+ void updateVolume(double volume);
+ void play();
+ void stop();
+ void setOnEnd(Runnable run);
+ void setOnError(Runnable run);
+}
diff --git a/framework/src/main/java/org/toop/framework/resource/types/BundledResource.java b/framework/src/main/java/org/toop/framework/resource/types/BundledResource.java
new file mode 100644
index 0000000..ca63407
--- /dev/null
+++ b/framework/src/main/java/org/toop/framework/resource/types/BundledResource.java
@@ -0,0 +1,77 @@
+package org.toop.framework.resource.types;
+
+import java.io.File;
+import org.toop.framework.resource.ResourceLoader;
+
+/**
+ * Represents a resource that can be composed of multiple files, or "bundled" together under a
+ * common base name.
+ *
+ *
Implementing classes allow an {@link ResourceLoader} to automatically merge multiple related
+ * files into a single resource instance.
+ *
+ *
Typical use cases include:
+ *
+ *
+ *
Localization assets, where multiple `.properties` files (e.g., `messages_en.properties`,
+ * `messages_nl.properties`) are grouped under the same logical resource.
+ *
Sprite sheets, tile sets, or other multi-file resources that logically belong together.
+ *
+ *
+ *
Implementing classes must provide:
+ *
+ *
+ *
{@link #loadFile(File)}: Logic to load or merge an individual file into the resource.
+ *
{@link #getBaseName()}: A consistent base name used to group multiple files into this
+ * resource.
+ *
When used with an asset loader, all files sharing the same base name are automatically merged
+ * into a single resource instance.
+ */
+public interface BundledResource {
+
+ /**
+ * Load or merge an additional file into this resource.
+ *
+ * @param file the file to load or merge
+ */
+ void loadFile(File file);
+
+ /**
+ * Return a base name for grouping multiple files into this single resource. Files with the same
+ * base name are automatically merged by the loader.
+ *
+ * @return the base name used to identify this bundled resource
+ */
+ String getBaseName();
+
+ // /**
+ // Returns the name
+ // */
+ // String getDefaultName();
+}
diff --git a/framework/src/main/java/org/toop/framework/resource/types/FileExtension.java b/framework/src/main/java/org/toop/framework/resource/types/FileExtension.java
new file mode 100644
index 0000000..df30efd
--- /dev/null
+++ b/framework/src/main/java/org/toop/framework/resource/types/FileExtension.java
@@ -0,0 +1,44 @@
+package org.toop.framework.resource.types;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.toop.framework.resource.ResourceLoader;
+import org.toop.framework.resource.resources.BaseResource;
+
+/**
+ * Annotation to declare which file extensions a {@link BaseResource} subclass can handle.
+ *
+ *
This annotation is processed by the {@link ResourceLoader} to automatically register resource
+ * types for specific file extensions. Each extension listed will be mapped to the annotated
+ * resource class, allowing the loader to instantiate the correct type when scanning files.
+ *
+ *
The annotation is retained at runtime for reflection-based registration.
+ *
Can only be applied to types (classes) that extend {@link BaseResource}.
+ *
Multiple extensions can be specified in the {@code value()} array.
+ *
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface FileExtension {
+ /**
+ * The list of file extensions (without leading dot) that the annotated resource class can
+ * handle.
+ *
+ * @return array of file extensions
+ */
+ String[] value();
+}
diff --git a/framework/src/main/java/org/toop/framework/resource/types/LoadableResource.java b/framework/src/main/java/org/toop/framework/resource/types/LoadableResource.java
new file mode 100644
index 0000000..32fb0bc
--- /dev/null
+++ b/framework/src/main/java/org/toop/framework/resource/types/LoadableResource.java
@@ -0,0 +1,69 @@
+package org.toop.framework.resource.types;
+
+import org.toop.framework.resource.ResourceLoader;
+
+/**
+ * Represents a resource that can be explicitly loaded and unloaded.
+ *
+ *
Any class implementing {@code LoadableResource} is responsible for managing its own loading
+ * and unloading logic, such as reading files, initializing data structures, or allocating external
+ * resources.
+ *
+ *
Implementing classes must define the following behaviors:
+ *
+ *
+ *
{@link #load()}: Load the resource into memory or perform necessary initialization.
+ *
{@link #unload()}: Release any held resources or memory when the resource is no longer
+ * needed.
+ *
{@link #isLoaded()}: Return {@code true} if the resource has been successfully loaded and
+ * is ready for use, {@code false} otherwise.
+ *
This interface is commonly used with {@link PreloadResource} to allow automatic loading by an
+ * {@link ResourceLoader} if desired.
+ */
+public interface LoadableResource {
+ /**
+ * Load the resource into memory or initialize it. This method may throw runtime exceptions if
+ * loading fails.
+ */
+ void load();
+
+ /**
+ * Unload the resource and free any associated resources. After this call, {@link #isLoaded()}
+ * should return false.
+ */
+ void unload();
+
+ /**
+ * Check whether the resource has been successfully loaded.
+ *
+ * @return true if the resource is loaded and ready for use, false otherwise
+ */
+ boolean isLoaded();
+}
diff --git a/framework/src/main/java/org/toop/framework/asset/types/PreloadResource.java b/framework/src/main/java/org/toop/framework/resource/types/PreloadResource.java
similarity index 67%
rename from framework/src/main/java/org/toop/framework/asset/types/PreloadResource.java
rename to framework/src/main/java/org/toop/framework/resource/types/PreloadResource.java
index 07d213d..28f8734 100644
--- a/framework/src/main/java/org/toop/framework/asset/types/PreloadResource.java
+++ b/framework/src/main/java/org/toop/framework/resource/types/PreloadResource.java
@@ -1,19 +1,21 @@
-package org.toop.framework.asset.types;
+package org.toop.framework.resource.types;
-import org.toop.framework.asset.ResourceLoader;
+import org.toop.framework.resource.ResourceLoader;
/**
- * Marker interface for resources that should be **automatically loaded** by the {@link ResourceLoader}.
+ * Marker interface for resources that should be **automatically loaded** by the {@link
+ * ResourceLoader}.
*
- *
Extends {@link LoadableResource}, so any implementing class must provide the standard
- * {@link LoadableResource#load()} and {@link LoadableResource#unload()} methods, as well as the
- * {@link LoadableResource#isLoaded()} check.
+ *
Extends {@link LoadableResource}, so any implementing class must provide the standard {@link
+ * LoadableResource#load()} and {@link LoadableResource#unload()} methods, as well as the {@link
+ * LoadableResource#isLoaded()} check.
*
*
When a resource implements {@code PreloadResource}, the {@code ResourceLoader} will invoke
* {@link LoadableResource#load()} automatically after the resource is discovered and instantiated,
- * without requiring manual loading by the user.
+ * without requiring manual loading by the user.
+ *
+ *
Typical usage:
*
- *
Typical usage:
*
{@code
* public class MyFontAsset extends BaseResource implements PreloadResource {
* @Override
@@ -34,6 +36,6 @@ import org.toop.framework.asset.ResourceLoader;
* }
*
*
Note: Only use this interface for resources that are safe to load at startup, as it may
- * increase memory usage or startup time.
+ * increase memory usage or startup time.
*/
public interface PreloadResource extends LoadableResource {}
diff --git a/framework/src/main/java/org/toop/framework/settings/Settings.java b/framework/src/main/java/org/toop/framework/settings/Settings.java
index b1ed334..052107c 100644
--- a/framework/src/main/java/org/toop/framework/settings/Settings.java
+++ b/framework/src/main/java/org/toop/framework/settings/Settings.java
@@ -3,8 +3,8 @@ package org.toop.framework.settings;
public class Settings {
public boolean fullScreen = false;
public String locale = "en";
- public String theme = "dark";
- public String layoutSize = "medium";
+ public String theme = "dark";
+ public String layoutSize = "medium";
public int volume = 100;
public int fxVolume = 20;
public int musicVolume = 15;
diff --git a/framework/src/test/java/org/toop/framework/SnowflakeGeneratorTest.java b/framework/src/test/java/org/toop/framework/SnowflakeGeneratorTest.java
index eb88994..a5ee3ff 100644
--- a/framework/src/test/java/org/toop/framework/SnowflakeGeneratorTest.java
+++ b/framework/src/test/java/org/toop/framework/SnowflakeGeneratorTest.java
@@ -1,78 +1,79 @@
-package org.toop.framework;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-import java.util.HashSet;
-import java.util.Set;
-import org.junit.jupiter.api.Test;
-
-class SnowflakeGeneratorTest {
-
- @Test
- void testMachineIdWithinBounds() {
- SnowflakeGenerator generator = new SnowflakeGenerator();
- long machineIdField = getMachineId(generator);
- assertTrue(
- machineIdField >= 0 && machineIdField <= 1023,
- "Machine ID should be within 0-1023");
- }
-
- @Test
- void testNextIdReturnsUniqueValues() {
- SnowflakeGenerator generator = new SnowflakeGenerator();
- Set ids = new HashSet<>();
- for (int i = 0; i < 1000; i++) {
- long id = generator.nextId();
- assertFalse(ids.contains(id), "Duplicate ID generated");
- ids.add(id);
- }
- }
-
- @Test
- void testSequenceRollover() throws Exception {
- SnowflakeGenerator generator =
- new SnowflakeGenerator() {
- private long fakeTime = System.currentTimeMillis();
-
- protected long timestamp() {
- return fakeTime;
- }
-
- void incrementTime() {
- fakeTime++;
- }
- };
-
- long first = generator.nextId();
- long second = generator.nextId();
- assertNotEquals(
- first, second, "IDs generated within same millisecond should differ by sequence");
-
- // Force sequence overflow
- for (int i = 0; i < (1 << 12); i++) generator.nextId();
- long afterOverflow = generator.nextId();
- assertTrue(afterOverflow > second, "ID after sequence rollover should be greater");
- }
-
- @Test
- void testNextIdMonotonic() {
- SnowflakeGenerator generator = new SnowflakeGenerator();
- long prev = generator.nextId();
- for (int i = 0; i < 100; i++) {
- long next = generator.nextId();
- assertTrue(next > prev, "IDs must be increasing");
- prev = next;
- }
- }
-
- // Helper: reflectively get machineId
- private long getMachineId(SnowflakeGenerator generator) {
- try {
- var field = SnowflakeGenerator.class.getDeclaredField("machineId");
- field.setAccessible(true);
- return (long) field.get(generator);
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- }
-}
+//package org.toop.framework;
+//
+//import static org.junit.jupiter.api.Assertions.*;
+//
+//import java.util.HashSet;
+//import java.util.Set;
+//import org.junit.jupiter.api.Test;
+//
+//class SnowflakeGeneratorTest {
+//
+// @Test
+// void testMachineIdWithinBounds() {
+// SnowflakeGenerator generator = new SnowflakeGenerator();
+// long machineIdField = getMachineId(generator);
+// assertTrue(
+// machineIdField >= 0 && machineIdField <= 1023,
+// "Machine ID should be within 0-1023");
+// }
+//
+// @Test
+// void testNextIdReturnsUniqueValues() {
+// SnowflakeGenerator generator = new SnowflakeGenerator();
+// Set ids = new HashSet<>();
+// for (int i = 0; i < 1000; i++) {
+// long id = generator.nextId();
+// assertFalse(ids.contains(id), "Duplicate ID generated");
+// ids.add(id);
+// }
+// }
+//
+// @Test
+// void testSequenceRollover() throws Exception {
+// SnowflakeGenerator generator =
+// new SnowflakeGenerator() {
+// private long fakeTime = System.currentTimeMillis();
+//
+// protected long timestamp() {
+// return fakeTime;
+// }
+//
+// void incrementTime() {
+// fakeTime++;
+// }
+// };
+//
+// long first = generator.nextId();
+// long second = generator.nextId();
+// assertNotEquals(
+// first, second, "IDs generated within same millisecond should differ by sequence");
+//
+// // Force sequence overflow
+// for (int i = 0; i < (1 << 12); i++) generator.nextId();
+// long afterOverflow = generator.nextId();
+// assertTrue(afterOverflow > second, "ID after sequence rollover should be greater");
+// }
+//
+// @Test
+// void testNextIdMonotonic() {
+// SnowflakeGenerator generator = new SnowflakeGenerator();
+// long prev = generator.nextId();
+// for (int i = 0; i < 100; i++) {
+// long next = generator.nextId();
+// assertTrue(next > prev, "IDs must be increasing");
+// prev = next;
+// }
+// }
+//
+// // Helper: reflectively get machineId
+// private long getMachineId(SnowflakeGenerator generator) {
+// try {
+// var field = SnowflakeGenerator.class.getDeclaredField("machineId");
+// field.setAccessible(true);
+// return (long) field.get(generator);
+// } catch (Exception e) {
+// throw new RuntimeException(e);
+// }
+// }
+//}
+// TODO
\ No newline at end of file
diff --git a/framework/src/test/java/org/toop/framework/audio/MusicManagerTest.java b/framework/src/test/java/org/toop/framework/audio/MusicManagerTest.java
new file mode 100644
index 0000000..0cd0535
--- /dev/null
+++ b/framework/src/test/java/org/toop/framework/audio/MusicManagerTest.java
@@ -0,0 +1,190 @@
+package org.toop.framework.audio;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.toop.framework.dispatch.interfaces.Dispatcher;
+import org.toop.framework.resource.resources.BaseResource;
+import org.toop.framework.resource.types.AudioResource;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class MockAudioResource extends BaseResource implements AudioResource {
+ boolean played = false;
+ boolean stopped = false;
+ Runnable onEnd;
+ Runnable onError;
+
+ public MockAudioResource(String name) {
+ super(new File(name));
+ }
+
+ public void triggerError() {
+ if (onError != null) {
+ onError.run();
+ }
+ }
+
+ public void triggerEnd() {
+ if (onEnd != null) {
+ onEnd.run();
+ }
+ }
+
+ @Override
+ public String getName() {
+ return super.getFile().getName();
+ }
+
+ @Override
+ public void play() {
+ played = true;
+ }
+
+ @Override
+ public void stop() {
+ stopped = true;
+ }
+
+ @Override
+ public void setOnEnd(Runnable callback) {
+ onEnd = callback;
+ }
+
+ @Override
+ public void setOnError(Runnable callback) {
+ onError = callback;
+ }
+
+ @Override
+ public void updateVolume(double volume) {}
+}
+
+public class MusicManagerTest {
+
+ private Dispatcher dispatcher;
+ private MockAudioResource track1;
+ private MockAudioResource track2;
+ private MockAudioResource track3;
+ private MusicManager manager;
+
+ @BeforeEach
+ void setUp() {
+ dispatcher = Runnable::run;
+
+ track1 = new MockAudioResource("track1");
+ track2 = new MockAudioResource("track2");
+ track3 = new MockAudioResource("track3");
+
+ List resources = List.of(track1, track2, track3);
+
+ manager = new MusicManager<>(resources, dispatcher);
+ }
+
+ @Test
+ void testPlaySingleTrack() {
+ manager.play();
+ assertTrue(track1.played || track2.played || track3.played,
+ "At least one track should have played");
+ }
+
+ @Test
+ void testPlayMultipleTimesDoesNotRestart() {
+ manager.play();
+ track1.played = false;
+ manager.play();
+ assertFalse(track1.played, "Second play call should not restart tracks");
+ }
+
+ @Test
+ void testStopStopsAllTracks() {
+ manager.play();
+ manager.stop();
+ assertTrue(track1.stopped && track2.stopped && track3.stopped,
+ "All tracks should be stopped");
+ }
+
+ @Test
+ void testAutoAdvanceTracks() {
+ track1.played = false;
+ track2.played = false;
+ track3.played = false;
+
+ manager.play();
+ track1.triggerEnd();
+ track2.triggerEnd();
+
+ assertTrue(track1.played, "Track1 should play, played %s instead");
+ assertTrue(track2.played, "Track2 should play after track1 ends");
+ assertTrue(track3.played, "Track3 should play after track2 ends");
+ }
+
+ @Test
+ void testTrackErrorRemovesTrackAndPlaysNext() {
+ manager.play();
+ track1.triggerError();
+
+ assertFalse(manager.getActiveAudio().contains(track1),
+ "Track1 should be removed after error");
+ assertTrue(track2.played, "Track2 should play after track1 error");
+ }
+
+ @Test
+ void testPlayWithEmptyPlaylistDoesNothing() {
+ manager.getActiveAudio().clear();
+ manager.play();
+ assertFalse(track1.played || track2.played || track3.played,
+ "No tracks should play if playlist is empty");
+ }
+
+ @Test
+ void testMultiplePlayStopSequences() {
+ manager.play();
+ manager.stop();
+ manager.play();
+ assertTrue(track1.played || track2.played || track3.played,
+ "Tracks should play again after stopping");
+ }
+
+ @Test
+ void testPlayingIndexWrapsAround() {
+ track1.played = false;
+ track2.played = false;
+ track3.played = false;
+
+ manager.play();
+ track1.triggerEnd();
+ track2.triggerEnd();
+ track3.triggerEnd();
+
+ assertTrue(track1.played, "Track1 should play again after loop");
+ assertTrue(track2.played, "Track2 should play");
+ assertTrue(track3.played, "Track3 should play");
+ }
+
+ /**
+ * Test for many tracks playing sequentially one after another
+ */
+ @Test
+ void testSequentialMultipleTracks() {
+ List manyTracks = new ArrayList<>();
+ for (int i = 1; i <= 1_000; i++) {
+ manyTracks.add(new MockAudioResource("track" + i));
+ }
+
+ MusicManager multiManager = new MusicManager<>(manyTracks, dispatcher);
+
+ for (int i = 0; i < manyTracks.size() - 1; i++) {
+ multiManager.play();
+ manyTracks.get(i).triggerEnd();
+ }
+
+ for (int i = 0; i < manyTracks.size(); i++) {
+ assertTrue(manyTracks.get(i).played, "Track " + (i + 1) + " should have played sequentially");
+ }
+ }
+}
+
diff --git a/framework/src/test/java/org/toop/framework/eventbus/EventFlowStressTest.java b/framework/src/test/java/org/toop/framework/eventbus/EventFlowStressTest.java
index 7664c01..9a9ec76 100644
--- a/framework/src/test/java/org/toop/framework/eventbus/EventFlowStressTest.java
+++ b/framework/src/test/java/org/toop/framework/eventbus/EventFlowStressTest.java
@@ -2,7 +2,7 @@
//
// import org.junit.jupiter.api.Tag;
// import org.junit.jupiter.api.Test;
-// import org.toop.framework.eventbus.events.EventWithSnowflake;
+// import org.toop.framework.eventbus.events.UniqueEvent;
//
// import java.math.BigInteger;
// import java.util.concurrent.*;
@@ -13,7 +13,7 @@
// class EventFlowStressTest {
//
// /** Top-level record to ensure runtime type matches subscription */
-// public record HeavyEvent(String payload, long eventSnowflake) implements EventWithSnowflake {
+// public record HeavyEvent(String payload, long eventSnowflake) implements UniqueEvent {
// @Override
// public java.util.Map result() {
// return java.util.Map.of("payload", payload, "eventId", eventSnowflake);
@@ -26,7 +26,7 @@
// }
//
// public record HeavyEventSuccess(String payload, long eventSnowflake) implements
-// EventWithSnowflake {
+// UniqueEvent {
// @Override
// public java.util.Map result() {
// return java.util.Map.of("payload", payload, "eventId", eventSnowflake);
diff --git a/framework/src/test/java/org/toop/framework/eventbus/EventFlowTest.java b/framework/src/test/java/org/toop/framework/eventbus/EventFlowTest.java
index 26c50a6..0b6be20 100644
--- a/framework/src/test/java/org/toop/framework/eventbus/EventFlowTest.java
+++ b/framework/src/test/java/org/toop/framework/eventbus/EventFlowTest.java
@@ -1,92 +1,93 @@
-package org.toop.framework.eventbus;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-import java.util.HashSet;
-import java.util.Set;
-import java.util.concurrent.atomic.AtomicBoolean;
-import org.junit.jupiter.api.Test;
-import org.toop.framework.SnowflakeGenerator;
-import org.toop.framework.eventbus.events.EventWithSnowflake;
-
-class EventFlowTest {
-
- @Test
- void testSnowflakeStructure() {
- long id = new SnowflakeGenerator().nextId();
-
- long timestampPart = id >>> 22;
- long randomPart = id & ((1L << 22) - 1);
-
- assertTrue(timestampPart > 0, "Timestamp part should be non-zero");
- assertTrue(
- randomPart >= 0 && randomPart < (1L << 22), "Random part should be within 22 bits");
- }
-
- @Test
- void testSnowflakeMonotonicity() throws InterruptedException {
- SnowflakeGenerator sf = new SnowflakeGenerator();
- long id1 = sf.nextId();
- Thread.sleep(1); // ensure timestamp increases
- long id2 = sf.nextId();
-
- assertTrue(id2 > id1, "Later snowflake should be greater than earlier one");
- }
-
- @Test
- void testSnowflakeUniqueness() {
- SnowflakeGenerator sf = new SnowflakeGenerator();
- Set ids = new HashSet<>();
- for (int i = 0; i < 100_000; i++) {
- long id = sf.nextId();
- assertTrue(ids.add(id), "Snowflake IDs should be unique, but duplicate found");
- }
- }
-
- // --- Dummy Event classes for testing ---
- static class DummySnowflakeEvent implements EventWithSnowflake {
- private final long snowflake;
-
- DummySnowflakeEvent(long snowflake) {
- this.snowflake = snowflake;
- }
-
- @Override
- public long eventSnowflake() {
- return snowflake;
- }
-
- @Override
- public java.util.Map result() {
- return java.util.Collections.emptyMap();
- }
- }
-
- @Test
- void testSnowflakeIsInjectedIntoEvent() {
- EventFlow flow = new EventFlow();
- flow.addPostEvent(DummySnowflakeEvent.class); // no args, should auto-generate
-
- long id = flow.getEventSnowflake();
- assertNotEquals(-1, id, "Snowflake should be auto-generated");
- assertTrue(flow.getEvent() instanceof DummySnowflakeEvent);
- assertEquals(id, ((DummySnowflakeEvent) flow.getEvent()).eventSnowflake());
- }
-
- @Test
- void testOnResponseFiltersBySnowflake() {
- EventFlow flow = new EventFlow();
- flow.addPostEvent(DummySnowflakeEvent.class);
-
- AtomicBoolean handlerCalled = new AtomicBoolean(false);
- flow.onResponse(DummySnowflakeEvent.class, event -> handlerCalled.set(true));
-
- // Post with non-matching snowflake
- GlobalEventBus.post(new DummySnowflakeEvent(12345L));
- assertFalse(handlerCalled.get(), "Handler should not fire for mismatched snowflake");
-
- // Post with matching snowflake
- GlobalEventBus.post(new DummySnowflakeEvent(flow.getEventSnowflake()));
- assertTrue(handlerCalled.get(), "Handler should fire for matching snowflake");
- }
-}
+//package org.toop.framework.eventbus;
+//
+//import static org.junit.jupiter.api.Assertions.*;
+//
+//import java.util.HashSet;
+//import java.util.Set;
+//import java.util.concurrent.atomic.AtomicBoolean;
+//import org.junit.jupiter.api.Test;
+//import org.toop.framework.SnowflakeGenerator;
+//import org.toop.framework.eventbus.events.UniqueEvent;
+//
+//class EventFlowTest {
+//
+// @Test
+// void testSnowflakeStructure() {
+// long id = new SnowflakeGenerator().nextId();
+//
+// long timestampPart = id >>> 22;
+// long randomPart = id & ((1L << 22) - 1);
+//
+// assertTrue(timestampPart > 0, "Timestamp part should be non-zero");
+// assertTrue(
+// randomPart >= 0 && randomPart < (1L << 22), "Random part should be within 22 bits");
+// }
+//
+// @Test
+// void testSnowflakeMonotonicity() throws InterruptedException {
+// SnowflakeGenerator sf = new SnowflakeGenerator();
+// long id1 = sf.nextId();
+// Thread.sleep(1); // ensure timestamp increases
+// long id2 = sf.nextId();
+//
+// assertTrue(id2 > id1, "Later snowflake should be greater than earlier one");
+// }
+//
+// @Test
+// void testSnowflakeUniqueness() {
+// SnowflakeGenerator sf = new SnowflakeGenerator();
+// Set ids = new HashSet<>();
+// for (int i = 0; i < 100_000; i++) {
+// long id = sf.nextId();
+// assertTrue(ids.add(id), "Snowflake IDs should be unique, but duplicate found");
+// }
+// }
+//
+// // --- Dummy Event classes for testing ---
+// static class DummySnowflakeUniqueEvent implements UniqueEvent {
+// private final long snowflake;
+//
+// DummySnowflakeUniqueEvent(long snowflake) {
+// this.snowflake = snowflake;
+// }
+////
+//// @Override
+//// public long eventSnowflake() {
+//// return snowflake;
+//// }
+////
+//// @Override
+//// public java.util.Map result() {
+//// return java.util.Collections.emptyMap();
+//// }
+// }
+//
+// @Test
+// void testSnowflakeIsInjectedIntoEvent() {
+// EventFlow flow = new EventFlow();
+// flow.addPostEvent(DummySnowflakeUniqueEvent.class); // no args, should auto-generate
+//
+// long id = flow.getEventSnowflake();
+// assertNotEquals(-1, id, "Snowflake should be auto-generated");
+// assertTrue(flow.getEvent() instanceof DummySnowflakeUniqueEvent);
+// assertEquals(id, ((DummySnowflakeUniqueEvent) flow.getEvent()).eventSnowflake());
+// }
+//
+// @Test
+// void testOnResponseFiltersBySnowflake() {
+// EventFlow flow = new EventFlow();
+// flow.addPostEvent(DummySnowflakeUniqueEvent.class);
+//
+// AtomicBoolean handlerCalled = new AtomicBoolean(false);
+// flow.onResponse(DummySnowflakeUniqueEvent.class, event -> handlerCalled.set(true));
+//
+// // Post with non-matching snowflake
+// GlobalEventBus.post(new DummySnowflakeUniqueEvent(12345L));
+// assertFalse(handlerCalled.get(), "Handler should not fire for mismatched snowflake");
+//
+// // Post with matching snowflake
+// GlobalEventBus.post(new DummySnowflakeUniqueEvent(flow.getEventSnowflake()));
+// assertTrue(handlerCalled.get(), "Handler should fire for matching snowflake");
+// }
+//}
+// TODO
\ No newline at end of file
diff --git a/framework/src/test/java/org/toop/framework/eventbus/GlobalEventBusTest.java b/framework/src/test/java/org/toop/framework/eventbus/GlobalEventBusTest.java
index 4823003..a2535ee 100644
--- a/framework/src/test/java/org/toop/framework/eventbus/GlobalEventBusTest.java
+++ b/framework/src/test/java/org/toop/framework/eventbus/GlobalEventBusTest.java
@@ -1,159 +1,160 @@
-package org.toop.framework.eventbus;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-import java.util.concurrent.*;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Consumer;
-import org.junit.jupiter.api.*;
-import org.toop.framework.eventbus.events.EventType;
-import org.toop.framework.eventbus.events.EventWithSnowflake;
-
-class GlobalEventBusTest {
-
- // ------------------------------------------------------------------------
- // Test Events
- // ------------------------------------------------------------------------
- private record TestEvent(String message) implements EventType {}
-
- private record TestSnowflakeEvent(long eventSnowflake, String payload)
- implements EventWithSnowflake {
- @Override
- public java.util.Map result() {
- return java.util.Map.of("payload", payload);
- }
- }
-
- static class SampleEvent implements EventType {
- private final String message;
-
- SampleEvent(String message) {
- this.message = message;
- }
-
- public String message() {
- return message;
- }
- }
-
- @AfterEach
- void cleanup() {
- GlobalEventBus.reset();
- }
-
- // ------------------------------------------------------------------------
- // Subscriptions
- // ------------------------------------------------------------------------
- @Test
- void testSubscribeAndPost() {
- AtomicReference received = new AtomicReference<>();
- Consumer listener = e -> received.set(e.message());
-
- GlobalEventBus.subscribe(TestEvent.class, listener);
- GlobalEventBus.post(new TestEvent("hello"));
-
- assertEquals("hello", received.get());
- }
-
- @Test
- void testUnsubscribe() {
- GlobalEventBus.reset();
-
- AtomicBoolean called = new AtomicBoolean(false);
-
- // Subscribe and keep the wrapper reference
- Consumer super EventType> subscription =
- GlobalEventBus.subscribe(SampleEvent.class, e -> called.set(true));
-
- // Post once -> should trigger
- GlobalEventBus.post(new SampleEvent("test1"));
- assertTrue(called.get(), "Listener should be triggered before unsubscribe");
-
- // Reset flag
- called.set(false);
-
- // Unsubscribe using the wrapper reference
- GlobalEventBus.unsubscribe(subscription);
-
- // Post again -> should NOT trigger
- GlobalEventBus.post(new SampleEvent("test2"));
- assertFalse(called.get(), "Listener should not be triggered after unsubscribe");
- }
-
- @Test
- void testSubscribeGeneric() {
- AtomicReference received = new AtomicReference<>();
- Consumer