Merge remote-tracking branch 'origin/185-networkingeventlistener' into UI

# Conflicts:
#	app/src/main/java/org/toop/app/App.java
#	app/src/main/java/org/toop/app/layer/layers/ConnectedLayer.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
This commit is contained in:
lieght
2025-10-15 21:05:01 +02:00
72 changed files with 2972 additions and 1838 deletions

View File

@@ -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
View File

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

View File

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

View File

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

View File

@@ -1,11 +1,13 @@
package org.toop;
import org.toop.app.App;
import org.toop.framework.audio.SoundManager;
import org.toop.framework.audio.*;
import org.toop.framework.networking.NetworkingClientEventListener;
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.resources.MusicAsset;
import org.toop.framework.resource.resources.SoundEffectAsset;
public final class Main {
static void main(String[] args) {
@@ -13,9 +15,25 @@ public final class Main {
App.run(args);
}
private static void initSystems() throws NetworkingInitializationException {
private static void initSystems() {
ResourceManager.loadAssets(new ResourceLoader("app/src/main/resources/assets"));
new Thread(NetworkingClientManager::new).start();
new Thread(SoundManager::new).start();
new Thread(() -> new NetworkingClientEventListener(new NetworkingClientManager())).start();
new Thread(() -> {
MusicManager<MusicAsset> musicManager = new MusicManager<>(ResourceManager.getAllOfTypeAndRemoveWrapper(MusicAsset.class), true);
SoundEffectManager<SoundEffectAsset> soundEffectManager = new SoundEffectManager<>(ResourceManager.getAllOfType(SoundEffectAsset.class));
AudioVolumeManager audioVolumeManager = new AudioVolumeManager()
.registerManager(VolumeControl.MASTERVOLUME, musicManager)
.registerManager(VolumeControl.MASTERVOLUME, soundEffectManager)
.registerManager(VolumeControl.FX, soundEffectManager)
.registerManager(VolumeControl.MUSIC, musicManager);
new AudioEventListener<>(
musicManager,
soundEffectManager,
audioVolumeManager
).initListeners("medium-button-click.wav");
}).start();
}
}

View File

@@ -1,8 +1,15 @@
package org.toop.app;
import org.toop.app.view.ViewStack;
import org.toop.app.view.views.MainView;
import org.toop.app.view.views.QuitView;
import java.util.Stack;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import org.toop.app.layer.Layer;
import org.toop.app.layer.layers.MainLayer;
import org.toop.app.layer.layers.QuitPopup;
import org.toop.framework.audio.VolumeControl;
import org.toop.framework.audio.events.AudioEvents;
import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.resource.ResourceManager;
@@ -10,110 +17,154 @@ import org.toop.framework.resource.resources.CssAsset;
import org.toop.local.AppContext;
import org.toop.local.AppSettings;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public final class App extends Application {
private static Stage stage;
private static Scene scene;
private static Stage stage;
private static Scene scene;
private static StackPane root;
private static Stack<Layer> stack;
private static int height;
private static int width;
private static boolean isQuitting;
private static boolean isQuitting;
public static void run(String[] args) {
launch(args);
}
public static void run(String[] args) {
launch(args);
}
@Override
public void start(Stage stage) throws Exception {
@Override
public void start(Stage stage) throws Exception {
final StackPane root = new StackPane();
final Scene scene = new Scene(root);
ViewStack.setup(scene);
final Scene scene = new Scene(root);
stage.setTitle(AppContext.getString("app-title"));
stage.setWidth(1080);
stage.setHeight(720);
stage.setTitle(AppContext.getString("appTitle"));
stage.setWidth(1080);
stage.setHeight(720);
stage.setOnCloseRequest(event -> {
event.consume();
startQuit();
});
stage.setOnCloseRequest(
event -> {
event.consume();
stage.setScene(scene);
stage.setResizable(false);
if (!isQuitting) {
quitPopup();
}
});
stage.show();
stage.setScene(scene);
stage.setResizable(false);
App.stage = stage;
App.scene = scene;
stage.show();
App.width = (int)stage.getWidth();
App.height = (int)stage.getHeight();
App.stage = stage;
App.scene = scene;
App.root = root;
App.isQuitting = false;
App.stack = new Stack<>();
AppSettings.applySettings();
new EventFlow().addPostEvent(new AudioEvents.StartBackgroundMusic()).asyncPostEvent();
App.width = (int) stage.getWidth();
App.height = (int) stage.getHeight();
ViewStack.push(new MainView());
}
App.isQuitting = false;
public static void startQuit() {
if (isQuitting) {
return;
}
new EventFlow().addPostEvent(new AudioEvents.StartBackgroundMusic()).postEvent();
ViewStack.push(new QuitView());
isQuitting = true;
}
final AppSettings settings = new AppSettings();
settings.applySettings();
public static void stopQuit() {
ViewStack.pop();
isQuitting = false;
}
activate(new MainLayer());
}
public static void quit() {
ViewStack.cleanup();
stage.close();
}
public static void activate(Layer layer) {
Platform.runLater(
() -> {
popAll();
push(layer);
});
}
public static void reload() {
stage.setTitle(AppContext.getString("app-title"));
ViewStack.reload();
}
public static void push(Layer layer) {
Platform.runLater(
() -> {
root.getChildren().addLast(layer.getLayer());
stack.push(layer);
});
}
public static void setFullscreen(boolean fullscreen) {
stage.setFullScreen(fullscreen);
public static void pop() {
Platform.runLater(
() -> {
root.getChildren().removeLast();
stack.pop();
width = (int) stage.getWidth();
height = (int) stage.getHeight();
isQuitting = false;
});
}
reload();
}
public static void popAll() {
Platform.runLater(
() -> {
final int childrenCount = root.getChildren().size();
public static void setStyle(String theme, String layoutSize) {
final int stylesCount = scene.getStylesheets().size();
for (int i = 0; i < childrenCount; i++) {
try {
root.getChildren().removeLast();
} catch (Exception e) {
IO.println(e); // TODO: Use logger
}
}
for (int i = 0; i < stylesCount; i++) {
scene.getStylesheets().removeLast();
}
stack.removeAllElements();
});
}
scene.getStylesheets().add(ResourceManager.<CssAsset>get("general.css").getUrl());
scene.getStylesheets().add(ResourceManager.<CssAsset>get(theme + ".css").getUrl());
scene.getStylesheets().add(ResourceManager.<CssAsset>get(layoutSize + ".css").getUrl());
public static void quitPopup() {
Platform.runLater(
() -> {
push(new QuitPopup());
isQuitting = true;
});
}
reload();
}
public static void quit() {
new EventFlow().addPostEvent(new AudioEvents.StopAudioManager()).postEvent();
stage.close();
}
public static int getWidth() {
return width;
}
public static void reloadAll() {
stage.setTitle(AppContext.getString("appTitle"));
public static int getHeight() {
return height;
}
}
for (final Layer layer : stack) {
layer.reload();
}
}
public static void setFullscreen(boolean fullscreen) {
stage.setFullScreen(fullscreen);
width = (int) stage.getWidth();
height = (int) stage.getHeight();
reloadAll();
}
public static void setStyle(String theme, String layoutSize) {
final int stylesCount = scene.getStylesheets().size();
for (int i = 0; i < stylesCount; i++) {
scene.getStylesheets().removeLast();
}
scene.getStylesheets().add(ResourceManager.<CssAsset>get(theme + ".css").getUrl());
scene.getStylesheets().add(ResourceManager.<CssAsset>get(layoutSize + ".css").getUrl());
reloadAll();
}
public static int getWidth() {
return width;
}
public static int getHeight() {
return height;
}
}

View File

@@ -1,6 +1,9 @@
package org.toop.local;
import java.io.File;
import java.util.Locale;
import org.toop.app.App;
import org.toop.framework.audio.VolumeControl;
import org.toop.framework.audio.events.AudioEvents;
import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.resource.ResourceManager;
@@ -8,59 +11,57 @@ import org.toop.framework.resource.ResourceMeta;
import org.toop.framework.resource.resources.SettingsAsset;
import org.toop.framework.settings.Settings;
import java.io.File;
import java.util.Locale;
public class AppSettings {
private static SettingsAsset settingsAsset;
public static void applySettings() {
settingsAsset = getPath();
if (!settingsAsset.isLoaded()) {
settingsAsset.load();
}
private SettingsAsset settingsAsset;
Settings settingsData = settingsAsset.getContent();
public void applySettings() {
this.settingsAsset = getPath();
if (!this.settingsAsset.isLoaded()) {
this.settingsAsset.load();
}
AppContext.setLocale(Locale.of(settingsData.locale));
App.setFullscreen(settingsData.fullScreen);
new EventFlow()
.addPostEvent(new AudioEvents.ChangeVolume(settingsData.volume))
.asyncPostEvent();
new EventFlow()
.addPostEvent(new AudioEvents.ChangeFxVolume(settingsData.fxVolume))
.asyncPostEvent();
new EventFlow()
.addPostEvent(new AudioEvents.ChangeMusicVolume(settingsData.musicVolume))
.asyncPostEvent();
App.setStyle(settingsAsset.getTheme(), settingsAsset.getLayoutSize());
}
Settings settingsData = this.settingsAsset.getContent();
public static SettingsAsset getPath() {
if (settingsAsset == null) {
String os = System.getProperty("os.name").toLowerCase();
String basePath;
AppContext.setLocale(Locale.of(settingsData.locale));
App.setFullscreen(settingsData.fullScreen);
new EventFlow()
.addPostEvent(new AudioEvents.ChangeVolume(settingsData.volume, VolumeControl.MASTERVOLUME))
.asyncPostEvent();
new EventFlow()
.addPostEvent(new AudioEvents.ChangeVolume(settingsData.fxVolume, VolumeControl.FX))
.asyncPostEvent();
new EventFlow()
.addPostEvent(new AudioEvents.ChangeVolume(settingsData.musicVolume, VolumeControl.MUSIC))
.asyncPostEvent();
App.setStyle(settingsAsset.getTheme(), settingsAsset.getLayoutSize());
}
if (os.contains("win")) {
basePath = System.getenv("APPDATA");
if (basePath == null) {
basePath = System.getProperty("user.home");
}
} else if (os.contains("mac")) {
basePath = System.getProperty("user.home") + "/Library/Application Support";
} else {
basePath = System.getProperty("user.home") + "/.config";
}
public SettingsAsset getPath() {
if (this.settingsAsset == null) {
String os = System.getProperty("os.name").toLowerCase();
String basePath;
File settingsFile =
new File(basePath + File.separator + "ISY1" + File.separator + "settings.json");
// this.settingsAsset = new SettingsAsset(settingsFile);
ResourceManager.addAsset(new ResourceMeta<>("settings.json", new SettingsAsset(settingsFile)));
}
return ResourceManager.get("settings.json");
}
if (os.contains("win")) {
basePath = System.getenv("APPDATA");
if (basePath == null) {
basePath = System.getProperty("user.home");
}
} else if (os.contains("mac")) {
basePath = System.getProperty("user.home") + "/Library/Application Support";
} else {
basePath = System.getProperty("user.home") + "/.config";
}
public static SettingsAsset getSettings() {
return settingsAsset;
}
}
File settingsFile =
new File(basePath + File.separator + "ISY1" + File.separator + "settings.json");
return new SettingsAsset(settingsFile);
// this.settingsAsset = new SettingsAsset(settingsFile); // TODO
// ResourceManager.addAsset(new ResourceMeta<>("settings.json", new SettingsAsset(settingsFile))); // TODO
}
return this.settingsAsset;
// return ResourceManager.get("settings.json"); // TODO
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,68 @@
package org.toop.framework.audio;
import org.toop.framework.audio.events.AudioEvents;
import org.toop.framework.audio.interfaces.MusicManager;
import org.toop.framework.audio.interfaces.SoundEffectManager;
import org.toop.framework.audio.interfaces.VolumeManager;
import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.resource.types.AudioResource;
public class AudioEventListener<T extends AudioResource, K extends AudioResource> {
private final MusicManager<T> musicManager;
private final SoundEffectManager<K> soundEffectManager;
private final VolumeManager audioVolumeManager;
public AudioEventListener(
MusicManager<T> musicManager,
SoundEffectManager<K> soundEffectManager,
VolumeManager audioVolumeManager
) {
this.musicManager = musicManager;
this.soundEffectManager = soundEffectManager;
this.audioVolumeManager = audioVolumeManager;
}
public AudioEventListener<?, ?> initListeners(String buttonSoundToPlay) {
new EventFlow()
.listen(this::handleStopMusicManager)
.listen(this::handlePlaySound)
.listen(this::handleStopSound)
.listen(this::handleMusicStart)
.listen(this::handleVolumeChange)
.listen(this::handleGetVolume)
.listen(AudioEvents.ClickButton.class, _ ->
soundEffectManager.play(buttonSoundToPlay, false));
return this;
}
private void handleStopMusicManager(AudioEvents.StopAudioManager event) {
this.musicManager.stop();
}
private void handlePlaySound(AudioEvents.PlayEffect event) {
this.soundEffectManager.play(event.fileName(), event.loop());
}
private void handleStopSound(AudioEvents.StopEffect event) {
this.soundEffectManager.stop(event.fileName());
}
private void handleMusicStart(AudioEvents.StartBackgroundMusic event) {
this.musicManager.play();
}
private void handleVolumeChange(AudioEvents.ChangeVolume event) {
this.audioVolumeManager.setVolume(event.newVolume() / 100, event.controlType());
}
private void handleGetVolume(AudioEvents.GetVolume event) {
new EventFlow()
.addPostEvent(
new AudioEvents.GetVolumeResponse(
audioVolumeManager.getVolume(event.controlType()),
event.identifier()))
.asyncPostEvent();
}
}

View File

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

View File

@@ -0,0 +1,128 @@
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, boolean shuffleMusic) {
this.dispatcher = new JavaFXDispatcher();
this.resources = resources;
// Shuffle if wanting to shuffle
if (shuffleMusic) createShuffled();
else backgroundMusic.addAll(resources);
// ------------------------------
}
/**
* {@code @TestsOnly} DO NOT USE
*/
@TestsOnly
public MusicManager(List<T> resources, Dispatcher dispatcher) {
this.dispatcher = dispatcher;
this.resources = new ArrayList<>(resources);
backgroundMusic.addAll(resources);
}
@Override
public Collection<T> getActiveAudio() {
return backgroundMusic;
}
void addBackgroundMusic(T musicAsset) {
backgroundMusic.add(musicAsset);
}
private void createShuffled() {
backgroundMusic.clear();
Collections.shuffle(resources);
backgroundMusic.addAll(resources);
}
@Override
public void play() {
if (playing) {
logger.warn("MusicManager is already playing.");
return;
}
if (backgroundMusic.isEmpty()) return;
playingIndex = 0;
playing = true;
playCurrentTrack();
}
// Used in testing
void play(int index) {
if (playing) {
logger.warn("MusicManager is already playing.");
return;
}
if (backgroundMusic.isEmpty()) return;
playingIndex = index;
playing = true;
playCurrentTrack();
}
private void playCurrentTrack() {
if (playingIndex >= backgroundMusic.size()) {
playingIndex = 0;
}
T current = backgroundMusic.get(playingIndex);
if (current == null) {
logger.error("Current track is null!");
return;
}
dispatcher.run(() -> {
current.play();
setTrackRunnable(current);
});
}
private void setTrackRunnable(T track) {
track.setOnEnd(() -> {
playingIndex++;
playCurrentTrack();
});
track.setOnError(() -> {
logger.error("Error playing track: {}", track);
backgroundMusic.remove(track);
if (!backgroundMusic.isEmpty()) {
playCurrentTrack();
} else {
playing = false;
}
});
}
@Override
public void stop() {
if (!playing) return;
playing = false;
dispatcher.run(() -> backgroundMusic.forEach(T::stop));
}
}

