Merge remote-tracking branch 'origin/Development' into Reversi

# Conflicts:
#	app/src/main/java/org/toop/app/canvas/GameCanvas.java
#	app/src/main/java/org/toop/app/layer/layers/MainLayer.java
#	game/src/main/java/org/toop/game/Game.java
#	game/src/main/java/org/toop/game/othello/Othello.java
#	game/src/main/java/org/toop/game/othello/OthelloAI.java
This commit is contained in:
Ticho Hidding
2025-10-14 11:25:46 +02:00
107 changed files with 4507 additions and 3457 deletions

4
.gitignore vendored
View File

@@ -48,6 +48,8 @@ shelf/
*.ipr *.ipr
*.iws *.iws
misc.xml misc.xml
uiDesigner.xml
############################## ##############################
## Eclipse ## Eclipse
@@ -76,6 +78,8 @@ dist/
nbdist/ nbdist/
nbactions.xml nbactions.xml
nb-configuration.xml nb-configuration.xml
misc.xml
compiler.xml
############################## ##############################
## Visual Studio Code ## Visual Studio Code

22
.idea/compiler.xml generated
View File

@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="pism_framework" />
<module name="pism_game" />
<module name="pism_app" />
</profile>
</annotationProcessing>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="pism_app" options="" />
<module name="pism_framework" options="" />
<module name="pism_game" options="" />
</option>
</component>
</project>

View File

@@ -8,11 +8,13 @@
<w>flushnl</w> <w>flushnl</w>
<w>gaaf</w> <w>gaaf</w>
<w>gamelist</w> <w>gamelist</w>
<w>pism</w>
<w>playerlist</w> <w>playerlist</w>
<w>tictactoe</w> <w>tictactoe</w>
<w>toop</w> <w>toop</w>
<w>vmoptions</w> <w>vmoptions</w>
<w>xplugin</w> <w>xplugin</w>
<w>yourturn</w>
</words> </words>
</dictionary> </dictionary>
</component> </component>

4
.idea/encodings.xml generated
View File

@@ -1,12 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="Encoding"> <component name="Encoding">
<file url="file://$APPLICATION_HOME_DIR$/bin/src/main/java" charset="UTF-8" />
<file url="file://$APPLICATION_HOME_DIR$/bin/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/app/src/main/java" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/app/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/app/src/main/resources" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/app/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/framework/src/main/java" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/framework/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/framework/src/main/resources" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/framework/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/game/src/main/java" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/game/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/game/src/main/resources" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/game/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/processors/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/processors/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component> </component>

View File

@@ -2,7 +2,8 @@
<profile version="1.0"> <profile version="1.0">
<option name="myName" value="Project Default" /> <option name="myName" value="Project Default" />
<inspection_tool class="AutoCloseableResource" enabled="true" level="WARNING" enabled_by_default="true"> <inspection_tool class="AutoCloseableResource" enabled="true" level="WARNING" enabled_by_default="true">
<option name="METHOD_MATCHER_CONFIG" value="java.util.Formatter,format,java.io.Writer,append,com.google.common.base.Preconditions,checkNotNull,org.hibernate.Session,close,java.io.PrintWriter,printf,java.io.PrintStream,printf,java.lang.foreign.Arena,ofAuto,java.lang.foreign.Arena,global,org.toop.framework.audio.AudioPlayer,play" /> <option name="METHOD_MATCHER_CONFIG" value="java.util.Formatter,format,java.io.Writer,append,com.google.common.base.Preconditions,checkNotNull,org.hibernate.Session,close,java.io.PrintWriter,printf,java.io.PrintStream,printf,java.lang.foreign.Arena,ofAuto,java.lang.foreign.Arena,global,org.toop.framework.audio.AudioPlayer,play,java.util.Map,remove" />
</inspection_tool> </inspection_tool>
<inspection_tool class="WriteOnlyObject" enabled="false" level="WARNING" enabled_by_default="false" />
</profile> </profile>
</component> </component>

19
.idea/misc.xml generated
View File

@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EntryPointsManager">
<list size="1">
<item index="0" class="java.lang.String" itemvalue="com.google.common.eventbus.Subscribe" />
</list>
</component>
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" default="true" project-jdk-name="openjdk-25" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View File

@@ -1,8 +1,13 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"> <project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>org.toop</groupId> <parent>
<artifactId>pism_app</artifactId> <groupId>org.toop</groupId>
<artifactId>pism</artifactId>
<version>0.1</version>
</parent>
<artifactId>app</artifactId>
<version>0.1</version> <version>0.1</version>
<properties> <properties>
@@ -24,24 +29,36 @@
<artifactId>gson</artifactId> <artifactId>gson</artifactId>
<version>2.10.1</version> <version>2.10.1</version>
</dependency> </dependency>
<dependency>
<groupId>org.toop</groupId>
<artifactId>pism_framework</artifactId>
<version>0.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.toop</groupId>
<artifactId>pism_game</artifactId>
<version>0.1</version>
<scope>compile</scope>
</dependency>
<dependency> <dependency>
<groupId>org.openjfx</groupId> <groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId> <artifactId>javafx-controls</artifactId>
<version>25</version> <version>25</version>
</dependency> </dependency>
<dependency>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>2.42.0</version>
</dependency>
<dependency>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_annotations</artifactId>
<version>2.42.0</version>
</dependency>
<dependency>
<groupId>org.toop</groupId>
<artifactId>framework</artifactId>
<version>0.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.toop</groupId>
<artifactId>game</artifactId>
<version>0.1</version>
<scope>compile</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>
@@ -112,14 +129,56 @@
</java> </java>
</configuration> </configuration>
</plugin> </plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<configuration> <version>3.14.1</version>
<source>25</source> <configuration>
<target>25</target> <showWarnings>true</showWarnings>
</configuration> <fork>true</fork>
</plugin> <executable>${java.home}/bin/javac</executable>
<source>25</source>
<target>25</target>
<release>25</release>
<encoding>UTF-8</encoding>
<compilerArgs>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>
<arg>
-Xplugin:ErrorProne \
</arg>
<!-- TODO-->
<!-- -Xep:RestrictedApi:ERROR \-->
<!-- -XepOpt:RestrictedApi:annotation=org.toop.annotations.TestsOnly \-->
<!-- -XepOpt:RestrictedApi:allowlistRegex=(?s).*/src/test/java/.*|.*test\.java \-->
<!-- -XepOpt:RestrictedApi:message=This API is marked @TestsOnly and shouldn't be normally used.-->
<arg>-XDcompilePolicy=simple</arg>
<arg>--should-stop=ifError=FLOW</arg>
</compilerArgs>
<annotationProcessorPaths>
<path>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>2.42.0</version>
</path>
</annotationProcessorPaths>
</configuration>
<dependencies>
<dependency>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>2.42.0</version>
</dependency>
</dependencies>
</plugin>
</plugins> </plugins>
</build> </build>
</project> </project>

View File

