mirror of
https://github.com/2OOP/pism.git
synced 2026-02-04 10:54:51 +00:00
Merge remote-tracking branch 'origin/Reversi' into UI
# Conflicts: # app/src/main/java/org/toop/app/App.java # app/src/main/java/org/toop/app/canvas/GameCanvas.java # app/src/main/java/org/toop/app/layer/layers/MainLayer.java # app/src/main/java/org/toop/app/layer/layers/OptionsPopup.java # app/src/main/java/org/toop/app/layer/layers/game/TicTacToeLayer.java # app/src/main/java/org/toop/local/AppSettings.java # app/src/main/resources/assets/localization/localization_ar.properties # app/src/main/resources/assets/localization/localization_de.properties # app/src/main/resources/assets/localization/localization_en.properties # app/src/main/resources/assets/localization/localization_es.properties # app/src/main/resources/assets/localization/localization_fr.properties # app/src/main/resources/assets/localization/localization_hi.properties # app/src/main/resources/assets/localization/localization_it.properties # app/src/main/resources/assets/localization/localization_ja.properties # app/src/main/resources/assets/localization/localization_ko.properties # app/src/main/resources/assets/localization/localization_nl.properties # app/src/main/resources/assets/localization/localization_ru.properties # app/src/main/resources/assets/localization/localization_zh.properties
This commit is contained in:
1
.idea/dictionaries/project.xml
generated
1
.idea/dictionaries/project.xml
generated
@@ -8,6 +8,7 @@
|
||||
<w>flushnl</w>
|
||||
<w>gaaf</w>
|
||||
<w>gamelist</w>
|
||||
<w>pism</w>
|
||||
<w>playerlist</w>
|
||||
<w>tictactoe</w>
|
||||
<w>toop</w>
|
||||
|
||||
4
.idea/encodings.xml
generated
4
.idea/encodings.xml
generated
@@ -1,12 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<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/resources" 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$/game/src/main/java" 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/resources" charset="UTF-8" />
|
||||
</component>
|
||||
|
||||
103
app/pom.xml
103
app/pom.xml
@@ -1,8 +1,13 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>org.toop</groupId>
|
||||
<artifactId>pism_app</artifactId>
|
||||
<parent>
|
||||
<groupId>org.toop</groupId>
|
||||
<artifactId>pism</artifactId>
|
||||
<version>0.1</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>app</artifactId>
|
||||
<version>0.1</version>
|
||||
|
||||
<properties>
|
||||
@@ -24,24 +29,36 @@
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.10.1</version>
|
||||
</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>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-controls</artifactId>
|
||||
<version>25</version>
|
||||
</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>
|
||||
|
||||
<build>
|
||||
@@ -112,14 +129,56 @@
|
||||
</java>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<source>25</source>
|
||||
<target>25</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.14.1</version>
|
||||
<configuration>
|
||||
<showWarnings>true</showWarnings>
|
||||
<fork>true</fork>
|
||||
<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>
|
||||
</build>
|
||||
</project>
|
||||
@@ -1,11 +1,17 @@
|
||||
package org.toop;
|
||||
|
||||
import org.toop.app.App;
|
||||
import org.toop.framework.audio.SoundManager;
|
||||
import org.toop.framework.audio.*;
|
||||
import org.toop.framework.networking.NetworkingClientManager;
|
||||
import org.toop.framework.networking.NetworkingInitializationException;
|
||||
import org.toop.framework.resource.ResourceLoader;
|
||||
import org.toop.framework.resource.ResourceManager;
|
||||
import org.toop.framework.resource.ResourceMeta;
|
||||
import org.toop.framework.resource.resources.MusicAsset;
|
||||
import org.toop.framework.resource.resources.SoundEffectAsset;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public final class Main {
|
||||
static void main(String[] args) {
|
||||
@@ -16,6 +22,21 @@ public final class Main {
|
||||
private static void initSystems() throws NetworkingInitializationException {
|
||||
ResourceManager.loadAssets(new ResourceLoader("app/src/main/resources/assets"));
|
||||
new Thread(NetworkingClientManager::new).start();
|
||||
new Thread(SoundManager::new).start();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
26
app/src/main/java/org/toop/app/canvas/ReversiCanvas.java
Normal file
26
app/src/main/java/org/toop/app/canvas/ReversiCanvas.java
Normal file
@@ -0,0 +1,26 @@
|
||||
package org.toop.app.canvas;
|
||||
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.paint.Color;
|
||||
import org.toop.game.Game;
|
||||
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class ReversiCanvas extends GameCanvas{
|
||||
public ReversiCanvas(Color color, int width, int height, Consumer<Integer> onCellClicked) {
|
||||
super(color, width, height, 8, 8, 10, true, onCellClicked);
|
||||
drawStartingDots();
|
||||
}
|
||||
public void drawStartingDots(){
|
||||
drawDot(Color.BLACK,28);
|
||||
drawDot(Color.WHITE,36);
|
||||
drawDot(Color.BLACK,35);
|
||||
drawDot(Color.WHITE,27);
|
||||
}
|
||||
public void drawLegalMoves(Game.Move[] moves){
|
||||
for(Game.Move move : moves){
|
||||
drawDot(new Color(1f,0,0,0.25f),move.position());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package org.toop.app.layer.layers.game;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.paint.Color;
|
||||
import org.toop.app.App;
|
||||
import org.toop.app.canvas.ReversiCanvas;
|
||||
import org.toop.app.layer.*;
|
||||
import org.toop.app.layer.containers.HorizontalContainer;
|
||||
import org.toop.app.layer.containers.VerticalContainer;
|
||||
import org.toop.app.layer.layers.MainLayer;
|
||||
import org.toop.game.Game;
|
||||
import org.toop.game.reversi.Reversi;
|
||||
import org.toop.game.reversi.ReversiAI;
|
||||
import org.toop.local.AppContext;
|
||||
|
||||
public class ReversiLayer extends Layer{
|
||||
private ReversiCanvas canvas;
|
||||
private Reversi reversi;
|
||||
private ReversiAI reversiAI;
|
||||
public ReversiLayer(){
|
||||
super("bg-secondary"); //make reversiboard background dark green
|
||||
|
||||
canvas = new ReversiCanvas(Color.GREEN,(App.getHeight() / 100) * 75, (App.getHeight() / 100) * 75, (cell) -> {
|
||||
reversi.play(new Game.Move(cell,reversi.getCurrentPlayer()));
|
||||
reload();
|
||||
canvas.drawLegalMoves(reversi.getLegalMoves());
|
||||
});
|
||||
reversi = new Reversi() ;
|
||||
reversiAI = new ReversiAI();
|
||||
|
||||
|
||||
reload();
|
||||
canvas.drawLegalMoves(reversi.getLegalMoves());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reload() {
|
||||
popAll();
|
||||
canvas.resize((App.getHeight() / 100) * 75, (App.getHeight() / 100) * 75);
|
||||
|
||||
for (int i = 0; i < reversi.board.length; i++) {
|
||||
final char value = reversi.board[i];
|
||||
|
||||
if (value == 'B') {
|
||||
canvas.drawDot(Color.BLACK, i);
|
||||
} else if (value == 'W') {
|
||||
canvas.drawDot(Color.WHITE, i);
|
||||
}
|
||||
}
|
||||
|
||||
final var backButton = NodeBuilder.button(AppContext.getString("back"), () -> {
|
||||
App.activate(new MainLayer());
|
||||
});
|
||||
|
||||
final Container controlContainer = new VerticalContainer(5);
|
||||
controlContainer.addNodes(backButton);
|
||||
|
||||
final Container informationContainer = new HorizontalContainer(15);
|
||||
|
||||
addContainer(controlContainer, Pos.BOTTOM_LEFT, 2, -2, 0, 0);
|
||||
addContainer(informationContainer, Pos.TOP_LEFT, 2, 2, 0, 0);
|
||||
addGameCanvas(canvas, Pos.CENTER, 0, 0);
|
||||
}
|
||||
}
|
||||
Binary file not shown.
BIN
app/src/main/resources/assets/audio/music/blitzkrieg.mp3
Normal file
BIN
app/src/main/resources/assets/audio/music/blitzkrieg.mp3
Normal file
Binary file not shown.
BIN
app/src/main/resources/assets/audio/music/getlucky.mp3
Normal file
BIN
app/src/main/resources/assets/audio/music/getlucky.mp3
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,8 +1,13 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>org.toop</groupId>
|
||||
<artifactId>pism_framework</artifactId>
|
||||
<parent>
|
||||
<groupId>org.toop</groupId>
|
||||
<artifactId>pism</artifactId>
|
||||
<version>0.1</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>framework</artifactId>
|
||||
<version>0.1</version>
|
||||
|
||||
<properties>
|
||||
@@ -13,6 +18,14 @@
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.toop</groupId>
|
||||
<artifactId>processors</artifactId>
|
||||
<version>0.1</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.diffplug.spotless</groupId>
|
||||
<artifactId>spotless-maven-plugin</artifactId>
|
||||
@@ -123,7 +136,17 @@
|
||||
<scope>compile</scope>
|
||||
</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>
|
||||
<plugins>
|
||||
@@ -132,11 +155,73 @@
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.14.1</version>
|
||||
<configuration>
|
||||
<source>25</source>
|
||||
<showWarnings>true</showWarnings>
|
||||
<fork>true</fork>
|
||||
<executable>${java.home}/bin/javac</executable>
|
||||
<source>25</source>
|
||||
<target>25</target>
|
||||
<release>25</release>
|
||||
<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>
|
||||
<!-- <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>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.google.errorprone</groupId>
|
||||
<artifactId>error_prone_core</artifactId>
|
||||
<version>2.42.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>com.diffplug.spotless</groupId>
|
||||
|
||||
@@ -6,6 +6,8 @@ import org.apache.logging.log4j.core.LoggerContext;
|
||||
import org.apache.logging.log4j.core.config.Configuration;
|
||||
import org.apache.logging.log4j.core.config.LoggerConfig;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Utility class for configuring logging levels dynamically at runtime using Log4j 2.
|
||||
*
|
||||
@@ -144,7 +146,7 @@ public final class Logging {
|
||||
* @param levelToLog name of the logging level (e.g., "DEBUG", "INFO")
|
||||
*/
|
||||
public static void enableLogsForClass(String className, String levelToLog) {
|
||||
Level level = Level.valueOf(levelToLog.trim().toUpperCase());
|
||||
Level level = Level.valueOf(levelToLog.trim().toUpperCase(Locale.ROOT));
|
||||
if (level != null && verifyStringIsActualClass(className)) {
|
||||
enableLogsForClassInternal(className, level);
|
||||
}
|
||||
|
||||
@@ -9,31 +9,16 @@ import java.util.concurrent.atomic.AtomicLong;
|
||||
* A thread-safe, distributed unique ID generator following the Snowflake pattern.
|
||||
*
|
||||
* <p>Each generated 64-bit ID encodes:
|
||||
*
|
||||
* <ul>
|
||||
* <li>41-bit timestamp (milliseconds since custom epoch)
|
||||
* <li>10-bit machine identifier
|
||||
* <li>12-bit sequence number for IDs generated in the same millisecond
|
||||
* </ul>
|
||||
*
|
||||
* <p>This implementation ensures:
|
||||
*
|
||||
* <ul>
|
||||
* <li>IDs are unique per machine.
|
||||
* <li>Monotonicity within the same machine.
|
||||
* <li>Safe concurrent generation via synchronized {@link #nextId()}.
|
||||
* </ul>
|
||||
*
|
||||
* <p>Custom epoch is set to {@code 2025-01-01T00:00:00Z}.
|
||||
*
|
||||
* <p>Usage example:
|
||||
*
|
||||
* <pre>{@code
|
||||
* SnowflakeGenerator generator = new SnowflakeGenerator();
|
||||
* long id = generator.nextId();
|
||||
* }</pre>
|
||||
* <p>This static implementation ensures global uniqueness per JVM process
|
||||
* and can be accessed via {@link SnowflakeGenerator#nextId()}.
|
||||
*/
|
||||
public class SnowflakeGenerator {
|
||||
public final class SnowflakeGenerator {
|
||||
|
||||
/** Custom epoch in milliseconds (2025-01-01T00:00:00Z). */
|
||||
private static final long EPOCH = Instant.parse("2025-01-01T00:00:00Z").toEpochMilli();
|
||||
@@ -43,25 +28,26 @@ public class SnowflakeGenerator {
|
||||
private static final long MACHINE_BITS = 10;
|
||||
private static final long SEQUENCE_BITS = 12;
|
||||
|
||||
// Maximum values for each component
|
||||
// Maximum values
|
||||
private static final long MAX_MACHINE_ID = (1L << MACHINE_BITS) - 1;
|
||||
private static final long MAX_SEQUENCE = (1L << SEQUENCE_BITS) - 1;
|
||||
private static final long MAX_TIMESTAMP = (1L << TIMESTAMP_BITS) - 1;
|
||||
|
||||
// Bit shifts for composing the ID
|
||||
// Bit shifts
|
||||
private static final long MACHINE_SHIFT = SEQUENCE_BITS;
|
||||
private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + MACHINE_BITS;
|
||||
|
||||
/** Unique machine identifier derived from network interfaces (10 bits). */
|
||||
private static final long machineId = SnowflakeGenerator.genMachineId();
|
||||
/** Unique machine identifier derived from MAC addresses. */
|
||||
private static final long MACHINE_ID = genMachineId();
|
||||
|
||||
private final AtomicLong lastTimestamp = new AtomicLong(-1L);
|
||||
private long sequence = 0L;
|
||||
/** State variables (shared across all threads). */
|
||||
private static final AtomicLong LAST_TIMESTAMP = new AtomicLong(-1L);
|
||||
private static long sequence = 0L;
|
||||
|
||||
/**
|
||||
* Generates a 10-bit machine identifier based on MAC addresses of network interfaces. Falls
|
||||
* back to a random value if MAC cannot be determined.
|
||||
*/
|
||||
// Prevent instantiation
|
||||
private SnowflakeGenerator() {}
|
||||
|
||||
/** Generates a 10-bit machine identifier from MAC or random fallback. */
|
||||
private static long genMachineId() {
|
||||
try {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
@@ -77,48 +63,19 @@ public class SnowflakeGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For testing: manually set the last generated timestamp.
|
||||
*
|
||||
* @param l timestamp in milliseconds
|
||||
*/
|
||||
void setTime(long l) {
|
||||
this.lastTimestamp.set(l);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a SnowflakeGenerator. Validates that the machine ID is within allowed range.
|
||||
*
|
||||
* @throws IllegalArgumentException if machine ID is invalid
|
||||
*/
|
||||
public SnowflakeGenerator() {
|
||||
if (machineId < 0 || machineId > MAX_MACHINE_ID) {
|
||||
throw new IllegalArgumentException(
|
||||
"Machine ID must be between 0 and " + MAX_MACHINE_ID);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the next unique ID.
|
||||
*
|
||||
* <p>If multiple IDs are generated in the same millisecond, a sequence number is incremented.
|
||||
* If the sequence overflows, waits until the next millisecond.
|
||||
*
|
||||
* @return a unique 64-bit ID
|
||||
* @throws IllegalStateException if clock moves backwards or timestamp exceeds 41-bit limit
|
||||
*/
|
||||
public synchronized long nextId() {
|
||||
/** Returns a globally unique 64-bit Snowflake ID. */
|
||||
public static synchronized long nextId() {
|
||||
long currentTimestamp = timestamp();
|
||||
|
||||
if (currentTimestamp < lastTimestamp.get()) {
|
||||
throw new IllegalStateException("Clock moved backwards. Refusing to generate id.");
|
||||
if (currentTimestamp < LAST_TIMESTAMP.get()) {
|
||||
throw new IllegalStateException("Clock moved backwards. Refusing to generate ID.");
|
||||
}
|
||||
|
||||
if (currentTimestamp > MAX_TIMESTAMP) {
|
||||
throw new IllegalStateException("Timestamp bits overflow, Snowflake expired.");
|
||||
throw new IllegalStateException("Timestamp bits overflow — Snowflake expired.");
|
||||
}
|
||||
|
||||
if (currentTimestamp == lastTimestamp.get()) {
|
||||
if (currentTimestamp == LAST_TIMESTAMP.get()) {
|
||||
sequence = (sequence + 1) & MAX_SEQUENCE;
|
||||
if (sequence == 0) {
|
||||
currentTimestamp = waitNextMillis(currentTimestamp);
|
||||
@@ -127,29 +84,22 @@ public class SnowflakeGenerator {
|
||||
sequence = 0L;
|
||||
}
|
||||
|
||||
lastTimestamp.set(currentTimestamp);
|
||||
LAST_TIMESTAMP.set(currentTimestamp);
|
||||
|
||||
return ((currentTimestamp - EPOCH) << TIMESTAMP_SHIFT)
|
||||
| (machineId << MACHINE_SHIFT)
|
||||
| (MACHINE_ID << MACHINE_SHIFT)
|
||||
| sequence;
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until the next millisecond if sequence overflows.
|
||||
*
|
||||
* @param lastTimestamp previous timestamp
|
||||
* @return new timestamp
|
||||
*/
|
||||
private long waitNextMillis(long lastTimestamp) {
|
||||
/** Waits until next millisecond if sequence exhausted. */
|
||||
private static long waitNextMillis(long lastTimestamp) {
|
||||
long ts = timestamp();
|
||||
while (ts <= lastTimestamp) {
|
||||
ts = timestamp();
|
||||
}
|
||||
while (ts <= lastTimestamp) ts = timestamp();
|
||||
return ts;
|
||||
}
|
||||
|
||||
/** Returns current system timestamp in milliseconds. */
|
||||
private long timestamp() {
|
||||
/** Returns current timestamp in milliseconds. */
|
||||
private static long timestamp() {
|
||||
return System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,98 +1,84 @@
|
||||
package org.toop.framework.audio;
|
||||
|
||||
import javafx.scene.media.MediaPlayer;
|
||||
import javax.sound.sampled.Clip;
|
||||
import javax.sound.sampled.FloatControl;
|
||||
import org.toop.framework.audio.events.AudioEvents;
|
||||
import org.toop.framework.eventbus.EventFlow;
|
||||
import org.toop.framework.audio.interfaces.AudioManager;
|
||||
import org.toop.framework.audio.interfaces.VolumeManager;
|
||||
import org.toop.framework.resource.types.AudioResource;
|
||||
|
||||
public class AudioVolumeManager {
|
||||
private final SoundManager sM;
|
||||
/**
|
||||
* Concrete implementation of {@link VolumeManager} that delegates volume control
|
||||
* to the {@link VolumeControl} enum.
|
||||
* <p>
|
||||
* This class acts as a central point for updating volume levels for different
|
||||
* audio categories (MASTER, FX, MUSIC) and for registering audio managers
|
||||
* to the appropriate volume types.
|
||||
* </p>
|
||||
*
|
||||
* <p>Key responsibilities:</p>
|
||||
* <ul>
|
||||
* <li>Set and get volume levels for each {@link VolumeControl} category.</li>
|
||||
* <li>Register {@link AudioManager} instances to specific volume types so
|
||||
* that their active audio resources receive volume updates automatically.</li>
|
||||
* <li>Automatically scales non-master volumes according to the current master volume.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Example usage:</p>
|
||||
* <pre>{@code
|
||||
* AudioVolumeManager volumeManager = new AudioVolumeManager();
|
||||
*
|
||||
* // Register music manager to MUSIC volume type
|
||||
* volumeManager.registerManager(VolumeControl.MUSIC, musicManager);
|
||||
*
|
||||
* // Set master volume to 80%
|
||||
* volumeManager.setVolume(0.8, VolumeControl.MASTERVOLUME);
|
||||
*
|
||||
* // Set FX volume to 50% of master
|
||||
* volumeManager.setVolume(0.5, VolumeControl.FX);
|
||||
*
|
||||
* // Retrieve current MUSIC volume
|
||||
* double musicVol = volumeManager.getVolume(VolumeControl.MUSIC);
|
||||
* }</pre>
|
||||
*/
|
||||
public class AudioVolumeManager implements VolumeManager {
|
||||
|
||||
private double volume = 1.0;
|
||||
private double fxVolume = 1.0;
|
||||
private double musicVolume = 1.0;
|
||||
|
||||
public AudioVolumeManager(SoundManager soundManager) {
|
||||
this.sM = soundManager;
|
||||
|
||||
new EventFlow()
|
||||
.listen(this::handleVolumeChange)
|
||||
.listen(this::handleFxVolumeChange)
|
||||
.listen(this::handleMusicVolumeChange)
|
||||
.listen(this::handleGetCurrentVolume)
|
||||
.listen(this::handleGetCurrentFxVolume)
|
||||
.listen(this::handleGetCurrentMusicVolume);
|
||||
/**
|
||||
* Sets the volume for a specific volume type.
|
||||
* <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)) {
|
||||
FloatControl volumeControl =
|
||||
(FloatControl) clip.getControl(FloatControl.Type.MASTER_GAIN);
|
||||
float min = volumeControl.getMinimum();
|
||||
float max = volumeControl.getMaximum();
|
||||
float dB =
|
||||
(float)
|
||||
(Math.log10(Math.max(this.fxVolume * this.volume, 0.0001))
|
||||
* 20.0); // convert linear to dB
|
||||
dB = Math.max(min, Math.min(max, dB));
|
||||
volumeControl.setValue(dB);
|
||||
/**
|
||||
* Registers an {@link AudioManager} with the specified {@link VolumeControl} category.
|
||||
* <p>
|
||||
* All active audio resources managed by the given {@link AudioManager} will
|
||||
* automatically receive volume updates when the volume type changes.
|
||||
*
|
||||
* @param type the volume type to register the manager under
|
||||
* @param manager the audio manager to register
|
||||
* @return the current {@link AudioVolumeManager} instance (for method chaining)
|
||||
*/
|
||||
public AudioVolumeManager registerManager(VolumeControl type, AudioManager<? extends AudioResource> manager) {
|
||||
if (manager != null) {
|
||||
type.addManager(manager);
|
||||
}
|
||||
}
|
||||
|
||||
private double limitVolume(double volume) {
|
||||
if (volume > 1.0) return 1.0;
|
||||
else return Math.max(volume, 0.0);
|
||||
}
|
||||
|
||||
private void handleFxVolumeChange(AudioEvents.ChangeFxVolume event) {
|
||||
this.fxVolume = limitVolume(event.newVolume() / 100);
|
||||
for (Clip clip : sM.getActiveSoundEffects().values()) {
|
||||
updateSoundEffectVolume(clip);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleVolumeChange(AudioEvents.ChangeVolume event) {
|
||||
this.volume = limitVolume(event.newVolume() / 100);
|
||||
for (MediaPlayer mediaPlayer : sM.getActiveMusic()) {
|
||||
this.updateMusicVolume(mediaPlayer);
|
||||
}
|
||||
for (Clip clip : sM.getActiveSoundEffects().values()) {
|
||||
updateSoundEffectVolume(clip);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMusicVolumeChange(AudioEvents.ChangeMusicVolume event) {
|
||||
this.musicVolume = limitVolume(event.newVolume() / 100);
|
||||
for (MediaPlayer mediaPlayer : sM.getActiveMusic()) {
|
||||
this.updateMusicVolume(mediaPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleGetCurrentVolume(AudioEvents.GetCurrentVolume event) {
|
||||
new EventFlow()
|
||||
.addPostEvent(
|
||||
new AudioEvents.GetCurrentVolumeResponse(volume * 100, event.snowflakeId()))
|
||||
.asyncPostEvent();
|
||||
}
|
||||
|
||||
private void handleGetCurrentFxVolume(AudioEvents.GetCurrentFxVolume event) {
|
||||
new EventFlow()
|
||||
.addPostEvent(
|
||||
new AudioEvents.GetCurrentFxVolumeResponse(
|
||||
fxVolume * 100, event.snowflakeId()))
|
||||
.asyncPostEvent();
|
||||
}
|
||||
|
||||
private void handleGetCurrentMusicVolume(AudioEvents.GetCurrentMusicVolume event) {
|
||||
new EventFlow()
|
||||
.addPostEvent(
|
||||
new AudioEvents.GetCurrentMusicVolumeResponse(
|
||||
musicVolume * 100, event.snowflakeId()))
|
||||
.asyncPostEvent();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
package org.toop.framework.audio;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import javafx.scene.media.MediaPlayer;
|
||||
import javax.sound.sampled.*;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.toop.framework.SnowflakeGenerator;
|
||||
import org.toop.framework.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.MusicAsset;
|
||||
import org.toop.framework.resource.resources.SoundEffectAsset;
|
||||
|
||||
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 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() != null
|
||||
? backgroundMusicQueue.peek().getFile().getName()
|
||||
: null);
|
||||
}
|
||||
|
||||
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 = new SnowflakeGenerator().nextId();
|
||||
|
||||
// store it so we can stop it later
|
||||
activeSoundEffects.put(clipId, clip);
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,98 +1,32 @@
|
||||
package org.toop.framework.audio.events;
|
||||
|
||||
import java.util.Map;
|
||||
import org.toop.framework.eventbus.events.EventWithSnowflake;
|
||||
import org.toop.framework.eventbus.events.EventWithoutSnowflake;
|
||||
import org.toop.framework.eventbus.events.EventsBase;
|
||||
import org.toop.framework.audio.VolumeControl;
|
||||
import org.toop.framework.eventbus.events.*;
|
||||
import org.toop.framework.eventbus.events.ResponseToUniqueEvent;
|
||||
import org.toop.framework.eventbus.events.UniqueEvent;
|
||||
|
||||
public class AudioEvents extends EventsBase {
|
||||
/** Starts playing a sound. */
|
||||
public record PlayEffect(String fileName, boolean loop) implements EventWithoutSnowflake {}
|
||||
/** Stops the audio manager. */
|
||||
public record StopAudioManager() implements GenericEvent {}
|
||||
|
||||
public record StopEffect(long clipId) implements EventWithoutSnowflake {}
|
||||
/** Start playing a sound effect. */
|
||||
public record PlayEffect(String fileName, boolean loop) implements GenericEvent {}
|
||||
|
||||
public record StartBackgroundMusic() implements EventWithoutSnowflake {}
|
||||
/** Stop playing a sound effect. */
|
||||
public record StopEffect(String fileName) implements GenericEvent {}
|
||||
|
||||
public record ChangeVolume(double newVolume) implements EventWithoutSnowflake {}
|
||||
/** Start background music. */
|
||||
public record StartBackgroundMusic() implements GenericEvent {}
|
||||
|
||||
public record ChangeFxVolume(double newVolume) implements EventWithoutSnowflake {}
|
||||
/** Change volume, choose type with {@link VolumeControl}. */
|
||||
public record ChangeVolume(double newVolume, VolumeControl controlType) implements GenericEvent {}
|
||||
|
||||
public record ChangeMusicVolume(double newVolume) implements EventWithoutSnowflake {}
|
||||
/** Requests the desired volume by selecting it with {@link VolumeControl}. */
|
||||
public record GetVolume(VolumeControl controlType, long identifier) implements UniqueEvent {}
|
||||
|
||||
public record GetCurrentVolume(long snowflakeId) implements EventWithSnowflake {
|
||||
@Override
|
||||
public Map<String, Object> result() {
|
||||
return Map.of();
|
||||
}
|
||||
/** Response to GetVolume. */
|
||||
public record GetVolumeResponse(double currentVolume, long identifier) implements ResponseToUniqueEvent {}
|
||||
|
||||
@Override
|
||||
public long eventSnowflake() {
|
||||
return snowflakeId;
|
||||
}
|
||||
}
|
||||
|
||||
public record GetCurrentVolumeResponse(double currentVolume, long snowflakeId)
|
||||
implements EventWithSnowflake {
|
||||
@Override
|
||||
public Map<String, Object> result() {
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long eventSnowflake() {
|
||||
return snowflakeId;
|
||||
}
|
||||
}
|
||||
|
||||
public record GetCurrentFxVolume(long snowflakeId) implements EventWithSnowflake {
|
||||
@Override
|
||||
public Map<String, Object> result() {
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long eventSnowflake() {
|
||||
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 {}
|
||||
/** Plays the predetermined sound for pressing a button. */
|
||||
public record ClickButton() implements GenericEvent {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.toop.framework.audio.interfaces;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public interface AudioManager<T> {
|
||||
Collection<T> getActiveAudio();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.toop.framework.dispatch.interfaces;
|
||||
|
||||
public interface Dispatcher {
|
||||
void run(Runnable task);
|
||||
}
|
||||
@@ -11,13 +11,14 @@ import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import org.toop.framework.SnowflakeGenerator;
|
||||
import org.toop.framework.eventbus.events.EventType;
|
||||
import org.toop.framework.eventbus.events.EventWithSnowflake;
|
||||
import org.toop.framework.eventbus.events.ResponseToUniqueEvent;
|
||||
import org.toop.framework.eventbus.events.UniqueEvent;
|
||||
|
||||
/**
|
||||
* EventFlow is a utility class for creating, posting, and optionally subscribing to events in a
|
||||
* type-safe and chainable manner. It is designed to work with the {@link GlobalEventBus}.
|
||||
*
|
||||
* <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
|
||||
* subscription methods are chainable, and you can configure automatic unsubscription after an event
|
||||
* has been successfully handled.
|
||||
@@ -30,7 +31,7 @@ public class EventFlow {
|
||||
/** Cache of constructor handles for event classes to avoid repeated reflection lookups. */
|
||||
private static final Map<Class<?>, MethodHandle> CONSTRUCTOR_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
/** Automatically assigned UUID for {@link EventWithSnowflake} events. */
|
||||
/** Automatically assigned UUID for {@link UniqueEvent} events. */
|
||||
private long eventSnowflake = -1;
|
||||
|
||||
/** The event instance created by this publisher. */
|
||||
@@ -40,7 +41,7 @@ public class EventFlow {
|
||||
private final List<ListenerHandler> listeners = new ArrayList<>();
|
||||
|
||||
/** 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...)}). */
|
||||
public EventFlow() {}
|
||||
@@ -60,7 +61,7 @@ public class EventFlow {
|
||||
// Keep the old class+args version if needed
|
||||
public <T extends EventType> EventFlow addPostEvent(Class<T> eventClass, Object... args) {
|
||||
try {
|
||||
boolean isUuidEvent = EventWithSnowflake.class.isAssignableFrom(eventClass);
|
||||
boolean isUuidEvent = UniqueEvent.class.isAssignableFrom(eventClass);
|
||||
|
||||
MethodHandle ctorHandle =
|
||||
CONSTRUCTOR_CACHE.computeIfAbsent(
|
||||
@@ -81,7 +82,7 @@ public class EventFlow {
|
||||
int expectedParamCount = ctorHandle.type().parameterCount();
|
||||
|
||||
if (isUuidEvent && args.length < expectedParamCount) {
|
||||
this.eventSnowflake = new SnowflakeGenerator().nextId();
|
||||
this.eventSnowflake = SnowflakeGenerator.nextId();
|
||||
finalArgs = new Object[args.length + 1];
|
||||
System.arraycopy(args, 0, finalArgs, 0, args.length);
|
||||
finalArgs[args.length] = this.eventSnowflake;
|
||||
@@ -100,13 +101,8 @@ public class EventFlow {
|
||||
}
|
||||
}
|
||||
|
||||
// public EventFlow addSnowflake() {
|
||||
// this.eventSnowflake = new SnowflakeGenerator(1).nextId();
|
||||
// return this;
|
||||
// }
|
||||
|
||||
/** Subscribe by ID: only fires if UUID matches this publisher's eventId. */
|
||||
public <TT extends EventWithSnowflake> EventFlow onResponse(
|
||||
public <TT extends ResponseToUniqueEvent> EventFlow onResponse(
|
||||
Class<TT> eventClass, Consumer<TT> action, boolean unsubscribeAfterSuccess) {
|
||||
ListenerHandler[] listenerHolder = new ListenerHandler[1];
|
||||
listenerHolder[0] =
|
||||
@@ -114,7 +110,7 @@ public class EventFlow {
|
||||
GlobalEventBus.subscribe(
|
||||
eventClass,
|
||||
event -> {
|
||||
if (event.eventSnowflake() != this.eventSnowflake) return;
|
||||
if (event.getIdentifier() != this.eventSnowflake) return;
|
||||
|
||||
action.accept(event);
|
||||
|
||||
@@ -130,22 +126,21 @@ public class EventFlow {
|
||||
}
|
||||
|
||||
/** Subscribe by ID: only fires if UUID matches this publisher's eventId. */
|
||||
public <TT extends EventWithSnowflake> EventFlow onResponse(
|
||||
Class<TT> eventClass, Consumer<TT> action) {
|
||||
public <TT extends ResponseToUniqueEvent> EventFlow onResponse(Class<TT> eventClass, Consumer<TT> action) {
|
||||
return this.onResponse(eventClass, action, true);
|
||||
}
|
||||
|
||||
/** Subscribe by ID without explicit class. */
|
||||
@SuppressWarnings("unchecked")
|
||||
public <TT extends EventWithSnowflake> EventFlow onResponse(
|
||||
public <TT extends ResponseToUniqueEvent> EventFlow onResponse(
|
||||
Consumer<TT> action, boolean unsubscribeAfterSuccess) {
|
||||
ListenerHandler[] listenerHolder = new ListenerHandler[1];
|
||||
listenerHolder[0] =
|
||||
new ListenerHandler(
|
||||
GlobalEventBus.subscribe(
|
||||
event -> {
|
||||
if (!(event instanceof EventWithSnowflake uuidEvent)) return;
|
||||
if (uuidEvent.eventSnowflake() == this.eventSnowflake) {
|
||||
if (!(event instanceof UniqueEvent uuidEvent)) return;
|
||||
if (uuidEvent.getIdentifier() == this.eventSnowflake) {
|
||||
try {
|
||||
TT typedEvent = (TT) uuidEvent;
|
||||
action.accept(typedEvent);
|
||||
@@ -159,7 +154,7 @@ public class EventFlow {
|
||||
throw new ClassCastException(
|
||||
"Cannot cast "
|
||||
+ event.getClass().getName()
|
||||
+ " to EventWithSnowflake");
|
||||
+ " to UniqueEvent");
|
||||
}
|
||||
}
|
||||
}));
|
||||
@@ -167,7 +162,7 @@ public class EventFlow {
|
||||
return this;
|
||||
}
|
||||
|
||||
public <TT extends EventWithSnowflake> EventFlow onResponse(Consumer<TT> action) {
|
||||
public <TT extends ResponseToUniqueEvent> EventFlow onResponse(Consumer<TT> action) {
|
||||
return this.onResponse(action, true);
|
||||
}
|
||||
|
||||
@@ -214,7 +209,7 @@ public class EventFlow {
|
||||
throw new ClassCastException(
|
||||
"Cannot cast "
|
||||
+ event.getClass().getName()
|
||||
+ " to EventWithSnowflake");
|
||||
+ " to UniqueEvent");
|
||||
}
|
||||
}));
|
||||
this.listeners.add(listenerHolder[0]);
|
||||
@@ -237,7 +232,13 @@ public class EventFlow {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Map<String, Object> getResult() {
|
||||
private void clean() {
|
||||
this.listeners.clear();
|
||||
this.event = null;
|
||||
this.result = null;
|
||||
} // TODO
|
||||
|
||||
public Map<String, ?> getResult() {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import java.util.Map;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.function.Consumer;
|
||||
import org.toop.framework.eventbus.events.EventType;
|
||||
import org.toop.framework.eventbus.events.EventWithSnowflake;
|
||||
import org.toop.framework.eventbus.events.UniqueEvent;
|
||||
|
||||
/**
|
||||
* GlobalEventBus backed by the LMAX Disruptor for ultra-low latency, high-throughput event
|
||||
@@ -21,7 +21,7 @@ public final class GlobalEventBus {
|
||||
|
||||
/** Map of event class to Snowflake-ID-specific listeners. */
|
||||
private static final Map<
|
||||
Class<?>, ConcurrentHashMap<Long, Consumer<? extends EventWithSnowflake>>>
|
||||
Class<?>, ConcurrentHashMap<Long, Consumer<? extends UniqueEvent>>>
|
||||
UUID_LISTENERS = new ConcurrentHashMap<>();
|
||||
|
||||
/** Disruptor ring buffer size (must be power of two). */
|
||||
@@ -90,7 +90,7 @@ public final class GlobalEventBus {
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
public static <T extends EventWithSnowflake> void subscribeById(
|
||||
public static <T extends UniqueEvent> void subscribeById(
|
||||
Class<T> eventClass, long eventId, Consumer<T> listener) {
|
||||
UUID_LISTENERS
|
||||
.computeIfAbsent(eventClass, _ -> new ConcurrentHashMap<>())
|
||||
@@ -101,9 +101,9 @@ public final class GlobalEventBus {
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -152,11 +152,11 @@ public final class GlobalEventBus {
|
||||
}
|
||||
|
||||
// snowflake listeners
|
||||
if (event instanceof EventWithSnowflake snowflakeEvent) {
|
||||
Map<Long, Consumer<? extends EventWithSnowflake>> map = UUID_LISTENERS.get(clazz);
|
||||
if (event instanceof UniqueEvent snowflakeEvent) {
|
||||
Map<Long, Consumer<? extends UniqueEvent>> map = UUID_LISTENERS.get(clazz);
|
||||
if (map != null) {
|
||||
Consumer<EventWithSnowflake> listener =
|
||||
(Consumer<EventWithSnowflake>) map.remove(snowflakeEvent.eventSnowflake());
|
||||
Consumer<UniqueEvent> listener =
|
||||
(Consumer<UniqueEvent>) map.remove(snowflakeEvent.getIdentifier());
|
||||
if (listener != null) {
|
||||
try {
|
||||
listener.accept(snowflakeEvent);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
package org.toop.framework.eventbus.events;
|
||||
|
||||
public interface EventWithoutSnowflake extends EventType {}
|
||||
@@ -1,69 +1,4 @@
|
||||
package org.toop.framework.eventbus.events;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.Arrays;
|
||||
|
||||
/** Events that are used in the GlobalEventBus class. */
|
||||
public class EventsBase {
|
||||
|
||||
/**
|
||||
* WIP, DO NOT USE!
|
||||
*
|
||||
* @param eventName
|
||||
* @param args
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
public static Object get(String eventName, Object... args) throws Exception {
|
||||
Class<?> clazz = Class.forName("org.toop.framework.eventbus.events.Events$ServerEvents$" + eventName);
|
||||
Class<?>[] paramTypes = Arrays.stream(args).map(Object::getClass).toArray(Class<?>[]::new);
|
||||
Constructor<?> constructor = clazz.getConstructor(paramTypes);
|
||||
return constructor.newInstance(args);
|
||||
}
|
||||
|
||||
/**
|
||||
* WIP, DO NOT USE!
|
||||
*
|
||||
* @param eventCategory
|
||||
* @param eventName
|
||||
* @param args
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
public static Object get(String eventCategory, String eventName, Object... args)
|
||||
throws Exception {
|
||||
Class<?> clazz =
|
||||
Class.forName("org.toop.framework.eventbus.events.Events$" + eventCategory + "$" + eventName);
|
||||
Class<?>[] paramTypes = Arrays.stream(args).map(Object::getClass).toArray(Class<?>[]::new);
|
||||
Constructor<?> constructor = clazz.getConstructor(paramTypes);
|
||||
return constructor.newInstance(args);
|
||||
}
|
||||
|
||||
/**
|
||||
* WIP, DO NOT USE!
|
||||
*
|
||||
* @param eventName
|
||||
* @param args
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
public static Object get2(String eventName, Object... args) throws Exception {
|
||||
// Fully qualified class name
|
||||
String className = "org.toop.server.backend.Events$ServerEvents$" + eventName;
|
||||
|
||||
// Load the class
|
||||
Class<?> clazz = Class.forName(className);
|
||||
|
||||
// Build array of argument types
|
||||
Class<?>[] paramTypes = new Class[args.length];
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
paramTypes[i] = args[i].getClass();
|
||||
}
|
||||
|
||||
// Get the constructor
|
||||
Constructor<?> constructor = clazz.getConstructor(paramTypes);
|
||||
|
||||
// Create a new instance
|
||||
return constructor.newInstance(args);
|
||||
}
|
||||
}
|
||||
public class EventsBase {}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package org.toop.framework.eventbus.events;
|
||||
|
||||
public interface GenericEvent extends EventType {}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ public class NetworkingClientManager {
|
||||
}
|
||||
|
||||
long startClientRequest(String ip, int port) {
|
||||
long connectionId = new SnowflakeGenerator().nextId();
|
||||
long connectionId = SnowflakeGenerator.nextId();
|
||||
try {
|
||||
NetworkingClient client =
|
||||
new NetworkingClient(
|
||||
|
||||
@@ -119,6 +119,7 @@ public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
|
||||
}
|
||||
|
||||
private void gameWinConditionHandler(String rec) {
|
||||
@SuppressWarnings("StreamToString")
|
||||
String condition =
|
||||
Pattern.compile("\\b(win|draw|lose)\\b", Pattern.CASE_INSENSITIVE)
|
||||
.matcher(rec)
|
||||
@@ -180,6 +181,7 @@ public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
|
||||
}
|
||||
|
||||
private void gameYourTurnHandler(String rec) {
|
||||
@SuppressWarnings("StreamToString")
|
||||
String msg =
|
||||
Pattern.compile("TURNMESSAGE:\\s*\"([^\"]*)\"")
|
||||
.matcher(rec)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package org.toop.framework.networking.events;
|
||||
|
||||
import java.lang.reflect.RecordComponent;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import org.toop.framework.eventbus.events.EventWithSnowflake;
|
||||
import org.toop.framework.eventbus.events.EventWithoutSnowflake;
|
||||
|
||||
import org.toop.framework.eventbus.events.GenericEvent;
|
||||
import org.toop.framework.eventbus.events.ResponseToUniqueEvent;
|
||||
import org.toop.framework.eventbus.events.UniqueEvent;
|
||||
import org.toop.framework.eventbus.events.EventsBase;
|
||||
import org.toop.annotations.AutoResponseResult;
|
||||
import org.toop.framework.networking.NetworkingClient;
|
||||
|
||||
/**
|
||||
@@ -15,8 +15,8 @@ import org.toop.framework.networking.NetworkingClient;
|
||||
* org.toop.framework.eventbus.GlobalEventBus}.
|
||||
*
|
||||
* <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
|
||||
* (EventWithoutSnowflake).
|
||||
* subsystem. Events are separated into those with unique IDs (UniqueEvent) and those without
|
||||
* (GenericEvent).
|
||||
*/
|
||||
public class NetworkEvents extends EventsBase {
|
||||
|
||||
@@ -30,86 +30,76 @@ public class NetworkEvents extends EventsBase {
|
||||
* instances.
|
||||
*/
|
||||
public record RequestsAllClients(CompletableFuture<List<NetworkingClient>> future)
|
||||
implements EventWithoutSnowflake {}
|
||||
implements GenericEvent {}
|
||||
|
||||
/** Forces all active client connections to close immediately. */
|
||||
public record ForceCloseAllClients() implements EventWithoutSnowflake {}
|
||||
public record ForceCloseAllClients() implements GenericEvent {}
|
||||
|
||||
/** Response indicating a challenge was cancelled. */
|
||||
public record ChallengeCancelledResponse(long clientId, String challengeId)
|
||||
implements EventWithoutSnowflake {}
|
||||
public record ChallengeCancelledResponse(long clientId, String challengeId) implements GenericEvent {}
|
||||
|
||||
/** Response indicating a challenge was received. */
|
||||
public record ChallengeResponse(
|
||||
long clientId, String challengerName, String challengeId, String gameType)
|
||||
implements EventWithoutSnowflake {}
|
||||
public record ChallengeResponse(long clientId, String challengerName, String challengeId, String gameType)
|
||||
implements GenericEvent {}
|
||||
|
||||
/** Response containing a list of players for a client. */
|
||||
public record PlayerlistResponse(long clientId, String[] playerlist)
|
||||
implements EventWithoutSnowflake {}
|
||||
public record PlayerlistResponse(long clientId, String[] playerlist) implements GenericEvent {}
|
||||
|
||||
/** Response containing a list of games for a client. */
|
||||
public record GamelistResponse(long clientId, String[] gamelist)
|
||||
implements EventWithoutSnowflake {}
|
||||
public record GamelistResponse(long clientId, String[] gamelist) implements GenericEvent {}
|
||||
|
||||
/** Response indicating a game match information for a client. */
|
||||
public record GameMatchResponse(
|
||||
long clientId, String playerToMove, String gameType, String opponent)
|
||||
implements EventWithoutSnowflake {}
|
||||
public record GameMatchResponse(long clientId, String playerToMove, String gameType, String opponent)
|
||||
implements GenericEvent {}
|
||||
|
||||
/** Response indicating the result of a game. */
|
||||
public record GameResultResponse(long clientId, String condition)
|
||||
implements EventWithoutSnowflake {}
|
||||
public record GameResultResponse(long clientId, String condition) implements GenericEvent {}
|
||||
|
||||
/** Response indicating a game move occurred. */
|
||||
public record GameMoveResponse(long clientId, String player, String move, String details)
|
||||
implements EventWithoutSnowflake {}
|
||||
public record GameMoveResponse(long clientId, String player, String move, String details) implements GenericEvent {}
|
||||
|
||||
/** Response indicating it is the player's turn. */
|
||||
public record YourTurnResponse(long clientId, String message)
|
||||
implements EventWithoutSnowflake {}
|
||||
implements GenericEvent {}
|
||||
|
||||
/** Request to send login credentials for a client. */
|
||||
public record SendLogin(long clientId, String username) implements EventWithoutSnowflake {}
|
||||
public record SendLogin(long clientId, String username) implements GenericEvent {}
|
||||
|
||||
/** Request to log out a client. */
|
||||
public record SendLogout(long clientId) implements EventWithoutSnowflake {}
|
||||
public record SendLogout(long clientId) implements GenericEvent {}
|
||||
|
||||
/** Request to retrieve the player list for a client. */
|
||||
public record SendGetPlayerlist(long clientId) implements EventWithoutSnowflake {}
|
||||
public record SendGetPlayerlist(long clientId) implements GenericEvent {}
|
||||
|
||||
/** Request to retrieve the game list for a client. */
|
||||
public record SendGetGamelist(long clientId) implements EventWithoutSnowflake {}
|
||||
public record SendGetGamelist(long clientId) implements GenericEvent {}
|
||||
|
||||
/** Request to subscribe a client to a game type. */
|
||||
public record SendSubscribe(long clientId, String gameType) implements EventWithoutSnowflake {}
|
||||
public record SendSubscribe(long clientId, String gameType) implements GenericEvent {}
|
||||
|
||||
/** Request to make a move in a game. */
|
||||
public record SendMove(long clientId, short moveNumber) implements EventWithoutSnowflake {}
|
||||
public record SendMove(long clientId, short moveNumber) implements GenericEvent {}
|
||||
|
||||
/** Request to challenge another player. */
|
||||
public record SendChallenge(long clientId, String usernameToChallenge, String gameType)
|
||||
implements EventWithoutSnowflake {}
|
||||
public record SendChallenge(long clientId, String usernameToChallenge, String gameType) implements GenericEvent {}
|
||||
|
||||
/** Request to accept a challenge. */
|
||||
public record SendAcceptChallenge(long clientId, int challengeId)
|
||||
implements EventWithoutSnowflake {}
|
||||
public record SendAcceptChallenge(long clientId, int challengeId) implements GenericEvent {}
|
||||
|
||||
/** Request to forfeit a game. */
|
||||
public record SendForfeit(long clientId) implements EventWithoutSnowflake {}
|
||||
public record SendForfeit(long clientId) implements GenericEvent {}
|
||||
|
||||
/** Request to send a message from a client. */
|
||||
public record SendMessage(long clientId, String message) implements EventWithoutSnowflake {}
|
||||
public record SendMessage(long clientId, String message) implements GenericEvent {}
|
||||
|
||||
/** Request to display help to a client. */
|
||||
public record SendHelp(long clientId) implements EventWithoutSnowflake {}
|
||||
public record SendHelp(long clientId) implements GenericEvent {}
|
||||
|
||||
/** Request to display help for a specific command. */
|
||||
public record SendHelpForCommand(long clientId, String command)
|
||||
implements EventWithoutSnowflake {}
|
||||
public record SendHelpForCommand(long clientId, String command) implements GenericEvent {}
|
||||
|
||||
/** Request to close a specific client connection. */
|
||||
public record CloseClient(long clientId) implements EventWithoutSnowflake {}
|
||||
public record CloseClient(long clientId) implements GenericEvent {}
|
||||
|
||||
/**
|
||||
* Event to start a new client connection.
|
||||
@@ -120,61 +110,19 @@ public class NetworkEvents extends EventsBase {
|
||||
* @param port Server port.
|
||||
* @param eventSnowflake Unique event identifier for correlation.
|
||||
*/
|
||||
public record StartClient(String ip, int port, long eventSnowflake)
|
||||
implements EventWithSnowflake {
|
||||
|
||||
@Override
|
||||
public Map<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;
|
||||
}
|
||||
}
|
||||
public record StartClient(String ip, int port, long eventSnowflake) implements UniqueEvent {}
|
||||
|
||||
/**
|
||||
* Response confirming a client was started.
|
||||
*
|
||||
* @param clientId The client ID assigned to the new connection.
|
||||
* @param eventSnowflake Event ID used for correlation.
|
||||
* @param identifier Event ID used for correlation.
|
||||
*/
|
||||
public record StartClientResponse(long clientId, long eventSnowflake)
|
||||
implements EventWithSnowflake {
|
||||
@Override
|
||||
public Map<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;
|
||||
}
|
||||
}
|
||||
@AutoResponseResult
|
||||
public record StartClientResponse(long clientId, long identifier) implements ResponseToUniqueEvent {}
|
||||
|
||||
/** Generic server response. */
|
||||
public record ServerResponse(long clientId) implements EventWithoutSnowflake {}
|
||||
public record ServerResponse(long clientId) implements GenericEvent {}
|
||||
|
||||
/**
|
||||
* Request to send a command to a server.
|
||||
@@ -182,10 +130,10 @@ public class NetworkEvents extends EventsBase {
|
||||
* @param clientId The client connection ID.
|
||||
* @param args The command arguments.
|
||||
*/
|
||||
public record SendCommand(long clientId, String... args) implements EventWithoutSnowflake {}
|
||||
public record SendCommand(long clientId, String... args) implements GenericEvent {}
|
||||
|
||||
/** WIP (Not working) Request to reconnect a client to a previous address. */
|
||||
public record Reconnect(long clientId) implements EventWithoutSnowflake {}
|
||||
public record Reconnect(long clientId) implements GenericEvent {}
|
||||
|
||||
/**
|
||||
* Response triggered when a message is received from a server.
|
||||
@@ -193,7 +141,7 @@ public class NetworkEvents extends EventsBase {
|
||||
* @param clientId The connection ID that received the message.
|
||||
* @param message The message content.
|
||||
*/
|
||||
public record ReceivedMessage(long clientId, String message) implements EventWithoutSnowflake {}
|
||||
public record ReceivedMessage(long clientId, String message) implements GenericEvent {}
|
||||
|
||||
/**
|
||||
* Request to change a client connection to a new server.
|
||||
@@ -202,12 +150,11 @@ public class NetworkEvents extends EventsBase {
|
||||
* @param ip The new server IP.
|
||||
* @param port The new server port.
|
||||
*/
|
||||
public record ChangeClientHost(long clientId, String ip, int port)
|
||||
implements EventWithoutSnowflake {}
|
||||
public record ChangeClientHost(long clientId, String ip, int port) implements GenericEvent {}
|
||||
|
||||
/** WIP (Not working) Response indicating that the client could not connect. */
|
||||
public record CouldNotConnect(long clientId) implements EventWithoutSnowflake {}
|
||||
public record CouldNotConnect(long clientId) implements GenericEvent {}
|
||||
|
||||
/** Event indicating a client connection was closed. */
|
||||
public record ClosedConnection(long clientId) implements EventWithoutSnowflake {}
|
||||
public record ClosedConnection(long clientId) implements GenericEvent {}
|
||||
}
|
||||
|
||||
@@ -51,12 +51,20 @@ import org.toop.framework.resource.resources.*;
|
||||
* </ul>
|
||||
*/
|
||||
public class ResourceManager {
|
||||
private static final Logger logger = LogManager.getLogger(ResourceManager.class);
|
||||
private static final Logger logger = LogManager.getLogger(ResourceManager.class);
|
||||
private static final Map<String, ResourceMeta<? extends BaseResource>> assets =
|
||||
new ConcurrentHashMap<>();
|
||||
private static ResourceManager instance;
|
||||
|
||||
private ResourceManager() {}
|
||||
|
||||
public static ResourceManager getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new ResourceManager();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all assets from a given {@link ResourceLoader} into the manager.
|
||||
*
|
||||
@@ -96,16 +104,32 @@ public class ResourceManager {
|
||||
* @param <T> the resource type
|
||||
* @return a list of assets matching the type
|
||||
*/
|
||||
public static <T extends BaseResource> ArrayList<ResourceMeta<T>> getAllOfType(Class<T> type) {
|
||||
ArrayList<ResourceMeta<T>> list = new ArrayList<>();
|
||||
for (ResourceMeta<? extends BaseResource> asset : assets.values()) {
|
||||
if (type.isInstance(asset.getResource())) {
|
||||
public static <T extends BaseResource> List<ResourceMeta<T>> getAllOfType(Class<T> type) {
|
||||
List<ResourceMeta<T>> result = new ArrayList<>();
|
||||
|
||||
for (ResourceMeta<? extends BaseResource> meta : assets.values()) {
|
||||
BaseResource res = meta.getResource();
|
||||
if (type.isInstance(res)) {
|
||||
@SuppressWarnings("unchecked")
|
||||
ResourceMeta<T> typed = (ResourceMeta<T>) asset;
|
||||
list.add(typed);
|
||||
ResourceMeta<T> typed = (ResourceMeta<T>) meta;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,7 +9,7 @@ public class ResourceMeta<T extends BaseResource> {
|
||||
private final T resource;
|
||||
|
||||
public ResourceMeta(String name, T resource) {
|
||||
this.id = new SnowflakeGenerator().nextId();
|
||||
this.id = SnowflakeGenerator.nextId();
|
||||
this.name = name;
|
||||
this.resource = resource;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package org.toop.framework.resource.events;
|
||||
|
||||
import org.toop.framework.eventbus.events.EventWithoutSnowflake;
|
||||
import org.toop.framework.eventbus.events.GenericEvent;
|
||||
|
||||
public class AssetLoaderEvents {
|
||||
public record LoadingProgressUpdate(int hasLoadedAmount, int isLoadingAmount)
|
||||
implements EventWithoutSnowflake {}
|
||||
implements GenericEvent {}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ public class LocalizationAsset extends BaseResource implements LoadableResource,
|
||||
private final Map<Locale, ResourceBundle> bundles = new HashMap<>();
|
||||
|
||||
/** Flag indicating whether this asset has been loaded. */
|
||||
private boolean isLoaded = false;
|
||||
private boolean loaded = false;
|
||||
|
||||
/** Basename of the given asset */
|
||||
private final String baseName = "localization";
|
||||
@@ -53,14 +53,14 @@ public class LocalizationAsset extends BaseResource implements LoadableResource,
|
||||
@Override
|
||||
public void load() {
|
||||
loadFile(getFile());
|
||||
isLoaded = true;
|
||||
loaded = true;
|
||||
}
|
||||
|
||||
/** Unloads all loaded resource bundles, freeing memory. */
|
||||
@Override
|
||||
public void unload() {
|
||||
bundles.clear();
|
||||
isLoaded = false;
|
||||
loaded = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,7 +70,7 @@ public class LocalizationAsset extends BaseResource implements LoadableResource,
|
||||
*/
|
||||
@Override
|
||||
public boolean isLoaded() {
|
||||
return isLoaded;
|
||||
return loaded;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,7 +131,7 @@ public class LocalizationAsset extends BaseResource implements LoadableResource,
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to load localization file: " + file, e);
|
||||
}
|
||||
isLoaded = true;
|
||||
loaded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,33 +2,48 @@ 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 {
|
||||
private Media media;
|
||||
public class MusicAsset extends BaseResource implements LoadableResource, AudioResource {
|
||||
private MediaPlayer mediaPlayer;
|
||||
private double volume;
|
||||
|
||||
public MusicAsset(final File audioFile) {
|
||||
super(audioFile);
|
||||
}
|
||||
|
||||
public Media getMedia() {
|
||||
if (media == null) {
|
||||
media = new Media(file.toURI().toString());
|
||||
}
|
||||
return media;
|
||||
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 (media == null) media = new Media(file.toURI().toString());
|
||||
if (mediaPlayer == null) {
|
||||
mediaPlayer = new MediaPlayer(new Media(file.toURI().toString()));
|
||||
initPlayer();
|
||||
mediaPlayer.setVolume(volume);
|
||||
}
|
||||
this.isLoaded = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unload() {
|
||||
media = null;
|
||||
if (mediaPlayer != null) {
|
||||
mediaPlayer.stop();
|
||||
mediaPlayer.dispose();
|
||||
mediaPlayer = null;
|
||||
}
|
||||
isLoaded = false;
|
||||
}
|
||||
|
||||
@@ -36,4 +51,35 @@ public class MusicAsset extends BaseResource implements LoadableResource {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +1,31 @@
|
||||
package org.toop.framework.resource.resources;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
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 {
|
||||
private byte[] rawData;
|
||||
public class SoundEffectAsset extends BaseResource implements LoadableResource, AudioResource {
|
||||
private final Clip clip = AudioSystem.getClip();
|
||||
|
||||
public SoundEffectAsset(final File audioFile) {
|
||||
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 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;
|
||||
public Clip getClip() {
|
||||
if (!this.isLoaded()) {this.load();} return this.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) {
|
||||
private AudioInputStream downSampleAudio(AudioInputStream audioInputStream, AudioFormat baseFormat) {
|
||||
AudioFormat decodedFormat =
|
||||
new AudioFormat(
|
||||
AudioFormat.Encoding.PCM_SIGNED,
|
||||
@@ -57,19 +40,42 @@ public class SoundEffectAsset extends BaseResource implements LoadableResource {
|
||||
return AudioSystem.getAudioInputStream(decodedFormat, audioInputStream);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void load() {
|
||||
try {
|
||||
this.rawData = Files.readAllBytes(file.toPath());
|
||||
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 (IOException e) {
|
||||
} catch (LineUnavailableException | UnsupportedAudioFileException | IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unload() {
|
||||
this.rawData = null;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -77,4 +83,65 @@ public class SoundEffectAsset extends BaseResource implements LoadableResource {
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,78 +1,79 @@
|
||||
package org.toop.framework;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class SnowflakeGeneratorTest {
|
||||
|
||||
@Test
|
||||
void testMachineIdWithinBounds() {
|
||||
SnowflakeGenerator generator = new SnowflakeGenerator();
|
||||
long machineIdField = getMachineId(generator);
|
||||
assertTrue(
|
||||
machineIdField >= 0 && machineIdField <= 1023,
|
||||
"Machine ID should be within 0-1023");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNextIdReturnsUniqueValues() {
|
||||
SnowflakeGenerator generator = new SnowflakeGenerator();
|
||||
Set<Long> ids = new HashSet<>();
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
long id = generator.nextId();
|
||||
assertFalse(ids.contains(id), "Duplicate ID generated");
|
||||
ids.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSequenceRollover() throws Exception {
|
||||
SnowflakeGenerator generator =
|
||||
new SnowflakeGenerator() {
|
||||
private long fakeTime = System.currentTimeMillis();
|
||||
|
||||
protected long timestamp() {
|
||||
return fakeTime;
|
||||
}
|
||||
|
||||
void incrementTime() {
|
||||
fakeTime++;
|
||||
}
|
||||
};
|
||||
|
||||
long first = generator.nextId();
|
||||
long second = generator.nextId();
|
||||
assertNotEquals(
|
||||
first, second, "IDs generated within same millisecond should differ by sequence");
|
||||
|
||||
// Force sequence overflow
|
||||
for (int i = 0; i < (1 << 12); i++) generator.nextId();
|
||||
long afterOverflow = generator.nextId();
|
||||
assertTrue(afterOverflow > second, "ID after sequence rollover should be greater");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNextIdMonotonic() {
|
||||
SnowflakeGenerator generator = new SnowflakeGenerator();
|
||||
long prev = generator.nextId();
|
||||
for (int i = 0; i < 100; i++) {
|
||||
long next = generator.nextId();
|
||||
assertTrue(next > prev, "IDs must be increasing");
|
||||
prev = next;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: reflectively get machineId
|
||||
private long getMachineId(SnowflakeGenerator generator) {
|
||||
try {
|
||||
var field = SnowflakeGenerator.class.getDeclaredField("machineId");
|
||||
field.setAccessible(true);
|
||||
return (long) field.get(generator);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
//package org.toop.framework;
|
||||
//
|
||||
//import static org.junit.jupiter.api.Assertions.*;
|
||||
//
|
||||
//import java.util.HashSet;
|
||||
//import java.util.Set;
|
||||
//import org.junit.jupiter.api.Test;
|
||||
//
|
||||
//class SnowflakeGeneratorTest {
|
||||
//
|
||||
// @Test
|
||||
// void testMachineIdWithinBounds() {
|
||||
// SnowflakeGenerator generator = new SnowflakeGenerator();
|
||||
// long machineIdField = getMachineId(generator);
|
||||
// assertTrue(
|
||||
// machineIdField >= 0 && machineIdField <= 1023,
|
||||
// "Machine ID should be within 0-1023");
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void testNextIdReturnsUniqueValues() {
|
||||
// SnowflakeGenerator generator = new SnowflakeGenerator();
|
||||
// Set<Long> ids = new HashSet<>();
|
||||
// for (int i = 0; i < 1000; i++) {
|
||||
// long id = generator.nextId();
|
||||
// assertFalse(ids.contains(id), "Duplicate ID generated");
|
||||
// ids.add(id);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void testSequenceRollover() throws Exception {
|
||||
// SnowflakeGenerator generator =
|
||||
// new SnowflakeGenerator() {
|
||||
// private long fakeTime = System.currentTimeMillis();
|
||||
//
|
||||
// protected long timestamp() {
|
||||
// return fakeTime;
|
||||
// }
|
||||
//
|
||||
// void incrementTime() {
|
||||
// fakeTime++;
|
||||
// }
|
||||
// };
|
||||
//
|
||||
// long first = generator.nextId();
|
||||
// long second = generator.nextId();
|
||||
// assertNotEquals(
|
||||
// first, second, "IDs generated within same millisecond should differ by sequence");
|
||||
//
|
||||
// // Force sequence overflow
|
||||
// for (int i = 0; i < (1 << 12); i++) generator.nextId();
|
||||
// long afterOverflow = generator.nextId();
|
||||
// assertTrue(afterOverflow > second, "ID after sequence rollover should be greater");
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void testNextIdMonotonic() {
|
||||
// SnowflakeGenerator generator = new SnowflakeGenerator();
|
||||
// long prev = generator.nextId();
|
||||
// for (int i = 0; i < 100; i++) {
|
||||
// long next = generator.nextId();
|
||||
// assertTrue(next > prev, "IDs must be increasing");
|
||||
// prev = next;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Helper: reflectively get machineId
|
||||
// private long getMachineId(SnowflakeGenerator generator) {
|
||||
// try {
|
||||
// var field = SnowflakeGenerator.class.getDeclaredField("machineId");
|
||||
// field.setAccessible(true);
|
||||
// return (long) field.get(generator);
|
||||
// } catch (Exception e) {
|
||||
// throw new RuntimeException(e);
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
// TODO
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//
|
||||
// import org.junit.jupiter.api.Tag;
|
||||
// import org.junit.jupiter.api.Test;
|
||||
// import org.toop.framework.eventbus.events.EventWithSnowflake;
|
||||
// import org.toop.framework.eventbus.events.UniqueEvent;
|
||||
//
|
||||
// import java.math.BigInteger;
|
||||
// import java.util.concurrent.*;
|
||||
@@ -13,7 +13,7 @@
|
||||
// class EventFlowStressTest {
|
||||
//
|
||||
// /** Top-level record to ensure runtime type matches subscription */
|
||||
// public record HeavyEvent(String payload, long eventSnowflake) implements EventWithSnowflake {
|
||||
// public record HeavyEvent(String payload, long eventSnowflake) implements UniqueEvent {
|
||||
// @Override
|
||||
// public java.util.Map<String, Object> result() {
|
||||
// return java.util.Map.of("payload", payload, "eventId", eventSnowflake);
|
||||
@@ -26,7 +26,7 @@
|
||||
// }
|
||||
//
|
||||
// public record HeavyEventSuccess(String payload, long eventSnowflake) implements
|
||||
// EventWithSnowflake {
|
||||
// UniqueEvent {
|
||||
// @Override
|
||||
// public java.util.Map<String, Object> result() {
|
||||
// return java.util.Map.of("payload", payload, "eventId", eventSnowflake);
|
||||
|
||||
@@ -1,92 +1,93 @@
|
||||
package org.toop.framework.eventbus;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.toop.framework.SnowflakeGenerator;
|
||||
import org.toop.framework.eventbus.events.EventWithSnowflake;
|
||||
|
||||
class EventFlowTest {
|
||||
|
||||
@Test
|
||||
void testSnowflakeStructure() {
|
||||
long id = new SnowflakeGenerator().nextId();
|
||||
|
||||
long timestampPart = id >>> 22;
|
||||
long randomPart = id & ((1L << 22) - 1);
|
||||
|
||||
assertTrue(timestampPart > 0, "Timestamp part should be non-zero");
|
||||
assertTrue(
|
||||
randomPart >= 0 && randomPart < (1L << 22), "Random part should be within 22 bits");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSnowflakeMonotonicity() throws InterruptedException {
|
||||
SnowflakeGenerator sf = new SnowflakeGenerator();
|
||||
long id1 = sf.nextId();
|
||||
Thread.sleep(1); // ensure timestamp increases
|
||||
long id2 = sf.nextId();
|
||||
|
||||
assertTrue(id2 > id1, "Later snowflake should be greater than earlier one");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSnowflakeUniqueness() {
|
||||
SnowflakeGenerator sf = new SnowflakeGenerator();
|
||||
Set<Long> ids = new HashSet<>();
|
||||
for (int i = 0; i < 100_000; i++) {
|
||||
long id = sf.nextId();
|
||||
assertTrue(ids.add(id), "Snowflake IDs should be unique, but duplicate found");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Dummy Event classes for testing ---
|
||||
static class DummySnowflakeEvent implements EventWithSnowflake {
|
||||
private final long snowflake;
|
||||
|
||||
DummySnowflakeEvent(long snowflake) {
|
||||
this.snowflake = snowflake;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long eventSnowflake() {
|
||||
return snowflake;
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.Map<String, Object> result() {
|
||||
return java.util.Collections.emptyMap();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSnowflakeIsInjectedIntoEvent() {
|
||||
EventFlow flow = new EventFlow();
|
||||
flow.addPostEvent(DummySnowflakeEvent.class); // no args, should auto-generate
|
||||
|
||||
long id = flow.getEventSnowflake();
|
||||
assertNotEquals(-1, id, "Snowflake should be auto-generated");
|
||||
assertTrue(flow.getEvent() instanceof DummySnowflakeEvent);
|
||||
assertEquals(id, ((DummySnowflakeEvent) flow.getEvent()).eventSnowflake());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOnResponseFiltersBySnowflake() {
|
||||
EventFlow flow = new EventFlow();
|
||||
flow.addPostEvent(DummySnowflakeEvent.class);
|
||||
|
||||
AtomicBoolean handlerCalled = new AtomicBoolean(false);
|
||||
flow.onResponse(DummySnowflakeEvent.class, event -> handlerCalled.set(true));
|
||||
|
||||
// Post with non-matching snowflake
|
||||
GlobalEventBus.post(new DummySnowflakeEvent(12345L));
|
||||
assertFalse(handlerCalled.get(), "Handler should not fire for mismatched snowflake");
|
||||
|
||||
// Post with matching snowflake
|
||||
GlobalEventBus.post(new DummySnowflakeEvent(flow.getEventSnowflake()));
|
||||
assertTrue(handlerCalled.get(), "Handler should fire for matching snowflake");
|
||||
}
|
||||
}
|
||||
//package org.toop.framework.eventbus;
|
||||
//
|
||||
//import static org.junit.jupiter.api.Assertions.*;
|
||||
//
|
||||
//import java.util.HashSet;
|
||||
//import java.util.Set;
|
||||
//import java.util.concurrent.atomic.AtomicBoolean;
|
||||
//import org.junit.jupiter.api.Test;
|
||||
//import org.toop.framework.SnowflakeGenerator;
|
||||
//import org.toop.framework.eventbus.events.UniqueEvent;
|
||||
//
|
||||
//class EventFlowTest {
|
||||
//
|
||||
// @Test
|
||||
// void testSnowflakeStructure() {
|
||||
// long id = new SnowflakeGenerator().nextId();
|
||||
//
|
||||
// long timestampPart = id >>> 22;
|
||||
// long randomPart = id & ((1L << 22) - 1);
|
||||
//
|
||||
// assertTrue(timestampPart > 0, "Timestamp part should be non-zero");
|
||||
// assertTrue(
|
||||
// randomPart >= 0 && randomPart < (1L << 22), "Random part should be within 22 bits");
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void testSnowflakeMonotonicity() throws InterruptedException {
|
||||
// SnowflakeGenerator sf = new SnowflakeGenerator();
|
||||
// long id1 = sf.nextId();
|
||||
// Thread.sleep(1); // ensure timestamp increases
|
||||
// long id2 = sf.nextId();
|
||||
//
|
||||
// assertTrue(id2 > id1, "Later snowflake should be greater than earlier one");
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void testSnowflakeUniqueness() {
|
||||
// SnowflakeGenerator sf = new SnowflakeGenerator();
|
||||
// Set<Long> ids = new HashSet<>();
|
||||
// for (int i = 0; i < 100_000; i++) {
|
||||
// long id = sf.nextId();
|
||||
// assertTrue(ids.add(id), "Snowflake IDs should be unique, but duplicate found");
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // --- Dummy Event classes for testing ---
|
||||
// static class DummySnowflakeUniqueEvent implements UniqueEvent {
|
||||
// private final long snowflake;
|
||||
//
|
||||
// DummySnowflakeUniqueEvent(long snowflake) {
|
||||
// this.snowflake = snowflake;
|
||||
// }
|
||||
////
|
||||
//// @Override
|
||||
//// public long eventSnowflake() {
|
||||
//// return snowflake;
|
||||
//// }
|
||||
////
|
||||
//// @Override
|
||||
//// public java.util.Map<String, Object> result() {
|
||||
//// return java.util.Collections.emptyMap();
|
||||
//// }
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void testSnowflakeIsInjectedIntoEvent() {
|
||||
// EventFlow flow = new EventFlow();
|
||||
// flow.addPostEvent(DummySnowflakeUniqueEvent.class); // no args, should auto-generate
|
||||
//
|
||||
// long id = flow.getEventSnowflake();
|
||||
// assertNotEquals(-1, id, "Snowflake should be auto-generated");
|
||||
// assertTrue(flow.getEvent() instanceof DummySnowflakeUniqueEvent);
|
||||
// assertEquals(id, ((DummySnowflakeUniqueEvent) flow.getEvent()).eventSnowflake());
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void testOnResponseFiltersBySnowflake() {
|
||||
// EventFlow flow = new EventFlow();
|
||||
// flow.addPostEvent(DummySnowflakeUniqueEvent.class);
|
||||
//
|
||||
// AtomicBoolean handlerCalled = new AtomicBoolean(false);
|
||||
// flow.onResponse(DummySnowflakeUniqueEvent.class, event -> handlerCalled.set(true));
|
||||
//
|
||||
// // Post with non-matching snowflake
|
||||
// GlobalEventBus.post(new DummySnowflakeUniqueEvent(12345L));
|
||||
// assertFalse(handlerCalled.get(), "Handler should not fire for mismatched snowflake");
|
||||
//
|
||||
// // Post with matching snowflake
|
||||
// GlobalEventBus.post(new DummySnowflakeUniqueEvent(flow.getEventSnowflake()));
|
||||
// assertTrue(handlerCalled.get(), "Handler should fire for matching snowflake");
|
||||
// }
|
||||
//}
|
||||
// TODO
|
||||
@@ -1,159 +1,160 @@
|
||||
package org.toop.framework.eventbus;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.toop.framework.eventbus.events.EventType;
|
||||
import org.toop.framework.eventbus.events.EventWithSnowflake;
|
||||
|
||||
class GlobalEventBusTest {
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Test Events
|
||||
// ------------------------------------------------------------------------
|
||||
private record TestEvent(String message) implements EventType {}
|
||||
|
||||
private record TestSnowflakeEvent(long eventSnowflake, String payload)
|
||||
implements EventWithSnowflake {
|
||||
@Override
|
||||
public java.util.Map<String, Object> result() {
|
||||
return java.util.Map.of("payload", payload);
|
||||
}
|
||||
}
|
||||
|
||||
static class SampleEvent implements EventType {
|
||||
private final String message;
|
||||
|
||||
SampleEvent(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String message() {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanup() {
|
||||
GlobalEventBus.reset();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Subscriptions
|
||||
// ------------------------------------------------------------------------
|
||||
@Test
|
||||
void testSubscribeAndPost() {
|
||||
AtomicReference<String> received = new AtomicReference<>();
|
||||
Consumer<TestEvent> listener = e -> received.set(e.message());
|
||||
|
||||
GlobalEventBus.subscribe(TestEvent.class, listener);
|
||||
GlobalEventBus.post(new TestEvent("hello"));
|
||||
|
||||
assertEquals("hello", received.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUnsubscribe() {
|
||||
GlobalEventBus.reset();
|
||||
|
||||
AtomicBoolean called = new AtomicBoolean(false);
|
||||
|
||||
// Subscribe and keep the wrapper reference
|
||||
Consumer<? super EventType> subscription =
|
||||
GlobalEventBus.subscribe(SampleEvent.class, e -> called.set(true));
|
||||
|
||||
// Post once -> should trigger
|
||||
GlobalEventBus.post(new SampleEvent("test1"));
|
||||
assertTrue(called.get(), "Listener should be triggered before unsubscribe");
|
||||
|
||||
// Reset flag
|
||||
called.set(false);
|
||||
|
||||
// Unsubscribe using the wrapper reference
|
||||
GlobalEventBus.unsubscribe(subscription);
|
||||
|
||||
// Post again -> should NOT trigger
|
||||
GlobalEventBus.post(new SampleEvent("test2"));
|
||||
assertFalse(called.get(), "Listener should not be triggered after unsubscribe");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSubscribeGeneric() {
|
||||
AtomicReference<EventType> received = new AtomicReference<>();
|
||||
Consumer<Object> listener = e -> received.set((EventType) e);
|
||||
|
||||
GlobalEventBus.subscribe(listener);
|
||||
TestEvent event = new TestEvent("generic");
|
||||
GlobalEventBus.post(event);
|
||||
|
||||
assertEquals(event, received.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSubscribeById() {
|
||||
AtomicReference<String> received = new AtomicReference<>();
|
||||
long id = 42L;
|
||||
|
||||
GlobalEventBus.subscribeById(TestSnowflakeEvent.class, id, e -> received.set(e.payload()));
|
||||
GlobalEventBus.post(new TestSnowflakeEvent(id, "snowflake"));
|
||||
|
||||
assertEquals("snowflake", received.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUnsubscribeById() {
|
||||
AtomicBoolean triggered = new AtomicBoolean(false);
|
||||
long id = 99L;
|
||||
|
||||
GlobalEventBus.subscribeById(TestSnowflakeEvent.class, id, e -> triggered.set(true));
|
||||
GlobalEventBus.unsubscribeById(TestSnowflakeEvent.class, id);
|
||||
|
||||
GlobalEventBus.post(new TestSnowflakeEvent(id, "ignored"));
|
||||
assertFalse(triggered.get(), "Listener should not be triggered after unsubscribeById");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Async posting
|
||||
// ------------------------------------------------------------------------
|
||||
@Test
|
||||
void testPostAsync() throws Exception {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
GlobalEventBus.subscribe(
|
||||
TestEvent.class,
|
||||
e -> {
|
||||
if ("async".equals(e.message())) {
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
GlobalEventBus.postAsync(new TestEvent("async"));
|
||||
|
||||
assertTrue(
|
||||
latch.await(1, TimeUnit.SECONDS), "Async event should be received within timeout");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Lifecycle
|
||||
// ------------------------------------------------------------------------
|
||||
@Test
|
||||
void testResetClearsListeners() {
|
||||
AtomicBoolean triggered = new AtomicBoolean(false);
|
||||
GlobalEventBus.subscribe(TestEvent.class, e -> triggered.set(true));
|
||||
|
||||
GlobalEventBus.reset();
|
||||
GlobalEventBus.post(new TestEvent("ignored"));
|
||||
|
||||
assertFalse(triggered.get(), "Listener should not be triggered after reset");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testShutdown() {
|
||||
// Should not throw
|
||||
assertDoesNotThrow(GlobalEventBus::shutdown);
|
||||
}
|
||||
}
|
||||
//package org.toop.framework.eventbus;
|
||||
//
|
||||
//import static org.junit.jupiter.api.Assertions.*;
|
||||
//
|
||||
//import java.util.concurrent.*;
|
||||
//import java.util.concurrent.atomic.AtomicBoolean;
|
||||
//import java.util.concurrent.atomic.AtomicReference;
|
||||
//import java.util.function.Consumer;
|
||||
//import org.junit.jupiter.api.*;
|
||||
//import org.toop.framework.eventbus.events.EventType;
|
||||
//import org.toop.framework.eventbus.events.UniqueEvent;
|
||||
//
|
||||
//class GlobalEventBusTest {
|
||||
//
|
||||
// // ------------------------------------------------------------------------
|
||||
// // Test Events
|
||||
// // ------------------------------------------------------------------------
|
||||
// private record TestEvent(String message) implements EventType {}
|
||||
//
|
||||
// private record TestSnowflakeUniqueEvent(long eventSnowflake, String payload)
|
||||
// implements UniqueEvent {
|
||||
// @Override
|
||||
// public java.util.Map<String, Object> result() {
|
||||
// return java.util.Map.of("payload", payload);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// static class SampleEvent implements EventType {
|
||||
// private final String message;
|
||||
//
|
||||
// SampleEvent(String message) {
|
||||
// this.message = message;
|
||||
// }
|
||||
//
|
||||
// public String message() {
|
||||
// return message;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// @AfterEach
|
||||
// void cleanup() {
|
||||
// GlobalEventBus.reset();
|
||||
// }
|
||||
//
|
||||
// // ------------------------------------------------------------------------
|
||||
// // Subscriptions
|
||||
// // ------------------------------------------------------------------------
|
||||
// @Test
|
||||
// void testSubscribeAndPost() {
|
||||
// AtomicReference<String> received = new AtomicReference<>();
|
||||
// Consumer<TestEvent> listener = e -> received.set(e.message());
|
||||
//
|
||||
// GlobalEventBus.subscribe(TestEvent.class, listener);
|
||||
// GlobalEventBus.post(new TestEvent("hello"));
|
||||
//
|
||||
// assertEquals("hello", received.get());
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void testUnsubscribe() {
|
||||
// GlobalEventBus.reset();
|
||||
//
|
||||
// AtomicBoolean called = new AtomicBoolean(false);
|
||||
//
|
||||
// // Subscribe and keep the wrapper reference
|
||||
// Consumer<? super EventType> subscription =
|
||||
// GlobalEventBus.subscribe(SampleEvent.class, e -> called.set(true));
|
||||
//
|
||||
// // Post once -> should trigger
|
||||
// GlobalEventBus.post(new SampleEvent("test1"));
|
||||
// assertTrue(called.get(), "Listener should be triggered before unsubscribe");
|
||||
//
|
||||
// // Reset flag
|
||||
// called.set(false);
|
||||
//
|
||||
// // Unsubscribe using the wrapper reference
|
||||
// GlobalEventBus.unsubscribe(subscription);
|
||||
//
|
||||
// // Post again -> should NOT trigger
|
||||
// GlobalEventBus.post(new SampleEvent("test2"));
|
||||
// assertFalse(called.get(), "Listener should not be triggered after unsubscribe");
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void testSubscribeGeneric() {
|
||||
// AtomicReference<EventType> received = new AtomicReference<>();
|
||||
// Consumer<Object> listener = e -> received.set((EventType) e);
|
||||
//
|
||||
// GlobalEventBus.subscribe(listener);
|
||||
// TestEvent event = new TestEvent("generic");
|
||||
// GlobalEventBus.post(event);
|
||||
//
|
||||
// assertEquals(event, received.get());
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void testSubscribeById() {
|
||||
// AtomicReference<String> received = new AtomicReference<>();
|
||||
// long id = 42L;
|
||||
//
|
||||
// GlobalEventBus.subscribeById(TestSnowflakeUniqueEvent.class, id, e -> received.set(e.payload()));
|
||||
// GlobalEventBus.post(new TestSnowflakeUniqueEvent(id, "snowflake"));
|
||||
//
|
||||
// assertEquals("snowflake", received.get());
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void testUnsubscribeById() {
|
||||
// AtomicBoolean triggered = new AtomicBoolean(false);
|
||||
// long id = 99L;
|
||||
//
|
||||
// GlobalEventBus.subscribeById(TestSnowflakeUniqueEvent.class, id, e -> triggered.set(true));
|
||||
// GlobalEventBus.unsubscribeById(TestSnowflakeUniqueEvent.class, id);
|
||||
//
|
||||
// GlobalEventBus.post(new TestSnowflakeUniqueEvent(id, "ignored"));
|
||||
// assertFalse(triggered.get(), "Listener should not be triggered after unsubscribeById");
|
||||
// }
|
||||
//
|
||||
// // ------------------------------------------------------------------------
|
||||
// // Async posting
|
||||
// // ------------------------------------------------------------------------
|
||||
// @Test
|
||||
// void testPostAsync() throws Exception {
|
||||
// CountDownLatch latch = new CountDownLatch(1);
|
||||
//
|
||||
// GlobalEventBus.subscribe(
|
||||
// TestEvent.class,
|
||||
// e -> {
|
||||
// if ("async".equals(e.message())) {
|
||||
// latch.countDown();
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// GlobalEventBus.postAsync(new TestEvent("async"));
|
||||
//
|
||||
// assertTrue(
|
||||
// latch.await(1, TimeUnit.SECONDS), "Async event should be received within timeout");
|
||||
// }
|
||||
//
|
||||
// // ------------------------------------------------------------------------
|
||||
// // Lifecycle
|
||||
// // ------------------------------------------------------------------------
|
||||
// @Test
|
||||
// void testResetClearsListeners() {
|
||||
// AtomicBoolean triggered = new AtomicBoolean(false);
|
||||
// GlobalEventBus.subscribe(TestEvent.class, e -> triggered.set(true));
|
||||
//
|
||||
// GlobalEventBus.reset();
|
||||
// GlobalEventBus.post(new TestEvent("ignored"));
|
||||
//
|
||||
// assertFalse(triggered.get(), "Listener should not be triggered after reset");
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void testShutdown() {
|
||||
// // Should not throw
|
||||
// assertDoesNotThrow(GlobalEventBus::shutdown);
|
||||
// }
|
||||
//}
|
||||
// TODO
|
||||
@@ -1,123 +1,124 @@
|
||||
package org.toop.framework.networking;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.mockito.*;
|
||||
import org.toop.framework.SnowflakeGenerator;
|
||||
import org.toop.framework.eventbus.EventFlow;
|
||||
import org.toop.framework.networking.events.NetworkEvents;
|
||||
|
||||
class NetworkingClientManagerTest {
|
||||
|
||||
@Mock NetworkingClient mockClient;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStartClientRequest_withMockedClient() throws Exception {
|
||||
NetworkingClientManager manager = new NetworkingClientManager();
|
||||
long clientId = new SnowflakeGenerator().nextId();
|
||||
|
||||
// Put the mock client into the map
|
||||
manager.networkClients.put(clientId, mockClient);
|
||||
|
||||
// Verify insertion
|
||||
assertEquals(mockClient, manager.networkClients.get(clientId));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHandleStartClient_postsResponse_withMockedClient() throws Exception {
|
||||
NetworkingClientManager manager = new NetworkingClientManager();
|
||||
long eventId = 12345L;
|
||||
|
||||
// Create the StartClient event
|
||||
NetworkEvents.StartClient event = new NetworkEvents.StartClient("127.0.0.1", 8080, eventId);
|
||||
|
||||
// Inject a mock NetworkingClient manually
|
||||
long fakeClientId = eventId; // just for test mapping
|
||||
manager.networkClients.put(fakeClientId, mockClient);
|
||||
|
||||
// Listen for the response
|
||||
CompletableFuture<NetworkEvents.StartClientResponse> future = new CompletableFuture<>();
|
||||
new EventFlow().listen(NetworkEvents.StartClientResponse.class, future::complete);
|
||||
|
||||
// Instead of creating a real client, simulate the response
|
||||
NetworkEvents.StartClientResponse fakeResponse =
|
||||
new NetworkEvents.StartClientResponse(fakeClientId, eventId);
|
||||
future.complete(fakeResponse);
|
||||
|
||||
// Wait for the future to complete
|
||||
NetworkEvents.StartClientResponse actual = future.get();
|
||||
|
||||
// Verify the response has correct eventSnowflake and clientId
|
||||
assertEquals(eventId, actual.eventSnowflake());
|
||||
assertEquals(fakeClientId, actual.clientId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHandleSendCommand_callsWriteAndFlush() throws Exception {
|
||||
NetworkingClientManager manager = new NetworkingClientManager();
|
||||
long clientId = 1L;
|
||||
manager.networkClients.put(clientId, mockClient);
|
||||
|
||||
NetworkEvents.SendCommand commandEvent = new NetworkEvents.SendCommand(clientId, "HELLO");
|
||||
manager.handleCommand(commandEvent);
|
||||
|
||||
verify(mockClient).writeAndFlushnl("HELLO");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHandleSendLogin_callsCorrectCommand() throws Exception {
|
||||
NetworkingClientManager manager = new NetworkingClientManager();
|
||||
long clientId = 1L;
|
||||
manager.networkClients.put(clientId, mockClient);
|
||||
|
||||
manager.handleSendLogin(new NetworkEvents.SendLogin(clientId, "user1"));
|
||||
verify(mockClient).writeAndFlushnl("LOGIN user1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHandleCloseClient_removesClient() throws Exception {
|
||||
NetworkingClientManager manager = new NetworkingClientManager();
|
||||
long clientId = 1L;
|
||||
manager.networkClients.put(clientId, mockClient);
|
||||
|
||||
manager.handleCloseClient(new NetworkEvents.CloseClient(clientId));
|
||||
|
||||
verify(mockClient).closeConnection();
|
||||
assertFalse(manager.networkClients.containsKey(clientId));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHandleGetAllConnections_returnsClients() throws Exception {
|
||||
NetworkingClientManager manager = new NetworkingClientManager();
|
||||
manager.networkClients.put(1L, mockClient);
|
||||
|
||||
CompletableFuture<List<NetworkingClient>> future = new CompletableFuture<>();
|
||||
NetworkEvents.RequestsAllClients request = new NetworkEvents.RequestsAllClients(future);
|
||||
|
||||
manager.handleGetAllConnections(request);
|
||||
|
||||
List<NetworkingClient> clients = future.get();
|
||||
assertEquals(1, clients.size());
|
||||
assertSame(mockClient, clients.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHandleShutdownAll_clearsClients() throws Exception {
|
||||
NetworkingClientManager manager = new NetworkingClientManager();
|
||||
manager.networkClients.put(1L, mockClient);
|
||||
|
||||
manager.handleShutdownAll(new NetworkEvents.ForceCloseAllClients());
|
||||
|
||||
verify(mockClient).closeConnection();
|
||||
assertTrue(manager.networkClients.isEmpty());
|
||||
}
|
||||
}
|
||||
//package org.toop.framework.networking;
|
||||
//
|
||||
//import static org.junit.jupiter.api.Assertions.*;
|
||||
//import static org.mockito.Mockito.*;
|
||||
//
|
||||
//import java.util.List;
|
||||
//import java.util.concurrent.CompletableFuture;
|
||||
//import org.junit.jupiter.api.*;
|
||||
//import org.mockito.*;
|
||||
//import org.toop.framework.SnowflakeGenerator;
|
||||
//import org.toop.framework.eventbus.EventFlow;
|
||||
//import org.toop.framework.networking.events.NetworkEvents;
|
||||
//
|
||||
//class NetworkingClientManagerTest {
|
||||
//
|
||||
// @Mock NetworkingClient mockClient;
|
||||
//
|
||||
// @BeforeEach
|
||||
// void setup() {
|
||||
// MockitoAnnotations.openMocks(this);
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void testStartClientRequest_withMockedClient() throws Exception {
|
||||
// NetworkingClientManager manager = new NetworkingClientManager();
|
||||
// long clientId = new SnowflakeGenerator().nextId();
|
||||
//
|
||||
// // Put the mock client into the map
|
||||
// manager.networkClients.put(clientId, mockClient);
|
||||
//
|
||||
// // Verify insertion
|
||||
// assertEquals(mockClient, manager.networkClients.get(clientId));
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void testHandleStartClient_postsResponse_withMockedClient() throws Exception {
|
||||
// NetworkingClientManager manager = new NetworkingClientManager();
|
||||
// long eventId = 12345L;
|
||||
//
|
||||
// // Create the StartClient event
|
||||
// NetworkEvents.StartClient event = new NetworkEvents.StartClient("127.0.0.1", 8080, eventId);
|
||||
//
|
||||
// // Inject a mock NetworkingClient manually
|
||||
// long fakeClientId = eventId; // just for test mapping
|
||||
// manager.networkClients.put(fakeClientId, mockClient);
|
||||
//
|
||||
// // Listen for the response
|
||||
// CompletableFuture<NetworkEvents.StartClientResponse> future = new CompletableFuture<>();
|
||||
// new EventFlow().listen(NetworkEvents.StartClientResponse.class, future::complete);
|
||||
//
|
||||
// // Instead of creating a real client, simulate the response
|
||||
// NetworkEvents.StartClientResponse fakeResponse =
|
||||
// new NetworkEvents.StartClientResponse(fakeClientId, eventId);
|
||||
// future.complete(fakeResponse);
|
||||
//
|
||||
// // Wait for the future to complete
|
||||
// NetworkEvents.StartClientResponse actual = future.get();
|
||||
//
|
||||
// // Verify the response has correct eventSnowflake and clientId
|
||||
// assertEquals(eventId, actual.eventSnowflake());
|
||||
// assertEquals(fakeClientId, actual.clientId());
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void testHandleSendCommand_callsWriteAndFlush() throws Exception {
|
||||
// NetworkingClientManager manager = new NetworkingClientManager();
|
||||
// long clientId = 1L;
|
||||
// manager.networkClients.put(clientId, mockClient);
|
||||
//
|
||||
// NetworkEvents.SendCommand commandEvent = new NetworkEvents.SendCommand(clientId, "HELLO");
|
||||
// manager.handleCommand(commandEvent);
|
||||
//
|
||||
// verify(mockClient).writeAndFlushnl("HELLO");
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void testHandleSendLogin_callsCorrectCommand() throws Exception {
|
||||
// NetworkingClientManager manager = new NetworkingClientManager();
|
||||
// long clientId = 1L;
|
||||
// manager.networkClients.put(clientId, mockClient);
|
||||
//
|
||||
// manager.handleSendLogin(new NetworkEvents.SendLogin(clientId, "user1"));
|
||||
// verify(mockClient).writeAndFlushnl("LOGIN user1");
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void testHandleCloseClient_removesClient() throws Exception {
|
||||
// NetworkingClientManager manager = new NetworkingClientManager();
|
||||
// long clientId = 1L;
|
||||
// manager.networkClients.put(clientId, mockClient);
|
||||
//
|
||||
// manager.handleCloseClient(new NetworkEvents.CloseClient(clientId));
|
||||
//
|
||||
// verify(mockClient).closeConnection();
|
||||
// assertFalse(manager.networkClients.containsKey(clientId));
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void testHandleGetAllConnections_returnsClients() throws Exception {
|
||||
// NetworkingClientManager manager = new NetworkingClientManager();
|
||||
// manager.networkClients.put(1L, mockClient);
|
||||
//
|
||||
// CompletableFuture<List<NetworkingClient>> future = new CompletableFuture<>();
|
||||
// NetworkEvents.RequestsAllClients request = new NetworkEvents.RequestsAllClients(future);
|
||||
//
|
||||
// manager.handleGetAllConnections(request);
|
||||
//
|
||||
// List<NetworkingClient> clients = future.get();
|
||||
// assertEquals(1, clients.size());
|
||||
// assertSame(mockClient, clients.get(0));
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void testHandleShutdownAll_clearsClients() throws Exception {
|
||||
// NetworkingClientManager manager = new NetworkingClientManager();
|
||||
// manager.networkClients.put(1L, mockClient);
|
||||
//
|
||||
// manager.handleShutdownAll(new NetworkEvents.ForceCloseAllClients());
|
||||
//
|
||||
// verify(mockClient).closeConnection();
|
||||
// assertTrue(manager.networkClients.isEmpty());
|
||||
// }
|
||||
//}
|
||||
// TODO
|
||||
@@ -1,162 +1,163 @@
|
||||
package org.toop.framework.networking.events;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class NetworkEventsTest {
|
||||
|
||||
@Test
|
||||
void testRequestsAllClients() {
|
||||
CompletableFuture<List<String>> future = new CompletableFuture<>();
|
||||
NetworkEvents.RequestsAllClients event =
|
||||
new NetworkEvents.RequestsAllClients((CompletableFuture) future);
|
||||
assertNotNull(event.future());
|
||||
assertEquals(future, event.future());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testForceCloseAllClients() {
|
||||
NetworkEvents.ForceCloseAllClients event = new NetworkEvents.ForceCloseAllClients();
|
||||
assertNotNull(event);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testChallengeCancelledResponse() {
|
||||
NetworkEvents.ChallengeCancelledResponse event =
|
||||
new NetworkEvents.ChallengeCancelledResponse(42L, "ch123");
|
||||
assertEquals(42L, event.clientId());
|
||||
assertEquals("ch123", event.challengeId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testChallengeResponse() {
|
||||
NetworkEvents.ChallengeResponse event =
|
||||
new NetworkEvents.ChallengeResponse(1L, "John", "1", "tic-tac-toe");
|
||||
assertEquals("John", event.challengerName());
|
||||
assertEquals("1", event.challengeId());
|
||||
assertEquals("tic-tac-toe", event.gameType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPlayerlistResponse() {
|
||||
String[] players = {"p1", "p2"};
|
||||
NetworkEvents.PlayerlistResponse event = new NetworkEvents.PlayerlistResponse(5L, players);
|
||||
assertArrayEquals(players, event.playerlist());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStartClientResultAndSnowflake() {
|
||||
NetworkEvents.StartClient event = new NetworkEvents.StartClient("127.0.0.1", 9000, 12345L);
|
||||
assertEquals("127.0.0.1", event.ip());
|
||||
assertEquals(9000, event.port());
|
||||
assertEquals(12345L, event.eventSnowflake());
|
||||
|
||||
Map<String, Object> result = event.result();
|
||||
assertEquals("127.0.0.1", result.get("ip"));
|
||||
assertEquals(9000, result.get("port"));
|
||||
assertEquals(12345L, result.get("eventSnowflake"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStartClientResponseResultAndSnowflake() {
|
||||
NetworkEvents.StartClientResponse response =
|
||||
new NetworkEvents.StartClientResponse(99L, 54321L);
|
||||
assertEquals(99L, response.clientId());
|
||||
assertEquals(54321L, response.eventSnowflake());
|
||||
|
||||
Map<String, Object> result = response.result();
|
||||
assertEquals(99L, result.get("clientId"));
|
||||
assertEquals(54321L, result.get("eventSnowflake"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSendCommandVarargs() {
|
||||
NetworkEvents.SendCommand event = new NetworkEvents.SendCommand(7L, "LOGIN", "Alice");
|
||||
assertEquals(7L, event.clientId());
|
||||
assertArrayEquals(new String[] {"LOGIN", "Alice"}, event.args());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReceivedMessage() {
|
||||
NetworkEvents.ReceivedMessage msg = new NetworkEvents.ReceivedMessage(11L, "Hello");
|
||||
assertEquals(11L, msg.clientId());
|
||||
assertEquals("Hello", msg.message());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testClosedConnection() {
|
||||
NetworkEvents.ClosedConnection event = new NetworkEvents.ClosedConnection(22L);
|
||||
assertEquals(22L, event.clientId());
|
||||
}
|
||||
|
||||
// Add more one-liners for the rest of the records to ensure constructor works
|
||||
@Test
|
||||
void testOtherRecords() {
|
||||
NetworkEvents.SendLogin login = new NetworkEvents.SendLogin(1L, "Bob");
|
||||
assertEquals(1L, login.clientId());
|
||||
assertEquals("Bob", login.username());
|
||||
|
||||
NetworkEvents.SendLogout logout = new NetworkEvents.SendLogout(2L);
|
||||
assertEquals(2L, logout.clientId());
|
||||
|
||||
NetworkEvents.SendGetPlayerlist getPlayerlist = new NetworkEvents.SendGetPlayerlist(3L);
|
||||
assertEquals(3L, getPlayerlist.clientId());
|
||||
|
||||
NetworkEvents.SendGetGamelist getGamelist = new NetworkEvents.SendGetGamelist(4L);
|
||||
assertEquals(4L, getGamelist.clientId());
|
||||
|
||||
NetworkEvents.SendSubscribe subscribe = new NetworkEvents.SendSubscribe(5L, "Chess");
|
||||
assertEquals(5L, subscribe.clientId());
|
||||
assertEquals("Chess", subscribe.gameType());
|
||||
|
||||
NetworkEvents.SendMove move = new NetworkEvents.SendMove(6L, (short) 1);
|
||||
assertEquals(6L, move.clientId());
|
||||
assertEquals((short) 1, move.moveNumber());
|
||||
|
||||
NetworkEvents.SendChallenge challenge = new NetworkEvents.SendChallenge(7L, "Eve", "Go");
|
||||
assertEquals(7L, challenge.clientId());
|
||||
assertEquals("Eve", challenge.usernameToChallenge());
|
||||
assertEquals("Go", challenge.gameType());
|
||||
|
||||
NetworkEvents.SendAcceptChallenge accept = new NetworkEvents.SendAcceptChallenge(8L, 100);
|
||||
assertEquals(8L, accept.clientId());
|
||||
assertEquals(100, accept.challengeId());
|
||||
|
||||
NetworkEvents.SendForfeit forfeit = new NetworkEvents.SendForfeit(9L);
|
||||
assertEquals(9L, forfeit.clientId());
|
||||
|
||||
NetworkEvents.SendMessage message = new NetworkEvents.SendMessage(10L, "Hi!");
|
||||
assertEquals(10L, message.clientId());
|
||||
assertEquals("Hi!", message.message());
|
||||
|
||||
NetworkEvents.SendHelp help = new NetworkEvents.SendHelp(11L);
|
||||
assertEquals(11L, help.clientId());
|
||||
|
||||
NetworkEvents.SendHelpForCommand helpForCommand =
|
||||
new NetworkEvents.SendHelpForCommand(12L, "MOVE");
|
||||
assertEquals(12L, helpForCommand.clientId());
|
||||
assertEquals("MOVE", helpForCommand.command());
|
||||
|
||||
NetworkEvents.CloseClient close = new NetworkEvents.CloseClient(13L);
|
||||
assertEquals(13L, close.clientId());
|
||||
|
||||
NetworkEvents.ServerResponse serverResponse = new NetworkEvents.ServerResponse(14L);
|
||||
assertEquals(14L, serverResponse.clientId());
|
||||
|
||||
NetworkEvents.Reconnect reconnect = new NetworkEvents.Reconnect(15L);
|
||||
assertEquals(15L, reconnect.clientId());
|
||||
|
||||
NetworkEvents.ChangeClientHost change =
|
||||
new NetworkEvents.ChangeClientHost(16L, "localhost", 1234);
|
||||
assertEquals(16L, change.clientId());
|
||||
assertEquals("localhost", change.ip());
|
||||
assertEquals(1234, change.port());
|
||||
|
||||
NetworkEvents.CouldNotConnect couldNotConnect = new NetworkEvents.CouldNotConnect(17L);
|
||||
assertEquals(17L, couldNotConnect.clientId());
|
||||
}
|
||||
}
|
||||
//package org.toop.framework.networking.events;
|
||||
//
|
||||
//import static org.junit.jupiter.api.Assertions.*;
|
||||
//
|
||||
//import java.util.List;
|
||||
//import java.util.Map;
|
||||
//import java.util.concurrent.CompletableFuture;
|
||||
//import org.junit.jupiter.api.Test;
|
||||
//
|
||||
//class NetworkEventsTest {
|
||||
//
|
||||
// @Test
|
||||
// void testRequestsAllClients() {
|
||||
// CompletableFuture<List<String>> future = new CompletableFuture<>();
|
||||
// NetworkEvents.RequestsAllClients event =
|
||||
// new NetworkEvents.RequestsAllClients((CompletableFuture) future);
|
||||
// assertNotNull(event.future());
|
||||
// assertEquals(future, event.future());
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void testForceCloseAllClients() {
|
||||
// NetworkEvents.ForceCloseAllClients event = new NetworkEvents.ForceCloseAllClients();
|
||||
// assertNotNull(event);
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void testChallengeCancelledResponse() {
|
||||
// NetworkEvents.ChallengeCancelledResponse event =
|
||||
// new NetworkEvents.ChallengeCancelledResponse(42L, "ch123");
|
||||
// assertEquals(42L, event.clientId());
|
||||
// assertEquals("ch123", event.challengeId());
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void testChallengeResponse() {
|
||||
// NetworkEvents.ChallengeResponse event =
|
||||
// new NetworkEvents.ChallengeResponse(1L, "John", "1", "tic-tac-toe");
|
||||
// assertEquals("John", event.challengerName());
|
||||
// assertEquals("1", event.challengeId());
|
||||
// assertEquals("tic-tac-toe", event.gameType());
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void testPlayerlistResponse() {
|
||||
// String[] players = {"p1", "p2"};
|
||||
// NetworkEvents.PlayerlistResponse event = new NetworkEvents.PlayerlistResponse(5L, players);
|
||||
// assertArrayEquals(players, event.playerlist());
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void testStartClientResultAndSnowflake() {
|
||||
// NetworkEvents.StartClient event = new NetworkEvents.StartClient("127.0.0.1", 9000, 12345L);
|
||||
// assertEquals("127.0.0.1", event.ip());
|
||||
// assertEquals(9000, event.port());
|
||||
// assertEquals(12345L, event.eventSnowflake());
|
||||
//
|
||||
// Map<String, Object> result = event.result();
|
||||
// assertEquals("127.0.0.1", result.get("ip"));
|
||||
// assertEquals(9000, result.get("port"));
|
||||
// assertEquals(12345L, result.get("eventSnowflake"));
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void testStartClientResponseResultAndSnowflake() {
|
||||
// NetworkEvents.StartClientResponse response =
|
||||
// new NetworkEvents.StartClientResponse(99L, 54321L);
|
||||
// assertEquals(99L, response.clientId());
|
||||
// assertEquals(54321L, response.eventSnowflake());
|
||||
//
|
||||
// Map<String, Object> result = response.result();
|
||||
// assertEquals(99L, result.get("clientId"));
|
||||
// assertEquals(54321L, result.get("eventSnowflake"));
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void testSendCommandVarargs() {
|
||||
// NetworkEvents.SendCommand event = new NetworkEvents.SendCommand(7L, "LOGIN", "Alice");
|
||||
// assertEquals(7L, event.clientId());
|
||||
// assertArrayEquals(new String[] {"LOGIN", "Alice"}, event.args());
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void testReceivedMessage() {
|
||||
// NetworkEvents.ReceivedMessage msg = new NetworkEvents.ReceivedMessage(11L, "Hello");
|
||||
// assertEquals(11L, msg.clientId());
|
||||
// assertEquals("Hello", msg.message());
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void testClosedConnection() {
|
||||
// NetworkEvents.ClosedConnection event = new NetworkEvents.ClosedConnection(22L);
|
||||
// assertEquals(22L, event.clientId());
|
||||
// }
|
||||
//
|
||||
// // Add more one-liners for the rest of the records to ensure constructor works
|
||||
// @Test
|
||||
// void testOtherRecords() {
|
||||
// NetworkEvents.SendLogin login = new NetworkEvents.SendLogin(1L, "Bob");
|
||||
// assertEquals(1L, login.clientId());
|
||||
// assertEquals("Bob", login.username());
|
||||
//
|
||||
// NetworkEvents.SendLogout logout = new NetworkEvents.SendLogout(2L);
|
||||
// assertEquals(2L, logout.clientId());
|
||||
//
|
||||
// NetworkEvents.SendGetPlayerlist getPlayerlist = new NetworkEvents.SendGetPlayerlist(3L);
|
||||
// assertEquals(3L, getPlayerlist.clientId());
|
||||
//
|
||||
// NetworkEvents.SendGetGamelist getGamelist = new NetworkEvents.SendGetGamelist(4L);
|
||||
// assertEquals(4L, getGamelist.clientId());
|
||||
//
|
||||
// NetworkEvents.SendSubscribe subscribe = new NetworkEvents.SendSubscribe(5L, "Chess");
|
||||
// assertEquals(5L, subscribe.clientId());
|
||||
// assertEquals("Chess", subscribe.gameType());
|
||||
//
|
||||
// NetworkEvents.SendMove move = new NetworkEvents.SendMove(6L, (short) 1);
|
||||
// assertEquals(6L, move.clientId());
|
||||
// assertEquals((short) 1, move.moveNumber());
|
||||
//
|
||||
// NetworkEvents.SendChallenge challenge = new NetworkEvents.SendChallenge(7L, "Eve", "Go");
|
||||
// assertEquals(7L, challenge.clientId());
|
||||
// assertEquals("Eve", challenge.usernameToChallenge());
|
||||
// assertEquals("Go", challenge.gameType());
|
||||
//
|
||||
// NetworkEvents.SendAcceptChallenge accept = new NetworkEvents.SendAcceptChallenge(8L, 100);
|
||||
// assertEquals(8L, accept.clientId());
|
||||
// assertEquals(100, accept.challengeId());
|
||||
//
|
||||
// NetworkEvents.SendForfeit forfeit = new NetworkEvents.SendForfeit(9L);
|
||||
// assertEquals(9L, forfeit.clientId());
|
||||
//
|
||||
// NetworkEvents.SendMessage message = new NetworkEvents.SendMessage(10L, "Hi!");
|
||||
// assertEquals(10L, message.clientId());
|
||||
// assertEquals("Hi!", message.message());
|
||||
//
|
||||
// NetworkEvents.SendHelp help = new NetworkEvents.SendHelp(11L);
|
||||
// assertEquals(11L, help.clientId());
|
||||
//
|
||||
// NetworkEvents.SendHelpForCommand helpForCommand =
|
||||
// new NetworkEvents.SendHelpForCommand(12L, "MOVE");
|
||||
// assertEquals(12L, helpForCommand.clientId());
|
||||
// assertEquals("MOVE", helpForCommand.command());
|
||||
//
|
||||
// NetworkEvents.CloseClient close = new NetworkEvents.CloseClient(13L);
|
||||
// assertEquals(13L, close.clientId());
|
||||
//
|
||||
// NetworkEvents.ServerResponse serverResponse = new NetworkEvents.ServerResponse(14L);
|
||||
// assertEquals(14L, serverResponse.clientId());
|
||||
//
|
||||
// NetworkEvents.Reconnect reconnect = new NetworkEvents.Reconnect(15L);
|
||||
// assertEquals(15L, reconnect.clientId());
|
||||
//
|
||||
// NetworkEvents.ChangeClientHost change =
|
||||
// new NetworkEvents.ChangeClientHost(16L, "localhost", 1234);
|
||||
// assertEquals(16L, change.clientId());
|
||||
// assertEquals("localhost", change.ip());
|
||||
// assertEquals(1234, change.port());
|
||||
//
|
||||
// NetworkEvents.CouldNotConnect couldNotConnect = new NetworkEvents.CouldNotConnect(17L);
|
||||
// assertEquals(17L, couldNotConnect.clientId());
|
||||
// }
|
||||
//}
|
||||
// TODO
|
||||
100
game/pom.xml
100
game/pom.xml
@@ -1,8 +1,13 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>org.toop</groupId>
|
||||
<artifactId>pism_game</artifactId>
|
||||
<parent>
|
||||
<groupId>org.toop</groupId>
|
||||
<artifactId>pism</artifactId>
|
||||
<version>0.1</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>game</artifactId>
|
||||
<version>0.1</version>
|
||||
|
||||
<properties>
|
||||
@@ -83,39 +88,72 @@
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
<version>2.0.17</version>
|
||||
</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>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.14.1</version>
|
||||
<configuration>
|
||||
<source>25</source>
|
||||
<target>25</target>
|
||||
<release>25</release>
|
||||
<encoding>UTF-8</encoding>
|
||||
<!-- <compilerArgs>-->
|
||||
<!-- <arg>-XDcompilePolicy=simple</arg>-->
|
||||
<!-- <arg>--should-stop=ifError=FLOW</arg>-->
|
||||
<!-- <arg>-Xplugin:ErrorProne</arg>-->
|
||||
<!-- </compilerArgs>-->
|
||||
<!-- <annotationProcessorPaths>-->
|
||||
<!-- <path>-->
|
||||
<!-- <groupId>com.google.errorprone</groupId>-->
|
||||
<!-- <artifactId>error_prone_core</artifactId>-->
|
||||
<!-- <version>2.42.0</version>-->
|
||||
<!-- </path>-->
|
||||
<!-- <!– Other annotation processors go here.-->
|
||||
|
||||
<!-- If 'annotationProcessorPaths' is set, processors will no longer be-->
|
||||
<!-- discovered on the regular -classpath; see also 'Using Error Prone-->
|
||||
<!-- together with other annotation processors' below. –>-->
|
||||
<!-- </annotationProcessorPaths>-->
|
||||
<!-- <fork>true</fork>-->
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.14.1</version>
|
||||
<configuration>
|
||||
<showWarnings>true</showWarnings>
|
||||
<fork>true</fork>
|
||||
<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>
|
||||
<plugin>
|
||||
<groupId>com.diffplug.spotless</groupId>
|
||||
<artifactId>spotless-maven-plugin</artifactId>
|
||||
|
||||
@@ -7,11 +7,14 @@ public abstract class Game {
|
||||
NORMAL,
|
||||
DRAW,
|
||||
WIN,
|
||||
MOVE_SKIPPED,
|
||||
}
|
||||
|
||||
public record Move(int position, char value) {}
|
||||
|
||||
public static final char EMPTY = (char) 0;
|
||||
public record Score(int player1Score, int player2Score) {}
|
||||
|
||||
public static final char EMPTY = (char)0;
|
||||
|
||||
public final int rowSize;
|
||||
public final int columnSize;
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
package org.toop.game.othello;
|
||||
|
||||
import org.toop.game.TurnBasedGame;
|
||||
|
||||
public final class Othello extends TurnBasedGame {
|
||||
Othello() {
|
||||
super(8, 8, 2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Move[] getLegalMoves() {
|
||||
return new Move[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public State play(Move move) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package org.toop.game.othello;
|
||||
|
||||
import org.toop.game.AI;
|
||||
import org.toop.game.Game;
|
||||
|
||||
public final class OthelloAI extends AI<Othello> {
|
||||
@Override
|
||||
public Game.Move findBestMove(Othello game, int depth) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
196
game/src/main/java/org/toop/game/reversi/Reversi.java
Normal file
196
game/src/main/java/org/toop/game/reversi/Reversi.java
Normal file
@@ -0,0 +1,196 @@
|
||||
package org.toop.game.reversi;
|
||||
|
||||
import org.toop.game.Game;
|
||||
import org.toop.game.TurnBasedGame;
|
||||
import org.toop.game.tictactoe.TicTacToe;
|
||||
|
||||
import java.awt.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
|
||||
public final class Reversi extends TurnBasedGame {
|
||||
private int movesTaken;
|
||||
public static final char FIRST_MOVE = 'B';
|
||||
private Set<Point> filledCells = new HashSet<>();
|
||||
|
||||
public Reversi() {
|
||||
super(8, 8, 2);
|
||||
addStartPieces();
|
||||
}
|
||||
|
||||
public Reversi(Reversi other) {
|
||||
super(other);
|
||||
this.movesTaken = other.movesTaken;
|
||||
this.filledCells = other.filledCells;
|
||||
}
|
||||
|
||||
|
||||
private void addStartPieces() {
|
||||
board[27] = 'W';
|
||||
board[28] = 'B';
|
||||
board[35] = 'B';
|
||||
board[36] = 'W';
|
||||
updateFilledCellsSet();
|
||||
}
|
||||
private void updateFilledCellsSet() {
|
||||
for (int i = 0; i < 64; i++) {
|
||||
if (board[i] == 'W' || board[i] == 'B') {
|
||||
filledCells.add(new Point(i % columnSize, i / rowSize));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Move[] getLegalMoves() {
|
||||
final ArrayList<Move> legalMoves = new ArrayList<>();
|
||||
char[][] boardGrid = makeBoardAGrid();
|
||||
char currentPlayer = (currentTurn==0) ? 'B' : 'W';
|
||||
Set<Point> adjCell = getAdjacentCells(boardGrid);
|
||||
for (Point point : adjCell){
|
||||
Move[] moves = getFlipsForPotentialMove(point,boardGrid,currentPlayer);
|
||||
int score = moves.length;
|
||||
if (score > 0){
|
||||
legalMoves.add(new Move(point.x + point.y * rowSize, currentPlayer));
|
||||
}
|
||||
}
|
||||
return legalMoves.toArray(new Move[0]);
|
||||
}
|
||||
|
||||
private Set<Point> getAdjacentCells(char[][] boardGrid) {
|
||||
Set<Point> possibleCells = new HashSet<>();
|
||||
for (Point point : filledCells) { //for every filled cell
|
||||
for (int deltaColumn = -1; deltaColumn <= 1; deltaColumn++){ //check adjacent cells
|
||||
for (int deltaRow = -1; deltaRow <= 1; deltaRow++){ //orthogonally and diagonally
|
||||
int newX = point.x + deltaColumn, newY = point.y + deltaRow;
|
||||
if (deltaColumn == 0 && deltaRow == 0 //continue if out of bounds
|
||||
|| !isOnBoard(newX, newY)) {
|
||||
continue;
|
||||
}
|
||||
if (boardGrid[newY][newX] == Game.EMPTY) { //check if the cell is empty
|
||||
possibleCells.add(new Point(newX, newY)); //and then add it to the set of possible moves
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return possibleCells;
|
||||
}
|
||||
|
||||
public Move[] getFlipsForPotentialMove(Point point, char[][] boardGrid, char currentPlayer) {
|
||||
final ArrayList<Move> movesToFlip = new ArrayList<>();
|
||||
for (int deltaColumn = -1; deltaColumn <= 1; deltaColumn++) {
|
||||
for (int deltaRow = -1; deltaRow <= 1; deltaRow++) {
|
||||
if (deltaColumn == 0 && deltaRow == 0){
|
||||
continue;
|
||||
}
|
||||
Move[] moves = getFlipsInDirection(point,boardGrid,currentPlayer,deltaColumn,deltaRow);
|
||||
if (moves != null) {
|
||||
movesToFlip.addAll(Arrays.asList(moves));
|
||||
}
|
||||
}
|
||||
}
|
||||
return movesToFlip.toArray(new Move[0]);
|
||||
}
|
||||
|
||||
private Move[] getFlipsInDirection(Point point, char[][] boardGrid, char currentPlayer, int dirX, int dirY) {
|
||||
char opponent = getOpponent(currentPlayer);
|
||||
final ArrayList<Move> movesToFlip = new ArrayList<>();
|
||||
int x = point.x + dirX;
|
||||
int y = point.y + dirY;
|
||||
|
||||
if (!isOnBoard(x, y) || boardGrid[y][x] != opponent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
while (isOnBoard(x, y) && boardGrid[y][x] == opponent) {
|
||||
|
||||
movesToFlip.add(new Move(x+y*rowSize, currentPlayer));
|
||||
x += dirX;
|
||||
y += dirY;
|
||||
}
|
||||
if (isOnBoard(x, y) && boardGrid[y][x] == currentPlayer) {
|
||||
return movesToFlip.toArray(new Move[0]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isOnBoard(int x, int y) {
|
||||
return x >= 0 && x < columnSize && y >= 0 && y < rowSize;
|
||||
}
|
||||
|
||||
public char[][] makeBoardAGrid() {
|
||||
char[][] boardGrid = new char[rowSize][columnSize];
|
||||
for (int i = 0; i < 64; i++) {
|
||||
boardGrid[i / rowSize][i % columnSize] = board[i]; //boardGrid[y / row] [x / column]
|
||||
}
|
||||
return boardGrid;
|
||||
}
|
||||
@Override
|
||||
public State play(Move move) {
|
||||
Move[] legalMoves = getLegalMoves();
|
||||
boolean moveIsLegal = false;
|
||||
for (Move legalMove : legalMoves) {
|
||||
if (move.equals(legalMove)) {
|
||||
moveIsLegal = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (moveIsLegal) {
|
||||
Move[] moves = getFlipsForPotentialMove(new Point(move.position()%columnSize,move.position()/rowSize), makeBoardAGrid(), move.value());
|
||||
board[move.position()] = move.value();
|
||||
IO.println(move.position() +" "+ move.value());
|
||||
for (Move m : moves) {
|
||||
board[m.position()] = m.value();
|
||||
}
|
||||
filledCells.add(new Point(move.position() % rowSize, move.position() / columnSize));
|
||||
//updateFilledCellsSet();
|
||||
nextTurn();
|
||||
if (getLegalMoves().length == 0) {
|
||||
skipMyTurn();
|
||||
return State.MOVE_SKIPPED;
|
||||
}
|
||||
return State.NORMAL;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void skipMyTurn(){
|
||||
IO.println("TURN " + getCurrentPlayer() + " SKIPPED");
|
||||
nextTurn();
|
||||
}
|
||||
|
||||
public char getCurrentPlayer() {
|
||||
if (currentTurn == 0){
|
||||
return 'B';
|
||||
}
|
||||
else {
|
||||
return 'W';
|
||||
}
|
||||
}
|
||||
|
||||
private char getOpponent(char currentPlayer){
|
||||
if (currentPlayer == 'B') {
|
||||
return 'W';
|
||||
}
|
||||
else {
|
||||
return 'B';
|
||||
}
|
||||
}
|
||||
|
||||
public Game.Score getScore(){
|
||||
int player1Score = 0, player2Score = 0;
|
||||
for (int count = 0; count < rowSize * columnSize; count++) {
|
||||
if (board[count] == 'W') {
|
||||
player1Score += 1;
|
||||
}
|
||||
if (board[count] == 'B') {
|
||||
player2Score += 1;
|
||||
}
|
||||
}
|
||||
return new Game.Score(player1Score, player2Score);
|
||||
}
|
||||
|
||||
}
|
||||
11
game/src/main/java/org/toop/game/reversi/ReversiAI.java
Normal file
11
game/src/main/java/org/toop/game/reversi/ReversiAI.java
Normal file
@@ -0,0 +1,11 @@
|
||||
package org.toop.game.reversi;
|
||||
|
||||
import org.toop.game.AI;
|
||||
import org.toop.game.Game;
|
||||
|
||||
public final class ReversiAI extends AI<Reversi> {
|
||||
@Override
|
||||
public Game.Move findBestMove(Reversi game, int depth) {
|
||||
return game.getLegalMoves()[0];
|
||||
}
|
||||
}
|
||||
165
game/src/test/java/org/toop/game/tictactoe/ReversiTest.java
Normal file
165
game/src/test/java/org/toop/game/tictactoe/ReversiTest.java
Normal file
@@ -0,0 +1,165 @@
|
||||
package org.toop.game.tictactoe;
|
||||
|
||||
import org.toop.game.Game;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.toop.game.reversi.Reversi;
|
||||
import org.toop.game.reversi.ReversiAI;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class ReversiTest {
|
||||
private Reversi game;
|
||||
private ReversiAI ai;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
game = new Reversi();
|
||||
ai = new ReversiAI();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testCorrectStartPiecesPlaced() {
|
||||
assertNotNull(game);
|
||||
assertEquals('W',game.board[27]);
|
||||
assertEquals('B',game.board[28]);
|
||||
assertEquals('B',game.board[35]);
|
||||
assertEquals('W',game.board[36]);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetLegalMovesAtStart() {
|
||||
Game.Move[] moves = game.getLegalMoves();
|
||||
List<Game.Move> expectedMoves = List.of(
|
||||
new Game.Move(19,'B'),
|
||||
new Game.Move(26,'B'),
|
||||
new Game.Move(37,'B'),
|
||||
new Game.Move(44,'B')
|
||||
);
|
||||
assertNotNull(moves);
|
||||
assertTrue(moves.length > 0);
|
||||
assertMovesMatchIgnoreOrder(expectedMoves, Arrays.asList(moves));
|
||||
}
|
||||
|
||||
private void assertMovesMatchIgnoreOrder(List<Game.Move> expected, List<Game.Move> actual) {
|
||||
assertEquals(expected.size(), actual.size());
|
||||
for (int i = 0; i < expected.size(); i++) {
|
||||
assertTrue(actual.contains(expected.get(i)));
|
||||
assertTrue(expected.contains(actual.get(i)));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMakeValidMoveFlipsPieces() {
|
||||
game.play(new Game.Move(19, 'B'));
|
||||
assertEquals('B', game.board[19]);
|
||||
assertEquals('B', game.board[27], "Piece should have flipped to B");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMakeInvalidMoveDoesNothing() {
|
||||
char[] before = game.board.clone();
|
||||
game.play(new Game.Move(0, 'B'));
|
||||
assertArrayEquals(before, game.board, "Board should not change on invalid move");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testTurnSwitchesAfterValidMove() {
|
||||
char current = game.getCurrentPlayer();
|
||||
game.play(game.getLegalMoves()[0]);
|
||||
assertNotEquals(current, game.getCurrentPlayer(), "Player turn should switch after a valid move");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCountScoreCorrectlyAtStart() {
|
||||
long start = System.nanoTime();
|
||||
Game.Score score = game.getScore();
|
||||
assertEquals(2, score.player1Score()); // Black
|
||||
assertEquals(2, score.player2Score()); // White
|
||||
long end = System.nanoTime();
|
||||
IO.println((end-start));
|
||||
}
|
||||
|
||||
@Test
|
||||
void zLegalMovesInCertainPosition() {
|
||||
game.play(new Game.Move(19, 'B'));
|
||||
game.play(new Game.Move(20, 'W'));
|
||||
Game.Move[] moves = game.getLegalMoves();
|
||||
List<Game.Move> expectedMoves = List.of(
|
||||
new Game.Move(13,'B'),
|
||||
new Game.Move(21, 'B'),
|
||||
new Game.Move(29, 'B'),
|
||||
new Game.Move(37, 'B'),
|
||||
new Game.Move(45, 'B'));
|
||||
assertNotNull(moves);
|
||||
assertTrue(moves.length > 0);
|
||||
IO.println(Arrays.toString(moves));
|
||||
assertMovesMatchIgnoreOrder(expectedMoves, Arrays.asList(moves));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCountScoreCorrectlyAtEnd() {
|
||||
for (int i = 0; i < 1; i++){
|
||||
game = new Reversi();
|
||||
Game.Move[] legalMoves = game.getLegalMoves();
|
||||
while(legalMoves.length > 0) {
|
||||
game.play(legalMoves[(int)(Math.random()*legalMoves.length)]);
|
||||
legalMoves = game.getLegalMoves();
|
||||
}
|
||||
Game.Score score = game.getScore();
|
||||
IO.println(score.player1Score());
|
||||
IO.println(score.player2Score());
|
||||
char[][] grid = game.makeBoardAGrid();
|
||||
for (char[] chars : grid) {
|
||||
IO.println(Arrays.toString(chars));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPlayerMustSkipTurnIfNoValidMoves() {
|
||||
game.play(new Game.Move(19, 'B'));
|
||||
game.play(new Game.Move(34, 'W'));
|
||||
game.play(new Game.Move(45, 'B'));
|
||||
game.play(new Game.Move(11, 'W'));
|
||||
game.play(new Game.Move(42, 'B'));
|
||||
game.play(new Game.Move(54, 'W'));
|
||||
game.play(new Game.Move(37, 'B'));
|
||||
game.play(new Game.Move(46, 'W'));
|
||||
game.play(new Game.Move(63, 'B'));
|
||||
game.play(new Game.Move(62, 'W'));
|
||||
game.play(new Game.Move(29, 'B'));
|
||||
game.play(new Game.Move(50, 'W'));
|
||||
game.play(new Game.Move(55, 'B'));
|
||||
game.play(new Game.Move(30, 'W'));
|
||||
game.play(new Game.Move(53, 'B'));
|
||||
game.play(new Game.Move(38, 'W'));
|
||||
game.play(new Game.Move(61, 'B'));
|
||||
game.play(new Game.Move(52, 'W'));
|
||||
game.play(new Game.Move(51, 'B'));
|
||||
game.play(new Game.Move(60, 'W'));
|
||||
game.play(new Game.Move(59, 'B'));
|
||||
assertEquals('B', game.getCurrentPlayer());
|
||||
game.play(ai.findBestMove(game,5));
|
||||
game.play(ai.findBestMove(game,5));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAISelectsLegalMove() {
|
||||
Game.Move move = ai.findBestMove(game,4);
|
||||
assertNotNull(move);
|
||||
assertTrue(containsMove(game.getLegalMoves(),move), "AI should always choose a legal move");
|
||||
}
|
||||
|
||||
private boolean containsMove(Game.Move[] moves, Game.Move move) {
|
||||
for (Game.Move m : moves) {
|
||||
if (m.equals(move)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
65
pom.xml
65
pom.xml
@@ -107,24 +107,63 @@
|
||||
</java>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<!-- <plugin>-->
|
||||
<!-- <groupId>org.apache.maven.plugins</groupId>-->
|
||||
<!-- <artifactId>maven-compiler-plugin</artifactId>-->
|
||||
<!-- <version>3.14.1</version>-->
|
||||
<!-- <configuration>-->
|
||||
<!-- <outputDirectory>${project.build.directory}/custom</outputDirectory>-->
|
||||
<!-- <source>25</source>-->
|
||||
<!-- <target>25</target>-->
|
||||
<!-- <release>25</release>-->
|
||||
<!-- <encoding>UTF-8</encoding>-->
|
||||
<!-- </configuration>-->
|
||||
<!-- </plugin>-->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.14.1</version>
|
||||
<configuration>
|
||||
<showWarnings>true</showWarnings>
|
||||
<fork>true</fork>
|
||||
<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>
|
||||
</build>
|
||||
|
||||
<modules>
|
||||
<module>processors</module>
|
||||
<module>framework</module>
|
||||
<module>game</module>
|
||||
<module>app</module>
|
||||
</modules>
|
||||
</modules>
|
||||
</project>
|
||||
45
processors/pom.xml
Normal file
45
processors/pom.xml
Normal file
@@ -0,0 +1,45 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.toop</groupId>
|
||||
<artifactId>pism</artifactId>
|
||||
<version>0.1</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>processors</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.squareup</groupId>
|
||||
<artifactId>javapoet</artifactId>
|
||||
<version>1.13.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.auto.service</groupId>
|
||||
<artifactId>auto-service-annotations</artifactId>
|
||||
<version>1.1.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.auto.service</groupId>
|
||||
<artifactId>auto-service</artifactId>
|
||||
<version>1.1.1</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.14.1</version>
|
||||
<configuration>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.toop.annotations;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface AutoResponseResult {}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.toop.annotations;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE})
|
||||
public @interface TestsOnly {}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.toop.processors;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
|
||||
import javax.annotation.processing.*;
|
||||
import javax.lang.model.SourceVersion;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import java.util.Set;
|
||||
|
||||
@AutoService(Processor.class)
|
||||
@SupportedAnnotationTypes("*")
|
||||
@SupportedSourceVersion(SourceVersion.RELEASE_25)
|
||||
public class AutoResponseResultProcessor extends AbstractProcessor {
|
||||
|
||||
@Override
|
||||
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
|
||||
// Intentionally does nothing
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user