View File

@@ -0,0 +1,65 @@
package org.toop.framework.audio;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.toop.framework.resource.ResourceManager;
import org.toop.framework.resource.ResourceMeta;
import org.toop.framework.resource.resources.BaseResource;
import org.toop.framework.resource.resources.MusicAsset;
import org.toop.framework.resource.resources.SoundEffectAsset;
import org.toop.framework.resource.types.AudioResource;
import javax.sound.sampled.Clip;
import javax.sound.sampled.LineEvent;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.UnsupportedAudioFileException;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;
public class SoundEffectManager<T extends AudioResource> implements org.toop.framework.audio.interfaces.SoundEffectManager<T> {
private static final Logger logger = LogManager.getLogger(SoundEffectManager.class);
private final HashMap<String, T> soundEffectResources;
public <K extends BaseResource & AudioResource> SoundEffectManager(List<ResourceMeta<K>> resources) {
// If there are duplicates, takes discards the first
this.soundEffectResources = (HashMap<String, T>) resources
.stream()
.collect(Collectors.
toMap(ResourceMeta::getName, ResourceMeta::getResource, (a, b) -> b, HashMap::new));
}
@Override
public Collection<T> getActiveAudio() {
return this.soundEffectResources.values();
}
@Override
public void play(String name, boolean loop) {
T asset = soundEffectResources.get(name);
if (asset == null) {
logger.warn("Unable to load audio asset: {}", name);
return;
}
asset.play();
logger.debug("Playing sound: {}", asset.getName());
}
@Override
public void stop(String name){
T asset = soundEffectResources.get(name);
if (asset == null) {
logger.warn("Unable to load audio asset: {}", name);
return;
}
asset.stop();
logger.debug("Stopped sound: {}", asset.getName());
}
}