@@ -1,21 +1,42 @@
package org.toop; package org.toop;
import org.toop.app.App; import org.toop.app.App;
import org.toop.framework.asset.ResourceLoader; import org.toop.framework.audio.*;
import org.toop.framework.asset.ResourceManager;
import org.toop.framework.audio.SoundManager;
import org.toop.framework.networking.NetworkingClientManager; import org.toop.framework.networking.NetworkingClientManager;
import org.toop.framework.networking.NetworkingInitializationException; import org.toop.framework.networking.NetworkingInitializationException;
import org.toop.framework.resource.ResourceLoader;
import org.toop.framework.resource.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 final class Main {
public static void main(String[] args) { static void main(String[] args) {
initSystems(); initSystems();
App.run(args); App.run(args);
} }
private static void initSystems() throws NetworkingInitializationException { private static void initSystems() throws NetworkingInitializationException {
ResourceManager.loadAssets(new ResourceLoader("app/src/main/resources/assets")); ResourceManager.loadAssets(new ResourceLoader("app/src/main/resources/assets"));
new Thread(NetworkingClientManager::new).start(); new Thread(NetworkingClientManager::new).start();
new Thread(SoundManager::new).start(); new Thread(() -> {
} MusicManager<MusicAsset> musicManager = new MusicManager<>(ResourceManager.getAllOfTypeAndRemoveWrapper(MusicAsset.class));
} SoundEffectManager<SoundEffectAsset> 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();
}
}

View File

@@ -1,163 +1,170 @@
package org.toop.app; package org.toop.app;
import javafx.application.Platform; import java.util.Stack;
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 javafx.application.Application; import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.stage.Stage; 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 org.toop.local.AppSettings;
import java.util.Stack;
public final class App extends Application { public final class App extends Application {
private static Stage stage; private static Stage stage;
private static Scene scene; private static Scene scene;
private static StackPane root; private static StackPane root;
private static Stack<Layer> stack; private static Stack<Layer> stack;
private static int height; private static int height;
private static int width; private static int width;
private static boolean isQuitting; private static boolean isQuitting;
public static void run(String[] args) { public static void run(String[] args) {
launch(args); launch(args);
} }
@Override @Override
public void start(Stage stage) throws Exception { public void start(Stage stage) throws Exception {
final StackPane root = new StackPane(); final StackPane root = new StackPane();
final Scene scene = new Scene(root); final Scene scene = new Scene(root);
stage.setTitle(AppContext.getString("appTitle")); stage.setTitle(AppContext.getString("appTitle"));
stage.setWidth(1080); stage.setWidth(1080);
stage.setHeight(720); stage.setHeight(720);
stage.setOnCloseRequest(event -> { stage.setOnCloseRequest(
event.consume(); event -> {
event.consume();
if (!isQuitting) { if (!isQuitting) {
quitPopup(); quitPopup();
} }
}); });
stage.setScene(scene); stage.setScene(scene);
stage.setResizable(false); stage.setResizable(false);
stage.show(); stage.show();
App.stage = stage; App.stage = stage;
App.scene = scene; App.scene = scene;
App.root = root; App.root = root;
App.stack = new Stack<>(); App.stack = new Stack<>();
App.width = (int) stage.getWidth(); App.width = (int) stage.getWidth();
App.height = (int) stage.getHeight(); App.height = (int) stage.getHeight();
App.isQuitting = false; App.isQuitting = false;
final AppSettings settings = new AppSettings(); new EventFlow().addPostEvent(new AudioEvents.StartBackgroundMusic()).postEvent();
settings.applySettings();
new EventFlow().addPostEvent(new AudioEvents.StartBackgroundMusic()).asyncPostEvent(); final AppSettings settings = new AppSettings();
activate(new MainLayer()); settings.applySettings();
}
public static void activate(Layer layer) { activate(new MainLayer());
Platform.runLater(() -> { }
popAll();
push(layer);
});
}
public static void push(Layer layer) { public static void activate(Layer layer) {
Platform.runLater(() -> { Platform.runLater(
root.getChildren().addLast(layer.getLayer()); () -> {
stack.push(layer); popAll();
}); push(layer);
} });
}
public static void pop() { public static void push(Layer layer) {
Platform.runLater(() -> { Platform.runLater(
root.getChildren().removeLast(); () -> {
stack.pop(); 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() { isQuitting = false;
Platform.runLater(() -> { });
final int childrenCount = root.getChildren().size(); }
for (int i = 0; i < childrenCount; i++) { public static void popAll() {
try { Platform.runLater(
root.getChildren().removeLast(); () -> {
} catch (Exception e) { final int childrenCount = root.getChildren().size();
IO.println(e);
}
}
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() { stack.removeAllElements();
Platform.runLater(() -> { });
push(new QuitPopup()); }
isQuitting = true;
});
}
public static void quit() { public static void quitPopup() {
stage.close(); Platform.runLater(
} () -> {
push(new QuitPopup());
isQuitting = true;
});
}
public static void reloadAll() { public static void quit() {
stage.setTitle(AppContext.getString("appTitle")); new EventFlow().addPostEvent(new AudioEvents.StopAudioManager()).postEvent();
stage.close();
}
for (final Layer layer : stack) { public static void reloadAll() {
layer.reload(); stage.setTitle(AppContext.getString("appTitle"));
}
}
public static void setFullscreen(boolean fullscreen) { for (final Layer layer : stack) {
stage.setFullScreen(fullscreen); layer.reload();
}
}
width = (int) stage.getWidth(); public static void setFullscreen(boolean fullscreen) {
height = (int) stage.getHeight(); stage.setFullScreen(fullscreen);
reloadAll(); width = (int) stage.getWidth();
} height = (int) stage.getHeight();
public static void setStyle(String theme, String layoutSize) { reloadAll();
final int stylesCount = scene.getStylesheets().size(); }
for (int i = 0; i < stylesCount; i++) { public static void setStyle(String theme, String layoutSize) {
scene.getStylesheets().removeLast(); final int stylesCount = scene.getStylesheets().size();
}
scene.getStylesheets().add(ResourceManager.<CssAsset>get(theme + ".css").getUrl()); for (int i = 0; i < stylesCount; i++) {
scene.getStylesheets().add(ResourceManager.<CssAsset>get(layoutSize + ".css").getUrl()); scene.getStylesheets().removeLast();
}
reloadAll(); scene.getStylesheets().add(ResourceManager.<CssAsset>get(theme + ".css").getUrl());
} scene.getStylesheets().add(ResourceManager.<CssAsset>get(layoutSize + ".css").getUrl());
public static int getWidth() { reloadAll();
return width; }
}
public static int getHeight() { public static int getWidth() {
return height; return width;
} }
}
public static int getHeight() {
return height;
}
}

View File

@@ -1,6 +1,10 @@
package org.toop.app; package org.toop.app;
public record GameInformation(String[] playerName, boolean[] isPlayerHuman, public record GameInformation(
int[] computerDifficulty, int[] computerThinkTime, String[] playerName,
boolean isConnectionLocal, String serverIP, String serverPort) { boolean[] isPlayerHuman,
} int[] computerDifficulty,
int[] computerThinkTime,
boolean isConnectionLocal,
String serverIP,
String serverPort) {}

View File

@@ -1,99 +1,106 @@
package org.toop.app.canvas; package org.toop.app.canvas;
import java.util.function.Consumer;
import javafx.scene.canvas.Canvas; import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext; import javafx.scene.canvas.GraphicsContext;
import javafx.scene.input.MouseButton; import javafx.scene.input.MouseButton;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import java.util.function.Consumer;
public abstract class GameCanvas { 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 Canvas canvas;
protected final GraphicsContext graphics; protected final GraphicsContext graphics;
protected final Color color; protected final Color color;
protected int width; protected int width;
protected int height; protected int height;
protected final int rows; protected final int rows;
protected final int columns; protected final int columns;
protected final int gapSize; protected final int gapSize;
protected final boolean edges; 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<Integer> onCellClicked) { protected GameCanvas(
canvas = new Canvas(width, height); Color color,
graphics = canvas.getGraphicsContext2D(); int width,
int height,
int rows,
int columns,
int gapSize,
boolean edges,
Consumer<Integer> onCellClicked) {
canvas = new Canvas(width, height);
graphics = canvas.getGraphicsContext2D();
this.color = color; this.color = color;
this.width = width; this.width = width;
this.height = height; this.height = height;
this.rows = rows; this.rows = rows;
this.columns = columns; this.columns = columns;
this.gapSize = gapSize; this.gapSize = gapSize;
this.edges = edges; this.edges = edges;
cells = new Cell[rows * columns]; cells = new Cell[rows * columns];
final float cellWidth = ((float) width - (rows - 1) * gapSize) / rows; final float cellWidth = ((float) width - (rows - 1) * gapSize) / rows;
final float cellHeight = ((float) height - (columns - 1) * gapSize) / columns; final float cellHeight = ((float) height - (columns - 1) * gapSize) / columns;
for (int y = 0; y < columns; y++) { for (int y = 0; y < columns; y++) {
final float startY = y * cellHeight + y * gapSize; final float startY = y * cellHeight + y * gapSize;
for (int x = 0; x < rows; x++) { for (int x = 0; x < rows; x++) {
final float startX = x * cellWidth + x * gapSize; final float startX = x * cellWidth + x * gapSize;
cells[y * rows + x] = new Cell(startX, startY, cellWidth, cellHeight); cells[y * rows + x] = new Cell(startX, startY, cellWidth, cellHeight);
} }
} }
canvas.setOnMouseClicked(event -> { canvas.setOnMouseClicked(
if (event.getButton() != MouseButton.PRIMARY) { event -> {
return; if (event.getButton() != MouseButton.PRIMARY) {
} return;
}
final int column = (int) ((event.getX() / width) * rows); final int column = (int) ((event.getX() / width) * rows);
final int row = (int) ((event.getY() / height) * columns); final int row = (int) ((event.getY() / height) * columns);
event.consume(); event.consume();
onCellClicked.accept(row * rows + column); onCellClicked.accept(row * rows + column);
}); });
render(); render();
} }
public void clear() { public void clear() {
graphics.clearRect(0, 0, width, height); graphics.clearRect(0, 0, width, height);
} }
public void render() { public void render() {
graphics.setFill(color); graphics.setFill(color);
for (int x = 1; x < rows; x++) { for (int x = 1; x < rows; x++) {
graphics.fillRect(cells[x].x() - gapSize, 0, gapSize, height); graphics.fillRect(cells[x].x() - gapSize, 0, gapSize, height);
} }
for (int y = 1; y < columns; y++) { for (int y = 1; y < columns; y++) {
graphics.fillRect(0, cells[y * rows].y() - gapSize, width, gapSize); graphics.fillRect(0, cells[y * rows].y() - gapSize, width, gapSize);
} }
if (edges) { if (edges) {
graphics.fillRect(-gapSize, 0, gapSize, height); graphics.fillRect(-gapSize, 0, gapSize, height);
graphics.fillRect(0, -gapSize, width, gapSize); graphics.fillRect(0, -gapSize, width, gapSize);
graphics.fillRect(width - gapSize, 0, gapSize, height); graphics.fillRect(width - gapSize, 0, gapSize, height);
graphics.fillRect(0, height - gapSize, width, gapSize); graphics.fillRect(0, height - gapSize, width, gapSize);
} }
} }
public void drawDot(Color color, int cell) { public void drawDot(Color color, int cell) {
final float x = cells[cell].x() + gapSize; final float x = cells[cell].x() + gapSize;
@@ -106,18 +113,29 @@ public abstract class GameCanvas {
graphics.fillOval(x, y, width, height); graphics.fillOval(x, y, width, height);
} }
public void resize(int width, int height) { public void draw(Color color, int cell) {
canvas.setWidth(width); final float x = cells[cell].x() + gapSize;
canvas.setHeight(height); final float y = cells[cell].y() + gapSize;
this.width = width; final float width = cells[cell].width() - gapSize * 2;
this.height = height; final float height = cells[cell].height() - gapSize * 2;
clear(); graphics.setFill(color);
render(); graphics.fillRect(x, y, width, height);
} }
public Canvas getCanvas() { public void resize(int width, int height) {
return canvas; canvas.setWidth(width);
} canvas.setHeight(height);
}
this.width = width;
this.height = height;
clear();
render();
}
public Canvas getCanvas() {
return canvas;
}
}

View File

@@ -1,38 +1,37 @@
package org.toop.app.canvas; package org.toop.app.canvas;
import java.util.function.Consumer;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import java.util.function.Consumer;
public class TicTacToeCanvas extends GameCanvas { public class TicTacToeCanvas extends GameCanvas {
public TicTacToeCanvas(Color color, int width, int height, Consumer<Integer> onCellClicked) { public TicTacToeCanvas(Color color, int width, int height, Consumer<Integer> onCellClicked) {
super(color, width, height, 3, 3, 10, false, onCellClicked); super(color, width, height, 3, 3, 10, false, onCellClicked);
} }
public void drawX(Color color, int cell) { public void drawX(Color color, int cell) {
graphics.setStroke(color); graphics.setStroke(color);
graphics.setLineWidth(gapSize); graphics.setLineWidth(gapSize);
final float x = cells[cell].x() + gapSize; final float x = cells[cell].x() + gapSize;
final float y = cells[cell].y() + gapSize; final float y = cells[cell].y() + gapSize;
final float width = cells[cell].width() - gapSize * 2; final float width = cells[cell].width() - gapSize * 2;
final float height = cells[cell].height() - gapSize * 2; final float height = cells[cell].height() - gapSize * 2;
graphics.strokeLine(x, y, x + width, y + height); graphics.strokeLine(x, y, x + width, y + height);
graphics.strokeLine(x + width, y, x, y + height); graphics.strokeLine(x + width, y, x, y + height);
} }
public void drawO(Color color, int cell) { public void drawO(Color color, int cell) {
graphics.setStroke(color); graphics.setStroke(color);
graphics.setLineWidth(gapSize); graphics.setLineWidth(gapSize);
final float x = cells[cell].x() + gapSize; final float x = cells[cell].x() + gapSize;
final float y = cells[cell].y() + gapSize; final float y = cells[cell].y() + gapSize;
final float width = cells[cell].width() - gapSize * 2; final float width = cells[cell].width() - gapSize * 2;
final float height = cells[cell].height() - gapSize * 2; final float height = cells[cell].height() - gapSize * 2;
graphics.strokeOval(x, y, width, height); graphics.strokeOval(x, y, width, height);
} }
} }

View File

@@ -4,8 +4,9 @@ import javafx.scene.Node;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
public abstract class Container { public abstract class Container {
public abstract Region getContainer(); public abstract Region getContainer();
public abstract void addNodes(Node... nodes); public abstract void addNodes(Node... nodes);
public abstract void addContainer(Container container, boolean fill);
} public abstract void addContainer(Container container, boolean fill);
}

View File

@@ -1,81 +1,86 @@
package org.toop.app.layer; package org.toop.app.layer;
import org.toop.app.App;
import org.toop.app.canvas.GameCanvas;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import org.toop.app.App;
import org.toop.app.canvas.GameCanvas;
public abstract class Layer { public abstract class Layer {
protected StackPane layer; protected StackPane layer;
protected Region background; protected Region background;
protected Layer(String... backgroundStyles) { protected Layer(String... backgroundStyles) {
layer = new StackPane(); layer = new StackPane();
background = new Region(); background = new Region();
background.getStyleClass().addAll(backgroundStyles); background.getStyleClass().addAll(backgroundStyles);
background.setPrefSize(Double.MAX_VALUE, Double.MAX_VALUE); 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) { protected void addContainer(
StackPane.setAlignment(container.getContainer(), position); 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 widthUnit = App.getWidth() / 100.0;
final double heightUnit = App.getHeight() / 100.0; final double heightUnit = App.getHeight() / 100.0;
if (widthPercent > 0) { if (widthPercent > 0) {
container.getContainer().setMaxWidth(widthPercent * widthUnit); container.getContainer().setMaxWidth(widthPercent * widthUnit);
} else { } else {
container.getContainer().setMaxWidth(Region.USE_PREF_SIZE); container.getContainer().setMaxWidth(Region.USE_PREF_SIZE);
} }
if (heightPercent > 0) { if (heightPercent > 0) {
container.getContainer().setMaxHeight(heightPercent * heightUnit); container.getContainer().setMaxHeight(heightPercent * heightUnit);
} else { } else {
container.getContainer().setMaxHeight(Region.USE_PREF_SIZE); container.getContainer().setMaxHeight(Region.USE_PREF_SIZE);
} }
container.getContainer().setTranslateX(xOffset * widthUnit); container.getContainer().setTranslateX(xOffset * widthUnit);
container.getContainer().setTranslateY(yOffset * heightUnit); 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) { protected void addGameCanvas(GameCanvas canvas, Pos position, int xOffset, int yOffset) {
StackPane.setAlignment(canvas.getCanvas(), position); StackPane.setAlignment(canvas.getCanvas(), position);
final double widthUnit = App.getWidth() / 100.0; final double widthUnit = App.getWidth() / 100.0;
final double heightUnit = App.getHeight() / 100.0; final double heightUnit = App.getHeight() / 100.0;
canvas.getCanvas().setTranslateX(xOffset * widthUnit); canvas.getCanvas().setTranslateX(xOffset * widthUnit);
canvas.getCanvas().setTranslateY(yOffset * heightUnit); canvas.getCanvas().setTranslateY(yOffset * heightUnit);
layer.getChildren().addLast(canvas.getCanvas()); layer.getChildren().addLast(canvas.getCanvas());
} }
protected void pop() { protected void pop() {
if (layer.getChildren().size() <= 1) { if (layer.getChildren().size() <= 1) {
return; return;
} }
layer.getChildren().removeLast(); layer.getChildren().removeLast();
} }
protected void popAll() { protected void popAll() {
final int containers = layer.getChildren().size(); final int containers = layer.getChildren().size();
for (int i = 1; i < containers; i++) { for (int i = 1; i < containers; i++) {
layer.getChildren().removeLast(); layer.getChildren().removeLast();
} }
} }
public StackPane getLayer() { public StackPane getLayer() {
return layer; return layer;
} }
public abstract void reload(); public abstract void reload();
} }

View File

@@ -1,131 +1,140 @@
package org.toop.app.layer; package org.toop.app.layer;
import org.toop.framework.audio.events.AudioEvents; import java.util.function.Consumer;
import org.toop.framework.eventbus.EventFlow;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Orientation; import javafx.geometry.Orientation;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.text.Text; import javafx.scene.text.Text;
import org.toop.framework.audio.events.AudioEvents;
import java.util.function.Consumer; import org.toop.framework.eventbus.EventFlow;
public final class NodeBuilder { public final class NodeBuilder {
public static void addCss(Node node, String... cssClasses) { public static void addCss(Node node, String... cssClasses) {
node.getStyleClass().addAll(cssClasses); node.getStyleClass().addAll(cssClasses);
} }
public static void setCss(Node node, String... cssClasses) { public static void setCss(Node node, String... cssClasses) {
node.getStyleClass().removeAll(); node.getStyleClass().removeAll();
node.getStyleClass().addAll(cssClasses); node.getStyleClass().addAll(cssClasses);
} }
public static Text header(String x) { public static Text header(String x) {
final Text element = new Text(x); final Text element = new Text(x);
setCss(element, "text-primary", "text-header"); setCss(element, "text-primary", "text-header");
return element; return element;
} }
public static Text text(String x) { public static Text text(String x) {
final Text element = new Text(x); final Text element = new Text(x);
setCss(element, "text-secondary", "text-normal"); setCss(element, "text-secondary", "text-normal");
return element; return element;
} }
public static Label button(String x, Runnable runnable) { public static Label button(String x, Runnable runnable) {
final Label element = new Label(x); final Label element = new Label(x);
setCss(element, "button", "text-normal"); setCss(element, "button", "text-normal");
element.setOnMouseClicked(_ -> { element.setOnMouseClicked(
new EventFlow().addPostEvent(new AudioEvents.ClickButton()).asyncPostEvent(); _ -> {
runnable.run(); new EventFlow().addPostEvent(new AudioEvents.ClickButton()).asyncPostEvent();
}); runnable.run();
});
return element; return element;
} }
public static Label toggle(String x1, String x2, boolean toggled, Consumer<Boolean> consumer) { public static Label toggle(String x1, String x2, boolean toggled, Consumer<Boolean> consumer) {
final Label element = new Label(toggled ? x2 : x1); final Label element = new Label(toggled ? x2 : x1);
setCss(element, "toggle", "text-normal"); setCss(element, "toggle", "text-normal");
final BooleanProperty checked = new SimpleBooleanProperty(toggled); final BooleanProperty checked = new SimpleBooleanProperty(toggled);
element.setOnMouseClicked(_ -> { element.setOnMouseClicked(
new EventFlow().addPostEvent(new AudioEvents.ClickButton()).asyncPostEvent(); _ -> {
checked.set(!checked.get()); new EventFlow().addPostEvent(new AudioEvents.ClickButton()).asyncPostEvent();
checked.set(!checked.get());
if (checked.get()) { if (checked.get()) {
element.setText(x1); element.setText(x1);
} else { } else {
element.setText(x2); element.setText(x2);
} }
consumer.accept(checked.get()); consumer.accept(checked.get());
}); });
return element; return element;
} }
public static Slider slider(int max, int initial, Consumer<Integer> consumer) { public static Slider slider(int max, int initial, Consumer<Integer> consumer) {
final Slider element = new Slider(0, max, initial); final Slider element = new Slider(0, max, initial);
setCss(element, "bg-slider-track"); setCss(element, "bg-slider-track");
element.setMinorTickCount(0); element.setMinorTickCount(0);
element.setMajorTickUnit(1); element.setMajorTickUnit(1);
element.setBlockIncrement(1); element.setBlockIncrement(1);
element.setSnapToTicks(true); element.setSnapToTicks(true);
element.setShowTickLabels(true); element.setShowTickLabels(true);
element.setOnMouseClicked(_ -> { element.setOnMouseClicked(
new EventFlow().addPostEvent(new AudioEvents.ClickButton()).asyncPostEvent(); _ -> {
}); new EventFlow().addPostEvent(new AudioEvents.ClickButton()).asyncPostEvent();
});
element.valueProperty().addListener((_, _, newValue) -> { element.valueProperty()
consumer.accept(newValue.intValue()); .addListener(
}); (_, _, newValue) -> {
consumer.accept(newValue.intValue());
});
return element; return element;
} }
public static TextField input(String x, Consumer<String> consumer) { public static TextField input(String x, Consumer<String> consumer) {
final TextField element = new TextField(x); final TextField element = new TextField(x);
setCss(element, "input", "text-normal"); setCss(element, "input", "text-normal");
element.setOnMouseClicked(_ -> { element.setOnMouseClicked(
new EventFlow().addPostEvent(new AudioEvents.ClickButton()).asyncPostEvent(); _ -> {
}); new EventFlow().addPostEvent(new AudioEvents.ClickButton()).asyncPostEvent();
});
element.textProperty().addListener((_, _, newValue) -> { element.textProperty()
consumer.accept(newValue); .addListener(
}); (_, _, newValue) -> {
consumer.accept(newValue);
});
return element; return element;
} }
public static <T> ChoiceBox<T> choiceBox(Consumer<T> consumer) { public static <T> ChoiceBox<T> choiceBox(Consumer<T> consumer) {
final ChoiceBox<T> element = new ChoiceBox<>(); final ChoiceBox<T> element = new ChoiceBox<>();
setCss(element, "choice-box", "text-normal"); setCss(element, "choice-box", "text-normal");
element.setOnMouseClicked(_ -> { element.setOnMouseClicked(
new EventFlow().addPostEvent(new AudioEvents.ClickButton()).asyncPostEvent(); _ -> {
}); new EventFlow().addPostEvent(new AudioEvents.ClickButton()).asyncPostEvent();
});
element.valueProperty().addListener((_, _, newValue) -> { element.valueProperty()
consumer.accept(newValue); .addListener(
}); (_, _, newValue) -> {
consumer.accept(newValue);
});
return element; return element;
} }
public static Separator separator() { public static Separator separator() {
final Separator element = new Separator(Orientation.HORIZONTAL); final Separator element = new Separator(Orientation.HORIZONTAL);
setCss(element, "separator"); setCss(element, "separator");
return element; return element;
} }
} }

View File

@@ -3,17 +3,18 @@ package org.toop.app.layer;
import org.toop.app.App; import org.toop.app.App;
public abstract class Popup extends Layer { public abstract class Popup extends Layer {
protected Popup(boolean popOnBackground, String... backgroundStyles) { protected Popup(boolean popOnBackground, String... backgroundStyles) {
super(backgroundStyles); super(backgroundStyles);
if (popOnBackground) { if (popOnBackground) {
background.setOnMouseClicked(_ -> { background.setOnMouseClicked(
App.pop(); _ -> {
}); App.pop();
} });
} }
}
protected Popup(boolean popOnBackground) { protected Popup(boolean popOnBackground) {
this(popOnBackground, "bg-popup"); this(popOnBackground, "bg-popup");
} }
} }

View File

@@ -1,60 +1,59 @@
package org.toop.app.layer.containers; package org.toop.app.layer.containers;
import org.toop.app.layer.Container;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import org.toop.app.layer.Container;
public final class HorizontalContainer extends Container { public final class HorizontalContainer extends Container {
private final HBox container; private final HBox container;
public HorizontalContainer(int spacing, String... cssClasses) { public HorizontalContainer(int spacing, String... cssClasses) {
container = new HBox(spacing); container = new HBox(spacing);
container.getStyleClass().addAll(cssClasses); container.getStyleClass().addAll(cssClasses);
} }
public HorizontalContainer(int spacing) { public HorizontalContainer(int spacing) {
this(spacing, "container"); this(spacing, "container");
} }
@Override @Override
public Region getContainer() { public Region getContainer() {
return container; return container;
} }
@Override @Override
public void addNodes(Node... nodes) { public void addNodes(Node... nodes) {
container.getChildren().addAll(nodes); container.getChildren().addAll(nodes);
} }
@Override @Override
public void addContainer(Container container, boolean fill) { public void addContainer(Container container, boolean fill) {
if (fill) { if (fill) {
container.getContainer().setMinSize(0, 0); container.getContainer().setMinSize(0, 0);
container.getContainer().setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); container.getContainer().setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
HBox.setHgrow(container.getContainer(), Priority.ALWAYS); HBox.setHgrow(container.getContainer(), Priority.ALWAYS);
} else { } else {
container.getContainer().setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); 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) { if (fill) {
balanceChildWidths(); balanceChildWidths();
} }
} }
private void balanceChildWidths() { private void balanceChildWidths() {
final ObservableList<Node> children = container.getChildren(); final ObservableList<Node> children = container.getChildren();
final double widthPerChild = container.getWidth() / children.size(); final double widthPerChild = container.getWidth() / children.size();
for (final Node child : children) { for (final Node child : children) {
if (child instanceof Region) { if (child instanceof Region) {
((Region) child).setPrefWidth(widthPerChild); ((Region) child).setPrefWidth(widthPerChild);
} }
} }
} }
} }

View File

@@ -1,60 +1,59 @@
package org.toop.app.layer.containers; package org.toop.app.layer.containers;
import org.toop.app.layer.Container;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import org.toop.app.layer.Container;
public final class VerticalContainer extends Container { public final class VerticalContainer extends Container {
private final VBox container; private final VBox container;
public VerticalContainer(int spacing, String... cssClasses) { public VerticalContainer(int spacing, String... cssClasses) {
container = new VBox(spacing); container = new VBox(spacing);
container.getStyleClass().addAll(cssClasses); container.getStyleClass().addAll(cssClasses);
} }
public VerticalContainer(int spacing) { public VerticalContainer(int spacing) {
this(spacing, "container"); this(spacing, "container");
} }
@Override @Override
public Region getContainer() { public Region getContainer() {
return container; return container;
} }
@Override @Override
public void addNodes(Node... nodes) { public void addNodes(Node... nodes) {
container.getChildren().addAll(nodes); container.getChildren().addAll(nodes);
} }
@Override @Override
public void addContainer(Container container, boolean fill) { public void addContainer(Container container, boolean fill) {
if (fill) { if (fill) {
container.getContainer().setMinSize(0, 0); container.getContainer().setMinSize(0, 0);
container.getContainer().setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); container.getContainer().setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
VBox.setVgrow(container.getContainer(), Priority.ALWAYS); VBox.setVgrow(container.getContainer(), Priority.ALWAYS);
} else { } else {
container.getContainer().setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); 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) { if (fill) {
balanceChildHeights(); balanceChildHeights();
} }
} }
private void balanceChildHeights() { private void balanceChildHeights() {
final ObservableList<Node> children = container.getChildren(); final ObservableList<Node> children = container.getChildren();
final double heightPerChild = container.getHeight() / children.size(); final double heightPerChild = container.getHeight() / children.size();
for (final Node child : children) { for (final Node child : children) {
if (child instanceof Region) { if (child instanceof Region) {
((Region) child).setPrefHeight(heightPerChild); ((Region) child).setPrefHeight(heightPerChild);
} }
} }
} }
} }

View File

@@ -1,6 +1,14 @@
package org.toop.app.layer.layers; 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.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.App;
import org.toop.app.GameInformation; import org.toop.app.GameInformation;
import org.toop.app.layer.Container; 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.app.layer.layers.game.TicTacToeLayer;
import org.toop.framework.eventbus.EventFlow; import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.networking.events.NetworkEvents; 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 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 { 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 static class ChallengePopup extends Popup {
private final GameInformation information; private final GameInformation information;
private final String challenger; private final String challenger;
private final String game; private final String game;
private final long clientID; private final long clientID;
private final int challengeID; private final int challengeID;
public ChallengePopup(GameInformation information, String challenger, String game, long clientID, String challengeID) { public ChallengePopup(
super(false, "bg-popup"); GameInformation information,
String challenger,
String game,
long clientID,
String challengeID) {
super(false, "bg-popup");
this.information = information; this.information = information;
this.challenger = challenger; this.challenger = challenger;
this.game = game; this.game = game;
this.clientID = clientID; this.clientID = clientID;
this.challengeID = Integer.parseInt(challengeID.substring(18, challengeID.length() - 2)); this.challengeID =
Integer.parseInt(challengeID.substring(18, challengeID.length() - 2));
reload(); reload();
} }
@Override @Override
public void reload() { public void reload() {
popAll(); popAll();
final var challengeText = NodeBuilder.header(AppContext.getString("challengeText")); final var challengeText = NodeBuilder.header(AppContext.getString("challengeText"));
final var challengerNameText = NodeBuilder.header(challenger); final var challengerNameText = NodeBuilder.header(challenger);
final var gameText = NodeBuilder.text(AppContext.getString("gameIsText")); final var gameText = NodeBuilder.text(AppContext.getString("gameIsText"));
final var gameNameText = NodeBuilder.text(game); final var gameNameText = NodeBuilder.text(game);
final var acceptButton = NodeBuilder.button(AppContext.getString("accept"), () -> { final var acceptButton =
pollTimer.cancel(); NodeBuilder.button(
AppContext.getString("accept"),
() -> {
pollTimer.cancel();
new EventFlow().addPostEvent(new NetworkEvents.SendAcceptChallenge(clientID, challengeID)).postEvent(); new EventFlow()
App.activate(new TicTacToeLayer(information, clientID)); .addPostEvent(
}); new NetworkEvents.SendAcceptChallenge(
clientID, challengeID))
.postEvent();
App.activate(new TicTacToeLayer(information, clientID));
});
final var denyButton = NodeBuilder.button(AppContext.getString("deny"), () -> { final var denyButton =
App.pop(); NodeBuilder.button(
}); AppContext.getString("deny"),
() -> {
App.pop();
});
final Container controlContainer = new HorizontalContainer(30); final Container controlContainer = new HorizontalContainer(30);
controlContainer.addNodes(acceptButton, denyButton); controlContainer.addNodes(acceptButton, denyButton);
final Container mainContainer = new VerticalContainer(30); final Container mainContainer = new VerticalContainer(30);
mainContainer.addNodes(challengeText, challengerNameText); mainContainer.addNodes(challengeText, challengerNameText);
mainContainer.addNodes(gameText, gameNameText); 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; GameInformation information;
long clientId; long clientId;
String user; String user;
List<String> onlinePlayers = new CopyOnWriteArrayList<>(); List<String> onlinePlayers = new CopyOnWriteArrayList<>();
public ConnectedLayer(GameInformation information) { public ConnectedLayer(GameInformation information) {
super("bg-primary"); super("bg-primary");
this.information = information; this.information = information;
new EventFlow() new EventFlow()
.addPostEvent(NetworkEvents.StartClient.class, information.serverIP(), Integer.parseInt(information.serverPort())) .addPostEvent(
.onResponse(NetworkEvents.StartClientResponse.class, e -> { NetworkEvents.StartClient.class,
clientId = e.clientId(); information.serverIP(),
user = information.playerName()[0].replaceAll("\\s+", ""); 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); Thread popThread = new Thread(this::populatePlayerList);
popThread.setDaemon(false); popThread.setDaemon(false);
popThread.start(); popThread.start();
}).postEvent(); })
.postEvent();
new EventFlow().listen(this::handleReceivedChallenge); new EventFlow().listen(this::handleReceivedChallenge);
reload(); reload();
} }
private void populatePlayerList() { private void populatePlayerList() {
EventFlow sendGetPlayerList = new EventFlow().addPostEvent(new NetworkEvents.SendGetPlayerlist(this.clientId)); EventFlow sendGetPlayerList =
new EventFlow().listen(NetworkEvents.PlayerlistResponse.class, e -> { new EventFlow().addPostEvent(new NetworkEvents.SendGetPlayerlist(this.clientId));
if (e.clientId() == this.clientId) { new EventFlow()
List<String> playerList = new java.util.ArrayList<>(List.of(e.playerlist())); // TODO: Garbage, but works .listen(
playerList.removeIf(name -> name.equalsIgnoreCase(user)); NetworkEvents.PlayerlistResponse.class,
if (this.onlinePlayers != playerList) { e -> {
this.onlinePlayers.clear(); if (e.clientId() == this.clientId) {
this.onlinePlayers.addAll(playerList); List<String> 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() { TimerTask task =
public void run() { new TimerTask() {
sendGetPlayerList.postEvent(); public void run() {
Platform.runLater(() -> reload()); 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) { private void sendChallenge(String oppUsername, String gameType) {
final AtomicInteger challengeId = new AtomicInteger(-1); final AtomicInteger challengeId = new AtomicInteger(-1);
if (onlinePlayers.contains(oppUsername)) { if (onlinePlayers.contains(oppUsername)) {
new EventFlow().addPostEvent(new NetworkEvents.SendChallenge(this.clientId, oppUsername, gameType)) new EventFlow()
.listen(NetworkEvents.ChallengeResponse.class, e -> { .addPostEvent(
challengeId.set(Integer.parseInt(e.challengeId().substring(18, e.challengeId().length() - 2))); new NetworkEvents.SendChallenge(this.clientId, oppUsername, gameType))
}) .listen(
.listen(NetworkEvents.GameMatchResponse.class, e -> { NetworkEvents.ChallengeResponse.class,
if (e.clientId() == this.clientId) { e -> {
pollTimer.cancel(); challengeId.set(
App.activate(new TicTacToeLayer(information, this.clientId)); Integer.parseInt(
} e.challengeId()
}, false).postEvent(); .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) { private void handleReceivedChallenge(NetworkEvents.ChallengeResponse response) {
App.push(new ChallengePopup(information, response.challengerName(), response.gameType(), clientId, response.challengeId())); App.push(
} new ChallengePopup(
information,
response.challengerName(),
response.gameType(),
clientId,
response.challengeId()));
}
@Override @Override
public void reload() { public void reload() {
popAll(); popAll();
ListView<Label> players = new ListView<>(); ListView<Label> players = new ListView<>();
for (int i = 0; i < onlinePlayers.size(); i++) { for (int i = 0; i < onlinePlayers.size(); i++) {
int finalI = i; int finalI = i;
players.getItems().add(NodeBuilder.button(onlinePlayers.get(i), () -> { players.getItems()
String clickedPlayer = onlinePlayers.get(finalI); .add(
sendChallenge(clickedPlayer, "tic-tac-toe"); NodeBuilder.button(
})); onlinePlayers.get(i),
} () -> {
String clickedPlayer = onlinePlayers.get(finalI);
sendChallenge(clickedPlayer, "tic-tac-toe");
}));
}
final Container playersContainer = new VerticalContainer(10); final Container playersContainer = new VerticalContainer(10);
playersContainer.addNodes(players); playersContainer.addNodes(players);
addContainer(playersContainer, Pos.CENTER, 0, 0, 0, 0); addContainer(playersContainer, Pos.CENTER, 0, 0, 0, 0);
} }
} }

View File

@@ -1,5 +1,10 @@
package org.toop.app.layer.layers; package org.toop.app.layer.layers;
import javafx.animation.PauseTransition;
import javafx.animation.TranslateTransition;
import javafx.geometry.Pos;
import javafx.scene.text.Text;
import javafx.util.Duration;
import org.toop.app.App; import org.toop.app.App;
import org.toop.app.layer.Container; import org.toop.app.layer.Container;
import org.toop.app.layer.NodeBuilder; import org.toop.app.layer.NodeBuilder;
@@ -8,65 +13,61 @@ import org.toop.app.layer.containers.HorizontalContainer;
import org.toop.app.layer.containers.VerticalContainer; import org.toop.app.layer.containers.VerticalContainer;
import org.toop.local.AppContext; import org.toop.local.AppContext;
import javafx.animation.PauseTransition;
import javafx.animation.TranslateTransition;
import javafx.geometry.Pos;
import javafx.scene.text.Text;
import javafx.util.Duration;
public final class CreditsPopup extends Popup { public final class CreditsPopup extends Popup {
private final int lineHeight = 100; private final int lineHeight = 100;
public CreditsPopup() { public CreditsPopup() {
super(true, "bg-primary"); super(true, "bg-primary");
reload(); reload();
} }
@Override @Override
public void reload() { public void reload() {
popAll(); popAll();
final String[] credits = { final String[] credits = {
AppContext.getString("scrumMaster") + ": Stef", AppContext.getString("scrumMaster") + ": Stef",
AppContext.getString("productOwner") + ": Omar", AppContext.getString("productOwner") + ": Omar",
AppContext.getString("mergeCommander") + ": Bas", AppContext.getString("mergeCommander") + ": Bas",
AppContext.getString("localization") + ": Ticho", AppContext.getString("localization") + ": Ticho",
AppContext.getString("ai") + ": Michiel", AppContext.getString("ai") + ": Michiel",
AppContext.getString("developers") + ": Michiel, Bas, Stef, Omar, Ticho", AppContext.getString("developers") + ": Michiel, Bas, Stef, Omar, Ticho",
AppContext.getString("moralSupport") + ": Wesley", AppContext.getString("moralSupport") + ": Wesley",
AppContext.getString("opengl") + ": Omar" AppContext.getString("opengl") + ": Omar"
}; };
final Text[] creditsHeaders = new Text[credits.length]; final Text[] creditsHeaders = new Text[credits.length];
for (int i = 0; i < credits.length; i++) { for (int i = 0; i < credits.length; i++) {
creditsHeaders[i] = NodeBuilder.header(credits[i]); creditsHeaders[i] = NodeBuilder.header(credits[i]);
} }
final Container creditsContainer = new HorizontalContainer(0); final Container creditsContainer = new HorizontalContainer(0);
final Container animatedContainer = new VerticalContainer(lineHeight); final Container animatedContainer = new VerticalContainer(lineHeight);
creditsContainer.addContainer(animatedContainer, true); creditsContainer.addContainer(animatedContainer, true);
animatedContainer.addNodes(creditsHeaders); animatedContainer.addNodes(creditsHeaders);
addContainer(creditsContainer, Pos.CENTER, 0, 0, 50, 100); addContainer(creditsContainer, Pos.CENTER, 0, 0, 50, 100);
playCredits(animatedContainer, App.getHeight()); playCredits(animatedContainer, App.getHeight());
} }
private void playCredits(Container container, double sceneLength) { private void playCredits(Container container, double sceneLength) {
container.getContainer().setTranslateY(-sceneLength); container.getContainer().setTranslateY(-sceneLength);
final TranslateTransition scrollCredits = new TranslateTransition(Duration.seconds(20), container.getContainer()); final TranslateTransition scrollCredits =
scrollCredits.setFromY(-sceneLength - lineHeight); new TranslateTransition(Duration.seconds(20), container.getContainer());
scrollCredits.setToY(sceneLength + lineHeight); scrollCredits.setFromY(-sceneLength - lineHeight);
scrollCredits.setToY(sceneLength + lineHeight);
scrollCredits.setOnFinished(_ -> { scrollCredits.setOnFinished(
final PauseTransition pauseCredits = new PauseTransition(Duration.seconds(3)); _ -> {
pauseCredits.setOnFinished(_ -> playCredits(container, sceneLength)); final PauseTransition pauseCredits = new PauseTransition(Duration.seconds(3));
pauseCredits.play(); pauseCredits.setOnFinished(_ -> playCredits(container, sceneLength));
}); pauseCredits.play();
});
scrollCredits.play(); scrollCredits.play();
} }
} }

View File

@@ -1,52 +1,69 @@
package org.toop.app.layer.layers; package org.toop.app.layer.layers;
import javafx.geometry.Pos;
import org.toop.app.App; import org.toop.app.App;
import org.toop.app.layer.Container; import org.toop.app.layer.Container;
import org.toop.app.layer.Layer; import org.toop.app.layer.Layer;
import org.toop.app.layer.NodeBuilder; import org.toop.app.layer.NodeBuilder;
import org.toop.app.layer.containers.VerticalContainer; import org.toop.app.layer.containers.VerticalContainer;
import org.toop.framework.audio.VolumeControl;
import org.toop.framework.audio.events.AudioEvents;
import org.toop.framework.eventbus.EventFlow;
import org.toop.app.layer.layers.game.ReversiLayer; import org.toop.app.layer.layers.game.ReversiLayer;
import org.toop.local.AppContext; import org.toop.local.AppContext;
import javafx.geometry.Pos;
public final class MainLayer extends Layer { public final class MainLayer extends Layer {
public MainLayer() { public MainLayer() {
super("bg-primary"); super("bg-primary");
reload(); reload();
} }
@Override @Override
public void reload() { public void reload() {
popAll(); popAll();
final var tictactoeButton = NodeBuilder.button(AppContext.getString("tictactoe"), () -> { final var tictactoeButton =
App.activate(new MultiplayerLayer()); NodeBuilder.button(
AppContext.getString("tictactoe"),
() -> {
App.activate(new MultiplayerLayer());
});
final var othelloButton =
NodeBuilder.button(
AppContext.getString("othello"),
() -> {
App.activate(new ReversiLayer());
}); });
final var othelloButton = NodeBuilder.button(AppContext.getString("othello"), () -> { final var creditsButton =
App.activate(new ReversiLayer()); NodeBuilder.button(
}); AppContext.getString("credits"),
() -> {
App.push(new CreditsPopup());
});
final var creditsButton = NodeBuilder.button(AppContext.getString("credits"), () -> { final var optionsButton =
App.push(new CreditsPopup()); NodeBuilder.button(
}); AppContext.getString("options"),
() -> {
App.push(new OptionsPopup());
});
final var optionsButton = NodeBuilder.button(AppContext.getString("options"), () -> { final var quitButton =
App.push(new OptionsPopup()); NodeBuilder.button(
}); AppContext.getString("quit"),
() -> {
App.quitPopup();
});
final var quitButton = NodeBuilder.button(AppContext.getString("quit"), () -> { final Container gamesContainer = new VerticalContainer(5);
App.quitPopup(); gamesContainer.addNodes(tictactoeButton, othelloButton);
});
final Container gamesContainer = new VerticalContainer(5); final Container controlContainer = new VerticalContainer(5);
gamesContainer.addNodes(tictactoeButton, othelloButton); controlContainer.addNodes(creditsButton, optionsButton, quitButton);
final Container controlContainer = new VerticalContainer(5); addContainer(gamesContainer, Pos.TOP_LEFT, 2, 2, 20, 0);
controlContainer.addNodes(creditsButton, optionsButton, quitButton); addContainer(controlContainer, Pos.BOTTOM_LEFT, 2, -2, 20, 0);
}
addContainer(gamesContainer, Pos.TOP_LEFT, 2, 2, 20, 0);
addContainer(controlContainer, Pos.BOTTOM_LEFT, 2, -2, 20, 0);
}
} }

View File

@@ -1,5 +1,7 @@
package org.toop.app.layer.layers; package org.toop.app.layer.layers;
import java.time.LocalDateTime;
import javafx.geometry.Pos;
import org.toop.app.App; import org.toop.app.App;
import org.toop.app.GameInformation; import org.toop.app.GameInformation;
import org.toop.app.layer.Container; import org.toop.app.layer.Container;
@@ -10,167 +12,230 @@ import org.toop.app.layer.containers.VerticalContainer;
import org.toop.app.layer.layers.game.TicTacToeLayer; import org.toop.app.layer.layers.game.TicTacToeLayer;
import org.toop.local.AppContext; import org.toop.local.AppContext;
import javafx.geometry.Pos;
import java.time.LocalDateTime;
public final class MultiplayerLayer extends Layer { public final class MultiplayerLayer extends Layer {
private boolean isConnectionLocal = true; private boolean isConnectionLocal = true;
private boolean isPlayer1Human = true; private boolean isPlayer1Human = true;
private String player1Name = ""; private String player1Name = "";
private int computer1Difficulty = 0; private int computer1Difficulty = 0;
private int computer1ThinkTime = 0; private int computer1ThinkTime = 0;
private boolean isPlayer2Human = true; private boolean isPlayer2Human = true;
private String player2Name = ""; private String player2Name = "";
private int computer2Difficulty = 0; private int computer2Difficulty = 0;
private int computer2ThinkTime = 0; private int computer2ThinkTime = 0;
private String serverIP = ""; private String serverIP = "";
private String serverPort = ""; private String serverPort = "";
public MultiplayerLayer() { public MultiplayerLayer() {
super("bg-primary"); super("bg-primary");
reload(); reload();
} }
@Override @Override
public void reload() { public void reload() {
popAll(); popAll();
final Container player1Container = new VerticalContainer(20); final Container player1Container = new VerticalContainer(20);
final Container player2Container = new VerticalContainer(20); final Container player2Container = new VerticalContainer(20);
final var isPlayer1HumanToggle = NodeBuilder.toggle(AppContext.getString("human"), AppContext.getString("computer"), !isPlayer1Human, (computer) -> { final var isPlayer1HumanToggle =
isPlayer1Human = !computer; NodeBuilder.toggle(
reload(); AppContext.getString("human"),
}); AppContext.getString("computer"),
!isPlayer1Human,
(computer) -> {
isPlayer1Human = !computer;
reload();
});
player1Container.addNodes(isPlayer1HumanToggle); player1Container.addNodes(isPlayer1HumanToggle);
if (isPlayer1Human) { if (isPlayer1Human) {
final var playerNameText = NodeBuilder.text(AppContext.getString("playerName")); final var playerNameText = NodeBuilder.text(AppContext.getString("playerName"));
final var playerNameInput = NodeBuilder.input(player1Name, (name) -> { final var playerNameInput =
player1Name = name; NodeBuilder.input(
}); player1Name,
(name) -> {
player1Name = name;
});
player1Container.addNodes(playerNameText, playerNameInput); player1Container.addNodes(playerNameText, playerNameInput);
} else { } else {
player1Name = "Pism Bot V" + LocalDateTime.now().getSecond(); player1Name = "Pism Bot V" + LocalDateTime.now().getSecond();
final var computerNameText = NodeBuilder.text(player1Name); final var computerNameText = NodeBuilder.text(player1Name);
final var computerNameSeparator = NodeBuilder.separator(); final var computerNameSeparator = NodeBuilder.separator();
final var computerDifficultyText = NodeBuilder.text(AppContext.getString("computerDifficulty")); final var computerDifficultyText =
final var computerDifficultySeparator = NodeBuilder.separator(); NodeBuilder.text(AppContext.getString("computerDifficulty"));
final var computerDifficultySlider = NodeBuilder.slider(10, computer1Difficulty, (difficulty) -> final var computerDifficultySeparator = NodeBuilder.separator();
computer1Difficulty = difficulty); final var computerDifficultySlider =
NodeBuilder.slider(
10,
computer1Difficulty,
(difficulty) -> computer1Difficulty = difficulty);
final var computerThinkTimeText = NodeBuilder.text(AppContext.getString("computerThinkTime")); final var computerThinkTimeText =
final var computerThinkTimeSlider = NodeBuilder.slider(5, computer1ThinkTime, (thinkTime) -> NodeBuilder.text(AppContext.getString("computerThinkTime"));
computer1ThinkTime = thinkTime); final var computerThinkTimeSlider =
NodeBuilder.slider(
5, computer1ThinkTime, (thinkTime) -> computer1ThinkTime = thinkTime);
player1Container.addNodes(computerNameText, computerNameSeparator, player1Container.addNodes(
computerDifficultyText, computerDifficultySlider, computerDifficultySeparator, computerNameText,
computerThinkTimeText, computerThinkTimeSlider); computerNameSeparator,
} computerDifficultyText,
computerDifficultySlider,
computerDifficultySeparator,
computerThinkTimeText,
computerThinkTimeSlider);
}
if (isConnectionLocal) { if (isConnectionLocal) {
final var isPlayer2HumanToggle = NodeBuilder.toggle(AppContext.getString("human"), AppContext.getString("computer"), !isPlayer2Human, (computer) -> { final var isPlayer2HumanToggle =
isPlayer2Human = !computer; NodeBuilder.toggle(
reload(); AppContext.getString("human"),
}); AppContext.getString("computer"),
!isPlayer2Human,
(computer) -> {
isPlayer2Human = !computer;
reload();
});
player2Container.addNodes(isPlayer2HumanToggle); player2Container.addNodes(isPlayer2HumanToggle);
if (isPlayer2Human) { if (isPlayer2Human) {
final var playerNameText = NodeBuilder.text(AppContext.getString("playerName")); final var playerNameText = NodeBuilder.text(AppContext.getString("playerName"));
final var playerNameInput = NodeBuilder.input(player2Name, (name) -> { final var playerNameInput =
player2Name = name; NodeBuilder.input(
}); player2Name,
(name) -> {
player2Name = name;
});
player2Container.addNodes(playerNameText, playerNameInput); player2Container.addNodes(playerNameText, playerNameInput);
} else { } else {
player2Name = "Pism Bot V" + LocalDateTime.now().getSecond(); player2Name = "Pism Bot V" + LocalDateTime.now().getSecond();
final var computerNameText = NodeBuilder.text(player2Name); final var computerNameText = NodeBuilder.text(player2Name);
final var computerNameSeparator = NodeBuilder.separator(); final var computerNameSeparator = NodeBuilder.separator();
final var computerDifficultyText = NodeBuilder.text(AppContext.getString("computerDifficulty")); final var computerDifficultyText =
final var computerDifficultySeparator = NodeBuilder.separator(); NodeBuilder.text(AppContext.getString("computerDifficulty"));
final var computerDifficultySlider = NodeBuilder.slider(10, computer2Difficulty, (difficulty) -> final var computerDifficultySeparator = NodeBuilder.separator();
computer2Difficulty = difficulty); final var computerDifficultySlider =
NodeBuilder.slider(
10,
computer2Difficulty,
(difficulty) -> computer2Difficulty = difficulty);
final var computerThinkTimeText = NodeBuilder.text(AppContext.getString("computerThinkTime")); final var computerThinkTimeText =
final var computerThinkTimeSlider = NodeBuilder.slider(5, computer2ThinkTime, (thinkTime) -> NodeBuilder.text(AppContext.getString("computerThinkTime"));
computer2ThinkTime = thinkTime); final var computerThinkTimeSlider =
NodeBuilder.slider(
5,
computer2ThinkTime,
(thinkTime) -> computer2ThinkTime = thinkTime);
player2Container.addNodes(computerNameText, computerNameSeparator, player2Container.addNodes(
computerDifficultyText, computerDifficultySlider, computerDifficultySeparator, computerNameText,
computerThinkTimeText, computerThinkTimeSlider); computerNameSeparator,
} computerDifficultyText,
} else { computerDifficultySlider,
final var serverIPText = NodeBuilder.text(AppContext.getString("serverIP")); computerDifficultySeparator,
final var serverIPSeparator = NodeBuilder.separator(); computerThinkTimeText,
final var serverIPInput = NodeBuilder.input(serverIP, (ip) -> { computerThinkTimeSlider);
serverIP = ip; }
}); } else {
final var serverIPText = NodeBuilder.text(AppContext.getString("serverIP"));
final var serverIPSeparator = NodeBuilder.separator();
final var serverIPInput =
NodeBuilder.input(
serverIP,
(ip) -> {
serverIP = ip;
});
final var serverPortText = NodeBuilder.text(AppContext.getString("serverPort")); final var serverPortText = NodeBuilder.text(AppContext.getString("serverPort"));
final var serverPortInput = NodeBuilder.input(serverPort, (port) -> { final var serverPortInput =
serverPort = port; NodeBuilder.input(
}); serverPort,
(port) -> {
serverPort = port;
});
player2Container.addNodes(serverIPText, serverIPInput, serverIPSeparator, player2Container.addNodes(
serverPortText, serverPortInput); serverIPText,
} serverIPInput,
serverIPSeparator,
serverPortText,
serverPortInput);
}
final var versusText = NodeBuilder.header("VS"); final var versusText = NodeBuilder.header("VS");
final var connectionTypeText = NodeBuilder.text(AppContext.getString("connectionType") + ":"); final var connectionTypeText =
final var connectionTypeToggle = NodeBuilder.toggle(AppContext.getString("local"), AppContext.getString("server"), !isConnectionLocal, (server) -> { NodeBuilder.text(AppContext.getString("connectionType") + ":");
isConnectionLocal = !server; final var connectionTypeToggle =
reload(); NodeBuilder.toggle(
}); AppContext.getString("local"),
AppContext.getString("server"),
!isConnectionLocal,
(server) -> {
isConnectionLocal = !server;
reload();
});
final var playButton = NodeBuilder.button(isConnectionLocal ? AppContext.getString("start") : AppContext.getString("connect"), () -> { final var playButton =
final var information = new GameInformation( NodeBuilder.button(
new String[]{player1Name, player2Name}, isConnectionLocal
new boolean[]{isPlayer1Human, isPlayer2Human}, ? AppContext.getString("start")
new int[]{computer1Difficulty, computer2Difficulty}, : AppContext.getString("connect"),
new int[]{computer1ThinkTime, computer2ThinkTime}, () -> {
isConnectionLocal, serverIP, serverPort); final var information =
new GameInformation(
new String[] {player1Name, player2Name},
new boolean[] {isPlayer1Human, isPlayer2Human},
new int[] {computer1Difficulty, computer2Difficulty},
new int[] {computer1ThinkTime, computer2ThinkTime},
isConnectionLocal,
serverIP,
serverPort);
if (isConnectionLocal) { if (isConnectionLocal) {
App.activate(new TicTacToeLayer(information)); App.activate(new TicTacToeLayer(information));
} else { } else {
App.activate(new ConnectedLayer(information)); App.activate(new ConnectedLayer(information));
} }
}); });
final Container mainContainer = new VerticalContainer(10); final Container mainContainer = new VerticalContainer(10);
final Container playersContainer = new HorizontalContainer(5); final Container playersContainer = new HorizontalContainer(5);
final Container connectionTypeContainer = new HorizontalContainer(10); final Container connectionTypeContainer = new HorizontalContainer(10);
mainContainer.addContainer(playersContainer, true); mainContainer.addContainer(playersContainer, true);
mainContainer.addContainer(connectionTypeContainer, false); mainContainer.addContainer(connectionTypeContainer, false);
mainContainer.addNodes(playButton); mainContainer.addNodes(playButton);
connectionTypeContainer.addNodes(connectionTypeText, connectionTypeToggle); connectionTypeContainer.addNodes(connectionTypeText, connectionTypeToggle);
playersContainer.addContainer(player1Container, true); playersContainer.addContainer(player1Container, true);
playersContainer.addNodes(versusText); playersContainer.addNodes(versusText);
playersContainer.addContainer(player2Container, true); playersContainer.addContainer(player2Container, true);
final var backButton = NodeBuilder.button(AppContext.getString("back"), () -> { final var backButton =
App.activate(new MainLayer()); NodeBuilder.button(
}); AppContext.getString("back"),
() -> {
App.activate(new MainLayer());
});
final Container controlContainer = new VerticalContainer(0); final Container controlContainer = new VerticalContainer(0);
controlContainer.addNodes(backButton); controlContainer.addNodes(backButton);
addContainer(mainContainer, Pos.CENTER, 0, 0, 75, 75); addContainer(mainContainer, Pos.CENTER, 0, 0, 75, 75);
addContainer(controlContainer, Pos.BOTTOM_LEFT, 2, -2, 0, 0); addContainer(controlContainer, Pos.BOTTOM_LEFT, 2, -2, 0, 0);
} }
} }

View File

@@ -1,42 +1,41 @@
package org.toop.app.layer.layers; package org.toop.app.layer.layers;
import java.util.Locale;
import javafx.geometry.Pos;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import org.toop.app.App; import org.toop.app.App;
import org.toop.app.layer.Container; import org.toop.app.layer.Container;
import org.toop.app.layer.NodeBuilder; import org.toop.app.layer.NodeBuilder;
import org.toop.app.layer.Popup; import org.toop.app.layer.Popup;
import org.toop.app.layer.containers.VerticalContainer; import org.toop.app.layer.containers.VerticalContainer;
import org.toop.framework.asset.resources.SettingsAsset; import org.toop.framework.audio.VolumeControl;
import org.toop.framework.audio.events.AudioEvents; import org.toop.framework.audio.events.AudioEvents;
import org.toop.framework.eventbus.EventFlow; import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.resource.resources.SettingsAsset;
import org.toop.local.AppContext; import org.toop.local.AppContext;
import org.toop.local.AppSettings; import org.toop.local.AppSettings;
import javafx.geometry.Pos;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import java.util.Locale;
public final class OptionsPopup extends Popup { public final class OptionsPopup extends Popup {
AppSettings appSettings = new AppSettings(); AppSettings appSettings = new AppSettings();
SettingsAsset settings = appSettings.getPath(); SettingsAsset settings = appSettings.getPath();
private boolean isWindowed = !(settings.getFullscreen()); private boolean isWindowed = !(settings.getFullscreen());
public OptionsPopup() { public OptionsPopup() {
super(true, "bg-primary"); super(true, "bg-primary");
reload(); reload();
} }
@Override @Override
public void reload() { public void reload() {
popAll(); popAll();
final var languageHeader = NodeBuilder.header(AppContext.getString("language")); final var languageHeader = NodeBuilder.header(AppContext.getString("language"));
final var languageSeparator = NodeBuilder.separator(); final var languageSeparator = NodeBuilder.separator();
final var volumeHeader = NodeBuilder.header(AppContext.getString("volume")); final var volumeHeader = NodeBuilder.header(AppContext.getString("volume"));
final var volumeSeparator = NodeBuilder.separator(); final var volumeSeparator = NodeBuilder.separator();
final var fxVolumeHeader = NodeBuilder.header(AppContext.getString("effectsVolume")); final var fxVolumeHeader = NodeBuilder.header(AppContext.getString("effectsVolume"));
final var fxVolumeSeparator = NodeBuilder.separator(); final var fxVolumeSeparator = NodeBuilder.separator();
@@ -44,151 +43,181 @@ public final class OptionsPopup extends Popup {
final var musicVolumeHeader = NodeBuilder.header(AppContext.getString("musicVolume")); final var musicVolumeHeader = NodeBuilder.header(AppContext.getString("musicVolume"));
final var musicVolumeSeparator = NodeBuilder.separator(); final var musicVolumeSeparator = NodeBuilder.separator();
final var themeHeader = NodeBuilder.header(AppContext.getString("theme")); final var themeHeader = NodeBuilder.header(AppContext.getString("theme"));
final var themeSeparator = NodeBuilder.separator(); final var themeSeparator = NodeBuilder.separator();
final var layoutSizeHeader = NodeBuilder.header(AppContext.getString("layoutSize")); final var layoutSizeHeader = NodeBuilder.header(AppContext.getString("layoutSize"));
final var layoutSizeSeparator = NodeBuilder.separator(); final var layoutSizeSeparator = NodeBuilder.separator();
final var optionsContainer = new VerticalContainer(5); final var optionsContainer = new VerticalContainer(5);
optionsContainer.addNodes(languageHeader, languageChoiceBox(), languageSeparator); optionsContainer.addNodes(languageHeader, languageChoiceBox(), languageSeparator);
optionsContainer.addNodes(volumeHeader, volumeSlider(), volumeSeparator); optionsContainer.addNodes(volumeHeader, volumeSlider(), volumeSeparator);
optionsContainer.addNodes(fxVolumeHeader, fxVolumeSlider(), fxVolumeSeparator); optionsContainer.addNodes(fxVolumeHeader, fxVolumeSlider(), fxVolumeSeparator);
optionsContainer.addNodes(musicVolumeHeader, musicVolumeSlider(), musicVolumeSeparator); optionsContainer.addNodes(musicVolumeHeader, musicVolumeSlider(), musicVolumeSeparator);
optionsContainer.addNodes(themeHeader, themeChoiceBox(), themeSeparator); optionsContainer.addNodes(themeHeader, themeChoiceBox(), themeSeparator);
optionsContainer.addNodes(layoutSizeHeader, layoutSizeChoiceBox(), layoutSizeSeparator); optionsContainer.addNodes(layoutSizeHeader, layoutSizeChoiceBox(), layoutSizeSeparator);
optionsContainer.addNodes(fullscreenToggle()); optionsContainer.addNodes(fullscreenToggle());
final Container mainContainer = new VerticalContainer(50, ""); final Container mainContainer = new VerticalContainer(50, "");
mainContainer.addContainer(optionsContainer, true); mainContainer.addContainer(optionsContainer, true);
final var backButton = NodeBuilder.button(AppContext.getString("back"), () -> { final var backButton =
App.pop(); NodeBuilder.button(
}); AppContext.getString("back"),
() -> {
App.pop();
});
final Container controlContainer = new VerticalContainer(5); final Container controlContainer = new VerticalContainer(5);
controlContainer.addNodes(backButton); controlContainer.addNodes(backButton);
addContainer(mainContainer, Pos.CENTER, 0, 0, 0, 0); addContainer(mainContainer, Pos.CENTER, 0, 0, 0, 0);
addContainer(controlContainer, Pos.BOTTOM_LEFT, 2, -2, 0, 0); addContainer(controlContainer, Pos.BOTTOM_LEFT, 2, -2, 0, 0);
} }
private ChoiceBox<Locale> languageChoiceBox() { private ChoiceBox<Locale> languageChoiceBox() {
assert AppContext.getLocalization() != null; assert AppContext.getLocalization() != null;
final ChoiceBox<Locale> languageChoiceBox = NodeBuilder.choiceBox((locale) -> { final ChoiceBox<Locale> languageChoiceBox =
if (locale == AppContext.getLocale()) { NodeBuilder.choiceBox(
return; (locale) -> {
} if (locale == AppContext.getLocale()) {
return;
}
settings.setLocale(locale.toString()); settings.setLocale(locale.toString());
AppContext.setLocale(locale); AppContext.setLocale(locale);
App.reloadAll(); App.reloadAll();
}); });
languageChoiceBox.setConverter(new javafx.util.StringConverter<>() { languageChoiceBox.setConverter(
@Override new javafx.util.StringConverter<>() {
public String toString(Locale locale) { @Override
return AppContext.getString(locale.getDisplayName().toLowerCase()); public String toString(Locale locale) {
} return AppContext.getString(locale.getDisplayName().toLowerCase());
}
@Override @Override
public Locale fromString(String string) { public Locale fromString(String string) {
return null; return null;
} }
}); });
languageChoiceBox.getItems().addAll(AppContext.getLocalization().getAvailableLocales()); languageChoiceBox.getItems().addAll(AppContext.getLocalization().getAvailableLocales());
languageChoiceBox.setValue(AppContext.getLocale()); languageChoiceBox.setValue(AppContext.getLocale());
return languageChoiceBox; return languageChoiceBox;
} }
private Slider volumeSlider() { private Slider volumeSlider() {
return NodeBuilder.slider(100, settings.getVolume(), (volume) -> { return NodeBuilder.slider(
settings.setVolume(volume); 100,
new EventFlow().addPostEvent(new AudioEvents.ChangeVolume(volume.doubleValue())).asyncPostEvent(); settings.getVolume(),
}); (volume) -> {
} settings.setVolume(volume);
new EventFlow()
.addPostEvent(new AudioEvents.ChangeVolume(volume.doubleValue(), VolumeControl.MASTERVOLUME))
.asyncPostEvent();
});
}
private Slider fxVolumeSlider() { private Slider fxVolumeSlider() {
return NodeBuilder.slider(100, settings.getFxVolume(), (volume) -> { return NodeBuilder.slider(
settings.setFxVolume(volume); 100,
new EventFlow().addPostEvent(new AudioEvents.ChangeFxVolume(volume.doubleValue())).asyncPostEvent(); settings.getFxVolume(),
}); (volume) -> {
settings.setFxVolume(volume);
new EventFlow()
.addPostEvent(new AudioEvents.ChangeVolume(volume.doubleValue(), VolumeControl.FX))
.asyncPostEvent();
});
} }
private Slider musicVolumeSlider() { private Slider musicVolumeSlider() {
return NodeBuilder.slider(100, settings.getMusicVolume(), (volume) -> { return NodeBuilder.slider(
settings.setMusicVolume(volume); 100,
new EventFlow().addPostEvent(new AudioEvents.ChangeMusicVolume(volume.doubleValue())).asyncPostEvent(); settings.getMusicVolume(),
}); (volume) -> {
settings.setMusicVolume(volume);
new EventFlow()
.addPostEvent(new AudioEvents.ChangeVolume(volume.doubleValue(), VolumeControl.MUSIC))
.asyncPostEvent();
});
} }
private Label fullscreenToggle() {
return NodeBuilder.toggle(
AppContext.getString("windowed"),
AppContext.getString("fullscreen"),
!isWindowed,
(fullscreen) -> {
isWindowed = !fullscreen;
private Label fullscreenToggle() { settings.setFullscreen(fullscreen);
return NodeBuilder.toggle(AppContext.getString("windowed"), AppContext.getString("fullscreen"), !isWindowed, (fullscreen) -> { App.setFullscreen(fullscreen);
isWindowed = !fullscreen; });
}
settings.setFullscreen(fullscreen); private ChoiceBox<String> themeChoiceBox() {
App.setFullscreen(fullscreen); final ChoiceBox<String> themeChoiceBox =
}); NodeBuilder.choiceBox(
} (theme) -> {
if (theme.equalsIgnoreCase(settings.getTheme())) {
return;
}
private ChoiceBox<String> themeChoiceBox() { settings.setTheme(theme);
final ChoiceBox<String> themeChoiceBox = NodeBuilder.choiceBox((theme) -> { App.setStyle(theme, settings.getLayoutSize());
if (theme.equalsIgnoreCase(settings.getTheme())) { });
return;
}
settings.setTheme(theme); themeChoiceBox.setConverter(
App.setStyle(theme, settings.getLayoutSize()); new javafx.util.StringConverter<>() {
}); @Override
public String toString(String theme) {
return AppContext.getString(theme);
}
themeChoiceBox.setConverter(new javafx.util.StringConverter<>() { @Override
@Override public String fromString(String string) {
public String toString(String theme) { return null;
return AppContext.getString(theme); }
} });
@Override themeChoiceBox.getItems().addAll("dark", "light", "dark-hc", "light-hc");
public String fromString(String string) { themeChoiceBox.setValue(settings.getTheme());
return null;
}
});
themeChoiceBox.getItems().addAll("dark", "light", "dark-hc", "light-hc"); return themeChoiceBox;
themeChoiceBox.setValue(settings.getTheme()); }
return themeChoiceBox; private ChoiceBox<String> layoutSizeChoiceBox() {
} final ChoiceBox<String> layoutSizeChoiceBox =
NodeBuilder.choiceBox(
(layoutSize) -> {
if (layoutSize.equalsIgnoreCase(settings.getLayoutSize())) {
return;
}
private ChoiceBox<String> layoutSizeChoiceBox() { settings.setLayoutSize(layoutSize);
final ChoiceBox<String> layoutSizeChoiceBox = NodeBuilder.choiceBox((layoutSize) -> { App.setStyle(settings.getTheme(), layoutSize);
if (layoutSize.equalsIgnoreCase(settings.getLayoutSize())) { });
return;
}
settings.setLayoutSize(layoutSize); layoutSizeChoiceBox.setConverter(
App.setStyle(settings.getTheme(), layoutSize); new javafx.util.StringConverter<>() {
}); @Override
public String toString(String layoutSize) {
return AppContext.getString(layoutSize);
}
layoutSizeChoiceBox.setConverter(new javafx.util.StringConverter<>() { @Override
@Override public String fromString(String string) {
public String toString(String layoutSize) { return null;
return AppContext.getString(layoutSize); }
} });
@Override layoutSizeChoiceBox.getItems().addAll("small", "medium", "large");
public String fromString(String string) { layoutSizeChoiceBox.setValue(settings.getLayoutSize());
return null;
}
});
layoutSizeChoiceBox.getItems().addAll("small", "medium", "large"); return layoutSizeChoiceBox;
layoutSizeChoiceBox.setValue(settings.getLayoutSize()); }
}
return layoutSizeChoiceBox;
}
}

View File

@@ -1,5 +1,6 @@
package org.toop.app.layer.layers; package org.toop.app.layer.layers;
import javafx.geometry.Pos;
import org.toop.app.App; import org.toop.app.App;
import org.toop.app.layer.Container; import org.toop.app.layer.Container;
import org.toop.app.layer.NodeBuilder; import org.toop.app.layer.NodeBuilder;
@@ -8,35 +9,39 @@ import org.toop.app.layer.containers.HorizontalContainer;
import org.toop.app.layer.containers.VerticalContainer; import org.toop.app.layer.containers.VerticalContainer;
import org.toop.local.AppContext; import org.toop.local.AppContext;
import javafx.geometry.Pos;
public final class QuitPopup extends Popup { public final class QuitPopup extends Popup {
public QuitPopup() { public QuitPopup() {
super(true); super(true);
reload(); reload();
} }
@Override @Override
public void reload() { public void reload() {
popAll(); popAll();
final var sureText = NodeBuilder.header(AppContext.getString("quitSure")); final var sureText = NodeBuilder.header(AppContext.getString("quitSure"));
final var yesButton = NodeBuilder.button(AppContext.getString("yes"), () -> { final var yesButton =
App.quit(); NodeBuilder.button(
}); AppContext.getString("yes"),
() -> {
App.quit();
});
final var noButton = NodeBuilder.button(AppContext.getString("no"), () -> { final var noButton =
App.pop(); NodeBuilder.button(
}); AppContext.getString("no"),
() -> {
App.pop();
});
final Container controlContainer = new HorizontalContainer(30); final Container controlContainer = new HorizontalContainer(30);
controlContainer.addNodes(yesButton, noButton); controlContainer.addNodes(yesButton, noButton);
final Container mainContainer = new VerticalContainer(30); final Container mainContainer = new VerticalContainer(30);
mainContainer.addNodes(sureText); mainContainer.addNodes(sureText);
mainContainer.addContainer(controlContainer, false); mainContainer.addContainer(controlContainer, false);
addContainer(mainContainer, Pos.CENTER, 0, 0, 30, 30); addContainer(mainContainer, Pos.CENTER, 0, 0, 30, 30);
} }
} }

View File

@@ -1,5 +1,6 @@
package org.toop.app.layer.layers.game; package org.toop.app.layer.layers.game;
import javafx.geometry.Pos;
import org.toop.app.App; import org.toop.app.App;
import org.toop.app.layer.Container; import org.toop.app.layer.Container;
import org.toop.app.layer.NodeBuilder; import org.toop.app.layer.NodeBuilder;
@@ -8,45 +9,47 @@ import org.toop.app.layer.containers.VerticalContainer;
import org.toop.app.layer.layers.MainLayer; import org.toop.app.layer.layers.MainLayer;
import org.toop.local.AppContext; import org.toop.local.AppContext;
import javafx.geometry.Pos;
public class GameFinishedPopup extends Popup { public class GameFinishedPopup extends Popup {
private final boolean isDraw; private final boolean isDraw;
private final String winner; private final String winner;
public GameFinishedPopup(boolean isDraw, String winner) { public GameFinishedPopup(boolean isDraw, String winner) {
super(true, "bg-popup"); super(true, "bg-popup");
this.isDraw = isDraw; this.isDraw = isDraw;
this.winner = winner; this.winner = winner;
reload(); reload();
} }
@Override @Override
public void reload() { public void reload() {
popAll(); popAll();
final Container mainContainer = new VerticalContainer(30); final Container mainContainer = new VerticalContainer(30);
if (isDraw) { if (isDraw) {
final var drawHeader = NodeBuilder.header(AppContext.getString("drawText")); final var drawHeader = NodeBuilder.header(AppContext.getString("drawText"));
final var goodGameText = NodeBuilder.text(AppContext.getString("goodGameText")); final var goodGameText = NodeBuilder.text(AppContext.getString("goodGameText"));
mainContainer.addNodes(drawHeader, goodGameText); mainContainer.addNodes(drawHeader, goodGameText);
} else { } else {
final var winHeader = NodeBuilder.header(AppContext.getString("congratulations") + ": " + winner); final var winHeader =
final var goodGameText = NodeBuilder.text(AppContext.getString("goodGameText")); NodeBuilder.header(AppContext.getString("congratulations") + ": " + winner);
final var goodGameText = NodeBuilder.text(AppContext.getString("goodGameText"));
mainContainer.addNodes(winHeader, goodGameText); mainContainer.addNodes(winHeader, goodGameText);
} }
final var backToMainMenuButton = NodeBuilder.button(AppContext.getString("backToMainMenu"), () -> { final var backToMainMenuButton =
App.activate(new MainLayer()); NodeBuilder.button(
}); AppContext.getString("backToMainMenu"),
() -> {
App.activate(new MainLayer());
});
mainContainer.addNodes(backToMainMenuButton); mainContainer.addNodes(backToMainMenuButton);
addContainer(mainContainer, Pos.CENTER, 0, 0, 30, 30); addContainer(mainContainer, Pos.CENTER, 0, 0, 30, 30);
} }
} }

View File

@@ -1,5 +1,12 @@
package org.toop.app.layer.layers.game; package org.toop.app.layer.layers.game;
import java.util.Objects;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import javafx.geometry.Pos;
import javafx.scene.paint.Color;
import javafx.scene.text.Text; import javafx.scene.text.Text;
import org.toop.app.App; import org.toop.app.App;
import org.toop.app.GameInformation; import org.toop.app.GameInformation;
@@ -17,280 +24,308 @@ import org.toop.game.tictactoe.TicTacToe;
import org.toop.game.tictactoe.TicTacToeAI; import org.toop.game.tictactoe.TicTacToeAI;
import org.toop.local.AppContext; import org.toop.local.AppContext;
import javafx.geometry.Pos;
import javafx.scene.paint.Color;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
public final class TicTacToeLayer extends Layer { public final class TicTacToeLayer extends Layer {
private TicTacToeCanvas canvas; private TicTacToeCanvas canvas;
private AtomicReference<TicTacToe> ticTacToe; private AtomicReference<TicTacToe> ticTacToe;
private TicTacToeAI ticTacToeAI; private TicTacToeAI ticTacToeAI;
private GameInformation information; private GameInformation information;
private final Text currentPlayerNameText; private final Text currentPlayerNameText;
private final Text currentPlayerMoveText; private final Text currentPlayerMoveText;
private final BlockingQueue<Game.Move> playerMoveQueue = new LinkedBlockingQueue<>(); private final BlockingQueue<Game.Move> playerMoveQueue = new LinkedBlockingQueue<>();
// Todo: set these from the server // Todo: set these from the server
private char currentPlayerMove = Game.EMPTY; private char currentPlayerMove = Game.EMPTY;
private String player2Name = ""; private String player2Name = "";
final AtomicBoolean firstPlayerIsMe = new AtomicBoolean(true); final AtomicBoolean firstPlayerIsMe = new AtomicBoolean(true);
public TicTacToeLayer(GameInformation information) { public TicTacToeLayer(GameInformation information) {
super("bg-primary"); super("bg-primary");
canvas = new TicTacToeCanvas(Color.LIME, (App.getHeight() / 100) * 75, (App.getHeight() / 100) * 75, (cell) -> { canvas =
try { new TicTacToeCanvas(
if (information.isConnectionLocal()) { Color.LIME,
if (ticTacToe.get().getCurrentTurn() == 0) { (App.getHeight() / 100) * 75,
playerMoveQueue.put(new Game.Move(cell, 'X')); (App.getHeight() / 100) * 75,
} else { (cell) -> {
playerMoveQueue.put(new Game.Move(cell, 'O')); try {
} if (information.isConnectionLocal()) {
} else { if (ticTacToe.get().getCurrentTurn() == 0) {
if (information.isPlayerHuman()[0] && currentPlayerMove != Game.EMPTY) { playerMoveQueue.put(new Game.Move(cell, 'X'));
playerMoveQueue.put(new Game.Move(cell, firstPlayerIsMe.get()? 'X' : 'O')); } else {
} playerMoveQueue.put(new Game.Move(cell, 'O'));
} }
} catch (InterruptedException _) {} } else {
}); if (information.isPlayerHuman()[0]
&& currentPlayerMove != Game.EMPTY) {
playerMoveQueue.put(
new Game.Move(
cell, firstPlayerIsMe.get() ? 'X' : 'O'));
}
}
} catch (InterruptedException _) {
}
});
ticTacToe = new AtomicReference<>(new TicTacToe()); ticTacToe = new AtomicReference<>(new TicTacToe());
ticTacToeAI = new TicTacToeAI(); ticTacToeAI = new TicTacToeAI();
this.information = information; this.information = information;
if (information.isConnectionLocal()) { if (information.isConnectionLocal()) {
new Thread(this::localGameThread).start(); new Thread(this::localGameThread).start();
} }
currentPlayerNameText = NodeBuilder.header(""); currentPlayerNameText = NodeBuilder.header("");
currentPlayerMoveText = NodeBuilder.header(""); currentPlayerMoveText = NodeBuilder.header("");
reload(); reload();
} }
public TicTacToeLayer(GameInformation information, long clientID) { public TicTacToeLayer(GameInformation information, long clientID) {
this(information); this(information);
Thread a = new Thread(this::serverGameThread); Thread a = new Thread(this::serverGameThread);
a.setDaemon(false); a.setDaemon(false);
a.start(); a.start();
reload(); reload();
} }
@Override @Override
public void reload() { public void reload() {
popAll(); popAll();
canvas.resize((App.getHeight() / 100) * 75, (App.getHeight() / 100) * 75); canvas.resize((App.getHeight() / 100) * 75, (App.getHeight() / 100) * 75);
for (int i = 0; i < ticTacToe.get().board.length; i++) { for (int i = 0; i < ticTacToe.get().board.length; i++) {
final char value = ticTacToe.get().board[i]; final char value = ticTacToe.get().board[i];
if (value == 'X') { if (value == 'X') {
canvas.drawX(Color.RED, i); canvas.drawX(Color.RED, i);
} else if (value == 'O') { } else if (value == 'O') {
canvas.drawO(Color.BLUE, i); canvas.drawO(Color.BLUE, i);
} }
} }
final var backButton = NodeBuilder.button(AppContext.getString("back"), () -> { final var backButton =
App.activate(new MainLayer()); NodeBuilder.button(
}); AppContext.getString("back"),
() -> {
App.activate(new MainLayer());
});
final Container controlContainer = new VerticalContainer(5); final Container controlContainer = new VerticalContainer(5);
controlContainer.addNodes(backButton); controlContainer.addNodes(backButton);
final Container informationContainer = new HorizontalContainer(15); final Container informationContainer = new HorizontalContainer(15);
informationContainer.addNodes(currentPlayerNameText, currentPlayerMoveText); informationContainer.addNodes(currentPlayerNameText, currentPlayerMoveText);
addContainer(controlContainer, Pos.BOTTOM_LEFT, 2, -2, 0, 0); addContainer(controlContainer, Pos.BOTTOM_LEFT, 2, -2, 0, 0);
addContainer(informationContainer, Pos.TOP_LEFT, 2, 2, 0, 0); addContainer(informationContainer, Pos.TOP_LEFT, 2, 2, 0, 0);
addGameCanvas(canvas, Pos.CENTER, 0, 0); addGameCanvas(canvas, Pos.CENTER, 0, 0);
} }
private int compurterDifficultyToDepth(int maxDifficulty, int difficulty) { private int compurterDifficultyToDepth(int maxDifficulty, int difficulty) {
return (int) (((float) maxDifficulty / difficulty) * 9); return (int) (((float) maxDifficulty / difficulty) * 9);
} }
private void localGameThread() { private void localGameThread() {
boolean running = true; boolean running = true;
while (running) { while (running) {
final int currentPlayer = ticTacToe.get().getCurrentTurn(); final int currentPlayer = ticTacToe.get().getCurrentTurn();
currentPlayerNameText.setText(information.playerName()[currentPlayer]); currentPlayerNameText.setText(information.playerName()[currentPlayer]);
currentPlayerMoveText.setText(ticTacToe.get().getCurrentTurn() == 0? "X" : "O"); currentPlayerMoveText.setText(ticTacToe.get().getCurrentTurn() == 0 ? "X" : "O");
Game.Move move = null; Game.Move move = null;
if (information.isPlayerHuman()[currentPlayer]) { if (information.isPlayerHuman()[currentPlayer]) {
try { try {
final Game.Move wants = playerMoveQueue.take(); final Game.Move wants = playerMoveQueue.take();
final Game.Move[] legalMoves = ticTacToe.get().getLegalMoves(); final Game.Move[] legalMoves = ticTacToe.get().getLegalMoves();
for (final Game.Move legalMove : legalMoves) { for (final Game.Move legalMove : legalMoves) {
if (legalMove.position() == wants.position() && legalMove.value() == wants.value()) { if (legalMove.position() == wants.position()
move = wants; && legalMove.value() == wants.value()) {
} move = wants;
} // TODO: maybe add break?
} catch (InterruptedException _) {} }
} else { }
final long start = System.currentTimeMillis(); } catch (InterruptedException _) {
}
} else {
final long start = System.currentTimeMillis();
move = ticTacToeAI.findBestMove(ticTacToe.get(), compurterDifficultyToDepth(10, move =
information.computerDifficulty()[currentPlayer])); ticTacToeAI.findBestMove(
ticTacToe.get(),
compurterDifficultyToDepth(
10, information.computerDifficulty()[currentPlayer]));
if (information.computerThinkTime()[currentPlayer] > 0) { if (information.computerThinkTime()[currentPlayer] > 0) {
final long elapsedTime = System.currentTimeMillis() - start; final long elapsedTime = System.currentTimeMillis() - start;
final long sleepTime = information.computerThinkTime()[currentPlayer] * 1000L - elapsedTime; final long sleepTime =
information.computerThinkTime()[currentPlayer] * 1000L - elapsedTime;
try { try {
Thread.sleep(sleepTime); Thread.sleep(sleepTime);
} catch (InterruptedException _) {} } catch (InterruptedException _) {
} }
} }
}
if (move == null) { if (move == null) {
continue; continue;
} }
final Game.State state = ticTacToe.get().play(move); final Game.State state = ticTacToe.get().play(move);
if (move.value() == 'X') { if (move.value() == 'X') {
canvas.drawX(Color.RED, move.position()); canvas.drawX(Color.RED, move.position());
} else if (move.value() == 'O') { } else if (move.value() == 'O') {
canvas.drawO(Color.BLUE, move.position()); canvas.drawO(Color.BLUE, move.position());
} }
if (state != Game.State.NORMAL) { if (state != Game.State.NORMAL) {
if (state == Game.State.WIN) { if (state == Game.State.WIN) {
App.push(new GameFinishedPopup(false, information.playerName()[ticTacToe.get().getCurrentTurn()])); App.push(
} else if (state == Game.State.DRAW) { new GameFinishedPopup(
App.push(new GameFinishedPopup(true, "")); false,
} information.playerName()[ticTacToe.get().getCurrentTurn()]));
} else if (state == Game.State.DRAW) {
App.push(new GameFinishedPopup(true, ""));
}
running = false; running = false;
} }
} }
} }
private void serverGameThread() { private void serverGameThread() {
new EventFlow() new EventFlow()
.listen(this::handleServerGameStart) // <----------- .listen(this::handleServerGameStart) // <-----------
.listen(this::yourTurnResponse) .listen(this::yourTurnResponse)
.listen(this::onMoveResponse) .listen(this::onMoveResponse)
.listen(this::handleReceivedMessage); .listen(this::handleReceivedMessage);
} }
private void handleServerGameStart(NetworkEvents.GameMatchResponse resp) { private void handleServerGameStart(NetworkEvents.GameMatchResponse resp) {
// Meneer Bas de Jong. Dit functie wordt niet aangeroepen als je de challenger bent. // Meneer Bas de Jong. Dit functie wordt niet aangeroepen als je de challenger bent.
// Ik heb veel dingen geprobeert. FUCKING veel dingen. Hij doet het niet. // Ik heb veel dingen geprobeerd. FUCKING veel dingen. Hij doet het niet.
// Ik heb zelfs in jou code gekeken en unsubscribeAfterSuccess op false gezet. (zie ConnectedLayer). // Ik heb zelfs in jouw code gekeken en unsubscribeAfterSuccess op false gezet. (zie
// Alle andere functies worden wel gecalt. Behalve dit. // ConnectedLayer).
// Alle andere functies worden wel gecalld. Behalve dit.
// Ben jij gehandicapt of ik? Want het moet 1 van de 2 zijn. Ik ben dit al 2 uur aan het debuggen. // Ben jij gehandicapt of ik? Want het moet 1 van de 2 zijn. Ik ben dit al 2 uur aan het
// Ik ga nu slapen (04:46). // debuggen.
// Ik ga nu slapen (04:46).
// ⠀⠀⠀⠀⠀⠀⣀⣀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ // ⠀⠀⠀⠀⠀⠀⣀⣀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
// ⠀⠀⠀⢀⣴⣿⣿⠿⣟⢷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ // ⠀⠀⠀⢀⣴⣿⣿⠿⣟⢷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
// ⠀⠀⠀⢸⣏⡏⠀⠀⠀⢣⢻⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ // ⠀⠀⠀⢸⣏⡏⠀⠀⠀⢣⢻⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
// ⠀⠀⠀⢸⣟⠧⠤⠤⠔⠋⠀⢿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ // ⠀⠀⠀⢸⣟⠧⠤⠤⠔⠋⠀⢿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
// ⠀⠀⠀⠀⣿⡆⠀⠀⠀⠀⠀⠸⣷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ // ⠀⠀⠀⠀⣿⡆⠀⠀⠀⠀⠀⠸⣷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
// ⠀⠀⠀⠀⠘⣿⡀⢀⣶⠤⠒⠀⢻⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ // ⠀⠀⠀⠀⠘⣿⡀⢀⣶⠤⠒⠀⢻⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
// ⠀⠀⠀⠀⠀⢹⣧⠀⠀⠀⠀⠀⠈⢿⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ // ⠀⠀⠀⠀⠀⢹⣧⠀⠀⠀⠀⠀⠈⢿⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
// ⠀⠀⠀⠀⠀⠀⣿⡆⠀⠀⠀⠀⠀⠈⢿⣆⣠⣤⣤⣤⣤⣴⣦⣄⡀⠀⠀⠀⠀⠀⠀⠀ // ⠀⠀⠀⠀⠀⠀⣿⡆⠀⠀⠀⠀⠀⠈⢿⣆⣠⣤⣤⣤⣤⣴⣦⣄⡀⠀⠀⠀⠀⠀⠀⠀
// ⠀⠀⠀⠀⢀⣾⢿⢿⠀⠀⠀⢀⣀⣀⠘⣿⠋⠁⠀⠙⢇⠀⠀⠙⢿⣦⡀⠀⠀⠀⠀⠀ // ⠀⠀⠀⠀⢀⣾⢿⢿⠀⠀⠀⢀⣀⣀⠘⣿⠋⠁⠀⠙⢇⠀⠀⠙⢿⣦⡀⠀⠀⠀⠀⠀
// ⠀⠀⠀⢀⣾⢇⡞⠘⣧⠀⢖⡭⠞⢛⡄⠘⣆⠀⠀⠀⠈⢧⠀⠀⠀⠙⢿⣄⠀⠀⠀⠀ // ⠀⠀⠀⢀⣾⢇⡞⠘⣧⠀⢖⡭⠞⢛⡄⠘⣆⠀⠀⠀⠈⢧⠀⠀⠀⠙⢿⣄⠀⠀⠀⠀
// ⠀⠀⣠⣿⣛⣥⠤⠤⢿⡄⠀⠀⠈⠉⠀⠀⠹⡄⠀⠀⠀⠈⢧⠀⠀⠀⠈⠻⣦⠀⠀⠀ // ⠀⠀⣠⣿⣛⣥⠤⠤⢿⡄⠀⠀⠈⠉⠀⠀⠹⡄⠀⠀⠀⠈⢧⠀⠀⠀⠈⠻⣦⠀⠀⠀
// ⠀⣼⡟⡱⠛⠙⠀⠀⠘⢷⡀⠀⠀⠀⠀⠀⠀⠹⡀⠀⠀⠀⠈⣧⠀⠀⠀⠀⠹⣧⡀⠀ // ⠀⣼⡟⡱⠛⠙⠀⠀⠘⢷⡀⠀⠀⠀⠀⠀⠀⠹⡀⠀⠀⠀⠈⣧⠀⠀⠀⠀⠹⣧⡀⠀
// ⢸⡏⢠⠃⠀⠀⠀⠀⠀⠀⢳⡀⠀⠀⠀⠀⠀⠀⢳⡀⠀⠀⠀⠘⣧⠀⠀⠀⠀⠸⣷⡀ // ⢸⡏⢠⠃⠀⠀⠀⠀⠀⠀⢳⡀⠀⠀⠀⠀⠀⠀⢳⡀⠀⠀⠀⠘⣧⠀⠀⠀⠀⠸⣷⡀
// ⠸⣧⠘⡇⠀⠀⠀⠀⠀⠀⠀⢳⡀⠀⠀⠀⠀⠀⠀⢣⠀⠀⠀⠀⢹⡇⠀⠀⠀⠀⣿⠇ // ⠸⣧⠘⡇⠀⠀⠀⠀⠀⠀⠀⢳⡀⠀⠀⠀⠀⠀⠀⢣⠀⠀⠀⠀⢹⡇⠀⠀⠀⠀⣿⠇
// ⠀⣿⡄⢳⠀⠀⠀⠀⠀⠀⠀⠈⣷⠀⠀⠀⠀⠀⠀⠈⠆⠀⠀⠀⠀⠀⠀⠀⠀⣼⡟⠀ // ⠀⣿⡄⢳⠀⠀⠀⠀⠀⠀⠀⠈⣷⠀⠀⠀⠀⠀⠀⠈⠆⠀⠀⠀⠀⠀⠀⠀⠀⣼⡟⠀
// ⠀⢹⡇⠘⣇⠀⠀⠀⠀⠀⠀⠰⣿⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡄⠀⣼⡟⠀⠀ // ⠀⢹⡇⠘⣇⠀⠀⠀⠀⠀⠀⠰⣿⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡄⠀⣼⡟⠀⠀
// ⠀⢸⡇⠀⢹⡆⠀⠀⠀⠀⠀⠀⠙⠁⠀⠀⠀⠀⠀⠀⠀⠀⡀⠀⠀⠀⢳⣼⠟⠀⠀⠀ // ⠀⢸⡇⠀⢹⡆⠀⠀⠀⠀⠀⠀⠙⠁⠀⠀⠀⠀⠀⠀⠀⠀⡀⠀⠀⠀⢳⣼⠟⠀⠀⠀
// ⠀⠸⣧⣀⠀⢳⡀⠀⠀⠀⠀⠀⠀⠀⡄⠀⠀⠀⠀⠀⠀⠀⢃⠀⢀⣴⡿⠁⠀⠀⠀⠀ // ⠀⠸⣧⣀⠀⢳⡀⠀⠀⠀⠀⠀⠀⠀⡄⠀⠀⠀⠀⠀⠀⠀⢃⠀⢀⣴⡿⠁⠀⠀⠀⠀
// ⠀⠀⠈⠙⢷⣄⢳⡀⠀⠀⠀⠀⠀⠀⢳⡀⠀⠀⠀⠀⠀⣠⡿⠟⠛⠉⠀⠀⠀⠀⠀⠀ // ⠀⠀⠈⠙⢷⣄⢳⡀⠀⠀⠀⠀⠀⠀⢳⡀⠀⠀⠀⠀⠀⣠⡿⠟⠛⠉⠀⠀⠀⠀⠀⠀
// ⠀⠀⠀⠀⠈⠻⢿⣷⣦⣄⣀⣀⣠⣤⠾⠷⣦⣤⣤⡶⠟⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ // ⠀⠀⠀⠀⠈⠻⢿⣷⣦⣄⣀⣀⣠⣤⠾⠷⣦⣤⣤⡶⠟⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
// ⠀⠀⠀⠀⠀⠀⠀⠈⠉⠛⠛⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ // ⠀⠀⠀⠀⠀⠀⠀⠈⠉⠛⠛⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
player2Name = resp.opponent(); player2Name = resp.opponent();
System.out.println(player2Name); System.out.println(player2Name);
currentPlayerMoveText.setText("X"); currentPlayerMoveText.setText("X");
if(!resp.playerToMove().equalsIgnoreCase(resp.opponent())) { if (!resp.playerToMove().equalsIgnoreCase(resp.opponent())) {
currentPlayerNameText.setText(information.playerName()[0]); currentPlayerNameText.setText(information.playerName()[0]);
firstPlayerIsMe.set(true); firstPlayerIsMe.set(true);
System.out.printf("I am starting: My client id is %d\n", resp.clientId()); System.out.printf("I am starting: My client id is %d\n", resp.clientId());
} else { } else {
currentPlayerNameText.setText(player2Name); currentPlayerNameText.setText(player2Name);
firstPlayerIsMe.set(false); firstPlayerIsMe.set(false);
System.out.printf("I am NOT starting: My client id is %d\n", resp.clientId()); System.out.printf("I am NOT starting: My client id is %d\n", resp.clientId());
} }
} }
private void onMoveResponse(NetworkEvents.GameMoveResponse resp) { private void onMoveResponse(NetworkEvents.GameMoveResponse resp) {
char playerChar; char playerChar;
if (!resp.player().equalsIgnoreCase(player2Name)) { if (!resp.player().equalsIgnoreCase(player2Name)) {
playerChar = firstPlayerIsMe.get()? 'X' : 'O'; playerChar = firstPlayerIsMe.get() ? 'X' : 'O';
} else { } else {
playerChar = firstPlayerIsMe.get()? 'O' : 'X'; playerChar = firstPlayerIsMe.get() ? 'O' : 'X';
} }
final Game.Move move = new Game.Move(Integer.parseInt(resp.move()), playerChar); final Game.Move move = new Game.Move(Integer.parseInt(resp.move()), playerChar);
final Game.State state = ticTacToe.get().play(move); final Game.State state = ticTacToe.get().play(move);
if (state != Game.State.NORMAL) { //todo differentiate between future draw guaranteed and is currently a draw if (state
if (state == Game.State.WIN) { != Game.State.NORMAL) { // todo differentiate between future draw guaranteed and is
App.push(new GameFinishedPopup(false, information.playerName()[ticTacToe.get().getCurrentTurn()])); // currently a draw
} else if (state == Game.State.DRAW) { if (state == Game.State.WIN) {
App.push(new GameFinishedPopup(true, "")); App.push(
} new GameFinishedPopup(
} false, information.playerName()[ticTacToe.get().getCurrentTurn()]));
} else if (state == Game.State.DRAW) {
App.push(new GameFinishedPopup(true, ""));
}
}
if (move.value() == 'X') { if (move.value() == 'X') {
canvas.drawX(Color.RED, move.position()); canvas.drawX(Color.RED, move.position());
} else if (move.value() == 'O') { } else if (move.value() == 'O') {
canvas.drawO(Color.BLUE, move.position()); canvas.drawO(Color.BLUE, move.position());
} }
currentPlayerNameText.setText(ticTacToe.get().getCurrentTurn() == (firstPlayerIsMe.get()? 0 : 1)? information.playerName()[0] : player2Name); currentPlayerNameText.setText(
currentPlayerMoveText.setText(ticTacToe.get().getCurrentTurn() == 0? "X" : "O"); ticTacToe.get().getCurrentTurn() == (firstPlayerIsMe.get() ? 0 : 1)
} ? information.playerName()[0]
: player2Name);
currentPlayerMoveText.setText(ticTacToe.get().getCurrentTurn() == 0 ? "X" : "O");
}
private void yourTurnResponse(NetworkEvents.YourTurnResponse response) { private void yourTurnResponse(NetworkEvents.YourTurnResponse response) {
int position = -1; int position = -1;
if (information.isPlayerHuman()[0]) { if (information.isPlayerHuman()[0]) {
try { try {
position = playerMoveQueue.take().position(); position = playerMoveQueue.take().position();
} catch (InterruptedException _) {} } catch (InterruptedException _) {
} else { }
final Game.Move move = ticTacToeAI.findBestMove(ticTacToe.get(), compurterDifficultyToDepth(10, } else {
information.computerDifficulty()[0])); final Game.Move move =
ticTacToeAI.findBestMove(
ticTacToe.get(),
compurterDifficultyToDepth(10, information.computerDifficulty()[0]));
position = move.position(); position = Objects.requireNonNull(move).position();
} }
new EventFlow().addPostEvent(new NetworkEvents.SendMove(response.clientId(), (short)position)) new EventFlow()
.postEvent(); .addPostEvent(new NetworkEvents.SendMove(response.clientId(), (short) position))
} .postEvent();
}
private void handleReceivedMessage(NetworkEvents.ReceivedMessage msg) { private void handleReceivedMessage(NetworkEvents.ReceivedMessage msg) {
System.out.println("Received Message: " + msg.message()); //todo add chat window System.out.println("Received Message: " + msg.message()); // todo add chat window
} }
} }

View File

@@ -1,28 +1,27 @@
package org.toop.local; package org.toop.local;
import org.toop.framework.asset.ResourceManager;
import org.toop.framework.asset.resources.LocalizationAsset;
import java.util.Locale; import java.util.Locale;
import org.toop.framework.resource.ResourceManager;
import org.toop.framework.resource.resources.LocalizationAsset;
public class AppContext { public class AppContext {
private static final LocalizationAsset localization = ResourceManager.get("localization"); private static final LocalizationAsset localization = ResourceManager.get("localization");
private static Locale locale = Locale.forLanguageTag("en"); private static Locale locale = Locale.forLanguageTag("en");
public static LocalizationAsset getLocalization() { public static LocalizationAsset getLocalization() {
return localization; return localization;
} }
public static void setLocale(Locale locale) { public static void setLocale(Locale locale) {
AppContext.locale = locale; AppContext.locale = locale;
} }
public static Locale getLocale() { public static Locale getLocale() {
return locale; return locale;
} }
public static String getString(String key) { public static String getString(String key) {
assert localization != null; assert localization != null;
return localization.getString(key, locale); return localization.getString(key, locale);
} }
} }

View File

@@ -1,32 +1,40 @@
package org.toop.local; package org.toop.local;
import jdk.jfr.Event;
import org.toop.app.App;
import org.toop.framework.asset.resources.SettingsAsset;
import org.toop.framework.audio.events.AudioEvents;
import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.settings.Settings;
import java.io.File; import java.io.File;
import java.util.Locale; import java.util.Locale;
import org.toop.app.App;
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.ResourceMeta;
import org.toop.framework.resource.resources.SettingsAsset;
import org.toop.framework.settings.Settings;
public class AppSettings { public class AppSettings {
private SettingsAsset settingsAsset; private SettingsAsset settingsAsset;
public void applySettings() { public void applySettings() {
SettingsAsset settings = getPath(); this.settingsAsset = getPath();
if (!settings.isLoaded()) { if (!this.settingsAsset.isLoaded()) {
settings.load(); this.settingsAsset.load();
} }
Settings settingsData = settings.getContent();
Settings settingsData = this.settingsAsset.getContent();
AppContext.setLocale(Locale.of(settingsData.locale)); AppContext.setLocale(Locale.of(settingsData.locale));
App.setFullscreen(settingsData.fullScreen); App.setFullscreen(settingsData.fullScreen);
new EventFlow().addPostEvent(new AudioEvents.ChangeVolume(settingsData.volume)).asyncPostEvent(); new EventFlow()
new EventFlow().addPostEvent(new AudioEvents.ChangeFxVolume(settingsData.fxVolume)).asyncPostEvent(); .addPostEvent(new AudioEvents.ChangeVolume(settingsData.volume, VolumeControl.MASTERVOLUME))
new EventFlow().addPostEvent(new AudioEvents.ChangeMusicVolume(settingsData.musicVolume)).asyncPostEvent(); .asyncPostEvent();
App.setStyle(settingsAsset.getTheme(), settingsAsset.getLayoutSize()); new EventFlow()
.addPostEvent(new AudioEvents.ChangeVolume(settingsData.fxVolume, VolumeControl.FX))
.asyncPostEvent();
new EventFlow()
.addPostEvent(new AudioEvents.ChangeVolume(settingsData.musicVolume, VolumeControl.MUSIC))
.asyncPostEvent();
App.setStyle(settingsAsset.getTheme(), settingsAsset.getLayoutSize());
} }
public SettingsAsset getPath() { public SettingsAsset getPath() {
@@ -45,9 +53,15 @@ public class AppSettings {
basePath = System.getProperty("user.home") + "/.config"; basePath = System.getProperty("user.home") + "/.config";
} }
File settingsFile = new File(basePath + File.separator + "ISY1" + File.separator + "settings.json"); File settingsFile =
this.settingsAsset = new SettingsAsset(settingsFile); new File(basePath + File.separator + "ISY1" + File.separator + "settings.json");
return new SettingsAsset(settingsFile);
// this.settingsAsset = new SettingsAsset(settingsFile); // TODO
// ResourceManager.addAsset(new ResourceMeta<>("settings.json", new SettingsAsset(settingsFile))); // TODO
} }
return this.settingsAsset; return this.settingsAsset;
// return ResourceManager.get("settings.json"); // TODO
} }
} }

View File

@@ -1,8 +1,13 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"> <project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>org.toop</groupId> <parent>
<artifactId>pism_framework</artifactId> <groupId>org.toop</groupId>
<artifactId>pism</artifactId>
<version>0.1</version>
</parent>
<artifactId>framework</artifactId>
<version>0.1</version> <version>0.1</version>
<properties> <properties>
@@ -13,6 +18,14 @@
</properties> </properties>
<dependencies> <dependencies>
<dependency>
<groupId>org.toop</groupId>
<artifactId>processors</artifactId>
<version>0.1</version>
<scope>compile</scope>
</dependency>
<dependency> <dependency>
<groupId>com.diffplug.spotless</groupId> <groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId> <artifactId>spotless-maven-plugin</artifactId>
@@ -123,7 +136,17 @@
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
</dependencies> <dependency>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>2.42.0</version>
</dependency>
<dependency>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_annotations</artifactId>
<version>2.42.0</version>
</dependency>
</dependencies>
<build> <build>
<plugins> <plugins>
@@ -132,11 +155,73 @@
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<version>3.14.1</version> <version>3.14.1</version>
<configuration> <configuration>
<source>25</source> <showWarnings>true</showWarnings>
<fork>true</fork>
<executable>${java.home}/bin/javac</executable>
<source>25</source>
<target>25</target> <target>25</target>
<release>25</release> <release>25</release>
<encoding>UTF-8</encoding> <encoding>UTF-8</encoding>
<compilerArgs>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>
<arg>
-Xplugin:ErrorProne
</arg>
<!-- TODO-->
<!-- -Xep:RestrictedApi:ERROR \-->
<!-- -XepOpt:RestrictedApi:annotation=org.toop.annotations.TestsOnly \-->
<!-- -XepOpt:RestrictedApi:allowlistRegex=(?s).*/src/test/java/.*|.*test\.java \-->
<!-- -XepOpt:RestrictedApi:message=This API is marked @TestsOnly and shouldn't be normally used.-->
<arg>-XDcompilePolicy=simple</arg>
<arg>--should-stop=ifError=FLOW</arg>
</compilerArgs>
<!-- <generatedSourcesDirectory>-->
<!-- ${project.build.directory}/generated-sources/-->
<!-- </generatedSourcesDirectory>-->
<!-- <annotationProcessors>-->
<!-- <annotationProcessor>-->
<!-- org.toop.processors.AutoResponseResultProcessor-->
<!-- </annotationProcessor>-->
<!-- </annotationProcessors>-->
<annotationProcessorPaths>
<path>
<groupId>org.toop</groupId>
<artifactId>processors</artifactId>
<version>0.1</version>
</path>
<path>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
<version>1.1.1</version>
</path>
<path>
<groupId>com.squareup</groupId>
<artifactId>javapoet</artifactId>
<version>1.13.0</version>
</path>
<path>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>2.42.0</version>
</path>
</annotationProcessorPaths>
</configuration> </configuration>
<dependencies>
<dependency>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>2.42.0</version>
</dependency>
</dependencies>
</plugin> </plugin>
<plugin> <plugin>
<groupId>com.diffplug.spotless</groupId> <groupId>com.diffplug.spotless</groupId>

View File

@@ -6,197 +6,198 @@ import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.config.Configuration; import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.LoggerConfig; import org.apache.logging.log4j.core.config.LoggerConfig;
import java.util.Locale;
/** /**
* Utility class for configuring logging levels dynamically at runtime using Log4j 2. * Utility class for configuring logging levels dynamically at runtime using Log4j 2.
* *
* <p>Provides methods to enable or disable logs globally or per class, with support for specifying * <p>Provides methods to enable or disable logs globally or per class, with support for specifying
* log levels either via {@link Level} enums or string names. * log levels either via {@link Level} enums or string names.
*/ */
// Todo: refactor
public final class Logging { public final class Logging {
/** Disables all logging globally by setting the root logger level to {@link Level#OFF}. */ /** Disables all logging globally by setting the root logger level to {@link Level#OFF}. */
public static void disableAllLogs() { public static void disableAllLogs() {
LoggerContext ctx = (LoggerContext) LogManager.getContext(false); LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
Configuration config = ctx.getConfiguration(); Configuration config = ctx.getConfiguration();
LoggerConfig rootLoggerConfig = config.getRootLogger(); LoggerConfig rootLoggerConfig = config.getRootLogger();
rootLoggerConfig.setLevel(Level.OFF); rootLoggerConfig.setLevel(Level.OFF);
ctx.updateLoggers(); ctx.updateLoggers();
} }
/** Enables all logging globally by setting the root logger level to {@link Level#ALL}. */ /** Enables all logging globally by setting the root logger level to {@link Level#ALL}. */
public static void enableAllLogs() { public static void enableAllLogs() {
LoggerContext ctx = (LoggerContext) LogManager.getContext(false); LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
Configuration config = ctx.getConfiguration(); Configuration config = ctx.getConfiguration();
LoggerConfig rootLoggerConfig = config.getRootLogger(); LoggerConfig rootLoggerConfig = config.getRootLogger();
rootLoggerConfig.setLevel(Level.ALL); rootLoggerConfig.setLevel(Level.ALL);
ctx.updateLoggers(); ctx.updateLoggers();
} }
/** /**
* Enables global logging at a specific level by setting the root logger. * Enables global logging at a specific level by setting the root logger.
* *
* @param level the logging level to enable for all logs * @param level the logging level to enable for all logs
*/ */
public static void enableAllLogs(Level level) { public static void enableAllLogs(Level level) {
LoggerContext ctx = (LoggerContext) LogManager.getContext(false); LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
Configuration config = ctx.getConfiguration(); Configuration config = ctx.getConfiguration();
LoggerConfig rootLoggerConfig = config.getRootLogger(); LoggerConfig rootLoggerConfig = config.getRootLogger();
rootLoggerConfig.setLevel(level); rootLoggerConfig.setLevel(level);
ctx.updateLoggers(); ctx.updateLoggers();
} }
/** /**
* Verifies whether the provided string corresponds to a valid class name. * Verifies whether the provided string corresponds to a valid class name.
* *
* @param className fully-qualified class name to check * @param className fully-qualified class name to check
* @return true if the class exists, false otherwise * @return true if the class exists, false otherwise
*/ */
private static boolean verifyStringIsActualClass(String className) { private static boolean verifyStringIsActualClass(String className) {
try { try {
Class.forName(className); Class.forName(className);
return true; return true;
} catch (ClassNotFoundException e) { } catch (ClassNotFoundException e) {
return false; return false;
} }
} }
/** /**
* Internal helper to disable logs for a specific class by name. * Internal helper to disable logs for a specific class by name.
* *
* @param className fully-qualified class name * @param className fully-qualified class name
*/ */
private static void disableLogsForClassInternal(String className) { private static void disableLogsForClassInternal(String className) {
LoggerContext ctx = (LoggerContext) LogManager.getContext(false); LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
Configuration config = ctx.getConfiguration(); Configuration config = ctx.getConfiguration();
config.removeLogger(className); config.removeLogger(className);
LoggerConfig specificConfig = new LoggerConfig(className, Level.OFF, false); LoggerConfig specificConfig = new LoggerConfig(className, Level.OFF, false);
config.addLogger(className, specificConfig); config.addLogger(className, specificConfig);
ctx.updateLoggers(); ctx.updateLoggers();
} }
/** /**
* Disables logs for a specific class. * Disables logs for a specific class.
* *
* @param class_ the class for which logs should be disabled * @param class_ the class for which logs should be disabled
* @param <T> type of the class * @param <T> type of the class
*/ */
public static <T> void disableLogsForClass(Class<T> class_) { public static <T> void disableLogsForClass(Class<T> class_) {
disableLogsForClassInternal(class_.getName()); disableLogsForClassInternal(class_.getName());
} }
/** /**
* Disables logs for a class specified by fully-qualified name, if the class exists. * Disables logs for a class specified by fully-qualified name, if the class exists.
* *
* @param className fully-qualified class name * @param className fully-qualified class name
*/ */
public static void disableLogsForClass(String className) { public static void disableLogsForClass(String className) {
if (verifyStringIsActualClass(className)) { if (verifyStringIsActualClass(className)) {
disableLogsForClassInternal(className); disableLogsForClassInternal(className);
} }
} }
/** /**
* Internal helper to enable logs for a specific class at a specific level. * Internal helper to enable logs for a specific class at a specific level.
* *
* @param className fully-qualified class name * @param className fully-qualified class name
* @param level logging level to set * @param level logging level to set
*/ */
private static void enableLogsForClassInternal(String className, Level level) { private static void enableLogsForClassInternal(String className, Level level) {
LoggerContext ctx = (LoggerContext) LogManager.getContext(false); LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
Configuration config = ctx.getConfiguration(); Configuration config = ctx.getConfiguration();
LoggerConfig loggerConfig = config.getLoggers().get(className); LoggerConfig loggerConfig = config.getLoggers().get(className);
if (loggerConfig == null) { if (loggerConfig == null) {
loggerConfig = new LoggerConfig(className, level, false); loggerConfig = new LoggerConfig(className, level, false);
config.addLogger(className, loggerConfig); config.addLogger(className, loggerConfig);
} else { } else {
loggerConfig.setLevel(level); loggerConfig.setLevel(level);
} }
ctx.updateLoggers(); ctx.updateLoggers();
} }
/** /**
* Enables logging for a class at a specific level. * Enables logging for a class at a specific level.
* *
* @param class_ class to configure * @param class_ class to configure
* @param levelToLog the logging level to set * @param levelToLog the logging level to set
* @param <T> type of the class * @param <T> type of the class
*/ */
public static <T> void enableLogsForClass(Class<T> class_, Level levelToLog) { public static <T> void enableLogsForClass(Class<T> class_, Level levelToLog) {
enableLogsForClassInternal(class_.getName(), levelToLog); enableLogsForClassInternal(class_.getName(), levelToLog);
} }
/** /**
* Enables logging for a class specified by name at a specific level, if the class exists. * Enables logging for a class specified by name at a specific level, if the class exists.
* *
* @param className fully-qualified class name * @param className fully-qualified class name
* @param levelToLog the logging level to set * @param levelToLog the logging level to set
*/ */
public static void enableLogsForClass(String className, Level levelToLog) { public static void enableLogsForClass(String className, Level levelToLog) {
if (verifyStringIsActualClass(className)) { if (verifyStringIsActualClass(className)) {
enableLogsForClassInternal(className, levelToLog); enableLogsForClassInternal(className, levelToLog);
} }
} }
/** /**
* Enables logging for a class specified by name at a specific level using a string. * Enables logging for a class specified by name at a specific level using a string.
* *
* @param className fully-qualified class name * @param className fully-qualified class name
* @param levelToLog name of the logging level (e.g., "DEBUG", "INFO") * @param levelToLog name of the logging level (e.g., "DEBUG", "INFO")
*/ */
public static void enableLogsForClass(String className, String levelToLog) { public static void enableLogsForClass(String className, String levelToLog) {
Level level = Level.valueOf(levelToLog.trim().toUpperCase()); Level level = Level.valueOf(levelToLog.trim().toUpperCase(Locale.ROOT));
if (level != null && verifyStringIsActualClass(className)) { if (level != null && verifyStringIsActualClass(className)) {
enableLogsForClassInternal(className, level); enableLogsForClassInternal(className, level);
} }
} }
/** Convenience methods for enabling logs at specific levels for classes. */ /** Convenience methods for enabling logs at specific levels for classes. */
public static <T> void enableAllLogsForClass(Class<T> class_) { public static <T> void enableAllLogsForClass(Class<T> class_) {
enableLogsForClass(class_, Level.ALL); enableLogsForClass(class_, Level.ALL);
} }
public static void enableAllLogsForClass(String className) { public static void enableAllLogsForClass(String className) {
enableLogsForClass(className, Level.ALL); enableLogsForClass(className, Level.ALL);
} }
public static <T> void enableDebugLogsForClass(Class<T> class_) { public static <T> void enableDebugLogsForClass(Class<T> class_) {
enableLogsForClass(class_, Level.DEBUG); enableLogsForClass(class_, Level.DEBUG);
} }
public static void enableDebugLogsForClass(String className) { public static void enableDebugLogsForClass(String className) {
enableLogsForClass(className, Level.DEBUG); enableLogsForClass(className, Level.DEBUG);
} }
public static <T> void enableErrorLogsForClass(Class<T> class_) { public static <T> void enableErrorLogsForClass(Class<T> class_) {
enableLogsForClass(class_, Level.ERROR); enableLogsForClass(class_, Level.ERROR);
} }
public static void enableErrorLogsForClass(String className) { public static void enableErrorLogsForClass(String className) {
enableLogsForClass(className, Level.ERROR); enableLogsForClass(className, Level.ERROR);
} }
public static <T> void enableFatalLogsForClass(Class<T> class_) { public static <T> void enableFatalLogsForClass(Class<T> class_) {
enableLogsForClass(class_, Level.FATAL); enableLogsForClass(class_, Level.FATAL);
} }
public static void enableFatalLogsForClass(String className) { public static void enableFatalLogsForClass(String className) {
enableLogsForClass(className, Level.FATAL); enableLogsForClass(className, Level.FATAL);
} }
public static <T> void enableInfoLogsForClass(Class<T> class_) { public static <T> void enableInfoLogsForClass(Class<T> class_) {
enableLogsForClass(class_, Level.INFO); enableLogsForClass(class_, Level.INFO);
} }
public static void enableInfoLogsForClass(String className) { public static void enableInfoLogsForClass(String className) {
enableLogsForClass(className, Level.INFO); enableLogsForClass(className, Level.INFO);
} }
public static <T> void enableTraceLogsForClass(Class<T> class_) { public static <T> void enableTraceLogsForClass(Class<T> class_) {
enableLogsForClass(class_, Level.TRACE); enableLogsForClass(class_, Level.TRACE);
} }
public static void enableTraceLogsForClass(String className) { public static void enableTraceLogsForClass(String className) {
enableLogsForClass(className, Level.TRACE); enableLogsForClass(className, Level.TRACE);
} }
} }

View File

@@ -7,36 +7,20 @@ import java.util.concurrent.atomic.AtomicLong;
/** /**
* A thread-safe, distributed unique ID generator following the Snowflake pattern. * A thread-safe, distributed unique ID generator following the Snowflake pattern.
* <p> *
* Each generated 64-bit ID encodes: * <p>Each generated 64-bit ID encodes:
* <ul> * <ul>
* <li>41-bit timestamp (milliseconds since custom epoch)</li> * <li>41-bit timestamp (milliseconds since custom epoch)
* <li>10-bit machine identifier</li> * <li>10-bit machine identifier
* <li>12-bit sequence number for IDs generated in the same millisecond</li> * <li>12-bit sequence number for IDs generated in the same millisecond
* </ul> * </ul>
* </p>
* *
* <p>This implementation ensures: * <p>This static implementation ensures global uniqueness per JVM process
* <ul> * and can be accessed via {@link SnowflakeGenerator#nextId()}.
* <li>IDs are unique per machine.</li>
* <li>Monotonicity within the same machine.</li>
* <li>Safe concurrent generation via synchronized {@link #nextId()}.</li>
* </ul>
* </p>
*
* <p>Custom epoch is set to {@code 2025-01-01T00:00:00Z}.</p>
*
* <p>Usage example:</p>
* <pre>{@code
* SnowflakeGenerator generator = new SnowflakeGenerator();
* long id = generator.nextId();
* }</pre>
*/ */
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(); private static final long EPOCH = Instant.parse("2025-01-01T00:00:00Z").toEpochMilli();
// Bit allocations // Bit allocations
@@ -44,27 +28,26 @@ public class SnowflakeGenerator {
private static final long MACHINE_BITS = 10; private static final long MACHINE_BITS = 10;
private static final long SEQUENCE_BITS = 12; 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_MACHINE_ID = (1L << MACHINE_BITS) - 1;
private static final long MAX_SEQUENCE = (1L << SEQUENCE_BITS) - 1; private static final long MAX_SEQUENCE = (1L << SEQUENCE_BITS) - 1;
private static final long MAX_TIMESTAMP = (1L << TIMESTAMP_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 MACHINE_SHIFT = SEQUENCE_BITS;
private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + MACHINE_BITS; private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + MACHINE_BITS;
/** /** Unique machine identifier derived from MAC addresses. */
* Unique machine identifier derived from network interfaces (10 bits). private static final long MACHINE_ID = genMachineId();
*/
private static final long machineId = SnowflakeGenerator.genMachineId();
private final AtomicLong lastTimestamp = new AtomicLong(-1L); /** State variables (shared across all threads). */
private long sequence = 0L; private static final AtomicLong LAST_TIMESTAMP = new AtomicLong(-1L);
private static long sequence = 0L;
/** // Prevent instantiation
* Generates a 10-bit machine identifier based on MAC addresses of network interfaces. private SnowflakeGenerator() {}
* Falls back to a random value if MAC cannot be determined.
*/ /** Generates a 10-bit machine identifier from MAC or random fallback. */
private static long genMachineId() { private static long genMachineId() {
try { try {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
@@ -80,48 +63,19 @@ public class SnowflakeGenerator {
} }
} }
/** /** Returns a globally unique 64-bit Snowflake ID. */
* For testing: manually set the last generated timestamp. public static synchronized long nextId() {
* @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.
* <p>
* If multiple IDs are generated in the same millisecond, a sequence number
* is incremented. If the sequence overflows, waits until the next millisecond.
* </p>
*
* @return a unique 64-bit ID
* @throws IllegalStateException if clock moves backwards or timestamp exceeds 41-bit limit
*/
public synchronized long nextId() {
long currentTimestamp = timestamp(); long currentTimestamp = timestamp();
if (currentTimestamp < lastTimestamp.get()) { if (currentTimestamp < LAST_TIMESTAMP.get()) {
throw new IllegalStateException("Clock moved backwards. Refusing to generate id."); throw new IllegalStateException("Clock moved backwards. Refusing to generate ID.");
} }
if (currentTimestamp > MAX_TIMESTAMP) { 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; sequence = (sequence + 1) & MAX_SEQUENCE;
if (sequence == 0) { if (sequence == 0) {
currentTimestamp = waitNextMillis(currentTimestamp); currentTimestamp = waitNextMillis(currentTimestamp);
@@ -130,30 +84,22 @@ public class SnowflakeGenerator {
sequence = 0L; sequence = 0L;
} }
lastTimestamp.set(currentTimestamp); LAST_TIMESTAMP.set(currentTimestamp);
return ((currentTimestamp - EPOCH) << TIMESTAMP_SHIFT) return ((currentTimestamp - EPOCH) << TIMESTAMP_SHIFT)
| (machineId << MACHINE_SHIFT) | (MACHINE_ID << MACHINE_SHIFT)
| sequence; | sequence;
} }
/** /** Waits until next millisecond if sequence exhausted. */
* Waits until the next millisecond if sequence overflows. private static long waitNextMillis(long lastTimestamp) {
* @param lastTimestamp previous timestamp
* @return new timestamp
*/
private long waitNextMillis(long lastTimestamp) {
long ts = timestamp(); long ts = timestamp();
while (ts <= lastTimestamp) { while (ts <= lastTimestamp) ts = timestamp();
ts = timestamp();
}
return ts; return ts;
} }
/** /** Returns current timestamp in milliseconds. */
* Returns current system timestamp in milliseconds. private static long timestamp() {
*/
private long timestamp() {
return System.currentTimeMillis(); return System.currentTimeMillis();
} }
} }

View File

@@ -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 {}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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.
*
* <p>Implementing classes allow an {@link ResourceLoader}
* to automatically merge multiple related files into a single resource instance.</p>
*
* <p>Typical use cases include:</p>
* <ul>
* <li>Localization assets, where multiple `.properties` files (e.g., `messages_en.properties`,
* `messages_nl.properties`) are grouped under the same logical resource.</li>
* <li>Sprite sheets, tile sets, or other multi-file resources that logically belong together.</li>
* </ul>
*
* <p>Implementing classes must provide:</p>
* <ul>
* <li>{@link #loadFile(File)}: Logic to load or merge an individual file into the resource.</li>
* <li>{@link #getBaseName()}: A consistent base name used to group multiple files into this resource.</li>
* </ul>
*
* <p>Example usage:</p>
* <pre>{@code
* public class LocalizationAsset extends BaseResource implements BundledResource {
* private final String baseName;
*
* public LocalizationAsset(File file) {
* super(file);
* this.baseName = extractBaseName(file.getName());
* loadFile(file);
* }
*
* @Override
* public void loadFile(File file) {
* // merge file into existing bundles
* }
*
* @Override
* public String getBaseName() {
* return baseName;
* }
* }
* }</pre>
*
* <p>When used with an asset loader, all files sharing the same base name are
* automatically merged into a single resource instance.</p>
*/
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();
}

View File

@@ -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.
*
* <p>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.</p>
*
* <p>Usage example:</p>
* <pre>{@code
* @FileExtension({"png", "jpg"})
* public class ImageAsset extends BaseResource implements LoadableResource {
* ...
* }
* }</pre>
*
* <p>Key points:</p>
* <ul>
* <li>The annotation is retained at runtime for reflection-based registration.</li>
* <li>Can only be applied to types (classes) that extend {@link BaseResource}.</li>
* <li>Multiple extensions can be specified in the {@code value()} array.</li>
* </ul>
*/
@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();
}

View File

@@ -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.
* <p>
* 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.
* </p>
*
* <p>Implementing classes must define the following behaviors:</p>
* <ul>
* <li>{@link #load()}: Load the resource into memory or perform necessary initialization.</li>
* <li>{@link #unload()}: Release any held resources or memory when the resource is no longer needed.</li>
* <li>{@link #isLoaded()}: Return {@code true} if the resource has been successfully loaded and is ready for use, {@code false} otherwise.</li>
* </ul>
*
* <p>Typical usage:</p>
* <pre>{@code
* public class MyFontAsset extends BaseResource implements LoadableResource {
* private boolean loaded = false;
*
* @Override
* public void load() {
* // Load font file into memory
* loaded = true;
* }
*
* @Override
* public void unload() {
* // Release resources if needed
* loaded = false;
* }
*
* @Override
* public boolean isLoaded() {
* return loaded;
* }
* }
* }</pre>
*
* <p>This interface is commonly used with {@link PreloadResource} to allow automatic
* loading by an {@link ResourceLoader} if desired.</p>
*/
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();
}

View File

@@ -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<T extends AudioResource, K extends AudioResource> {
private final MusicManager<T> musicManager;
private final SoundEffectManager<K> soundEffectManager;
private final VolumeManager audioVolumeManager;
public AudioEventListener(
MusicManager<T> musicManager,
SoundEffectManager<K> 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();
}
}

View File

@@ -1,91 +1,84 @@
package org.toop.framework.audio; package org.toop.framework.audio;
import com.sun.scenario.Settings; import org.toop.framework.audio.interfaces.AudioManager;
import javafx.scene.media.MediaPlayer; import org.toop.framework.audio.interfaces.VolumeManager;
import org.toop.framework.audio.events.AudioEvents; import org.toop.framework.resource.types.AudioResource;
import org.toop.framework.eventbus.EventFlow;
import javax.sound.sampled.Clip; /**
import javax.sound.sampled.FloatControl; * Concrete implementation of {@link VolumeManager} that delegates volume control
* to the {@link VolumeControl} enum.
public class AudioVolumeManager { * <p>
private final SoundManager sM; * This class acts as a central point for updating volume levels for different
* audio categories (MASTER, FX, MUSIC) and for registering audio managers
private double volume = 1.0; * to the appropriate volume types.
private double fxVolume = 1.0; * </p>
private double musicVolume = 1.0; *
* <p>Key responsibilities:</p>
public AudioVolumeManager(SoundManager soundManager){ * <ul>
this.sM = soundManager; * <li>Set and get volume levels for each {@link VolumeControl} category.</li>
* <li>Register {@link AudioManager} instances to specific volume types so
new EventFlow() * that their active audio resources receive volume updates automatically.</li>
.listen(this::handleVolumeChange) * <li>Automatically scales non-master volumes according to the current master volume.</li>
.listen(this::handleFxVolumeChange) * </ul>
.listen(this::handleMusicVolumeChange) *
.listen(this::handleGetCurrentVolume) * <p>Example usage:</p>
.listen(this::handleGetCurrentFxVolume) * <pre>{@code
.listen(this::handleGetCurrentMusicVolume); * 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);
* }</pre>
*/
public class AudioVolumeManager implements VolumeManager {
/**
* Sets the volume for a specific volume type.
* <p>
* 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)){ * Registers an {@link AudioManager} with the specified {@link VolumeControl} category.
FloatControl volumeControl = (FloatControl) clip.getControl(FloatControl.Type.MASTER_GAIN); * <p>
float min = volumeControl.getMinimum(); * All active audio resources managed by the given {@link AudioManager} will
float max = volumeControl.getMaximum(); * automatically receive volume updates when the volume type changes.
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)); * @param type the volume type to register the manager under
volumeControl.setValue(dB); * @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);
} }
} return this;
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();
} }
} }

View File

@@ -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<T extends AudioResource> implements org.toop.framework.audio.interfaces.MusicManager<T> {
private static final Logger logger = LogManager.getLogger(MusicManager.class);
private final List<T> backgroundMusic = new ArrayList<>();
private final Dispatcher dispatcher;
private final List<T> resources;
private int playingIndex = 0;
private boolean playing = false;
public MusicManager(List<T> resources) {
this.dispatcher = new JavaFXDispatcher();
this.resources = resources;
createShuffled();
}
/**
* {@code @TestsOnly} DO NOT USE
*/
@TestsOnly
public MusicManager(List<T> resources, Dispatcher dispatcher) {
this.dispatcher = dispatcher;
this.resources = new ArrayList<>(resources);
backgroundMusic.addAll(resources);
}
@Override
public Collection<T> 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));
}
}

View File

@@ -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<T extends AudioResource> implements org.toop.framework.audio.interfaces.SoundEffectManager<T> {
private static final Logger logger = LogManager.getLogger(SoundEffectManager.class);
private final HashMap<String, T> soundEffectResources;
public <K extends BaseResource & AudioResource> SoundEffectManager(List<ResourceMeta<K>> resources) {
// If there are duplicates, takes discards the first
this.soundEffectResources = (HashMap<String, T>) resources
.stream()
.collect(Collectors.
toMap(ResourceMeta::getName, ResourceMeta::getResource, (a, b) -> b, HashMap::new));
}
@Override
public Collection<T> 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());
}
}

View File

@@ -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<MediaPlayer> activeMusic = new ArrayList<>();
private final Queue<MusicAsset> backgroundMusicQueue = new LinkedList<>();
private final Map<Long, Clip> activeSoundEffects = new HashMap<>();
private final HashMap<String, SoundEffectAsset> 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<SoundEffectAsset> 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<SoundEffectAsset> audioAsset)
throws IOException, UnsupportedAudioFileException, LineUnavailableException {
this.audioResources.put(audioAsset.getName(), audioAsset.getResource());
}
private void handleMusicStart(AudioEvents.StartBackgroundMusic e) {
backgroundMusicQueue.clear();
List<MusicAsset> 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<Long, Clip> getActiveSoundEffects(){ return this.activeSoundEffects; }
public List<MediaPlayer> getActiveMusic() { return activeMusic; }
}

View File

@@ -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.
* <p>
* 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).
* </p>
*
* <p>Volume types:</p>
* <ul>
* <li>{@link #MASTERVOLUME}: The global/master volume that scales all other volume types.</li>
* <li>{@link #FX}: Volume for sound effects, scaled by the master volume.</li>
* <li>{@link #MUSIC}: Volume for music tracks, scaled by the master volume.</li>
* </ul>
*
* <p>Key features:</p>
* <ul>
* <li>Thread-safe management of audio managers using {@link CopyOnWriteArrayList}.</li>
* <li>Automatic propagation of master volume changes to dependent volume types.</li>
* <li>Clamping volume values between 0.0 and 1.0 to ensure valid audio levels.</li>
* <li>Dynamic registration and removal of audio managers for each volume type.</li>
* </ul>
*
* <p>Example usage:</p>
* <pre>{@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();
* }</pre>
*/
public enum VolumeControl {
MASTERVOLUME(),
FX(),
MUSIC();
@SuppressWarnings("ImmutableEnumChecker")
private final List<AudioManager<? extends AudioResource>> managers = new CopyOnWriteArrayList<>();
@SuppressWarnings("ImmutableEnumChecker")
private double volume = 1.0;
@SuppressWarnings("ImmutableEnumChecker")
private double masterVolume = 1.0;
/**
* Sets the volume for this volume type.
* <p>
* 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.
* <p>
* 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<AudioManager<? extends AudioResource>> getManagers() {
return Collections.unmodifiableList(managers);
}
}

View File

@@ -1,94 +1,32 @@
package org.toop.framework.audio.events; package org.toop.framework.audio.events;
import org.toop.framework.eventbus.events.EventWithSnowflake; import org.toop.framework.audio.VolumeControl;
import org.toop.framework.eventbus.events.EventWithoutSnowflake; import org.toop.framework.eventbus.events.*;
import org.toop.framework.eventbus.events.EventsBase; import org.toop.framework.eventbus.events.ResponseToUniqueEvent;
import org.toop.framework.eventbus.events.UniqueEvent;
import java.util.Map;
public class AudioEvents extends EventsBase { public class AudioEvents extends EventsBase {
/** Starts playing a sound. */ /** Stops the audio manager. */
public record PlayEffect(String fileName, boolean loop) public record StopAudioManager() implements GenericEvent {}
implements EventWithoutSnowflake {}
public record StopEffect(long clipId) implements EventWithoutSnowflake {} /** Start playing a sound effect. */
public record PlayEffect(String fileName, boolean loop) implements GenericEvent {}
public record StartBackgroundMusic() implements EventWithoutSnowflake {} /** Stop playing a sound effect. */
public record ChangeVolume(double newVolume) implements EventWithoutSnowflake {} public record StopEffect(String fileName) implements GenericEvent {}
public record ChangeFxVolume(double newVolume) implements EventWithoutSnowflake {}
public record ChangeMusicVolume(double newVolume) implements EventWithoutSnowflake {}
public record GetCurrentVolume(long snowflakeId) implements EventWithSnowflake { /** Start background music. */
@Override public record StartBackgroundMusic() implements GenericEvent {}
public Map<String, Object> result() {
return Map.of();
}
@Override /** Change volume, choose type with {@link VolumeControl}. */
public long eventSnowflake() { public record ChangeVolume(double newVolume, VolumeControl controlType) implements GenericEvent {}
return snowflakeId;
}
}
public record GetCurrentVolumeResponse(double currentVolume, long snowflakeId) implements EventWithSnowflake {
@Override
public Map<String, Object> result() {
return Map.of();
}
@Override /** Requests the desired volume by selecting it with {@link VolumeControl}. */
public long eventSnowflake() { public record GetVolume(VolumeControl controlType, long identifier) implements UniqueEvent {}
return snowflakeId;
}
}
public record GetCurrentFxVolume(long snowflakeId) implements EventWithSnowflake { /** Response to GetVolume. */
@Override public record GetVolumeResponse(double currentVolume, long identifier) implements ResponseToUniqueEvent {}
public Map<String, Object> result() {
return Map.of();
}
@Override /** Plays the predetermined sound for pressing a button. */
public long eventSnowflake() { public record ClickButton() implements GenericEvent {}
return this.snowflakeId; }
}
}
public record GetCurrentMusicVolume(long snowflakeId) implements EventWithSnowflake {
@Override
public Map<String, Object> result() {
return Map.of();
}
@Override
public long eventSnowflake() {
return this.snowflakeId;
}
}
public record GetCurrentFxVolumeResponse(double currentVolume, long snowflakeId) implements EventWithSnowflake {
@Override
public Map<String, Object> result() {
return Map.of();
}
@Override
public long eventSnowflake() {
return this.snowflakeId;
}
}
public record GetCurrentMusicVolumeResponse(double currentVolume, long snowflakeId) implements EventWithSnowflake {
@Override
public Map<String, Object> result() {
return Map.of();
}
@Override
public long eventSnowflake() {
return this.snowflakeId;
}
}
public record ClickButton() implements EventWithoutSnowflake {}
}

View File

@@ -0,0 +1,7 @@
package org.toop.framework.audio.interfaces;
import java.util.Collection;
public interface AudioManager<T> {
Collection<T> getActiveAudio();
}

View File

@@ -0,0 +1,8 @@
package org.toop.framework.audio.interfaces;
import org.toop.framework.resource.types.AudioResource;
public interface MusicManager<T extends AudioResource> extends AudioManager<T> {
void play();
void stop();
}

View File

@@ -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<T extends AudioResource> extends AudioManager<T> {
void play(String name, boolean loop);
void stop(String name);
}

View File

@@ -0,0 +1,53 @@
package org.toop.framework.audio.interfaces;
import org.toop.framework.audio.VolumeControl;
/**
* Interface for managing audio volumes in the application.
* <p>
* 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.
* </p>
*
* <p>Typical responsibilities include:</p>
* <ul>
* <li>Setting the volume for a specific category (master, music, FX).</li>
* <li>Retrieving the current volume of a category.</li>
* <li>Ensuring that changes in master volume propagate to dependent audio categories.</li>
* <li>Interfacing with {@link org.toop.framework.audio.interfaces.AudioManager} to update active audio resources.</li>
* </ul>
*
* <p>Example usage:</p>
* <pre>{@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);
* }</pre>
*/
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);
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,5 @@
package org.toop.framework.dispatch.interfaces;
public interface Dispatcher {
void run(Runnable task);
}

View File

@@ -11,13 +11,14 @@ import java.util.function.Consumer;
import java.util.function.Supplier; import java.util.function.Supplier;
import org.toop.framework.SnowflakeGenerator; import org.toop.framework.SnowflakeGenerator;
import org.toop.framework.eventbus.events.EventType; 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 * EventFlow is a utility class for creating, posting, and optionally subscribing to events in a
* type-safe and chainable manner. It is designed to work with the {@link GlobalEventBus}. * type-safe and chainable manner. It is designed to work with the {@link GlobalEventBus}.
* *
* <p>This class supports automatic UUID assignment for {@link EventWithSnowflake} events, and * <p>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 * 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 * subscription methods are chainable, and you can configure automatic unsubscription after an event
* has been successfully handled. * has been successfully handled.
@@ -30,7 +31,7 @@ public class EventFlow {
/** Cache of constructor handles for event classes to avoid repeated reflection lookups. */ /** Cache of constructor handles for event classes to avoid repeated reflection lookups. */
private static final Map<Class<?>, MethodHandle> CONSTRUCTOR_CACHE = new ConcurrentHashMap<>(); private static final Map<Class<?>, MethodHandle> CONSTRUCTOR_CACHE = new ConcurrentHashMap<>();
/** Automatically assigned UUID for {@link EventWithSnowflake} events. */ /** Automatically assigned UUID for {@link UniqueEvent} events. */
private long eventSnowflake = -1; private long eventSnowflake = -1;
/** The event instance created by this publisher. */ /** The event instance created by this publisher. */
@@ -40,7 +41,7 @@ public class EventFlow {
private final List<ListenerHandler> listeners = new ArrayList<>(); private final List<ListenerHandler> listeners = new ArrayList<>();
/** Holds the results returned from the subscribed event, if any. */ /** Holds the results returned from the subscribed event, if any. */
private Map<String, Object> result = null; private Map<String, ?> result = null;
/** Empty constructor (event must be added via {@link #addPostEvent(Class, Object...)}). */ /** Empty constructor (event must be added via {@link #addPostEvent(Class, Object...)}). */
public EventFlow() {} public EventFlow() {}
@@ -60,7 +61,7 @@ public class EventFlow {
// Keep the old class+args version if needed // Keep the old class+args version if needed
public <T extends EventType> EventFlow addPostEvent(Class<T> eventClass, Object... args) { public <T extends EventType> EventFlow addPostEvent(Class<T> eventClass, Object... args) {
try { try {
boolean isUuidEvent = EventWithSnowflake.class.isAssignableFrom(eventClass); boolean isUuidEvent = UniqueEvent.class.isAssignableFrom(eventClass);
MethodHandle ctorHandle = MethodHandle ctorHandle =
CONSTRUCTOR_CACHE.computeIfAbsent( CONSTRUCTOR_CACHE.computeIfAbsent(
@@ -81,7 +82,7 @@ public class EventFlow {
int expectedParamCount = ctorHandle.type().parameterCount(); int expectedParamCount = ctorHandle.type().parameterCount();
if (isUuidEvent && args.length < expectedParamCount) { if (isUuidEvent && args.length < expectedParamCount) {
this.eventSnowflake = new SnowflakeGenerator().nextId(); this.eventSnowflake = SnowflakeGenerator.nextId();
finalArgs = new Object[args.length + 1]; finalArgs = new Object[args.length + 1];
System.arraycopy(args, 0, finalArgs, 0, args.length); System.arraycopy(args, 0, finalArgs, 0, args.length);
finalArgs[args.length] = this.eventSnowflake; 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. */ /** Subscribe by ID: only fires if UUID matches this publisher's eventId. */
public <TT extends EventWithSnowflake> EventFlow onResponse( public <TT extends ResponseToUniqueEvent> EventFlow onResponse(
Class<TT> eventClass, Consumer<TT> action, boolean unsubscribeAfterSuccess) { Class<TT> eventClass, Consumer<TT> action, boolean unsubscribeAfterSuccess) {
ListenerHandler[] listenerHolder = new ListenerHandler[1]; ListenerHandler[] listenerHolder = new ListenerHandler[1];
listenerHolder[0] = listenerHolder[0] =
@@ -114,7 +110,7 @@ public class EventFlow {
GlobalEventBus.subscribe( GlobalEventBus.subscribe(
eventClass, eventClass,
event -> { event -> {
if (event.eventSnowflake() != this.eventSnowflake) return; if (event.getIdentifier() != this.eventSnowflake) return;
action.accept(event); action.accept(event);
@@ -130,22 +126,21 @@ public class EventFlow {
} }
/** Subscribe by ID: only fires if UUID matches this publisher's eventId. */ /** Subscribe by ID: only fires if UUID matches this publisher's eventId. */
public <TT extends EventWithSnowflake> EventFlow onResponse( public <TT extends ResponseToUniqueEvent> EventFlow onResponse(Class<TT> eventClass, Consumer<TT> action) {
Class<TT> eventClass, Consumer<TT> action) {
return this.onResponse(eventClass, action, true); return this.onResponse(eventClass, action, true);
} }
/** Subscribe by ID without explicit class. */ /** Subscribe by ID without explicit class. */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public <TT extends EventWithSnowflake> EventFlow onResponse( public <TT extends ResponseToUniqueEvent> EventFlow onResponse(
Consumer<TT> action, boolean unsubscribeAfterSuccess) { Consumer<TT> action, boolean unsubscribeAfterSuccess) {
ListenerHandler[] listenerHolder = new ListenerHandler[1]; ListenerHandler[] listenerHolder = new ListenerHandler[1];
listenerHolder[0] = listenerHolder[0] =
new ListenerHandler( new ListenerHandler(
GlobalEventBus.subscribe( GlobalEventBus.subscribe(
event -> { event -> {
if (!(event instanceof EventWithSnowflake uuidEvent)) return; if (!(event instanceof UniqueEvent uuidEvent)) return;
if (uuidEvent.eventSnowflake() == this.eventSnowflake) { if (uuidEvent.getIdentifier() == this.eventSnowflake) {
try { try {
TT typedEvent = (TT) uuidEvent; TT typedEvent = (TT) uuidEvent;
action.accept(typedEvent); action.accept(typedEvent);
@@ -159,7 +154,7 @@ public class EventFlow {
throw new ClassCastException( throw new ClassCastException(
"Cannot cast " "Cannot cast "
+ event.getClass().getName() + event.getClass().getName()
+ " to EventWithSnowflake"); + " to UniqueEvent");
} }
} }
})); }));
@@ -167,7 +162,7 @@ public class EventFlow {
return this; return this;
} }
public <TT extends EventWithSnowflake> EventFlow onResponse(Consumer<TT> action) { public <TT extends ResponseToUniqueEvent> EventFlow onResponse(Consumer<TT> action) {
return this.onResponse(action, true); return this.onResponse(action, true);
} }
@@ -214,7 +209,7 @@ public class EventFlow {
throw new ClassCastException( throw new ClassCastException(
"Cannot cast " "Cannot cast "
+ event.getClass().getName() + event.getClass().getName()
+ " to EventWithSnowflake"); + " to UniqueEvent");
} }
})); }));
this.listeners.add(listenerHolder[0]); this.listeners.add(listenerHolder[0]);
@@ -237,7 +232,13 @@ public class EventFlow {
return this; return this;
} }
public Map<String, Object> getResult() { private void clean() {
this.listeners.clear();
this.event = null;
this.result = null;
} // TODO
public Map<String, ?> getResult() {
return this.result; return this.result;
} }

View File

@@ -7,7 +7,7 @@ import java.util.Map;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.function.Consumer; import java.util.function.Consumer;
import org.toop.framework.eventbus.events.EventType; 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 * 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. */ /** Map of event class to Snowflake-ID-specific listeners. */
private static final Map< private static final Map<
Class<?>, ConcurrentHashMap<Long, Consumer<? extends EventWithSnowflake>>> Class<?>, ConcurrentHashMap<Long, Consumer<? extends UniqueEvent>>>
UUID_LISTENERS = new ConcurrentHashMap<>(); UUID_LISTENERS = new ConcurrentHashMap<>();
/** Disruptor ring buffer size (must be power of two). */ /** Disruptor ring buffer size (must be power of two). */
@@ -90,7 +90,7 @@ public final class GlobalEventBus {
return wrapper; return wrapper;
} }
public static <T extends EventWithSnowflake> void subscribeById( public static <T extends UniqueEvent> void subscribeById(
Class<T> eventClass, long eventId, Consumer<T> listener) { Class<T> eventClass, long eventId, Consumer<T> listener) {
UUID_LISTENERS UUID_LISTENERS
.computeIfAbsent(eventClass, _ -> new ConcurrentHashMap<>()) .computeIfAbsent(eventClass, _ -> new ConcurrentHashMap<>())
@@ -101,9 +101,9 @@ public final class GlobalEventBus {
LISTENERS.values().forEach(list -> list.remove(listener)); LISTENERS.values().forEach(list -> list.remove(listener));
} }
public static <T extends EventWithSnowflake> void unsubscribeById( public static <T extends UniqueEvent> void unsubscribeById(
Class<T> eventClass, long eventId) { Class<T> eventClass, long eventId) {
Map<Long, Consumer<? extends EventWithSnowflake>> map = UUID_LISTENERS.get(eventClass); Map<Long, Consumer<? extends UniqueEvent>> map = UUID_LISTENERS.get(eventClass);
if (map != null) map.remove(eventId); if (map != null) map.remove(eventId);
} }
@@ -152,11 +152,11 @@ public final class GlobalEventBus {
} }
// snowflake listeners // snowflake listeners
if (event instanceof EventWithSnowflake snowflakeEvent) { if (event instanceof UniqueEvent snowflakeEvent) {
Map<Long, Consumer<? extends EventWithSnowflake>> map = UUID_LISTENERS.get(clazz); Map<Long, Consumer<? extends UniqueEvent>> map = UUID_LISTENERS.get(clazz);
if (map != null) { if (map != null) {
Consumer<EventWithSnowflake> listener = Consumer<UniqueEvent> listener =
(Consumer<EventWithSnowflake>) map.remove(snowflakeEvent.eventSnowflake()); (Consumer<UniqueEvent>) map.remove(snowflakeEvent.getIdentifier());
if (listener != null) { if (listener != null) {
try { try {
listener.accept(snowflakeEvent); listener.accept(snowflakeEvent);

View File

@@ -1,7 +1,7 @@
package org.toop.framework.eventbus; package org.toop.framework.eventbus;
public class ListenerHandler { public class ListenerHandler {
private Object listener = null; private Object listener;
// private boolean unsubscribeAfterSuccess = true; // private boolean unsubscribeAfterSuccess = true;

View File

@@ -1,8 +0,0 @@
package org.toop.framework.eventbus.events;
import java.util.Map;
public interface EventWithSnowflake extends EventType {
Map<String, Object> result();
long eventSnowflake();
}

View File

@@ -1,3 +0,0 @@
package org.toop.framework.eventbus.events;
public interface EventWithoutSnowflake extends EventType {}

View File

@@ -1,69 +1,4 @@
package org.toop.framework.eventbus.events; package org.toop.framework.eventbus.events;
import java.lang.reflect.Constructor;
import java.util.Arrays;
/** Events that are used in the GlobalEventBus class. */ /** Events that are used in the GlobalEventBus class. */
public class EventsBase { 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);
}
}

View File

@@ -0,0 +1,3 @@
package org.toop.framework.eventbus.events;
public interface GenericEvent extends EventType {}

View File

@@ -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<String, Object> result() {
Map<String, Object> 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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -84,9 +84,9 @@ public class NetworkingClient {
if (isChannelActive()) { if (isChannelActive()) {
this.channel.writeAndFlush(msg); this.channel.writeAndFlush(msg);
logger.info( logger.info(
"Connection {} sent message: '{}'", this.channel.remoteAddress(), literalMsg); "Connection {} sent message: '{}' ", this.channel.remoteAddress(), literalMsg);
} else { } else {
logger.warn("Cannot send message: '{}', connection inactive.", literalMsg); logger.warn("Cannot send message: '{}', connection inactive. ", literalMsg);
} }
} }

View File

@@ -45,8 +45,8 @@ public class NetworkingClientManager {
} }
long startClientRequest(String ip, int port) { long startClientRequest(String ip, int port) {
long connectionId = new SnowflakeGenerator().nextId(); // TODO: Maybe use the one generated long connectionId = SnowflakeGenerator.nextId();
try { // With EventFlow try {
NetworkingClient client = NetworkingClient client =
new NetworkingClient( new NetworkingClient(
() -> new NetworkingGameClientHandler(connectionId), () -> new NetworkingGameClientHandler(connectionId),
@@ -81,19 +81,13 @@ public class NetworkingClientManager {
void handleStartClient(NetworkEvents.StartClient event) { void handleStartClient(NetworkEvents.StartClient event) {
long id = this.startClientRequest(event.ip(), event.port()); long id = this.startClientRequest(event.ip(), event.port());
new Thread( new Thread(
() -> { () ->
try {
Thread.sleep(100); // TODO: Is this a good idea?
new EventFlow() new EventFlow()
.addPostEvent( .addPostEvent(
NetworkEvents.StartClientResponse.class, NetworkEvents.StartClientResponse.class,
id, id,
event.eventSnowflake()) event.eventSnowflake())
.asyncPostEvent(); .asyncPostEvent())
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
})
.start(); .start();
} }
@@ -185,7 +179,7 @@ public class NetworkingClientManager {
void handleCloseClient(NetworkEvents.CloseClient event) { void handleCloseClient(NetworkEvents.CloseClient event) {
NetworkingClient client = this.networkClients.get(event.clientId()); 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()); this.networkClients.remove(event.clientId());
logger.info("Client {} closed successfully.", event.clientId()); logger.info("Client {} closed successfully.", event.clientId());
} }

View File

@@ -74,7 +74,7 @@ public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
gameWinConditionHandler(recSrvRemoved); gameWinConditionHandler(recSrvRemoved);
return; return;
default: default:
return; // return
} }
} else { } else {
@@ -93,10 +93,10 @@ public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
helpHandler(recSrvRemoved); helpHandler(recSrvRemoved);
return; return;
default: default:
return; // return
} }
} else { } 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) { private void gameWinConditionHandler(String rec) {
@SuppressWarnings("StreamToString")
String condition = String condition =
Pattern.compile("\\b(win|draw|lose)\\b", Pattern.CASE_INSENSITIVE) Pattern.compile("\\b(win|draw|lose)\\b", Pattern.CASE_INSENSITIVE)
.matcher(rec) .matcher(rec)
@@ -136,7 +137,7 @@ public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
try { try {
String[] msg = String[] msg =
Pattern.compile( Pattern.compile(
"(?:CHALLENGER|GAMETYPE|CHALLENGENUMBER):\\s*\"?(.*?)\"?\\s*(?:,|})") "(?:CHALLENGER|GAMETYPE|CHALLENGENUMBER):\\s*\"?(.*?)\"?\\s*[,}]")
.matcher(rec) .matcher(rec)
.results() .results()
.map(m -> m.group().trim()) .map(m -> m.group().trim())
@@ -180,6 +181,7 @@ public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
} }
private void gameYourTurnHandler(String rec) { private void gameYourTurnHandler(String rec) {
@SuppressWarnings("StreamToString")
String msg = String msg =
Pattern.compile("TURNMESSAGE:\\s*\"([^\"]*)\"") Pattern.compile("TURNMESSAGE:\\s*\"([^\"]*)\"")
.matcher(rec) .matcher(rec)

View File

@@ -1,13 +1,13 @@
package org.toop.framework.networking.events; package org.toop.framework.networking.events;
import java.lang.reflect.RecordComponent;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import java.util.stream.Stream; import org.toop.framework.eventbus.events.GenericEvent;
import org.toop.framework.eventbus.events.EventWithSnowflake; import org.toop.framework.eventbus.events.ResponseToUniqueEvent;
import org.toop.framework.eventbus.events.EventWithoutSnowflake; import org.toop.framework.eventbus.events.UniqueEvent;
import org.toop.framework.eventbus.events.EventsBase; import org.toop.framework.eventbus.events.EventsBase;
import org.toop.annotations.AutoResponseResult;
import org.toop.framework.networking.NetworkingClient; import org.toop.framework.networking.NetworkingClient;
/** /**
@@ -15,8 +15,8 @@ import org.toop.framework.networking.NetworkingClient;
* org.toop.framework.eventbus.GlobalEventBus}. * org.toop.framework.eventbus.GlobalEventBus}.
* *
* <p>This class defines all the events that can be posted or listened to in the networking * <p>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 * subsystem. Events are separated into those with unique IDs (UniqueEvent) and those without
* (EventWithoutSnowflake). * (GenericEvent).
*/ */
public class NetworkEvents extends EventsBase { public class NetworkEvents extends EventsBase {
@@ -30,86 +30,76 @@ public class NetworkEvents extends EventsBase {
* instances. * instances.
*/ */
public record RequestsAllClients(CompletableFuture<List<NetworkingClient>> future) public record RequestsAllClients(CompletableFuture<List<NetworkingClient>> future)
implements EventWithoutSnowflake {} implements GenericEvent {}
/** Forces all active client connections to close immediately. */ /** Forces all active client connections to close immediately. */
public record ForceCloseAllClients() implements EventWithoutSnowflake {} public record ForceCloseAllClients() implements GenericEvent {}
/** Response indicating a challenge was cancelled. */ /** Response indicating a challenge was cancelled. */
public record ChallengeCancelledResponse(long clientId, String challengeId) public record ChallengeCancelledResponse(long clientId, String challengeId) implements GenericEvent {}
implements EventWithoutSnowflake {}
/** Response indicating a challenge was received. */ /** Response indicating a challenge was received. */
public record ChallengeResponse( public record ChallengeResponse(long clientId, String challengerName, String challengeId, String gameType)
long clientId, String challengerName, String challengeId, String gameType) implements GenericEvent {}
implements EventWithoutSnowflake {}
/** Response containing a list of players for a client. */ /** Response containing a list of players for a client. */
public record PlayerlistResponse(long clientId, String[] playerlist) public record PlayerlistResponse(long clientId, String[] playerlist) implements GenericEvent {}
implements EventWithoutSnowflake {}
/** Response containing a list of games for a client. */ /** Response containing a list of games for a client. */
public record GamelistResponse(long clientId, String[] gamelist) public record GamelistResponse(long clientId, String[] gamelist) implements GenericEvent {}
implements EventWithoutSnowflake {}
/** Response indicating a game match information for a client. */ /** Response indicating a game match information for a client. */
public record GameMatchResponse( public record GameMatchResponse(long clientId, String playerToMove, String gameType, String opponent)
long clientId, String playerToMove, String gameType, String opponent) implements GenericEvent {}
implements EventWithoutSnowflake {}
/** Response indicating the result of a game. */ /** Response indicating the result of a game. */
public record GameResultResponse(long clientId, String condition) public record GameResultResponse(long clientId, String condition) implements GenericEvent {}
implements EventWithoutSnowflake {}
/** Response indicating a game move occurred. */ /** Response indicating a game move occurred. */
public record GameMoveResponse(long clientId, String player, String move, String details) public record GameMoveResponse(long clientId, String player, String move, String details) implements GenericEvent {}
implements EventWithoutSnowflake {}
/** Response indicating it is the player's turn. */ /** Response indicating it is the player's turn. */
public record YourTurnResponse(long clientId, String message) public record YourTurnResponse(long clientId, String message)
implements EventWithoutSnowflake {} implements GenericEvent {}
/** Request to send login credentials for a client. */ /** 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. */ /** 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. */ /** 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. */ /** 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. */ /** 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. */ /** 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. */ /** Request to challenge another player. */
public record SendChallenge(long clientId, String usernameToChallenge, String gameType) public record SendChallenge(long clientId, String usernameToChallenge, String gameType) implements GenericEvent {}
implements EventWithoutSnowflake {}
/** Request to accept a challenge. */ /** Request to accept a challenge. */
public record SendAcceptChallenge(long clientId, int challengeId) public record SendAcceptChallenge(long clientId, int challengeId) implements GenericEvent {}
implements EventWithoutSnowflake {}
/** Request to forfeit a game. */ /** 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. */ /** 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. */ /** 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. */ /** Request to display help for a specific command. */
public record SendHelpForCommand(long clientId, String command) public record SendHelpForCommand(long clientId, String command) implements GenericEvent {}
implements EventWithoutSnowflake {}
/** Request to close a specific client connection. */ /** 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. * Event to start a new client connection.
@@ -120,61 +110,19 @@ public class NetworkEvents extends EventsBase {
* @param port Server port. * @param port Server port.
* @param eventSnowflake Unique event identifier for correlation. * @param eventSnowflake Unique event identifier for correlation.
*/ */
public record StartClient(String ip, int port, long eventSnowflake) public record StartClient(String ip, int port, long eventSnowflake) implements UniqueEvent {}
implements EventWithSnowflake {
@Override
public Map<String, Object> 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;
}
}
/** /**
* Response confirming a client was started. * Response confirming a client was started.
* *
* @param clientId The client ID assigned to the new connection. * @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) @AutoResponseResult
implements EventWithSnowflake { public record StartClientResponse(long clientId, long identifier) implements ResponseToUniqueEvent {}
@Override
public Map<String, Object> 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;
}
}
/** Generic server response. */ /** 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. * Request to send a command to a server.
@@ -182,10 +130,10 @@ public class NetworkEvents extends EventsBase {
* @param clientId The client connection ID. * @param clientId The client connection ID.
* @param args The command arguments. * @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. */ /** 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. * 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 clientId The connection ID that received the message.
* @param message The message content. * @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. * 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 ip The new server IP.
* @param port The new server port. * @param port The new server port.
*/ */
public record ChangeClientHost(long clientId, String ip, int port) public record ChangeClientHost(long clientId, String ip, int port) implements GenericEvent {}
implements EventWithoutSnowflake {}
/** WIP (Not working) Response indicating that the client could not connect. */ /** 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. */ /** Event indicating a client connection was closed. */
public record ClosedConnection(long clientId) implements EventWithoutSnowflake {} public record ClosedConnection(long clientId) implements GenericEvent {}
} }

View File

@@ -1,14 +1,4 @@
package org.toop.framework.asset; package org.toop.framework.resource;
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;
import java.io.File; import java.io.File;
import java.util.*; import java.util.*;
@@ -16,30 +6,40 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function; 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. * Responsible for loading assets from a file system directory into memory.
* <p>
* 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).
* </p>
* *
* <p>Assets are stored in a static, thread-safe list and can be retrieved * <p>The {@code ResourceLoader} scans a root folder recursively, identifies files, and maps them to
* through {@link ResourceManager}.</p> * 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).
*
* <p>Assets are stored in a static, thread-safe list and can be retrieved through {@link
* ResourceManager}.
*
* <p>Features:
* *
* <p>Features:</p>
* <ul> * <ul>
* <li>Recursive directory scanning for assets.</li> * <li>Recursive directory scanning for assets.
* <li>Automatic registration of resource classes via reflection.</li> * <li>Automatic registration of resource classes via reflection.
* <li>Bundled resource support: multiple files merged into a single resource instance.</li> * <li>Bundled resource support: multiple files merged into a single resource instance.
* <li>Preload resources automatically invoke {@link PreloadResource#load()}.</li> * <li>Preload resources automatically invoke {@link PreloadResource#load()}.
* <li>Progress tracking via {@link AssetLoaderEvents.LoadingProgressUpdate} events.</li> * <li>Progress tracking via {@link AssetLoaderEvents.LoadingProgressUpdate} events.
* </ul> * </ul>
* *
* <p>Usage example:</p> * <p>Usage example:
*
* <pre>{@code * <pre>{@code
* ResourceLoader loader = new ResourceLoader("assets"); * ResourceLoader loader = new ResourceLoader("assets");
* double progress = loader.getProgress(); * double progress = loader.getProgress();
@@ -48,14 +48,17 @@ import java.util.function.Function;
*/ */
public class ResourceLoader { public class ResourceLoader {
private static final Logger logger = LogManager.getLogger(ResourceLoader.class); private static final Logger logger = LogManager.getLogger(ResourceLoader.class);
private static final List<ResourceMeta<? extends BaseResource>> assets = new CopyOnWriteArrayList<>(); private static final List<ResourceMeta<? extends BaseResource>> assets =
private final Map<String, Function<File, ? extends BaseResource>> registry = new ConcurrentHashMap<>(); new CopyOnWriteArrayList<>();
private final Map<String, Function<File, ? extends BaseResource>> registry =
new ConcurrentHashMap<>();
private final AtomicInteger loadedCount = new AtomicInteger(0); private final AtomicInteger loadedCount = new AtomicInteger(0);
private int totalCount = 0; private int totalCount = 0;
/** /**
* Constructs an ResourceLoader and loads assets from the given root folder. * Constructs an ResourceLoader and loads assets from the given root folder.
*
* @param rootFolder the folder containing asset files * @param rootFolder the folder containing asset files
*/ */
public ResourceLoader(File rootFolder) { public ResourceLoader(File rootFolder) {
@@ -84,6 +87,7 @@ public class ResourceLoader {
/** /**
* Constructs an ResourceLoader from a folder path. * Constructs an ResourceLoader from a folder path.
*
* @param rootFolder the folder path containing assets * @param rootFolder the folder path containing assets
*/ */
public ResourceLoader(String rootFolder) { public ResourceLoader(String rootFolder) {
@@ -92,6 +96,7 @@ public class ResourceLoader {
/** /**
* Returns the current progress of loading assets (0.0 to 1.0). * Returns the current progress of loading assets (0.0 to 1.0).
*
* @return progress as a double * @return progress as a double
*/ */
public double getProgress() { public double getProgress() {
@@ -100,6 +105,7 @@ public class ResourceLoader {
/** /**
* Returns the number of assets loaded so far. * Returns the number of assets loaded so far.
*
* @return loaded count * @return loaded count
*/ */
public int getLoadedCount() { public int getLoadedCount() {
@@ -108,6 +114,7 @@ public class ResourceLoader {
/** /**
* Returns the total number of files found to load. * Returns the total number of files found to load.
*
* @return total asset count * @return total asset count
*/ */
public int getTotalCount() { public int getTotalCount() {
@@ -116,6 +123,7 @@ public class ResourceLoader {
/** /**
* Returns a snapshot list of all assets loaded by this loader. * Returns a snapshot list of all assets loaded by this loader.
*
* @return list of loaded assets * @return list of loaded assets
*/ */
public List<ResourceMeta<? extends BaseResource>> getAssets() { public List<ResourceMeta<? extends BaseResource>> getAssets() {
@@ -124,6 +132,7 @@ public class ResourceLoader {
/** /**
* Registers a factory for a specific file extension. * Registers a factory for a specific file extension.
*
* @param extension the file extension (without dot) * @param extension the file extension (without dot)
* @param factory a function mapping a File to a resource instance * @param factory a function mapping a File to a resource instance
* @param <T> the type of resource * @param <T> the type of resource
@@ -132,69 +141,79 @@ public class ResourceLoader {
this.registry.put(extension, factory); this.registry.put(extension, factory);
} }
/** /** Maps a file to a resource instance based on its extension and registered factories. */
* Maps a file to a resource instance based on its extension and registered factories. private <T extends BaseResource> T resourceMapper(File file)
*/ throws CouldNotCreateResourceFactoryException, IllegalArgumentException {
private <T extends BaseResource> T resourceMapper(File file, Class<T> type) {
String ext = getExtension(file.getName()); String ext = getExtension(file.getName());
Function<File, ? extends BaseResource> factory = registry.get(ext); Function<File, ? extends BaseResource> factory = registry.get(ext);
if (factory == null) return null; if (factory == null)
throw new CouldNotCreateResourceFactoryException(registry, file.getName());
BaseResource resource = factory.apply(file); BaseResource resource = factory.apply(file);
if (!type.isInstance(resource)) { if (resource == null) {
throw new IllegalArgumentException( 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<T>) 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<File> files) { private void loader(List<File> files) {
Map<String, BundledResource> bundledResources = new HashMap<>(); Map<String, BundledResource> bundledResources = new HashMap<>();
for (File file : files) { for (File file : files) {
boolean skipAdd = false; 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) { switch (resource) {
case null -> { case null -> {
continue; continue;
} }
case BundledResource br -> { case BundledResource br -> {
String key = resource.getClass().getName() + ":" + br.getBaseName(); 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); bundledResources.get(key).loadFile(file);
resource = (BaseResource) bundledResources.get(key); resource = (BaseResource) bundledResources.get(key);
assets.add(new ResourceMeta<>(br.getBaseName(), resource)); assets.add(new ResourceMeta<>(br.getBaseName(), resource));
skipAdd = true; skipAdd = true;
} }
case PreloadResource pr -> pr.load(); case PreloadResource pr -> pr.load();
default -> { default -> {}
}
} }
BaseResource finalResource = resource; BaseResource finalResource = resource;
boolean alreadyAdded = assets.stream() boolean alreadyAdded = assets.stream().anyMatch(a -> a.getResource() == finalResource);
.anyMatch(a -> a.getResource() == finalResource);
if (!alreadyAdded && !skipAdd) { if (!alreadyAdded && !skipAdd) {
assets.add(new ResourceMeta<>(file.getName(), resource)); 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(); loadedCount.incrementAndGet();
new EventFlow() new EventFlow()
.addPostEvent(new AssetLoaderEvents.LoadingProgressUpdate(loadedCount.get(), totalCount)) .addPostEvent(
new AssetLoaderEvents.LoadingProgressUpdate(
loadedCount.get(), totalCount))
.postEvent(); .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<File> foundFiles) { private void fileSearcher(final File folder, List<File> foundFiles) {
for (File fileEntry : Objects.requireNonNull(folder.listFiles())) { for (File fileEntry : Objects.requireNonNull(folder.listFiles())) {
if (fileEntry.isDirectory()) { if (fileEntry.isDirectory()) {
@@ -206,31 +225,31 @@ public class ResourceLoader {
} }
/** /**
* Uses reflection to automatically register all {@link BaseResource} subclasses * Uses reflection to automatically register all {@link BaseResource} subclasses annotated with
* annotated with {@link FileExtension}. * {@link FileExtension}.
*/ */
private void autoRegisterResources() { private void autoRegisterResources() {
Reflections reflections = new Reflections("org.toop.framework.asset.resources"); Reflections reflections = new Reflections("org.toop.framework.resource.resources");
Set<Class<? extends BaseResource>> classes = reflections.getSubTypesOf(BaseResource.class); Set<Class<? extends BaseResource>> classes = reflections.getSubTypesOf(BaseResource.class);
for (Class<? extends BaseResource> cls : classes) { for (Class<? extends BaseResource> cls : classes) {
if (!cls.isAnnotationPresent(FileExtension.class)) continue; if (!cls.isAnnotationPresent(FileExtension.class)) continue;
FileExtension annotation = cls.getAnnotation(FileExtension.class); FileExtension annotation = cls.getAnnotation(FileExtension.class);
for (String ext : annotation.value()) { for (String ext : annotation.value()) {
registry.put(ext, file -> { registry.put(
try { ext,
return cls.getConstructor(File.class).newInstance(file); file -> {
} catch (Exception e) { try {
throw new RuntimeException(e); 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) { private static String getBaseName(String fileName) {
int underscoreIndex = fileName.indexOf('_'); int underscoreIndex = fileName.indexOf('_');
int dotIndex = fileName.lastIndexOf('.'); int dotIndex = fileName.lastIndexOf('.');
@@ -238,9 +257,7 @@ public class ResourceLoader {
return fileName.substring(0, dotIndex); 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) { public static String getExtension(String name) {
int i = name.lastIndexOf('.'); int i = name.lastIndexOf('.');
return (i > 0) ? name.substring(i + 1) : ""; return (i > 0) ? name.substring(i + 1) : "";

View File

@@ -1,30 +1,31 @@
package org.toop.framework.asset; package org.toop.framework.resource;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.toop.framework.asset.resources.*;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; 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. * Centralized manager for all loaded assets in the application.
* <p>
* {@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.
* </p>
* *
* <p>Key responsibilities:</p> * <p>{@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.
*
* <p>Key responsibilities:
*
* <ul> * <ul>
* <li>Storing all loaded assets in a concurrent map.</li> * <li>Storing all loaded assets in a concurrent map.
* <li>Providing typed access to asset resources.</li> * <li>Providing typed access to asset resources.
* <li>Allowing lookup by asset name or ID.</li> * <li>Allowing lookup by asset name or ID.
* <li>Supporting retrieval of all assets of a specific {@link BaseResource} subclass.</li> * <li>Supporting retrieval of all assets of a specific {@link BaseResource} subclass.
* </ul> * </ul>
* *
* <p>Example usage:</p> * <p>Example usage:
*
* <pre>{@code * <pre>{@code
* // Load assets from a loader * // Load assets from a loader
* ResourceLoader loader = new ResourceLoader(new File("RootFolder")); * ResourceLoader loader = new ResourceLoader(new File("RootFolder"));
@@ -40,27 +41,28 @@ import java.util.concurrent.ConcurrentHashMap;
* Optional<Asset<? extends BaseResource>> maybeAsset = ResourceManager.findByName("menu.css"); * Optional<Asset<? extends BaseResource>> maybeAsset = ResourceManager.findByName("menu.css");
* }</pre> * }</pre>
* *
* <p>Notes:</p> * <p>Notes:
*
* <ul> * <ul>
* <li>All retrieval methods are static and thread-safe.</li> * <li>All retrieval methods are static and thread-safe.
* <li>The {@link #get(String)} method may require casting if the asset type is not known at compile time.</li> * <li>The {@link #get(String)} method may require casting if the asset type is not known at
* <li>Assets should be loaded via {@link ResourceLoader} before retrieval.</li> * compile time.
* <li>Assets should be loaded via {@link ResourceLoader} before retrieval.
* </ul> * </ul>
*/ */
public class ResourceManager { public class ResourceManager {
private static final Logger logger = LogManager.getLogger(ResourceManager.class); private static final Logger logger = LogManager.getLogger(ResourceManager.class);
private static final ResourceManager INSTANCE = new ResourceManager(); private static final Map<String, ResourceMeta<? extends BaseResource>> assets =
private static final Map<String, ResourceMeta<? extends BaseResource>> assets = new ConcurrentHashMap<>(); new ConcurrentHashMap<>();
private static ResourceManager instance;
private ResourceManager() {} private ResourceManager() {}
/**
* Returns the singleton instance of {@code ResourceManager}.
*
* @return the shared instance
*/
public static ResourceManager getInstance() { 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 * @param loader the loader that has already loaded assets
*/ */
public synchronized static void loadAssets(ResourceLoader loader) { public static synchronized void loadAssets(ResourceLoader loader) {
for (var asset : loader.getAssets()) { for (ResourceMeta<? extends BaseResource> asset : loader.getAssets()) {
assets.put(asset.getName(), asset); assets.put(asset.getName(), asset);
} }
} }
@@ -85,15 +87,15 @@ public class ResourceManager {
public static <T extends BaseResource> T get(String name) { public static <T extends BaseResource> T get(String name) {
ResourceMeta<T> asset = (ResourceMeta<T>) assets.get(name); ResourceMeta<T> asset = (ResourceMeta<T>) assets.get(name);
if (asset == null) { 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(); return asset.getResource();
} }
// @SuppressWarnings("unchecked") // @SuppressWarnings("unchecked")
// public static <T extends BaseResource> ArrayList<ResourceMeta<T>> getAllOfType() { // public static <T extends BaseResource> ArrayList<ResourceMeta<T>> getAllOfType() {
// return (ArrayList<ResourceMeta<T>>) (ArrayList<?>) new ArrayList<>(assets.values()); // return (ArrayList<ResourceMeta<T>>) (ArrayList<?>) new ArrayList<>(assets.values());
// } // }
/** /**
* Retrieve all assets of a specific resource type. * Retrieve all assets of a specific resource type.
@@ -102,16 +104,32 @@ public class ResourceManager {
* @param <T> the resource type * @param <T> the resource type
* @return a list of assets matching the type * @return a list of assets matching the type
*/ */
public static <T extends BaseResource> ArrayList<ResourceMeta<T>> getAllOfType(Class<T> type) { public static <T extends BaseResource> List<ResourceMeta<T>> getAllOfType(Class<T> type) {
ArrayList<ResourceMeta<T>> list = new ArrayList<>(); List<ResourceMeta<T>> result = new ArrayList<>();
for (ResourceMeta<? extends BaseResource> asset : assets.values()) {
if (type.isInstance(asset.getResource())) { for (ResourceMeta<? extends BaseResource> meta : assets.values()) {
BaseResource res = meta.getResource();
if (type.isInstance(res)) {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
ResourceMeta<T> typed = (ResourceMeta<T>) asset; ResourceMeta<T> typed = (ResourceMeta<T>) meta;
list.add(typed); result.add(typed);
} }
} }
return list;
return result;
}
public static <T extends BaseResource> List<T> getAllOfTypeAndRemoveWrapper(Class<T> type) {
List<T> 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) { public static void addAsset(ResourceMeta<? extends BaseResource> asset) {
assets.put(asset.getName(), asset); assets.put(asset.getName(), asset);
logger.info("Successfully added asset: {}, to the asset list", asset.getName());
} }
} }

View File

@@ -1,7 +1,7 @@
package org.toop.framework.asset; package org.toop.framework.resource;
import org.toop.framework.SnowflakeGenerator; import org.toop.framework.SnowflakeGenerator;
import org.toop.framework.asset.resources.BaseResource; import org.toop.framework.resource.resources.BaseResource;
public class ResourceMeta<T extends BaseResource> { public class ResourceMeta<T extends BaseResource> {
private final Long id; private final Long id;
@@ -9,7 +9,7 @@ public class ResourceMeta<T extends BaseResource> {
private final T resource; private final T resource;
public ResourceMeta(String name, T resource) { public ResourceMeta(String name, T resource) {
this.id = new SnowflakeGenerator().nextId(); this.id = SnowflakeGenerator.nextId();
this.name = name; this.name = name;
this.resource = resource; this.resource = resource;
} }
@@ -25,5 +25,4 @@ public class ResourceMeta<T extends BaseResource> {
public T getResource() { public T getResource() {
return this.resource; return this.resource;
} }
} }

View File

@@ -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 {}
}

View File

@@ -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()));
}
}

View File

@@ -0,0 +1,7 @@
package org.toop.framework.resource.exceptions;
public class IsNotAResourceException extends RuntimeException {
public <T> IsNotAResourceException(Class<T> clazz, String message) {
super(clazz.getName() + " does not implement BaseResource");
}
}

View File

@@ -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);
}
}

View File

@@ -1,4 +1,4 @@
package org.toop.framework.asset.resources; package org.toop.framework.resource.resources;
import java.io.*; import java.io.*;
@@ -14,5 +14,4 @@ public abstract class BaseResource {
public File getFile() { public File getFile() {
return this.file; return this.file;
} }
} }

View File

@@ -1,8 +1,7 @@
package org.toop.framework.asset.resources; package org.toop.framework.resource.resources;
import org.toop.framework.asset.types.FileExtension;
import java.io.File; import java.io.File;
import org.toop.framework.resource.types.FileExtension;
@FileExtension({"css"}) @FileExtension({"css"})
public class CssAsset extends BaseResource { public class CssAsset extends BaseResource {

View File

@@ -1,12 +1,11 @@
package org.toop.framework.asset.resources; package org.toop.framework.resource.resources;
import javafx.scene.text.Font;
import org.toop.framework.asset.types.FileExtension;
import org.toop.framework.asset.types.PreloadResource;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException; 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"}) @FileExtension({"ttf", "otf"})
public class FontAsset extends BaseResource implements PreloadResource { public class FontAsset extends BaseResource implements PreloadResource {
@@ -60,4 +59,4 @@ public class FontAsset extends BaseResource implements PreloadResource {
} }
return this.family; return this.family;
} }
} }

View File

@@ -1,11 +1,10 @@
package org.toop.framework.asset.resources; package org.toop.framework.resource.resources;
import javafx.scene.image.Image;
import org.toop.framework.asset.types.FileExtension;
import org.toop.framework.asset.types.LoadableResource;
import java.io.File; import java.io.File;
import java.io.FileInputStream; 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"}) @FileExtension({"png", "jpg", "jpeg"})
public class ImageAsset extends BaseResource implements LoadableResource { public class ImageAsset extends BaseResource implements LoadableResource {
@@ -45,4 +44,4 @@ public class ImageAsset extends BaseResource implements LoadableResource {
} }
return null; return null;
} }
} }

View File

@@ -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.Gson;
import com.google.gson.GsonBuilder; 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.File;
import java.io.FileReader; import java.io.FileReader;
import java.io.FileWriter; import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
import org.toop.framework.resource.types.FileExtension;
import org.toop.framework.resource.types.LoadableResource;
@FileExtension({"json"}) @FileExtension({"json"})
public class JsonAsset<T> extends BaseResource implements LoadableResource { public class JsonAsset<T> extends BaseResource implements LoadableResource {
@@ -25,7 +26,8 @@ public class JsonAsset<T> extends BaseResource implements LoadableResource {
File file = getFile(); File file = getFile();
if (!file.exists()) { if (!file.exists()) {
try { 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(); content = type.getDeclaredConstructor().newInstance();
save(); save();
} catch (Exception e) { } catch (Exception e) {
@@ -36,7 +38,7 @@ public class JsonAsset<T> extends BaseResource implements LoadableResource {
try (FileReader reader = new FileReader(file)) { try (FileReader reader = new FileReader(file)) {
content = gson.fromJson(reader, type); content = gson.fromJson(reader, type);
this.isLoaded = true; this.isLoaded = true;
} catch(Exception e) { } catch (Exception e) {
throw new RuntimeException("Failed to load JSON asset" + getFile(), e); throw new RuntimeException("Failed to load JSON asset" + getFile(), e);
} }
} }
@@ -59,10 +61,11 @@ public class JsonAsset<T> extends BaseResource implements LoadableResource {
File file = getFile(); File file = getFile();
File parent = file.getParentFile(); File parent = file.getParentFile();
if (parent != null && !parent.exists()) { if (parent != null && !parent.exists()) {
parent.mkdirs(); boolean isDirectoryMade = parent.mkdirs();
assert isDirectoryMade;
} }
try (FileWriter writer = new FileWriter(file)) { try (FileWriter writer = new FileWriter(file)) {
gson.toJson(content, writer); gson.toJson(content, writer);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException("Failed to save JSON asset" + getFile(), e); throw new RuntimeException("Failed to save JSON asset" + getFile(), e);
} }

View File

@@ -1,34 +1,29 @@
package org.toop.framework.asset.resources; package org.toop.framework.resource.resources;
import org.toop.framework.asset.types.BundledResource;
import org.toop.framework.asset.types.FileExtension;
import org.toop.framework.asset.types.LoadableResource;
import java.io.*; import java.io.*;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.*; 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 * Represents a localization resource asset that loads and manages property files containing
* containing key-value pairs for different locales. * key-value pairs for different locales.
* <p> *
* This class implements {@link LoadableResource} to support loading/unloading * <p>This class implements {@link LoadableResource} to support loading/unloading and {@link
* and {@link BundledResource} to represent resources that can contain multiple * BundledResource} to represent resources that can contain multiple localized bundles.
* localized bundles. *
* </p> * <p>Files handled by this class must have the {@code .properties} extension, optionally with a
* <p> * locale suffix, e.g., {@code messages_en_US.properties}.
* Files handled by this class must have the {@code .properties} extension, *
* optionally with a locale suffix, e.g., {@code messages_en_US.properties}. * <p>Example usage:
* </p>
* *
* <p>
* Example usage:
* <pre>{@code * <pre>{@code
* LocalizationAsset asset = new LocalizationAsset(new File("messages_en_US.properties")); * LocalizationAsset asset = new LocalizationAsset(new File("messages_en_US.properties"));
* asset.load(); * asset.load();
* String greeting = asset.getString("hello", Locale.US); * String greeting = asset.getString("hello", Locale.US);
* }</pre> * }</pre>
* </p>
*/ */
@FileExtension({"properties"}) @FileExtension({"properties"})
public class LocalizationAsset extends BaseResource implements LoadableResource, BundledResource { public class LocalizationAsset extends BaseResource implements LoadableResource, BundledResource {
@@ -37,13 +32,13 @@ public class LocalizationAsset extends BaseResource implements LoadableResource,
private final Map<Locale, ResourceBundle> bundles = new HashMap<>(); private final Map<Locale, ResourceBundle> bundles = new HashMap<>();
/** Flag indicating whether this asset has been loaded. */ /** Flag indicating whether this asset has been loaded. */
private boolean isLoaded = false; private boolean loaded = false;
/** Basename of the given asset */ /** Basename of the given asset */
private final String baseName = "localization"; private final String baseName = "localization";
/** Fallback locale used when no matching locale is found. */ /** 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. * Constructs a new LocalizationAsset for the specified file.
@@ -54,22 +49,18 @@ public class LocalizationAsset extends BaseResource implements LoadableResource,
super(file); super(file);
} }
/** /** Loads the resource file into memory and prepares localized bundles. */
* Loads the resource file into memory and prepares localized bundles.
*/
@Override @Override
public void load() { public void load() {
loadFile(getFile()); loadFile(getFile());
isLoaded = true; loaded = true;
} }
/** /** Unloads all loaded resource bundles, freeing memory. */
* Unloads all loaded resource bundles, freeing memory.
*/
@Override @Override
public void unload() { public void unload() {
bundles.clear(); bundles.clear();
isLoaded = false; loaded = false;
} }
/** /**
@@ -79,15 +70,14 @@ public class LocalizationAsset extends BaseResource implements LoadableResource,
*/ */
@Override @Override
public boolean isLoaded() { public boolean isLoaded() {
return isLoaded; return loaded;
} }
/** /**
* Retrieves a localized string for the given key and locale. * Retrieves a localized string for the given key and locale. If an exact match for the locale
* If an exact match for the locale is not found, a fallback * is not found, a fallback matching the language or the default locale will be used.
* 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 * @param locale the desired locale
* @return the localized string * @return the localized string
* @throws MissingResourceException if no resource bundle is available for the locale * @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) { public String getString(String key, Locale locale) {
Locale target = findBestLocale(locale); Locale target = findBestLocale(locale);
ResourceBundle bundle = bundles.get(target); ResourceBundle bundle = bundles.get(target);
if (bundle == null) throw new MissingResourceException( if (bundle == null)
"No bundle for locale: " + target, getClass().getName(), key); throw new MissingResourceException(
"No bundle for locale: " + target, getClass().getName(), key);
return bundle.getString(key); return bundle.getString(key);
} }
/** /**
* Finds the best matching locale among loaded bundles. * Finds the best matching locale among loaded bundles. Prefers an exact match, then
* Prefers an exact match, then language-only match, then fallback. * language-only match, then fallback.
* *
* @param locale the desired locale * @param locale the desired locale
* @return the best matching 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. * Loads a specific property file as a resource bundle. The locale is extracted from the file
* The locale is extracted from the file name if present. * name if present.
* *
* @param file the property file to load * @param file the property file to load
* @throws RuntimeException if the file cannot be read * @throws RuntimeException if the file cannot be read
@@ -134,13 +125,13 @@ public class LocalizationAsset extends BaseResource implements LoadableResource,
@Override @Override
public void loadFile(File file) { public void loadFile(File file) {
try (InputStreamReader reader = 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); Locale locale = extractLocale(file.getName(), baseName);
bundles.put(locale, new PropertyResourceBundle(reader)); bundles.put(locale, new PropertyResourceBundle(reader));
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException("Failed to load localization file: " + file, 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; return this.baseName;
} }
// /** // /**
// * Extracts the base name from a file name. // * Extracts the base name from a file name.
// * // *
// * @param fileName the file name // * @param fileName the file name
// * @return base name without locale or extension // * @return base name without locale or extension
// */ // */
// private String getBaseName(String fileName) { // private String getBaseName(String fileName) {
// int dotIndex = fileName.lastIndexOf('.'); // int dotIndex = fileName.lastIndexOf('.');
// String nameWithoutExtension = (dotIndex > 0) ? fileName.substring(0, dotIndex) : fileName; // String nameWithoutExtension = (dotIndex > 0) ? fileName.substring(0, dotIndex) :
// // fileName;
// int underscoreIndex = nameWithoutExtension.indexOf('_'); //
// if (underscoreIndex > 0) { // int underscoreIndex = nameWithoutExtension.indexOf('_');
// return nameWithoutExtension.substring(0, underscoreIndex); // if (underscoreIndex > 0) {
// } // return nameWithoutExtension.substring(0, underscoreIndex);
// return nameWithoutExtension; // }
// } // return nameWithoutExtension;
// }
/** /**
* Extracts a locale from a file name based on the pattern "base_LOCALE.properties". * Extracts a locale from a file name based on the pattern "base_LOCALE.properties".

View File

@@ -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();
}
}

View File

@@ -1,10 +1,8 @@
package org.toop.framework.asset.resources; package org.toop.framework.resource.resources;
import org.toop.framework.settings.Settings;
import java.io.File; import java.io.File;
import java.util.Locale; import java.util.Locale;
import org.toop.framework.settings.Settings;
public class SettingsAsset extends JsonAsset<Settings> { public class SettingsAsset extends JsonAsset<Settings> {
@@ -32,13 +30,13 @@ public class SettingsAsset extends JsonAsset<Settings> {
return getContent().fullScreen; return getContent().fullScreen;
} }
public String getTheme() { public String getTheme() {
return getContent().theme; return getContent().theme;
} }
public String getLayoutSize() { public String getLayoutSize() {
return getContent().layoutSize; return getContent().layoutSize;
} }
public void setVolume(int volume) { public void setVolume(int volume) {
getContent().volume = volume; getContent().volume = volume;
@@ -65,13 +63,13 @@ public class SettingsAsset extends JsonAsset<Settings> {
save(); save();
} }
public void setTheme(String theme) { public void setTheme(String theme) {
getContent().theme = theme; getContent().theme = theme;
save(); save();
} }
public void setLayoutSize(String layoutSize) { public void setLayoutSize(String layoutSize) {
getContent().layoutSize = layoutSize; getContent().layoutSize = layoutSize;
save(); save();
} }
} }

View File

@@ -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();
}
}

View File

@@ -1,12 +1,11 @@
package org.toop.framework.asset.resources; package org.toop.framework.resource.resources;
import org.toop.framework.asset.types.FileExtension;
import org.toop.framework.asset.types.LoadableResource;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import org.toop.framework.resource.types.FileExtension;
import org.toop.framework.resource.types.LoadableResource;
@FileExtension({"txt", "json", "xml"}) @FileExtension({"txt", "json", "xml"})
public class TextAsset extends BaseResource implements LoadableResource { public class TextAsset extends BaseResource implements LoadableResource {
@@ -41,4 +40,4 @@ public class TextAsset extends BaseResource implements LoadableResource {
public String getContent() { public String getContent() {
return this.content; return this.content;
} }
} }

View File

@@ -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);
}

View File

@@ -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.
*
* <p>Implementing classes allow an {@link ResourceLoader} to automatically merge multiple related
* files into a single resource instance.
*
* <p>Typical use cases include:
*
* <ul>
* <li>Localization assets, where multiple `.properties` files (e.g., `messages_en.properties`,
* `messages_nl.properties`) are grouped under the same logical resource.
* <li>Sprite sheets, tile sets, or other multi-file resources that logically belong together.
* </ul>
*
* <p>Implementing classes must provide:
*
* <ul>
* <li>{@link #loadFile(File)}: Logic to load or merge an individual file into the resource.
* <li>{@link #getBaseName()}: A consistent base name used to group multiple files into this
* resource.
* </ul>
*
* <p>Example usage:
*
* <pre>{@code
* public class LocalizationAsset extends BaseResource implements BundledResource {
* private final String baseName;
*
* public LocalizationAsset(File file) {
* super(file);
* this.baseName = extractBaseName(file.getName());
* loadFile(file);
* }
*
* @Override
* public void loadFile(File file) {
* // merge file into existing bundles
* }
*
* @Override
* public String getBaseName() {
* return baseName;
* }
* }
* }</pre>
*
* <p>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();
}

View File

@@ -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.
*
* <p>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.
*
* <p>Usage example:
*
* <pre>{@code
* @FileExtension({"png", "jpg"})
* public class ImageAsset extends BaseResource implements LoadableResource {
* ...
* }
* }</pre>
*
* <p>Key points:
*
* <ul>
* <li>The annotation is retained at runtime for reflection-based registration.
* <li>Can only be applied to types (classes) that extend {@link BaseResource}.
* <li>Multiple extensions can be specified in the {@code value()} array.
* </ul>
*/
@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();
}

View File

@@ -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.
*
* <p>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.
*
* <p>Implementing classes must define the following behaviors:
*
* <ul>
* <li>{@link #load()}: Load the resource into memory or perform necessary initialization.
* <li>{@link #unload()}: Release any held resources or memory when the resource is no longer
* needed.
* <li>{@link #isLoaded()}: Return {@code true} if the resource has been successfully loaded and
* is ready for use, {@code false} otherwise.
* </ul>
*
* <p>Typical usage:
*
* <pre>{@code
* public class MyFontAsset extends BaseResource implements LoadableResource {
* private boolean loaded = false;
*
* @Override
* public void load() {
* // Load font file into memory
* loaded = true;
* }
*
* @Override
* public void unload() {
* // Release resources if needed
* loaded = false;
* }
*
* @Override
* public boolean isLoaded() {
* return loaded;
* }
* }
* }</pre>
*
* <p>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();
}

View File

@@ -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}.
* *
* <p>Extends {@link LoadableResource}, so any implementing class must provide the standard * <p>Extends {@link LoadableResource}, so any implementing class must provide the standard {@link
* {@link LoadableResource#load()} and {@link LoadableResource#unload()} methods, as well as the * LoadableResource#load()} and {@link LoadableResource#unload()} methods, as well as the {@link
* {@link LoadableResource#isLoaded()} check.</p> * LoadableResource#isLoaded()} check.
* *
* <p>When a resource implements {@code PreloadResource}, the {@code ResourceLoader} will invoke * <p>When a resource implements {@code PreloadResource}, the {@code ResourceLoader} will invoke
* {@link LoadableResource#load()} automatically after the resource is discovered and instantiated, * {@link LoadableResource#load()} automatically after the resource is discovered and instantiated,
* without requiring manual loading by the user.</p> * without requiring manual loading by the user.
*
* <p>Typical usage:
* *
* <p>Typical usage:</p>
* <pre>{@code * <pre>{@code
* public class MyFontAsset extends BaseResource implements PreloadResource { * public class MyFontAsset extends BaseResource implements PreloadResource {
* @Override * @Override
@@ -34,6 +36,6 @@ import org.toop.framework.asset.ResourceLoader;
* }</pre> * }</pre>
* *
* <p>Note: Only use this interface for resources that are safe to load at startup, as it may * <p>Note: Only use this interface for resources that are safe to load at startup, as it may
* increase memory usage or startup time.</p> * increase memory usage or startup time.
*/ */
public interface PreloadResource extends LoadableResource {} public interface PreloadResource extends LoadableResource {}

View File

@@ -3,8 +3,8 @@ package org.toop.framework.settings;
public class Settings { public class Settings {
public boolean fullScreen = false; public boolean fullScreen = false;
public String locale = "en"; public String locale = "en";
public String theme = "dark"; public String theme = "dark";
public String layoutSize = "medium"; public String layoutSize = "medium";
public int volume = 100; public int volume = 100;
public int fxVolume = 20; public int fxVolume = 20;
public int musicVolume = 15; public int musicVolume = 15;

View File

@@ -1,78 +1,79 @@
package org.toop.framework; //package org.toop.framework;
//
import static org.junit.jupiter.api.Assertions.*; //import static org.junit.jupiter.api.Assertions.*;
//
import java.util.HashSet; //import java.util.HashSet;
import java.util.Set; //import java.util.Set;
import org.junit.jupiter.api.Test; //import org.junit.jupiter.api.Test;
//
class SnowflakeGeneratorTest { //class SnowflakeGeneratorTest {
//
@Test // @Test
void testMachineIdWithinBounds() { // void testMachineIdWithinBounds() {
SnowflakeGenerator generator = new SnowflakeGenerator(); // SnowflakeGenerator generator = new SnowflakeGenerator();
long machineIdField = getMachineId(generator); // long machineIdField = getMachineId(generator);
assertTrue( // assertTrue(
machineIdField >= 0 && machineIdField <= 1023, // machineIdField >= 0 && machineIdField <= 1023,
"Machine ID should be within 0-1023"); // "Machine ID should be within 0-1023");
} // }
//
@Test // @Test
void testNextIdReturnsUniqueValues() { // void testNextIdReturnsUniqueValues() {
SnowflakeGenerator generator = new SnowflakeGenerator(); // SnowflakeGenerator generator = new SnowflakeGenerator();
Set<Long> ids = new HashSet<>(); // Set<Long> ids = new HashSet<>();
for (int i = 0; i < 1000; i++) { // for (int i = 0; i < 1000; i++) {
long id = generator.nextId(); // long id = generator.nextId();
assertFalse(ids.contains(id), "Duplicate ID generated"); // assertFalse(ids.contains(id), "Duplicate ID generated");
ids.add(id); // ids.add(id);
} // }
} // }
//
@Test // @Test
void testSequenceRollover() throws Exception { // void testSequenceRollover() throws Exception {
SnowflakeGenerator generator = // SnowflakeGenerator generator =
new SnowflakeGenerator() { // new SnowflakeGenerator() {
private long fakeTime = System.currentTimeMillis(); // private long fakeTime = System.currentTimeMillis();
//
protected long timestamp() { // protected long timestamp() {
return fakeTime; // return fakeTime;
} // }
//
void incrementTime() { // void incrementTime() {
fakeTime++; // fakeTime++;
} // }
}; // };
//
long first = generator.nextId(); // long first = generator.nextId();
long second = generator.nextId(); // long second = generator.nextId();
assertNotEquals( // assertNotEquals(
first, second, "IDs generated within same millisecond should differ by sequence"); // first, second, "IDs generated within same millisecond should differ by sequence");
//
// Force sequence overflow // // Force sequence overflow
for (int i = 0; i < (1 << 12); i++) generator.nextId(); // for (int i = 0; i < (1 << 12); i++) generator.nextId();
long afterOverflow = generator.nextId(); // long afterOverflow = generator.nextId();
assertTrue(afterOverflow > second, "ID after sequence rollover should be greater"); // assertTrue(afterOverflow > second, "ID after sequence rollover should be greater");
} // }
//
@Test // @Test
void testNextIdMonotonic() { // void testNextIdMonotonic() {
SnowflakeGenerator generator = new SnowflakeGenerator(); // SnowflakeGenerator generator = new SnowflakeGenerator();
long prev = generator.nextId(); // long prev = generator.nextId();
for (int i = 0; i < 100; i++) { // for (int i = 0; i < 100; i++) {
long next = generator.nextId(); // long next = generator.nextId();
assertTrue(next > prev, "IDs must be increasing"); // assertTrue(next > prev, "IDs must be increasing");
prev = next; // prev = next;
} // }
} // }
//
// Helper: reflectively get machineId // // Helper: reflectively get machineId
private long getMachineId(SnowflakeGenerator generator) { // private long getMachineId(SnowflakeGenerator generator) {
try { // try {
var field = SnowflakeGenerator.class.getDeclaredField("machineId"); // var field = SnowflakeGenerator.class.getDeclaredField("machineId");
field.setAccessible(true); // field.setAccessible(true);
return (long) field.get(generator); // return (long) field.get(generator);
} catch (Exception e) { // } catch (Exception e) {
throw new RuntimeException(e); // throw new RuntimeException(e);
} // }
} // }
} //}
// TODO

View File

@@ -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<MockAudioResource> manager;
@BeforeEach
void setUp() {
dispatcher = Runnable::run;
track1 = new MockAudioResource("track1");
track2 = new MockAudioResource("track2");
track3 = new MockAudioResource("track3");
List<MockAudioResource> 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<MockAudioResource> manyTracks = new ArrayList<>();
for (int i = 1; i <= 1_000; i++) {
manyTracks.add(new MockAudioResource("track" + i));
}
MusicManager<MockAudioResource> 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");
}
}
}

View File

@@ -2,7 +2,7 @@
// //
// import org.junit.jupiter.api.Tag; // import org.junit.jupiter.api.Tag;
// import org.junit.jupiter.api.Test; // 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.math.BigInteger;
// import java.util.concurrent.*; // import java.util.concurrent.*;
@@ -13,7 +13,7 @@
// class EventFlowStressTest { // class EventFlowStressTest {
// //
// /** Top-level record to ensure runtime type matches subscription */ // /** 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 // @Override
// public java.util.Map<String, Object> result() { // public java.util.Map<String, Object> result() {
// return java.util.Map.of("payload", payload, "eventId", eventSnowflake); // return java.util.Map.of("payload", payload, "eventId", eventSnowflake);
@@ -26,7 +26,7 @@
// } // }
// //
// public record HeavyEventSuccess(String payload, long eventSnowflake) implements // public record HeavyEventSuccess(String payload, long eventSnowflake) implements
// EventWithSnowflake { // UniqueEvent {
// @Override // @Override
// public java.util.Map<String, Object> result() { // public java.util.Map<String, Object> result() {
// return java.util.Map.of("payload", payload, "eventId", eventSnowflake); // return java.util.Map.of("payload", payload, "eventId", eventSnowflake);

View File

@@ -1,92 +1,93 @@
package org.toop.framework.eventbus; //package org.toop.framework.eventbus;
//
import static org.junit.jupiter.api.Assertions.*; //import static org.junit.jupiter.api.Assertions.*;
//
import java.util.HashSet; //import java.util.HashSet;
import java.util.Set; //import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean; //import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.jupiter.api.Test; //import org.junit.jupiter.api.Test;
import org.toop.framework.SnowflakeGenerator; //import org.toop.framework.SnowflakeGenerator;
import org.toop.framework.eventbus.events.EventWithSnowflake; //import org.toop.framework.eventbus.events.UniqueEvent;
//
class EventFlowTest { //class EventFlowTest {
//
@Test // @Test
void testSnowflakeStructure() { // void testSnowflakeStructure() {
long id = new SnowflakeGenerator().nextId(); // long id = new SnowflakeGenerator().nextId();
//
long timestampPart = id >>> 22; // long timestampPart = id >>> 22;
long randomPart = id & ((1L << 22) - 1); // long randomPart = id & ((1L << 22) - 1);
//
assertTrue(timestampPart > 0, "Timestamp part should be non-zero"); // assertTrue(timestampPart > 0, "Timestamp part should be non-zero");
assertTrue( // assertTrue(
randomPart >= 0 && randomPart < (1L << 22), "Random part should be within 22 bits"); // randomPart >= 0 && randomPart < (1L << 22), "Random part should be within 22 bits");
} // }
//
@Test // @Test
void testSnowflakeMonotonicity() throws InterruptedException { // void testSnowflakeMonotonicity() throws InterruptedException {
SnowflakeGenerator sf = new SnowflakeGenerator(); // SnowflakeGenerator sf = new SnowflakeGenerator();
long id1 = sf.nextId(); // long id1 = sf.nextId();
Thread.sleep(1); // ensure timestamp increases // Thread.sleep(1); // ensure timestamp increases
long id2 = sf.nextId(); // long id2 = sf.nextId();
//
assertTrue(id2 > id1, "Later snowflake should be greater than earlier one"); // assertTrue(id2 > id1, "Later snowflake should be greater than earlier one");
} // }
//
@Test // @Test
void testSnowflakeUniqueness() { // void testSnowflakeUniqueness() {
SnowflakeGenerator sf = new SnowflakeGenerator(); // SnowflakeGenerator sf = new SnowflakeGenerator();
Set<Long> ids = new HashSet<>(); // Set<Long> ids = new HashSet<>();
for (int i = 0; i < 100_000; i++) { // for (int i = 0; i < 100_000; i++) {
long id = sf.nextId(); // long id = sf.nextId();
assertTrue(ids.add(id), "Snowflake IDs should be unique, but duplicate found"); // assertTrue(ids.add(id), "Snowflake IDs should be unique, but duplicate found");
} // }
} // }
//
// --- Dummy Event classes for testing --- // // --- Dummy Event classes for testing ---
static class DummySnowflakeEvent implements EventWithSnowflake { // static class DummySnowflakeUniqueEvent implements UniqueEvent {
private final long snowflake; // private final long snowflake;
//
DummySnowflakeEvent(long snowflake) { // DummySnowflakeUniqueEvent(long snowflake) {
this.snowflake = snowflake; // this.snowflake = snowflake;
} // }
////
@Override //// @Override
public long eventSnowflake() { //// public long eventSnowflake() {
return snowflake; //// return snowflake;
} //// }
////
@Override //// @Override
public java.util.Map<String, Object> result() { //// public java.util.Map<String, Object> result() {
return java.util.Collections.emptyMap(); //// return java.util.Collections.emptyMap();
} //// }
} // }
//
@Test // @Test
void testSnowflakeIsInjectedIntoEvent() { // void testSnowflakeIsInjectedIntoEvent() {
EventFlow flow = new EventFlow(); // EventFlow flow = new EventFlow();
flow.addPostEvent(DummySnowflakeEvent.class); // no args, should auto-generate // flow.addPostEvent(DummySnowflakeUniqueEvent.class); // no args, should auto-generate
//
long id = flow.getEventSnowflake(); // long id = flow.getEventSnowflake();
assertNotEquals(-1, id, "Snowflake should be auto-generated"); // assertNotEquals(-1, id, "Snowflake should be auto-generated");
assertTrue(flow.getEvent() instanceof DummySnowflakeEvent); // assertTrue(flow.getEvent() instanceof DummySnowflakeUniqueEvent);
assertEquals(id, ((DummySnowflakeEvent) flow.getEvent()).eventSnowflake()); // assertEquals(id, ((DummySnowflakeUniqueEvent) flow.getEvent()).eventSnowflake());
} // }
//
@Test // @Test
void testOnResponseFiltersBySnowflake() { // void testOnResponseFiltersBySnowflake() {
EventFlow flow = new EventFlow(); // EventFlow flow = new EventFlow();
flow.addPostEvent(DummySnowflakeEvent.class); // flow.addPostEvent(DummySnowflakeUniqueEvent.class);
//
AtomicBoolean handlerCalled = new AtomicBoolean(false); // AtomicBoolean handlerCalled = new AtomicBoolean(false);
flow.onResponse(DummySnowflakeEvent.class, event -> handlerCalled.set(true)); // flow.onResponse(DummySnowflakeUniqueEvent.class, event -> handlerCalled.set(true));
//
// Post with non-matching snowflake // // Post with non-matching snowflake
GlobalEventBus.post(new DummySnowflakeEvent(12345L)); // GlobalEventBus.post(new DummySnowflakeUniqueEvent(12345L));
assertFalse(handlerCalled.get(), "Handler should not fire for mismatched snowflake"); // assertFalse(handlerCalled.get(), "Handler should not fire for mismatched snowflake");
//
// Post with matching snowflake // // Post with matching snowflake
GlobalEventBus.post(new DummySnowflakeEvent(flow.getEventSnowflake())); // GlobalEventBus.post(new DummySnowflakeUniqueEvent(flow.getEventSnowflake()));
assertTrue(handlerCalled.get(), "Handler should fire for matching snowflake"); // assertTrue(handlerCalled.get(), "Handler should fire for matching snowflake");
} // }
} //}
// TODO

View File

@@ -1,159 +1,160 @@
package org.toop.framework.eventbus; //package org.toop.framework.eventbus;
//
import static org.junit.jupiter.api.Assertions.*; //import static org.junit.jupiter.api.Assertions.*;
//
import java.util.concurrent.*; //import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean; //import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference; //import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer; //import java.util.function.Consumer;
import org.junit.jupiter.api.*; //import org.junit.jupiter.api.*;
import org.toop.framework.eventbus.events.EventType; //import org.toop.framework.eventbus.events.EventType;
import org.toop.framework.eventbus.events.EventWithSnowflake; //import org.toop.framework.eventbus.events.UniqueEvent;
//
class GlobalEventBusTest { //class GlobalEventBusTest {
//
// ------------------------------------------------------------------------ // // ------------------------------------------------------------------------
// Test Events // // Test Events
// ------------------------------------------------------------------------ // // ------------------------------------------------------------------------
private record TestEvent(String message) implements EventType {} // private record TestEvent(String message) implements EventType {}
//
private record TestSnowflakeEvent(long eventSnowflake, String payload) // private record TestSnowflakeUniqueEvent(long eventSnowflake, String payload)
implements EventWithSnowflake { // implements UniqueEvent {
@Override // @Override
public java.util.Map<String, Object> result() { // public java.util.Map<String, Object> result() {
return java.util.Map.of("payload", payload); // return java.util.Map.of("payload", payload);
} // }
} // }
//
static class SampleEvent implements EventType { // static class SampleEvent implements EventType {
private final String message; // private final String message;
//
SampleEvent(String message) { // SampleEvent(String message) {
this.message = message; // this.message = message;
} // }
//
public String message() { // public String message() {
return message; // return message;
} // }
} // }
//
@AfterEach // @AfterEach
void cleanup() { // void cleanup() {
GlobalEventBus.reset(); // GlobalEventBus.reset();
} // }
//
// ------------------------------------------------------------------------ // // ------------------------------------------------------------------------
// Subscriptions // // Subscriptions
// ------------------------------------------------------------------------ // // ------------------------------------------------------------------------
@Test // @Test
void testSubscribeAndPost() { // void testSubscribeAndPost() {
AtomicReference<String> received = new AtomicReference<>(); // AtomicReference<String> received = new AtomicReference<>();
Consumer<TestEvent> listener = e -> received.set(e.message()); // Consumer<TestEvent> listener = e -> received.set(e.message());
//
GlobalEventBus.subscribe(TestEvent.class, listener); // GlobalEventBus.subscribe(TestEvent.class, listener);
GlobalEventBus.post(new TestEvent("hello")); // GlobalEventBus.post(new TestEvent("hello"));
//
assertEquals("hello", received.get()); // assertEquals("hello", received.get());
} // }
//
@Test // @Test
void testUnsubscribe() { // void testUnsubscribe() {
GlobalEventBus.reset(); // GlobalEventBus.reset();
//
AtomicBoolean called = new AtomicBoolean(false); // AtomicBoolean called = new AtomicBoolean(false);
//
// Subscribe and keep the wrapper reference // // Subscribe and keep the wrapper reference
Consumer<? super EventType> subscription = // Consumer<? super EventType> subscription =
GlobalEventBus.subscribe(SampleEvent.class, e -> called.set(true)); // GlobalEventBus.subscribe(SampleEvent.class, e -> called.set(true));
//
// Post once -> should trigger // // Post once -> should trigger
GlobalEventBus.post(new SampleEvent("test1")); // GlobalEventBus.post(new SampleEvent("test1"));
assertTrue(called.get(), "Listener should be triggered before unsubscribe"); // assertTrue(called.get(), "Listener should be triggered before unsubscribe");
//
// Reset flag // // Reset flag
called.set(false); // called.set(false);
//
// Unsubscribe using the wrapper reference // // Unsubscribe using the wrapper reference
GlobalEventBus.unsubscribe(subscription); // GlobalEventBus.unsubscribe(subscription);
//
// Post again -> should NOT trigger // // Post again -> should NOT trigger
GlobalEventBus.post(new SampleEvent("test2")); // GlobalEventBus.post(new SampleEvent("test2"));
assertFalse(called.get(), "Listener should not be triggered after unsubscribe"); // assertFalse(called.get(), "Listener should not be triggered after unsubscribe");
} // }
//
@Test // @Test
void testSubscribeGeneric() { // void testSubscribeGeneric() {
AtomicReference<EventType> received = new AtomicReference<>(); // AtomicReference<EventType> received = new AtomicReference<>();
Consumer<Object> listener = e -> received.set((EventType) e); // Consumer<Object> listener = e -> received.set((EventType) e);
//
GlobalEventBus.subscribe(listener); // GlobalEventBus.subscribe(listener);
TestEvent event = new TestEvent("generic"); // TestEvent event = new TestEvent("generic");
GlobalEventBus.post(event); // GlobalEventBus.post(event);
//
assertEquals(event, received.get()); // assertEquals(event, received.get());
} // }
//
@Test // @Test
void testSubscribeById() { // void testSubscribeById() {
AtomicReference<String> received = new AtomicReference<>(); // AtomicReference<String> received = new AtomicReference<>();
long id = 42L; // long id = 42L;
//
GlobalEventBus.subscribeById(TestSnowflakeEvent.class, id, e -> received.set(e.payload())); // GlobalEventBus.subscribeById(TestSnowflakeUniqueEvent.class, id, e -> received.set(e.payload()));
GlobalEventBus.post(new TestSnowflakeEvent(id, "snowflake")); // GlobalEventBus.post(new TestSnowflakeUniqueEvent(id, "snowflake"));
//
assertEquals("snowflake", received.get()); // assertEquals("snowflake", received.get());
} // }
//
@Test // @Test
void testUnsubscribeById() { // void testUnsubscribeById() {
AtomicBoolean triggered = new AtomicBoolean(false); // AtomicBoolean triggered = new AtomicBoolean(false);
long id = 99L; // long id = 99L;
//
GlobalEventBus.subscribeById(TestSnowflakeEvent.class, id, e -> triggered.set(true)); // GlobalEventBus.subscribeById(TestSnowflakeUniqueEvent.class, id, e -> triggered.set(true));
GlobalEventBus.unsubscribeById(TestSnowflakeEvent.class, id); // GlobalEventBus.unsubscribeById(TestSnowflakeUniqueEvent.class, id);
//
GlobalEventBus.post(new TestSnowflakeEvent(id, "ignored")); // GlobalEventBus.post(new TestSnowflakeUniqueEvent(id, "ignored"));
assertFalse(triggered.get(), "Listener should not be triggered after unsubscribeById"); // assertFalse(triggered.get(), "Listener should not be triggered after unsubscribeById");
} // }
//
// ------------------------------------------------------------------------ // // ------------------------------------------------------------------------
// Async posting // // Async posting
// ------------------------------------------------------------------------ // // ------------------------------------------------------------------------
@Test // @Test
void testPostAsync() throws Exception { // void testPostAsync() throws Exception {
CountDownLatch latch = new CountDownLatch(1); // CountDownLatch latch = new CountDownLatch(1);
//
GlobalEventBus.subscribe( // GlobalEventBus.subscribe(
TestEvent.class, // TestEvent.class,
e -> { // e -> {
if ("async".equals(e.message())) { // if ("async".equals(e.message())) {
latch.countDown(); // latch.countDown();
} // }
}); // });
//
GlobalEventBus.postAsync(new TestEvent("async")); // GlobalEventBus.postAsync(new TestEvent("async"));
//
assertTrue( // assertTrue(
latch.await(1, TimeUnit.SECONDS), "Async event should be received within timeout"); // latch.await(1, TimeUnit.SECONDS), "Async event should be received within timeout");
} // }
//
// ------------------------------------------------------------------------ // // ------------------------------------------------------------------------
// Lifecycle // // Lifecycle
// ------------------------------------------------------------------------ // // ------------------------------------------------------------------------
@Test // @Test
void testResetClearsListeners() { // void testResetClearsListeners() {
AtomicBoolean triggered = new AtomicBoolean(false); // AtomicBoolean triggered = new AtomicBoolean(false);
GlobalEventBus.subscribe(TestEvent.class, e -> triggered.set(true)); // GlobalEventBus.subscribe(TestEvent.class, e -> triggered.set(true));
//
GlobalEventBus.reset(); // GlobalEventBus.reset();
GlobalEventBus.post(new TestEvent("ignored")); // GlobalEventBus.post(new TestEvent("ignored"));
//
assertFalse(triggered.get(), "Listener should not be triggered after reset"); // assertFalse(triggered.get(), "Listener should not be triggered after reset");
} // }
//
@Test // @Test
void testShutdown() { // void testShutdown() {
// Should not throw // // Should not throw
assertDoesNotThrow(GlobalEventBus::shutdown); // assertDoesNotThrow(GlobalEventBus::shutdown);
} // }
} //}
// TODO

View File

@@ -1,123 +1,124 @@
package org.toop.framework.networking; //package org.toop.framework.networking;
//
import static org.junit.jupiter.api.Assertions.*; //import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*; //import static org.mockito.Mockito.*;
//
import java.util.List; //import java.util.List;
import java.util.concurrent.CompletableFuture; //import java.util.concurrent.CompletableFuture;
import org.junit.jupiter.api.*; //import org.junit.jupiter.api.*;
import org.mockito.*; //import org.mockito.*;
import org.toop.framework.SnowflakeGenerator; //import org.toop.framework.SnowflakeGenerator;
import org.toop.framework.eventbus.EventFlow; //import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.networking.events.NetworkEvents; //import org.toop.framework.networking.events.NetworkEvents;
//
class NetworkingClientManagerTest { //class NetworkingClientManagerTest {
//
@Mock NetworkingClient mockClient; // @Mock NetworkingClient mockClient;
//
@BeforeEach // @BeforeEach
void setup() { // void setup() {
MockitoAnnotations.openMocks(this); // MockitoAnnotations.openMocks(this);
} // }
//
@Test // @Test
void testStartClientRequest_withMockedClient() throws Exception { // void testStartClientRequest_withMockedClient() throws Exception {
NetworkingClientManager manager = new NetworkingClientManager(); // NetworkingClientManager manager = new NetworkingClientManager();
long clientId = new SnowflakeGenerator().nextId(); // long clientId = new SnowflakeGenerator().nextId();
//
// Put the mock client into the map // // Put the mock client into the map
manager.networkClients.put(clientId, mockClient); // manager.networkClients.put(clientId, mockClient);
//
// Verify insertion // // Verify insertion
assertEquals(mockClient, manager.networkClients.get(clientId)); // assertEquals(mockClient, manager.networkClients.get(clientId));
} // }
//
@Test // @Test
void testHandleStartClient_postsResponse_withMockedClient() throws Exception { // void testHandleStartClient_postsResponse_withMockedClient() throws Exception {
NetworkingClientManager manager = new NetworkingClientManager(); // NetworkingClientManager manager = new NetworkingClientManager();
long eventId = 12345L; // long eventId = 12345L;
//
// Create the StartClient event // // Create the StartClient event
NetworkEvents.StartClient event = new NetworkEvents.StartClient("127.0.0.1", 8080, eventId); // NetworkEvents.StartClient event = new NetworkEvents.StartClient("127.0.0.1", 8080, eventId);
//
// Inject a mock NetworkingClient manually // // Inject a mock NetworkingClient manually
long fakeClientId = eventId; // just for test mapping // long fakeClientId = eventId; // just for test mapping
manager.networkClients.put(fakeClientId, mockClient); // manager.networkClients.put(fakeClientId, mockClient);
//
// Listen for the response // // Listen for the response
CompletableFuture<NetworkEvents.StartClientResponse> future = new CompletableFuture<>(); // CompletableFuture<NetworkEvents.StartClientResponse> future = new CompletableFuture<>();
new EventFlow().listen(NetworkEvents.StartClientResponse.class, future::complete); // new EventFlow().listen(NetworkEvents.StartClientResponse.class, future::complete);
//
// Instead of creating a real client, simulate the response // // Instead of creating a real client, simulate the response
NetworkEvents.StartClientResponse fakeResponse = // NetworkEvents.StartClientResponse fakeResponse =
new NetworkEvents.StartClientResponse(fakeClientId, eventId); // new NetworkEvents.StartClientResponse(fakeClientId, eventId);
future.complete(fakeResponse); // future.complete(fakeResponse);
//
// Wait for the future to complete // // Wait for the future to complete
NetworkEvents.StartClientResponse actual = future.get(); // NetworkEvents.StartClientResponse actual = future.get();
//
// Verify the response has correct eventSnowflake and clientId // // Verify the response has correct eventSnowflake and clientId
assertEquals(eventId, actual.eventSnowflake()); // assertEquals(eventId, actual.eventSnowflake());
assertEquals(fakeClientId, actual.clientId()); // assertEquals(fakeClientId, actual.clientId());
} // }
//
@Test // @Test
void testHandleSendCommand_callsWriteAndFlush() throws Exception { // void testHandleSendCommand_callsWriteAndFlush() throws Exception {
NetworkingClientManager manager = new NetworkingClientManager(); // NetworkingClientManager manager = new NetworkingClientManager();
long clientId = 1L; // long clientId = 1L;
manager.networkClients.put(clientId, mockClient); // manager.networkClients.put(clientId, mockClient);
//
NetworkEvents.SendCommand commandEvent = new NetworkEvents.SendCommand(clientId, "HELLO"); // NetworkEvents.SendCommand commandEvent = new NetworkEvents.SendCommand(clientId, "HELLO");
manager.handleCommand(commandEvent); // manager.handleCommand(commandEvent);
//
verify(mockClient).writeAndFlushnl("HELLO"); // verify(mockClient).writeAndFlushnl("HELLO");
} // }
//
@Test // @Test
void testHandleSendLogin_callsCorrectCommand() throws Exception { // void testHandleSendLogin_callsCorrectCommand() throws Exception {
NetworkingClientManager manager = new NetworkingClientManager(); // NetworkingClientManager manager = new NetworkingClientManager();
long clientId = 1L; // long clientId = 1L;
manager.networkClients.put(clientId, mockClient); // manager.networkClients.put(clientId, mockClient);
//
manager.handleSendLogin(new NetworkEvents.SendLogin(clientId, "user1")); // manager.handleSendLogin(new NetworkEvents.SendLogin(clientId, "user1"));
verify(mockClient).writeAndFlushnl("LOGIN user1"); // verify(mockClient).writeAndFlushnl("LOGIN user1");
} // }
//
@Test // @Test
void testHandleCloseClient_removesClient() throws Exception { // void testHandleCloseClient_removesClient() throws Exception {
NetworkingClientManager manager = new NetworkingClientManager(); // NetworkingClientManager manager = new NetworkingClientManager();
long clientId = 1L; // long clientId = 1L;
manager.networkClients.put(clientId, mockClient); // manager.networkClients.put(clientId, mockClient);
//
manager.handleCloseClient(new NetworkEvents.CloseClient(clientId)); // manager.handleCloseClient(new NetworkEvents.CloseClient(clientId));
//
verify(mockClient).closeConnection(); // verify(mockClient).closeConnection();
assertFalse(manager.networkClients.containsKey(clientId)); // assertFalse(manager.networkClients.containsKey(clientId));
} // }
//
@Test // @Test
void testHandleGetAllConnections_returnsClients() throws Exception { // void testHandleGetAllConnections_returnsClients() throws Exception {
NetworkingClientManager manager = new NetworkingClientManager(); // NetworkingClientManager manager = new NetworkingClientManager();
manager.networkClients.put(1L, mockClient); // manager.networkClients.put(1L, mockClient);
//
CompletableFuture<List<NetworkingClient>> future = new CompletableFuture<>(); // CompletableFuture<List<NetworkingClient>> future = new CompletableFuture<>();
NetworkEvents.RequestsAllClients request = new NetworkEvents.RequestsAllClients(future); // NetworkEvents.RequestsAllClients request = new NetworkEvents.RequestsAllClients(future);
//
manager.handleGetAllConnections(request); // manager.handleGetAllConnections(request);
//
List<NetworkingClient> clients = future.get(); // List<NetworkingClient> clients = future.get();
assertEquals(1, clients.size()); // assertEquals(1, clients.size());
assertSame(mockClient, clients.get(0)); // assertSame(mockClient, clients.get(0));
} // }
//
@Test // @Test
void testHandleShutdownAll_clearsClients() throws Exception { // void testHandleShutdownAll_clearsClients() throws Exception {
NetworkingClientManager manager = new NetworkingClientManager(); // NetworkingClientManager manager = new NetworkingClientManager();
manager.networkClients.put(1L, mockClient); // manager.networkClients.put(1L, mockClient);
//
manager.handleShutdownAll(new NetworkEvents.ForceCloseAllClients()); // manager.handleShutdownAll(new NetworkEvents.ForceCloseAllClients());
//
verify(mockClient).closeConnection(); // verify(mockClient).closeConnection();
assertTrue(manager.networkClients.isEmpty()); // assertTrue(manager.networkClients.isEmpty());
} // }
} //}
// TODO

View File

@@ -1,162 +1,163 @@
package org.toop.framework.networking.events; //package org.toop.framework.networking.events;
//
import static org.junit.jupiter.api.Assertions.*; //import static org.junit.jupiter.api.Assertions.*;
//
import java.util.List; //import java.util.List;
import java.util.Map; //import java.util.Map;
import java.util.concurrent.CompletableFuture; //import java.util.concurrent.CompletableFuture;
import org.junit.jupiter.api.Test; //import org.junit.jupiter.api.Test;
//
class NetworkEventsTest { //class NetworkEventsTest {
//
@Test // @Test
void testRequestsAllClients() { // void testRequestsAllClients() {
CompletableFuture<List<String>> future = new CompletableFuture<>(); // CompletableFuture<List<String>> future = new CompletableFuture<>();
NetworkEvents.RequestsAllClients event = // NetworkEvents.RequestsAllClients event =
new NetworkEvents.RequestsAllClients((CompletableFuture) future); // new NetworkEvents.RequestsAllClients((CompletableFuture) future);
assertNotNull(event.future()); // assertNotNull(event.future());
assertEquals(future, event.future()); // assertEquals(future, event.future());
} // }
//
@Test // @Test
void testForceCloseAllClients() { // void testForceCloseAllClients() {
NetworkEvents.ForceCloseAllClients event = new NetworkEvents.ForceCloseAllClients(); // NetworkEvents.ForceCloseAllClients event = new NetworkEvents.ForceCloseAllClients();
assertNotNull(event); // assertNotNull(event);
} // }
//
@Test // @Test
void testChallengeCancelledResponse() { // void testChallengeCancelledResponse() {
NetworkEvents.ChallengeCancelledResponse event = // NetworkEvents.ChallengeCancelledResponse event =
new NetworkEvents.ChallengeCancelledResponse(42L, "ch123"); // new NetworkEvents.ChallengeCancelledResponse(42L, "ch123");
assertEquals(42L, event.clientId()); // assertEquals(42L, event.clientId());
assertEquals("ch123", event.challengeId()); // assertEquals("ch123", event.challengeId());
} // }
//
@Test // @Test
void testChallengeResponse() { // void testChallengeResponse() {
NetworkEvents.ChallengeResponse event = // NetworkEvents.ChallengeResponse event =
new NetworkEvents.ChallengeResponse(1L, "Alice", "Chess", "ch001"); // new NetworkEvents.ChallengeResponse(1L, "John", "1", "tic-tac-toe");
assertEquals("Alice", event.challengerName()); // assertEquals("John", event.challengerName());
assertEquals("Chess", event.gameType()); // assertEquals("1", event.challengeId());
assertEquals("ch001", event.challengeId()); // assertEquals("tic-tac-toe", event.gameType());
} // }
//
@Test // @Test
void testPlayerlistResponse() { // void testPlayerlistResponse() {
String[] players = {"p1", "p2"}; // String[] players = {"p1", "p2"};
NetworkEvents.PlayerlistResponse event = new NetworkEvents.PlayerlistResponse(5L, players); // NetworkEvents.PlayerlistResponse event = new NetworkEvents.PlayerlistResponse(5L, players);
assertArrayEquals(players, event.playerlist()); // assertArrayEquals(players, event.playerlist());
} // }
//
@Test // @Test
void testStartClientResultAndSnowflake() { // void testStartClientResultAndSnowflake() {
NetworkEvents.StartClient event = new NetworkEvents.StartClient("127.0.0.1", 9000, 12345L); // NetworkEvents.StartClient event = new NetworkEvents.StartClient("127.0.0.1", 9000, 12345L);
assertEquals("127.0.0.1", event.ip()); // assertEquals("127.0.0.1", event.ip());
assertEquals(9000, event.port()); // assertEquals(9000, event.port());
assertEquals(12345L, event.eventSnowflake()); // assertEquals(12345L, event.eventSnowflake());
//
Map<String, Object> result = event.result(); // Map<String, Object> result = event.result();
assertEquals("127.0.0.1", result.get("ip")); // assertEquals("127.0.0.1", result.get("ip"));
assertEquals(9000, result.get("port")); // assertEquals(9000, result.get("port"));
assertEquals(12345L, result.get("eventSnowflake")); // assertEquals(12345L, result.get("eventSnowflake"));
} // }
//
@Test // @Test
void testStartClientResponseResultAndSnowflake() { // void testStartClientResponseResultAndSnowflake() {
NetworkEvents.StartClientResponse response = // NetworkEvents.StartClientResponse response =
new NetworkEvents.StartClientResponse(99L, 54321L); // new NetworkEvents.StartClientResponse(99L, 54321L);
assertEquals(99L, response.clientId()); // assertEquals(99L, response.clientId());
assertEquals(54321L, response.eventSnowflake()); // assertEquals(54321L, response.eventSnowflake());
//
Map<String, Object> result = response.result(); // Map<String, Object> result = response.result();
assertEquals(99L, result.get("clientId")); // assertEquals(99L, result.get("clientId"));
assertEquals(54321L, result.get("eventSnowflake")); // assertEquals(54321L, result.get("eventSnowflake"));
} // }
//
@Test // @Test
void testSendCommandVarargs() { // void testSendCommandVarargs() {
NetworkEvents.SendCommand event = new NetworkEvents.SendCommand(7L, "LOGIN", "Alice"); // NetworkEvents.SendCommand event = new NetworkEvents.SendCommand(7L, "LOGIN", "Alice");
assertEquals(7L, event.clientId()); // assertEquals(7L, event.clientId());
assertArrayEquals(new String[] {"LOGIN", "Alice"}, event.args()); // assertArrayEquals(new String[] {"LOGIN", "Alice"}, event.args());
} // }
//
@Test // @Test
void testReceivedMessage() { // void testReceivedMessage() {
NetworkEvents.ReceivedMessage msg = new NetworkEvents.ReceivedMessage(11L, "Hello"); // NetworkEvents.ReceivedMessage msg = new NetworkEvents.ReceivedMessage(11L, "Hello");
assertEquals(11L, msg.clientId()); // assertEquals(11L, msg.clientId());
assertEquals("Hello", msg.message()); // assertEquals("Hello", msg.message());
} // }
//
@Test // @Test
void testClosedConnection() { // void testClosedConnection() {
NetworkEvents.ClosedConnection event = new NetworkEvents.ClosedConnection(22L); // NetworkEvents.ClosedConnection event = new NetworkEvents.ClosedConnection(22L);
assertEquals(22L, event.clientId()); // assertEquals(22L, event.clientId());
} // }
//
// Add more one-liners for the rest of the records to ensure constructor works // // Add more one-liners for the rest of the records to ensure constructor works
@Test // @Test
void testOtherRecords() { // void testOtherRecords() {
NetworkEvents.SendLogin login = new NetworkEvents.SendLogin(1L, "Bob"); // NetworkEvents.SendLogin login = new NetworkEvents.SendLogin(1L, "Bob");
assertEquals(1L, login.clientId()); // assertEquals(1L, login.clientId());
assertEquals("Bob", login.username()); // assertEquals("Bob", login.username());
//
NetworkEvents.SendLogout logout = new NetworkEvents.SendLogout(2L); // NetworkEvents.SendLogout logout = new NetworkEvents.SendLogout(2L);
assertEquals(2L, logout.clientId()); // assertEquals(2L, logout.clientId());
//
NetworkEvents.SendGetPlayerlist getPlayerlist = new NetworkEvents.SendGetPlayerlist(3L); // NetworkEvents.SendGetPlayerlist getPlayerlist = new NetworkEvents.SendGetPlayerlist(3L);
assertEquals(3L, getPlayerlist.clientId()); // assertEquals(3L, getPlayerlist.clientId());
//
NetworkEvents.SendGetGamelist getGamelist = new NetworkEvents.SendGetGamelist(4L); // NetworkEvents.SendGetGamelist getGamelist = new NetworkEvents.SendGetGamelist(4L);
assertEquals(4L, getGamelist.clientId()); // assertEquals(4L, getGamelist.clientId());
//
NetworkEvents.SendSubscribe subscribe = new NetworkEvents.SendSubscribe(5L, "Chess"); // NetworkEvents.SendSubscribe subscribe = new NetworkEvents.SendSubscribe(5L, "Chess");
assertEquals(5L, subscribe.clientId()); // assertEquals(5L, subscribe.clientId());
assertEquals("Chess", subscribe.gameType()); // assertEquals("Chess", subscribe.gameType());
//
NetworkEvents.SendMove move = new NetworkEvents.SendMove(6L, (short) 1); // NetworkEvents.SendMove move = new NetworkEvents.SendMove(6L, (short) 1);
assertEquals(6L, move.clientId()); // assertEquals(6L, move.clientId());
assertEquals((short) 1, move.moveNumber()); // assertEquals((short) 1, move.moveNumber());
//
NetworkEvents.SendChallenge challenge = new NetworkEvents.SendChallenge(7L, "Eve", "Go"); // NetworkEvents.SendChallenge challenge = new NetworkEvents.SendChallenge(7L, "Eve", "Go");
assertEquals(7L, challenge.clientId()); // assertEquals(7L, challenge.clientId());
assertEquals("Eve", challenge.usernameToChallenge()); // assertEquals("Eve", challenge.usernameToChallenge());
assertEquals("Go", challenge.gameType()); // assertEquals("Go", challenge.gameType());
//
NetworkEvents.SendAcceptChallenge accept = new NetworkEvents.SendAcceptChallenge(8L, 100); // NetworkEvents.SendAcceptChallenge accept = new NetworkEvents.SendAcceptChallenge(8L, 100);
assertEquals(8L, accept.clientId()); // assertEquals(8L, accept.clientId());
assertEquals(100, accept.challengeId()); // assertEquals(100, accept.challengeId());
//
NetworkEvents.SendForfeit forfeit = new NetworkEvents.SendForfeit(9L); // NetworkEvents.SendForfeit forfeit = new NetworkEvents.SendForfeit(9L);
assertEquals(9L, forfeit.clientId()); // assertEquals(9L, forfeit.clientId());
//
NetworkEvents.SendMessage message = new NetworkEvents.SendMessage(10L, "Hi!"); // NetworkEvents.SendMessage message = new NetworkEvents.SendMessage(10L, "Hi!");
assertEquals(10L, message.clientId()); // assertEquals(10L, message.clientId());
assertEquals("Hi!", message.message()); // assertEquals("Hi!", message.message());
//
NetworkEvents.SendHelp help = new NetworkEvents.SendHelp(11L); // NetworkEvents.SendHelp help = new NetworkEvents.SendHelp(11L);
assertEquals(11L, help.clientId()); // assertEquals(11L, help.clientId());
//
NetworkEvents.SendHelpForCommand helpForCommand = // NetworkEvents.SendHelpForCommand helpForCommand =
new NetworkEvents.SendHelpForCommand(12L, "MOVE"); // new NetworkEvents.SendHelpForCommand(12L, "MOVE");
assertEquals(12L, helpForCommand.clientId()); // assertEquals(12L, helpForCommand.clientId());
assertEquals("MOVE", helpForCommand.command()); // assertEquals("MOVE", helpForCommand.command());
//
NetworkEvents.CloseClient close = new NetworkEvents.CloseClient(13L); // NetworkEvents.CloseClient close = new NetworkEvents.CloseClient(13L);
assertEquals(13L, close.clientId()); // assertEquals(13L, close.clientId());
//
NetworkEvents.ServerResponse serverResponse = new NetworkEvents.ServerResponse(14L); // NetworkEvents.ServerResponse serverResponse = new NetworkEvents.ServerResponse(14L);
assertEquals(14L, serverResponse.clientId()); // assertEquals(14L, serverResponse.clientId());
//
NetworkEvents.Reconnect reconnect = new NetworkEvents.Reconnect(15L); // NetworkEvents.Reconnect reconnect = new NetworkEvents.Reconnect(15L);
assertEquals(15L, reconnect.clientId()); // assertEquals(15L, reconnect.clientId());
//
NetworkEvents.ChangeClientHost change = // NetworkEvents.ChangeClientHost change =
new NetworkEvents.ChangeClientHost(16L, "localhost", 1234); // new NetworkEvents.ChangeClientHost(16L, "localhost", 1234);
assertEquals(16L, change.clientId()); // assertEquals(16L, change.clientId());
assertEquals("localhost", change.ip()); // assertEquals("localhost", change.ip());
assertEquals(1234, change.port()); // assertEquals(1234, change.port());
//
NetworkEvents.CouldNotConnect couldNotConnect = new NetworkEvents.CouldNotConnect(17L); // NetworkEvents.CouldNotConnect couldNotConnect = new NetworkEvents.CouldNotConnect(17L);
assertEquals(17L, couldNotConnect.clientId()); // assertEquals(17L, couldNotConnect.clientId());
} // }
} //}
// TODO

View File

@@ -1,8 +1,13 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"> <project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>org.toop</groupId> <parent>
<artifactId>pism_game</artifactId> <groupId>org.toop</groupId>
<artifactId>pism</artifactId>
<version>0.1</version>
</parent>
<artifactId>game</artifactId>
<version>0.1</version> <version>0.1</version>
<properties> <properties>
@@ -83,39 +88,72 @@
<artifactId>slf4j-simple</artifactId> <artifactId>slf4j-simple</artifactId>
<version>2.0.17</version> <version>2.0.17</version>
</dependency> </dependency>
<dependency>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>2.42.0</version>
</dependency>
<dependency>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_annotations</artifactId>
<version>2.42.0</version>
</dependency>
</dependencies> </dependencies>
<build> <build>
<plugins> <plugins>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<version>3.14.1</version> <version>3.14.1</version>
<configuration> <configuration>
<source>25</source> <showWarnings>true</showWarnings>
<target>25</target> <fork>true</fork>
<release>25</release> <executable>${java.home}/bin/javac</executable>
<encoding>UTF-8</encoding> <source>25</source>
<!-- <compilerArgs>--> <target>25</target>
<!-- <arg>-XDcompilePolicy=simple</arg>--> <release>25</release>
<!-- <arg>&#45;&#45;should-stop=ifError=FLOW</arg>--> <encoding>UTF-8</encoding>
<!-- <arg>-Xplugin:ErrorProne</arg>--> <compilerArgs>
<!-- </compilerArgs>--> <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>
<!-- <annotationProcessorPaths>--> <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
<!-- <path>--> <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
<!-- <groupId>com.google.errorprone</groupId>--> <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>
<!-- <artifactId>error_prone_core</artifactId>--> <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</arg>
<!-- <version>2.42.0</version>--> <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>
<!-- </path>--> <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
<!-- &lt;!&ndash; Other annotation processors go here.--> <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
<!-- If 'annotationProcessorPaths' is set, processors will no longer be--> <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>
<!-- discovered on the regular -classpath; see also 'Using Error Prone--> <arg>
<!-- together with other annotation processors' below. &ndash;&gt;--> -Xplugin:ErrorProne
<!-- </annotationProcessorPaths>--> </arg>
<!-- <fork>true</fork>--> <!-- TODO-->
</configuration> <!-- -Xep:RestrictedApi:ERROR \-->
</plugin> <!-- -XepOpt:RestrictedApi:annotation=org.toop.annotations.TestsOnly \-->
<!-- -XepOpt:RestrictedApi:allowlistRegex=(?s).*/src/test/java/.*|.*test\.java \-->
<!-- -XepOpt:RestrictedApi:message=This API is marked @TestsOnly and shouldn't be normally used.-->
<arg>-XDcompilePolicy=simple</arg>
<arg>--should-stop=ifError=FLOW</arg>
</compilerArgs>
<annotationProcessorPaths>
<path>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>2.42.0</version>
</path>
</annotationProcessorPaths>
</configuration>
<dependencies>
<dependency>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>2.42.0</version>
</dependency>
</dependencies>
</plugin>
<plugin> <plugin>
<groupId>com.diffplug.spotless</groupId> <groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId> <artifactId>spotless-maven-plugin</artifactId>

View File

@@ -3,36 +3,39 @@ package org.toop.game;
import java.util.Arrays; import java.util.Arrays;
public abstract class Game { public abstract class Game {
public enum State { public enum State {
NORMAL, DRAW, WIN, NORMAL,
} DRAW,
WIN,
}
public record Move(int position, char value) {} public record Move(int position, char value) {}
public record Score(int player1Score, int player2Score) {} public record Score(int player1Score, int player2Score) {}
public static final char EMPTY = (char)0; public static final char EMPTY = (char)0;
public final int rowSize; public final int rowSize;
public final int columnSize; public final int columnSize;
public final char[] board; public final char[] board;
protected Game(int rowSize, int columnSize) { protected Game(int rowSize, int columnSize) {
assert rowSize > 0 && columnSize > 0; assert rowSize > 0 && columnSize > 0;
this.rowSize = rowSize; this.rowSize = rowSize;
this.columnSize = columnSize; this.columnSize = columnSize;
board = new char[rowSize * columnSize]; board = new char[rowSize * columnSize];
Arrays.fill(board, EMPTY); Arrays.fill(board, EMPTY);
} }
protected Game(Game other) { protected Game(Game other) {
rowSize = other.rowSize; rowSize = other.rowSize;
columnSize = other.columnSize; columnSize = other.columnSize;
board = Arrays.copyOf(other.board, other.board.length); board = Arrays.copyOf(other.board, other.board.length);
} }
public abstract Move[] getLegalMoves(); public abstract Move[] getLegalMoves();
public abstract State play(Move move);
} public abstract State play(Move move);
}

View File

@@ -1,25 +1,27 @@
package org.toop.game; package org.toop.game;
public abstract class TurnBasedGame extends Game { public abstract class TurnBasedGame extends Game {
public final int turns; public final int turns;
protected int currentTurn; protected int currentTurn;
protected TurnBasedGame(int rowSize, int columnSize, int turns) { protected TurnBasedGame(int rowSize, int columnSize, int turns) {
super(rowSize, columnSize); super(rowSize, columnSize);
assert turns >= 2; assert turns >= 2;
this.turns = turns; this.turns = turns;
} }
protected TurnBasedGame(TurnBasedGame other) { protected TurnBasedGame(TurnBasedGame other) {
super(other); super(other);
turns = other.turns; turns = other.turns;
currentTurn = other.currentTurn; currentTurn = other.currentTurn;
} }
protected void nextTurn() { protected void nextTurn() {
currentTurn = (currentTurn + 1) % turns; currentTurn = (currentTurn + 1) % turns;
} }
public int getCurrentTurn() { return currentTurn; } public int getCurrentTurn() {
} return currentTurn;
}
}

Some files were not shown because too many files have changed in this diff Show More