View File

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

View File

@@ -0,0 +1,162 @@
package org.toop.framework.audio;
import org.toop.framework.audio.interfaces.AudioManager;
import org.toop.framework.resource.types.AudioResource;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Enum representing different categories of audio volume in the application.
* <p>
* Each volume type maintains its own volume level and a list of {@link AudioManager}s
* that manage audio resources of that type. The enum provides methods to set, get,
* and propagate volume changes, including master volume adjustments that automatically
* update dependent volume types (FX and MUSIC).
* </p>
*
* <p>Volume types:</p>
* <ul>
* <li>{@link #MASTERVOLUME}: The global/master volume that scales all other volume types.</li>
* <li>{@link #FX}: Volume for sound effects, scaled by the master volume.</li>
* <li>{@link #MUSIC}: Volume for music tracks, scaled by the master volume.</li>
* </ul>
*
* <p>Key features:</p>
* <ul>
* <li>Thread-safe management of audio managers using {@link CopyOnWriteArrayList}.</li>
* <li>Automatic propagation of master volume changes to dependent volume types.</li>
* <li>Clamping volume values between 0.0 and 1.0 to ensure valid audio levels.</li>
* <li>Dynamic registration and removal of audio managers for each volume type.</li>
* </ul>
*
* <p>Example usage:</p>
* <pre>{@code
* // Add a music manager to the MUSIC volume type
* VolumeControl.MUSIC.addManager(musicManager);
*
* // Set master volume to 80%
* VolumeControl.MASTERVOLUME.setVolume(0.8, 0);
*
* // Set FX volume to 50% of master
* VolumeControl.FX.setVolume(0.5, VolumeControl.MASTERVOLUME.getVolume());
*
* // Retrieve current music volume
* double musicVol = VolumeControl.MUSIC.getVolume();
* }</pre>
*/
public enum VolumeControl {
MASTERVOLUME(),
FX(),
MUSIC();
@SuppressWarnings("ImmutableEnumChecker")
private final List<AudioManager<? extends AudioResource>> managers = new CopyOnWriteArrayList<>();
@SuppressWarnings("ImmutableEnumChecker")
private double volume = 1.0;
@SuppressWarnings("ImmutableEnumChecker")
private double masterVolume = 1.0;
/**
* Sets the volume for this volume type.
* <p>
* If this type is {@link #MASTERVOLUME}, all dependent volume types
* (FX, MUSIC, etc.) are automatically updated to reflect the new master volume.
* Otherwise, the volume is scaled by the provided master volume.
*
* @param newVolume the new volume level (0.0 to 1.0)
* @param currentMasterVolume the current master volume for scaling non-master types
*/
public void setVolume(double newVolume, double currentMasterVolume) {
this.volume = clamp(newVolume);
if (this == MASTERVOLUME) {
for (VolumeControl type : VolumeControl.values()) {
if (type != MASTERVOLUME) {
type.masterVolume = this.volume;
type.broadcastVolume(type.computeEffectiveVolume());
}
}
} else {
this.masterVolume = clamp(currentMasterVolume);
broadcastVolume(computeEffectiveVolume());
}
}
/**
* Computes the effective volume for this type, taking into account
* the master volume if this is not {@link #MASTERVOLUME}.
*
* @return the effective volume (0.0 to 1.0)
*/
private double computeEffectiveVolume() {
return (this == MASTERVOLUME) ? volume : volume * masterVolume;
}
/**
* Updates all registered audio managers with the given effective volume.
*
* @param effectiveVolume the volume to apply to all active audio resources
*/
private void broadcastVolume(double effectiveVolume) {
managers.stream()
.filter(Objects::nonNull)
.forEach(manager -> manager.getActiveAudio()
.forEach(aud -> aud.updateVolume(effectiveVolume)));
}
/**
* Clamps a volume value to the valid range [0.0, 1.0].
*
* @param vol the volume to clamp
* @return the clamped volume
*/
private double clamp(double vol) {
return Math.max(0, Math.min(vol, 1.0));
}
/**
* Gets the current volume for this type.
*
* @return the current volume (0.0 to 1.0)
*/
public double getVolume() {
return volume;
}
/**
* Registers an {@link AudioManager} to this volume type.
* <p>
* Duplicate managers are ignored. Managers will receive volume updates
* when this type's volume changes.
*
* @param manager the audio manager to register
*/
public void addManager(AudioManager<? extends AudioResource> manager) {
if (manager != null && !managers.contains(manager)) {
managers.add(manager);
}
}
/**
* Removes a previously registered {@link AudioManager} from this type.
*
* @param manager the audio manager to remove
*/
public void removeManager(AudioManager<? extends AudioResource> manager) {
if (manager != null) {
managers.remove(manager);
}
}
/**
* Returns an unmodifiable view of all registered audio managers for this type.
*
* @return a list of registered audio managers
*/
public List<AudioManager<? extends AudioResource>> getManagers() {
return Collections.unmodifiableList(managers);
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,13 @@
package org.toop.framework.audio.interfaces;
import org.toop.framework.resource.resources.SoundEffectAsset;
import org.toop.framework.resource.types.AudioResource;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.UnsupportedAudioFileException;
import java.io.IOException;
public interface SoundEffectManager<T extends AudioResource> extends AudioManager<T> {
void play(String name, boolean loop);
void stop(String name);
}

View File

@@ -0,0 +1,53 @@
package org.toop.framework.audio.interfaces;
import org.toop.framework.audio.VolumeControl;
/**
* Interface for managing audio volumes in the application.
* <p>
* Implementations of this interface are responsible for controlling the volume levels
* of different categories of audio (e.g., master volume, music, sound effects) and
* updating the associated audio managers or resources accordingly.
* </p>
*
* <p>Typical responsibilities include:</p>
* <ul>
* <li>Setting the volume for a specific category (master, music, FX).</li>
* <li>Retrieving the current volume of a category.</li>
* <li>Ensuring that changes in master volume propagate to dependent audio categories.</li>
* <li>Interfacing with {@link org.toop.framework.audio.interfaces.AudioManager} to update active audio resources.</li>
* </ul>
*
* <p>Example usage:</p>
* <pre>{@code
* VolumeManager volumeManager = ...;
* // Set master volume to 80%
* volumeManager.setVolume(0.8, VolumeControl.MASTERVOLUME);
*
* // Set music volume to 50% of master
* volumeManager.setVolume(0.5, VolumeControl.MUSIC);
*
* // Retrieve current FX volume
* double fxVolume = volumeManager.getVolume(VolumeControl.FX);
* }</pre>
*/
public interface VolumeManager {
/**
*
* Sets the volume to for the specified {@link VolumeControl}.
*
* @param newVolume The volume to be set to.
* @param type The type of volume to change.
*/
void setVolume(double newVolume, VolumeControl type);
/**
* Gets the current volume for the specified {@link VolumeControl}.
*
* @param type the type of volume to get.
* @return The volume as a {@link Double}
*/
double getVolume(VolumeControl type);
}

View File

@@ -0,0 +1,11 @@
package org.toop.framework.dispatch;
import javafx.application.Platform;
import org.toop.framework.dispatch.interfaces.Dispatcher;
public class JavaFXDispatcher implements Dispatcher {
@Override
public void run(Runnable task) {
Platform.runLater(task);
}
}

View File

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

View File

@@ -11,13 +11,14 @@ import java.util.function.Consumer;
import java.util.function.Supplier;
import 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;
}

View File

@@ -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);
}
@@ -134,7 +134,8 @@ public final class GlobalEventBus {
for (Consumer<? super EventType> listener : classListeners) {
try {
listener.accept(event);
} catch (Throwable ignored) {
} catch (Throwable e) {
// e.printStackTrace();
}
}
}
@@ -146,17 +147,18 @@ public final class GlobalEventBus {
for (Consumer<? super EventType> listener : genericListeners) {
try {
listener.accept(event);
} catch (Throwable ignored) {
} catch (Throwable e) {
// e.printStackTrace();
}
}
}
// 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);

View File

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

View File

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

View File

@@ -1,69 +1,4 @@
package org.toop.framework.eventbus.events;
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 {}

View File

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

View File

@@ -0,0 +1,31 @@
package org.toop.framework.eventbus.events;
import java.lang.reflect.RecordComponent;
import java.util.HashMap;
import java.util.Map;
/**
* MUST HAVE long identifier at the end.
* e.g.
*
* <pre>{@code
* public record uniqueEventResponse(String content, long identifier) implements ResponseToUniqueEvent {};
* public record uniqueEventResponse(long identifier) implements ResponseToUniqueEvent {};
* public record uniqueEventResponse(String content, int number, long identifier) implements ResponseToUniqueEvent {};
* }</pre>
*
*/
public interface ResponseToUniqueEvent extends UniqueEvent {
default Map<String, Object> result() {
Map<String, Object> map = new HashMap<>();
try {
for (RecordComponent component : this.getClass().getRecordComponents()) {
Object value = component.getAccessor().invoke(this);
map.put(component.getName(), value);
}
} catch (Exception e) {
throw new RuntimeException("Failed to build result map via reflection", e);
}
return Map.copyOf(map);
}
}

View File

@@ -0,0 +1,23 @@
package org.toop.framework.eventbus.events;
/**
* MUST HAVE long identifier at the end.
* e.g.
*
* <pre>{@code
* public record uniqueEvent(String content, long identifier) implements UniqueEvent {};
* public record uniqueEvent(long identifier) implements UniqueEvent {};
* public record uniqueEvent(String content, int number, long identifier) implements UniqueEvent {};
* }</pre>
*
*/
public interface UniqueEvent extends EventType {
default long getIdentifier() {
try {
var method = this.getClass().getMethod("identifier");
return (long) method.invoke(this);
} catch (Exception e) {
throw new RuntimeException("No identifier accessor found", e);
}
}
}

View File

@@ -0,0 +1,150 @@
package org.toop.framework.networking;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.toop.framework.SnowflakeGenerator;
import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.networking.events.NetworkEvents;
import org.toop.framework.networking.exceptions.ClientNotFoundException;
import org.toop.framework.networking.interfaces.NetworkingClientManager;
public class NetworkingClientEventListener {
private static final Logger logger = LogManager.getLogger(NetworkingClientEventListener.class);
private final NetworkingClientManager clientManager;
/** Starts a connection manager, to manage, connections. */
public NetworkingClientEventListener(NetworkingClientManager clientManager) {
this.clientManager = clientManager;
new EventFlow()
.listen(this::handleStartClient)
.listen(this::handleCommand)
.listen(this::handleSendLogin)
.listen(this::handleSendLogout)
.listen(this::handleSendGetPlayerlist)
.listen(this::handleSendGetGamelist)
.listen(this::handleSendSubscribe)
.listen(this::handleSendMove)
.listen(this::handleSendChallenge)
.listen(this::handleSendAcceptChallenge)
.listen(this::handleSendForfeit)
.listen(this::handleSendMessage)
.listen(this::handleSendHelp)
.listen(this::handleSendHelpForCommand)
.listen(this::handleCloseClient)
.listen(this::handleReconnect)
.listen(this::handleChangeAddress)
.listen(this::handleGetAllConnections)
.listen(this::handleShutdownAll);
}
void handleStartClient(NetworkEvents.StartClient event) {
long clientId = SnowflakeGenerator.nextId();
clientManager.startClient(
clientId,
event.networkingClient(),
event.networkingConnector(),
() -> new EventFlow().addPostEvent(new NetworkEvents.StartClientResponse(clientId, true, event.identifier())).postEvent(),
() -> new EventFlow().addPostEvent(new NetworkEvents.StartClientResponse(clientId, false, event.identifier())).postEvent()
);
}
private void sendCommand(long clientId, String command) {
try {
clientManager.sendCommand(clientId, command);
} catch (ClientNotFoundException e) {
logger.error(e);
}
}
private void handleCommand(NetworkEvents.SendCommand event) {
String args = String.join(" ", event.args());
sendCommand(event.clientId(), args);
}
private void handleSendLogin(NetworkEvents.SendLogin event) {
sendCommand(event.clientId(), String.format("LOGIN %s", event.username()));
}
private void handleSendLogout(NetworkEvents.SendLogout event) {
sendCommand(event.clientId(), "LOGOUT");
}
private void handleSendGetPlayerlist(NetworkEvents.SendGetPlayerlist event) {
sendCommand(event.clientId(), "GET PLAYERLIST");
}
private void handleSendGetGamelist(NetworkEvents.SendGetGamelist event) {
sendCommand(event.clientId(), "GET GAMELIST");
}
private void handleSendSubscribe(NetworkEvents.SendSubscribe event) {
sendCommand(event.clientId(), String.format("SUBSCRIBE %s", event.gameType()));
}
private void handleSendMove(NetworkEvents.SendMove event) {
sendCommand(event.clientId(), String.format("MOVE %d", event.moveNumber()));
}
private void handleSendChallenge(NetworkEvents.SendChallenge event) {
sendCommand(event.clientId(), String.format("CHALLENGE %s %s", event.usernameToChallenge(), event.gameType()));
}
private void handleSendAcceptChallenge(NetworkEvents.SendAcceptChallenge event) {
sendCommand(event.clientId(), String.format("CHALLENGE ACCEPT %d", event.challengeId()));
}
private void handleSendForfeit(NetworkEvents.SendForfeit event) {
sendCommand(event.clientId(), "FORFEIT");
}
private void handleSendMessage(NetworkEvents.SendMessage event) {
sendCommand(event.clientId(), String.format("MESSAGE %s", event.message()));
}
private void handleSendHelp(NetworkEvents.SendHelp event) {
sendCommand(event.clientId(), "HELP");
}
private void handleSendHelpForCommand(NetworkEvents.SendHelpForCommand event) {
sendCommand(event.clientId(), String.format("HELP %s", event.command()));
}
private void handleReconnect(NetworkEvents.Reconnect event) {
clientManager.startClient(
event.clientId(),
event.networkingClient(),
event.networkingConnector(),
() -> new EventFlow().addPostEvent(new NetworkEvents.ReconnectResponse(true, event.identifier())).postEvent(),
() -> new EventFlow().addPostEvent(new NetworkEvents.ReconnectResponse(false, event.identifier())).postEvent()
);
}
private void handleChangeAddress(NetworkEvents.ChangeAddress event) {
clientManager.startClient(
event.clientId(),
event.networkingClient(),
event.networkingConnector(),
() -> new EventFlow().addPostEvent(new NetworkEvents.ChangeAddressResponse(true, event.identifier())).postEvent(),
() -> new EventFlow().addPostEvent(new NetworkEvents.ChangeAddressResponse(false, event.identifier())).postEvent()
);
}
void handleCloseClient(NetworkEvents.CloseClient event) {
try {
this.clientManager.closeClient(event.clientId());
} catch (ClientNotFoundException e) {
logger.error(e);
}
}
void handleGetAllConnections(NetworkEvents.RequestsAllClients request) {
// List<NetworkingClient> a = new ArrayList<>(this.networkClients.values());
// request.future().complete(a);
// TODO
}
public void handleShutdownAll(NetworkEvents.ForceCloseAllClients request) {
// TODO
}
}

View File

@@ -2,196 +2,118 @@ package org.toop.framework.networking;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.toop.framework.SnowflakeGenerator;
import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.networking.events.NetworkEvents;
public class NetworkingClientManager {
import org.toop.framework.networking.exceptions.ClientNotFoundException;
import org.toop.framework.networking.exceptions.CouldNotConnectException;
import org.toop.framework.networking.interfaces.NetworkingClient;
import org.toop.framework.networking.types.NetworkingConnector;
public class NetworkingClientManager implements org.toop.framework.networking.interfaces.NetworkingClientManager {
private static final Logger logger = LogManager.getLogger(NetworkingClientManager.class);
private final Map<Long, NetworkingClient> networkClients = new ConcurrentHashMap<>();
/** Map of serverId -> Server instances */
final Map<Long, NetworkingClient> networkClients = new ConcurrentHashMap<>();
public NetworkingClientManager() {}
/** Starts a connection manager, to manage, connections. */
public NetworkingClientManager() throws NetworkingInitializationException {
try {
new EventFlow()
.listen(this::handleStartClient)
.listen(this::handleCommand)
.listen(this::handleSendLogin)
.listen(this::handleSendLogout)
.listen(this::handleSendGetPlayerlist)
.listen(this::handleSendGetGamelist)
.listen(this::handleSendSubscribe)
.listen(this::handleSendMove)
.listen(this::handleSendChallenge)
.listen(this::handleSendAcceptChallenge)
.listen(this::handleSendForfeit)
.listen(this::handleSendMessage)
.listen(this::handleSendHelp)
.listen(this::handleSendHelpForCommand)
.listen(this::handleCloseClient)
.listen(this::handleChangeClientHost)
.listen(this::handleGetAllConnections)
.listen(this::handleShutdownAll);
logger.info("NetworkingClientManager initialized");
} catch (Exception e) {
logger.error("Failed to initialize the client manager", e);
throw e;
private void connectHelper(
long id,
NetworkingClient nClient,
NetworkingConnector nConnector,
Runnable onSuccess,
Runnable onFailure
) {
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
Runnable connectTask = new Runnable() {
int attempts = 0;
@Override
public void run() {
NetworkingClient qClient = networkClients.get(id);
if (qClient != null) {
qClient.closeConnection();
networkClients.remove(id);
}
try {
nClient.connect(id, nConnector.host(), nConnector.port());
networkClients.put(id, nClient);
logger.info("New client started successfully for {}:{}", nConnector.host(), nConnector.port());
onSuccess.run();
scheduler.shutdown();
} catch (CouldNotConnectException e) {
attempts++;
if (attempts < nConnector.reconnectAttempts()) {
logger.warn("Could not connect to {}:{}. Retrying in {} {}",
nConnector.host(), nConnector.port(), nConnector.timeout(), nConnector.timeUnit());
scheduler.schedule(this, nConnector.timeout(), nConnector.timeUnit());
} else {
logger.error("Failed to start client for {}:{} after {} attempts", nConnector.host(), nConnector.port(), attempts);
onFailure.run();
scheduler.shutdown();
}
} catch (Exception e) {
logger.error("Unexpected exception during startClient", e);
onFailure.run();
scheduler.shutdown();
}
}
};
scheduler.schedule(connectTask, 0, TimeUnit.MILLISECONDS);
}
@Override
public void startClient(
long id,
NetworkingClient nClient,
NetworkingConnector nConnector,
Runnable onSuccess,
Runnable onFailure
) {
connectHelper(
id,
nClient,
nConnector,
onSuccess,
onFailure
);
}
@Override
public void sendCommand(long id, String command) throws ClientNotFoundException {
logger.info("Sending command to client for {}:{}", id, command);
if (command.isEmpty()) {
IllegalArgumentException e = new IllegalArgumentException("command is empty");
logger.error("Invalid command received", e);
return;
}
}
long startClientRequest(String ip, int port) {
long connectionId = new SnowflakeGenerator().nextId();
try {
NetworkingClient client =
new NetworkingClient(
() -> new NetworkingGameClientHandler(connectionId),
ip,
port,
connectionId);
client.setConnectionId(connectionId);
this.networkClients.put(connectionId, client);
logger.info("New client started successfully for {}:{}", ip, port);
} catch (Exception e) {
logger.error(e);
NetworkingClient client = this.networkClients.get(id);
if (client == null) {
throw new ClientNotFoundException(id);
}
return connectionId;
String toSend = command.trim();
if (toSend.endsWith("\n")) { client.writeAndFlush(toSend); }
else { client.writeAndFlush(toSend + "\n"); }
}
private long startClientRequest(String ip, int port, long clientId) {
try { // With EventFlow
NetworkingClient client =
new NetworkingClient(
() -> new NetworkingGameClientHandler(clientId), ip, port, clientId);
client.setConnectionId(clientId);
this.networkClients.replace(clientId, client);
logger.info(
"New client started successfully for {}:{}, replaced: {}", ip, port, clientId);
} catch (Exception e) {
logger.error(e);
@Override
public void closeClient(long id) throws ClientNotFoundException {
NetworkingClient client = this.networkClients.get(id);
if (client == null) {
throw new ClientNotFoundException(id);
}
logger.info("Client {} started", clientId);
return clientId;
}
void handleStartClient(NetworkEvents.StartClient event) {
long id = this.startClientRequest(event.ip(), event.port());
new Thread(
() ->
new EventFlow()
.addPostEvent(
NetworkEvents.StartClientResponse.class,
id,
event.eventSnowflake())
.asyncPostEvent())
.start();
}
void handleCommand(
NetworkEvents.SendCommand
event) { // TODO: Move this to ServerConnection class, keep it internal.
NetworkingClient client = this.networkClients.get(event.clientId());
String args = String.join(" ", event.args());
sendCommand(client, args);
}
void handleSendLogin(NetworkEvents.SendLogin event) {
NetworkingClient client = this.networkClients.get(event.clientId());
sendCommand(client, String.format("LOGIN %s", event.username()));
}
private void handleSendLogout(NetworkEvents.SendLogout event) {
NetworkingClient client = this.networkClients.get(event.clientId());
sendCommand(client, "LOGOUT");
}
private void handleSendGetPlayerlist(NetworkEvents.SendGetPlayerlist event) {
NetworkingClient client = this.networkClients.get(event.clientId());
sendCommand(client, "GET PLAYERLIST");
}
private void handleSendGetGamelist(NetworkEvents.SendGetGamelist event) {
NetworkingClient client = this.networkClients.get(event.clientId());
sendCommand(client, "GET GAMELIST");
}
private void handleSendSubscribe(NetworkEvents.SendSubscribe event) {
NetworkingClient client = this.networkClients.get(event.clientId());
sendCommand(client, String.format("SUBSCRIBE %s", event.gameType()));
}
private void handleSendMove(NetworkEvents.SendMove event) {
NetworkingClient client = this.networkClients.get(event.clientId());
sendCommand(client, String.format("MOVE %d", event.moveNumber()));
}
private void handleSendChallenge(NetworkEvents.SendChallenge event) {
NetworkingClient client = this.networkClients.get(event.clientId());
sendCommand(
client,
String.format("CHALLENGE %s %s", event.usernameToChallenge(), event.gameType()));
}
private void handleSendAcceptChallenge(NetworkEvents.SendAcceptChallenge event) {
NetworkingClient client = this.networkClients.get(event.clientId());
sendCommand(client, String.format("CHALLENGE ACCEPT %d", event.challengeId()));
}
private void handleSendForfeit(NetworkEvents.SendForfeit event) {
NetworkingClient client = this.networkClients.get(event.clientId());
sendCommand(client, "FORFEIT");
}
private void handleSendMessage(NetworkEvents.SendMessage event) {
NetworkingClient client = this.networkClients.get(event.clientId());
sendCommand(client, String.format("MESSAGE %s", event.message()));
}
private void handleSendHelp(NetworkEvents.SendHelp event) {
NetworkingClient client = this.networkClients.get(event.clientId());
sendCommand(client, "HELP");
}
private void handleSendHelpForCommand(NetworkEvents.SendHelpForCommand event) {
NetworkingClient client = this.networkClients.get(event.clientId());
sendCommand(client, String.format("HELP %s", event.command()));
}
private void sendCommand(NetworkingClient client, String command) {
logger.info(
"Preparing to send command: {} to server: {}:{}. clientId: {}",
command.trim(),
client.getHost(),
client.getPort(),
client.getId());
client.writeAndFlushnl(command);
}
private void handleChangeClientHost(NetworkEvents.ChangeClientHost event) {
NetworkingClient client = this.networkClients.get(event.clientId());
client.closeConnection();
startClientRequest(event.ip(), event.port(), event.clientId());
}
void handleCloseClient(NetworkEvents.CloseClient event) {
NetworkingClient client = this.networkClients.get(event.clientId());
client.closeConnection();
this.networkClients.remove(event.clientId());
logger.info("Client {} closed successfully.", event.clientId());
}
void handleGetAllConnections(NetworkEvents.RequestsAllClients request) {
List<NetworkingClient> a = new ArrayList<>(this.networkClients.values());
request.future().complete(a);
}
public void handleShutdownAll(NetworkEvents.ForceCloseAllClients request) {
this.networkClients.values().forEach(NetworkingClient::closeConnection);
this.networkClients.clear();
logger.info("All servers shut down");
}
}

View File

@@ -1,4 +1,4 @@
package org.toop.framework.networking;
package org.toop.framework.networking.clients;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
@@ -9,27 +9,27 @@ import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;
import java.util.function.Supplier;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.networking.events.NetworkEvents;
import org.toop.framework.networking.exceptions.CouldNotConnectException;
import org.toop.framework.networking.handlers.NetworkingGameClientHandler;
import org.toop.framework.networking.interfaces.NetworkingClient;
public class NetworkingClient {
private static final Logger logger = LogManager.getLogger(NetworkingClient.class);
import java.net.InetSocketAddress;
private long connectionId;
private String host;
private int port;
public class TournamentNetworkingClient implements NetworkingClient {
private static final Logger logger = LogManager.getLogger(TournamentNetworkingClient.class);
private Channel channel;
private NetworkingGameClientHandler handler;
public NetworkingClient(
Supplier<NetworkingGameClientHandler> handlerFactory,
String host,
int port,
long connectionId) {
this.connectionId = connectionId;
public TournamentNetworkingClient() {}
@Override
public InetSocketAddress getAddress() {
return (InetSocketAddress) channel.remoteAddress();
}
@Override
public void connect(long clientId, String host, int port) throws CouldNotConnectException {
try {
Bootstrap bootstrap = new Bootstrap();
EventLoopGroup workerGroup = new MultiThreadIoEventLoopGroup(NioIoHandler.newFactory());
@@ -40,7 +40,7 @@ public class NetworkingClient {
new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
handler = handlerFactory.get();
NetworkingGameClientHandler handler = new NetworkingGameClientHandler(clientId);
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LineBasedFrameDecoder(1024)); // split at \n
@@ -52,53 +52,28 @@ public class NetworkingClient {
});
ChannelFuture channelFuture = bootstrap.connect(host, port).sync();
this.channel = channelFuture.channel();
this.host = host;
this.port = port;
} catch (Exception e) {
logger.error("Failed to create networking client instance", e);
} catch (Exception _) {
throw new CouldNotConnectException(clientId);
}
}
public NetworkingGameClientHandler getHandler() {
return this.handler;
}
public String getHost() {
return this.host;
}
public int getPort() {
return this.port;
}
public void setConnectionId(long connectionId) {
this.connectionId = connectionId;
}
public boolean isChannelActive() {
@Override
public boolean isActive() {
return this.channel != null && this.channel.isActive();
}
@Override
public void writeAndFlush(String msg) {
String literalMsg = msg.replace("\n", "\\n").replace("\r", "\\r");
if (isChannelActive()) {
if (isActive()) {
this.channel.writeAndFlush(msg);
logger.info(
"Connection {} sent message: '{}' ", this.channel.remoteAddress(), literalMsg);
logger.info("Connection {} sent message: '{}' ", this.channel.remoteAddress(), literalMsg);
} else {
logger.warn("Cannot send message: '{}', connection inactive. ", literalMsg);
}
}
public void writeAndFlushnl(String msg) {
if (isChannelActive()) {
this.channel.writeAndFlush(msg + "\r\n");
logger.info("Connection {} sent message: '{}'", this.channel.remoteAddress(), msg);
} else {
logger.warn("Cannot send message: '{}', connection inactive.", msg);
}
}
@Override
public void closeConnection() {
if (this.channel != null && this.channel.isActive()) {
this.channel
@@ -109,11 +84,6 @@ public class NetworkingClient {
logger.info(
"Connection {} closed successfully",
this.channel.remoteAddress());
new EventFlow()
.addPostEvent(
new NetworkEvents.ClosedConnection(
this.connectionId))
.asyncPostEvent();
} else {
logger.error(
"Error closing connection {}. Error: {}",
@@ -123,8 +93,4 @@ public class NetworkingClient {
});
}
}
public long getId() {
return this.connectionId;
}
}

View File

@@ -1,213 +1,217 @@
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.EventsBase;
import org.toop.framework.networking.NetworkingClient;
import org.toop.annotations.AutoResponseResult;
import org.toop.framework.eventbus.events.*;
import org.toop.framework.networking.interfaces.NetworkingClient;
import org.toop.framework.networking.types.NetworkingConnector;
/**
* A collection of networking-related event records for use with the {@link
* org.toop.framework.eventbus.GlobalEventBus}.
* Defines all event types related to the networking subsystem.
* <p>
* These events are used in conjunction with the {@link org.toop.framework.eventbus.GlobalEventBus}
* and {@link org.toop.framework.eventbus.EventFlow} to communicate between components
* such as networking clients, managers, and listeners.
* </p>
*
* <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).
* <h2>Important</h2>
* For all {@link UniqueEvent} and {@link ResponseToUniqueEvent} types:
* the {@code identifier} field is automatically generated and injected
* by {@link org.toop.framework.eventbus.EventFlow}. It should <strong>never</strong>
* be manually assigned by user code. (Exceptions may apply)
*/
public class NetworkEvents extends EventsBase {
// ------------------------------------------------------
// Generic Request & Response Events (no identifier)
// ------------------------------------------------------
/**
* Requests all active client connections.
*
* <p>This is a blocking event. The result will be delivered via the provided {@link
* CompletableFuture}.
*
* @param future CompletableFuture to receive the list of active {@link NetworkingClient}
* instances.
* Requests a list of all active networking clients.
* <p>
* This is a blocking request that returns the list asynchronously
* via the provided {@link CompletableFuture}.
*/
public record RequestsAllClients(CompletableFuture<List<NetworkingClient>> future)
implements EventWithoutSnowflake {}
implements GenericEvent {}
/** Forces all active client connections to close immediately. */
public record ForceCloseAllClients() implements EventWithoutSnowflake {}
/** Signals all active clients should be forcefully closed. */
public record ForceCloseAllClients() implements GenericEvent {}
/** Response indicating a challenge was cancelled. */
/** Indicates a challenge was cancelled by the server. */
public record ChallengeCancelledResponse(long clientId, String challengeId)
implements EventWithoutSnowflake {}
implements GenericEvent {}
/** Response indicating a challenge was received. */
public record ChallengeResponse(
long clientId, String challengerName, String challengeId, String gameType)
implements EventWithoutSnowflake {}
/** Indicates an incoming challenge from another player. */
public record ChallengeResponse(long clientId, String challengerName, String challengeId, String gameType)
implements GenericEvent {}
/** Response containing a list of players for a client. */
/** Contains the list of players currently available on the server. */
public record PlayerlistResponse(long clientId, String[] playerlist)
implements EventWithoutSnowflake {}
implements GenericEvent {}
/** Response containing a list of games for a client. */
/** Contains the list of available game types for a client. */
public record GamelistResponse(long clientId, String[] gamelist)
implements EventWithoutSnowflake {}
implements GenericEvent {}
/** Response indicating a game match information for a client. */
public record GameMatchResponse(
long clientId, String playerToMove, String gameType, String opponent)
implements EventWithoutSnowflake {}
/** Provides match information when a new game starts. */
public record GameMatchResponse(long clientId, String playerToMove, String gameType, String opponent)
implements GenericEvent {}
/** Response indicating the result of a game. */
/** Indicates the outcome or completion of a game. */
public record GameResultResponse(long clientId, String condition)
implements EventWithoutSnowflake {}
implements GenericEvent {}
/** Response indicating a game move occurred. */
/** Indicates that a game move has been processed or received. */
public record GameMoveResponse(long clientId, String player, String move, String details)
implements EventWithoutSnowflake {}
implements GenericEvent {}
/** Response indicating it is the player's turn. */
/** Indicates it is the current player's turn to move. */
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 {}
/** Requests a login operation for the given client. */
public record SendLogin(long clientId, String username)
implements GenericEvent {}
/** Request to log out a client. */
public record SendLogout(long clientId) implements EventWithoutSnowflake {}
/** Requests logout for the specified client. */
public record SendLogout(long clientId)
implements GenericEvent {}
/** Request to retrieve the player list for a client. */
public record SendGetPlayerlist(long clientId) implements EventWithoutSnowflake {}
/** Requests the player list from the server. */
public record SendGetPlayerlist(long clientId)
implements GenericEvent {}
/** Request to retrieve the game list for a client. */
public record SendGetGamelist(long clientId) implements EventWithoutSnowflake {}
/** Requests the game list from the server. */
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 {}
/** Requests a subscription to updates for a given game type. */
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 {}
/** Sends a game move command to the server. */
public record SendMove(long clientId, short moveNumber)
implements GenericEvent {}
/** Request to challenge another player. */
/** Requests to challenge another player to a game. */
public record SendChallenge(long clientId, String usernameToChallenge, String gameType)
implements EventWithoutSnowflake {}
implements GenericEvent {}
/** Request to accept a challenge. */
/** Requests to accept an existing challenge. */
public record SendAcceptChallenge(long clientId, int challengeId)
implements EventWithoutSnowflake {}
implements GenericEvent {}
/** Request to forfeit a game. */
public record SendForfeit(long clientId) implements EventWithoutSnowflake {}
/** Requests to forfeit the current game. */
public record SendForfeit(long clientId)
implements GenericEvent {}
/** Request to send a message from a client. */
public record SendMessage(long clientId, String message) implements EventWithoutSnowflake {}
/** Sends a chat or informational message from a client. */
public record SendMessage(long clientId, String message)
implements GenericEvent {}
/** Request to display help to a client. */
public record SendHelp(long clientId) implements EventWithoutSnowflake {}
/** Requests general help information from the server. */
public record SendHelp(long clientId)
implements GenericEvent {}
/** Request to display help for a specific command. */
/** Requests help information specific to a given command. */
public record SendHelpForCommand(long clientId, String command)
implements EventWithoutSnowflake {}
implements GenericEvent {}
/** Request to close a specific client connection. */
public record CloseClient(long clientId) implements EventWithoutSnowflake {}
/** Requests to close an active client connection. */
public record CloseClient(long clientId)
implements GenericEvent {}
/** A generic event indicating a raw server response. */
public record ServerResponse(long clientId)
implements GenericEvent {}
/**
* Event to start a new client connection.
* Sends a raw command string to the server.
*
* <p>Carries IP, port, and a unique event ID for correlation with responses.
*
* @param ip Server IP address.
* @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;
}
}
/**
* Response confirming a client was started.
*
* @param clientId The client ID assigned to the new connection.
* @param eventSnowflake 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;
}
}
/** Generic server response. */
public record ServerResponse(long clientId) implements EventWithoutSnowflake {}
/**
* Request to send a command to a server.
*
* @param clientId The client connection ID.
* @param clientId The client ID to send the command from.
* @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 {}
/** Event fired when a message is received from the server. */
public record ReceivedMessage(long clientId, String message)
implements GenericEvent {}
/** Indicates that a client connection has been closed. */
public record ClosedConnection(long clientId)
implements GenericEvent {}
// ------------------------------------------------------
// Unique Request & Response Events (with identifier)
// ------------------------------------------------------
/**
* Response triggered when a message is received from a server.
* Requests creation and connection of a new client.
* <p>
* The {@code identifier} is automatically assigned by {@link org.toop.framework.eventbus.EventFlow}
* to correlate with its corresponding {@link StartClientResponse}.
* </p>
*
* @param clientId The connection ID that received the message.
* @param message The message content.
* @param networkingClient The client instance to start.
* @param networkingConnector Connection details (host, port, etc.).
* @param identifier Automatically injected unique identifier.
*/
public record ReceivedMessage(long clientId, String message) implements EventWithoutSnowflake {}
public record StartClient(
NetworkingClient networkingClient,
NetworkingConnector networkingConnector,
long identifier)
implements UniqueEvent {}
/**
* Request to change a client connection to a new server.
* Response confirming that a client has been successfully started.
* <p>
* The {@code identifier} value is automatically propagated from
* the original {@link StartClient} request by {@link org.toop.framework.eventbus.EventFlow}.
* </p>
*
* @param clientId The client connection ID.
* @param ip The new server IP.
* @param port The new server port.
* @param clientId The newly assigned client ID.
* @param successful Whether the connection succeeded.
* @param identifier Automatically injected correlation ID.
*/
public record ChangeClientHost(long clientId, String ip, int port)
implements EventWithoutSnowflake {}
@AutoResponseResult
public record StartClientResponse(long clientId, boolean successful, long identifier)
implements ResponseToUniqueEvent {}
/** WIP (Not working) Response indicating that the client could not connect. */
public record CouldNotConnect(long clientId) implements EventWithoutSnowflake {}
/**
* Requests reconnection of an existing client using its previous configuration.
* <p>
* The {@code identifier} is automatically injected by {@link org.toop.framework.eventbus.EventFlow}.
* </p>
*/
public record Reconnect(
long clientId,
NetworkingClient networkingClient,
NetworkingConnector networkingConnector,
long identifier)
implements UniqueEvent {}
/** Event indicating a client connection was closed. */
public record ClosedConnection(long clientId) implements EventWithoutSnowflake {}
/** Response to a {@link Reconnect} event, carrying the success result. */
public record ReconnectResponse(boolean successful, long identifier)
implements ResponseToUniqueEvent {}
/**
* Requests to change the connection target (host/port) for a client.
* <p>
* The {@code identifier} is automatically injected by {@link org.toop.framework.eventbus.EventFlow}.
* </p>
*/
public record ChangeAddress(
long clientId,
NetworkingClient networkingClient,
NetworkingConnector networkingConnector,
long identifier)
implements UniqueEvent {}
/** Response to a {@link ChangeAddress} event, carrying the success result. */
public record ChangeAddressResponse(boolean successful, long identifier)
implements ResponseToUniqueEvent {}
}

View File

@@ -0,0 +1,25 @@
package org.toop.framework.networking.exceptions;
/**
* Thrown when an operation is attempted on a networking client
* that does not exist or has already been closed.
*/
public class ClientNotFoundException extends RuntimeException {
private final long clientId;
public ClientNotFoundException(long clientId) {
super("Networking client with ID " + clientId + " was not found.");
this.clientId = clientId;
}
public ClientNotFoundException(long clientId, Throwable cause) {
super("Networking client with ID " + clientId + " was not found.", cause);
this.clientId = clientId;
}
public long getClientId() {
return clientId;
}
}

View File

@@ -0,0 +1,21 @@
package org.toop.framework.networking.exceptions;
public class CouldNotConnectException extends RuntimeException {
private final long clientId;
public CouldNotConnectException(long clientId) {
super("Networking client with ID " + clientId + " could not connect.");
this.clientId = clientId;
}
public CouldNotConnectException(long clientId, Throwable cause) {
super("Networking client with ID " + clientId + " could not connect.", cause);
this.clientId = clientId;
}
public long getClientId() {
return clientId;
}
}

View File

@@ -1,4 +1,4 @@
package org.toop.framework.networking;
package org.toop.framework.networking.exceptions;
public class NetworkingInitializationException extends RuntimeException {
public NetworkingInitializationException(String message, Throwable cause) {

View File

@@ -1,4 +1,4 @@
package org.toop.framework.networking;
package org.toop.framework.networking.handlers;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
@@ -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)

View File

@@ -1,12 +0,0 @@
//package org.toop.frontend.networking.handlers;
//
//import io.netty.channel.ChannelHandlerContext;
//import org.apache.logging.log4j.LogManager;
//import org.apache.logging.log4j.Logger;
//import org.toop.frontend.networking.NetworkingGameClientHandler;
//
//public class NetworkingTicTacToeClientHandler extends NetworkingGameClientHandler {
// static final Logger logger = LogManager.getLogger(NetworkingTicTacToeClientHandler.class);
//
//
//}

View File

@@ -0,0 +1,13 @@
package org.toop.framework.networking.interfaces;
import org.toop.framework.networking.exceptions.CouldNotConnectException;
import java.net.InetSocketAddress;
public interface NetworkingClient {
InetSocketAddress getAddress();
void connect(long clientId, String host, int port) throws CouldNotConnectException;
boolean isActive();
void writeAndFlush(String msg);
void closeConnection();
}

View File

@@ -0,0 +1,17 @@
package org.toop.framework.networking.interfaces;
import org.toop.framework.networking.exceptions.ClientNotFoundException;
import org.toop.framework.networking.exceptions.CouldNotConnectException;
import org.toop.framework.networking.types.NetworkingConnector;
public interface NetworkingClientManager {
void startClient(
long id,
NetworkingClient nClient,
NetworkingConnector nConnector,
Runnable onSuccess,
Runnable onFailure
) throws CouldNotConnectException;
void sendCommand(long id, String command) throws ClientNotFoundException;
void closeClient(long id) throws ClientNotFoundException;
}

View File

@@ -0,0 +1,5 @@
package org.toop.framework.networking.types;
import java.util.concurrent.TimeUnit;
public record NetworkingConnector(String host, int port, int reconnectAttempts, long timeout, TimeUnit timeUnit) {}

View File

@@ -0,0 +1,3 @@
package org.toop.framework.networking.types;
public record ServerCommand(long clientId, String command) {}

View File

@@ -0,0 +1,3 @@
package org.toop.framework.networking.types;
public record ServerMessage(String message) {}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,10 @@
package org.toop.framework.resource.types;
public interface AudioResource {
String getName();
void updateVolume(double volume);
void play();
void stop();
void setOnEnd(Runnable run);
void setOnError(Runnable run);
}

View File

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

View File

@@ -0,0 +1,190 @@
package org.toop.framework.audio;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.toop.framework.dispatch.interfaces.Dispatcher;
import org.toop.framework.resource.resources.BaseResource;
import org.toop.framework.resource.types.AudioResource;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class MockAudioResource extends BaseResource implements AudioResource {
boolean played = false;
boolean stopped = false;
Runnable onEnd;
Runnable onError;
public MockAudioResource(String name) {
super(new File(name));
}
public void triggerError() {
if (onError != null) {
onError.run();
}
}
public void triggerEnd() {
if (onEnd != null) {
onEnd.run();
}
}
@Override
public String getName() {
return super.getFile().getName();
}
@Override
public void play() {
played = true;
}
@Override
public void stop() {
stopped = true;
}
@Override
public void setOnEnd(Runnable callback) {
onEnd = callback;
}
@Override
public void setOnError(Runnable callback) {
onError = callback;
}
@Override
public void updateVolume(double volume) {}
}
public class MusicManagerTest {
private Dispatcher dispatcher;
private MockAudioResource track1;
private MockAudioResource track2;
private MockAudioResource track3;
private MusicManager<MockAudioResource> manager;
@BeforeEach
void setUp() {
dispatcher = Runnable::run;
track1 = new MockAudioResource("track1");
track2 = new MockAudioResource("track2");
track3 = new MockAudioResource("track3");
List<MockAudioResource> resources = List.of(track1, track2, track3);
manager = new MusicManager<>(resources, dispatcher);
}
@Test
void testPlaySingleTrack() {
manager.play();
assertTrue(track1.played || track2.played || track3.played,
"At least one track should have played");
}
@Test
void testPlayMultipleTimesDoesNotRestart() {
manager.play();
track1.played = false;
manager.play();
assertFalse(track1.played, "Second play call should not restart tracks");
}
@Test
void testStopStopsAllTracks() {
manager.play();
manager.stop();
assertTrue(track1.stopped && track2.stopped && track3.stopped,
"All tracks should be stopped");
}
@Test
void testAutoAdvanceTracks() {
track1.played = false;
track2.played = false;
track3.played = false;
manager.play();
track1.triggerEnd();
track2.triggerEnd();
assertTrue(track1.played, "Track1 should play, played %s instead");
assertTrue(track2.played, "Track2 should play after track1 ends");
assertTrue(track3.played, "Track3 should play after track2 ends");
}
@Test
void testTrackErrorRemovesTrackAndPlaysNext() {
manager.play();
track1.triggerError();
assertFalse(manager.getActiveAudio().contains(track1),
"Track1 should be removed after error");
assertTrue(track2.played, "Track2 should play after track1 error");
}
@Test
void testPlayWithEmptyPlaylistDoesNothing() {
manager.getActiveAudio().clear();
manager.play();
assertFalse(track1.played || track2.played || track3.played,
"No tracks should play if playlist is empty");
}
@Test
void testMultiplePlayStopSequences() {
manager.play();
manager.stop();
manager.play();
assertTrue(track1.played || track2.played || track3.played,
"Tracks should play again after stopping");
}
@Test
void testPlayingIndexWrapsAround() {
track1.played = false;
track2.played = false;
track3.played = false;
manager.play();
track1.triggerEnd();
track2.triggerEnd();
track3.triggerEnd();
assertTrue(track1.played, "Track1 should play again after loop");
assertTrue(track2.played, "Track2 should play");
assertTrue(track3.played, "Track3 should play");
}
/**
* Test for many tracks playing sequentially one after another
*/
@Test
void testSequentialMultipleTracks() {
List<MockAudioResource> manyTracks = new ArrayList<>();
for (int i = 1; i <= 1_000; i++) {
manyTracks.add(new MockAudioResource("track" + i));
}
MusicManager<MockAudioResource> multiManager = new MusicManager<>(manyTracks, dispatcher);
for (int i = 0; i < manyTracks.size() - 1; i++) {
multiManager.play();
manyTracks.get(i).triggerEnd();
}
for (int i = 0; i < manyTracks.size(); i++) {
assertTrue(manyTracks.get(i).played, "Track " + (i + 1) + " should have played sequentially");
}
}
}

View File

@@ -0,0 +1,119 @@
package org.toop.framework.audio;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.toop.framework.resource.ResourceMeta;
import org.toop.framework.resource.resources.BaseResource;
import org.toop.framework.resource.types.AudioResource;
import java.io.File;
import java.util.List;
import java.util.ArrayList;
import java.util.Collection;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for SoundEffectManager.
*/
class MockSoundEffectResource extends BaseResource implements AudioResource {
boolean played = false;
boolean stopped = false;
public MockSoundEffectResource(String name) {
super(new File(name));
}
@Override
public String getName() {
return getFile().getName();
}
@Override
public void play() {
played = true;
}
@Override
public void stop() {
stopped = true;
}
@Override
public void setOnEnd(Runnable callback) {}
@Override
public void setOnError(Runnable callback) {}
@Override
public void updateVolume(double volume) {}
}
public class SoundEffectManagerTest {
private SoundEffectManager<MockAudioResource> manager;
private MockAudioResource sfx1;
private MockAudioResource sfx2;
private MockAudioResource sfx3;
@BeforeEach
void setUp() {
sfx1 = new MockAudioResource("explosion.wav");
sfx2 = new MockAudioResource("laser.wav");
sfx3 = new MockAudioResource("jump.wav");
List<ResourceMeta<MockAudioResource>> resources = List.of(
new ResourceMeta<>("explosion", sfx1),
new ResourceMeta<>("laser", sfx2),
new ResourceMeta<>("jump", sfx3)
);
manager = new SoundEffectManager<>(resources);
}
@Test
void testPlayValidSound() {
manager.play("explosion", false);
assertTrue(sfx1.played, "Sound 'explosion' should be played");
}
@Test
void testPlayInvalidSoundLogsWarning() {
// Nothing should crash or throw
assertDoesNotThrow(() -> manager.play("nonexistent", false));
}
@Test
void testStopValidSound() {
manager.stop("laser");
assertTrue(sfx2.stopped, "Sound 'laser' should be stopped");
}
@Test
void testStopInvalidSoundDoesNotThrow() {
assertDoesNotThrow(() -> manager.stop("does_not_exist"));
}
@Test
void testGetActiveAudioReturnsAll() {
Collection<MockAudioResource> active = manager.getActiveAudio();
assertEquals(3, active.size(), "All three sounds should be in active audio list");
assertTrue(active.containsAll(List.of(sfx1, sfx2, sfx3)));
}
@Test
void testDuplicateResourceKeepsLast() {
MockAudioResource oldRes = new MockAudioResource("duplicate_old.wav");
MockAudioResource newRes = new MockAudioResource("duplicate_new.wav");
List<ResourceMeta<MockAudioResource>> list = new ArrayList<>();
list.add(new ResourceMeta<>("dup", oldRes));
list.add(new ResourceMeta<>("dup", newRes)); // duplicate key
SoundEffectManager<MockAudioResource> dupManager = new SoundEffectManager<>(list);
dupManager.play("dup", false);
assertTrue(newRes.played, "New duplicate resource should override old one");
assertFalse(oldRes.played, "Old duplicate resource should be discarded");
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>&#45;&#45;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>-->
<!-- &lt;!&ndash; 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. &ndash;&gt;-->
<!-- </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>

65
pom.xml
View File

@@ -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
View 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>

View File

@@ -0,0 +1,7 @@
package org.toop.annotations;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface AutoResponseResult {}

View File

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

View File

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