mirror of
https://github.com/2OOP/pism.git
synced 2026-02-04 19:04:49 +00:00
Demo 5
* fast server connection
* Fixed bugs and oversights
* Renamed asset folder to resource, made resourceLoader more robust. Completed some TODO's, formatting
* AppSettings now also get loaded into the assetmanager
* start to reversi logic
* legal moves now get highlighted in red
* Moves flip dots. all tests pass. can play reversi local.
* Working state. Split AudioManager into 3 different branches for easier testing and srp
* Reworked to now use better defined generics and easier to use API. Added AudioResource to be used in changing volume
* Made all of the updated classes more generic for better flexibility in unittesting
* Added more flexible dependency injection to MusicManager for unittesting. Moved to event driven design for less complex code and lower runtime complexity.
* Split SoundEffectManager from AudioManager. (#171)
Clips no longer create a new clip instance each time they are played. A singular clip is made for each resource and is opened/closed when loaded/unloaded. When a clip is played that is already playing it'll stop playback and start again. Clip volume handling isn't done very well.
* Unit tests for MusicManager.java
* Hotfix for loading clip volume issue (#174)
* Fixed AudioVolumemanager, all volumes calculations are now made in VolumeTypes enum
* Added ability to remove a manager from VolumeTypes
* Removed file no longer in use
* Fixed grammer and spelling mistakes
* Renamed VOLUME to MASTERVOLUME for better naming
* Minor changes in API design
* Renamed VolumeTypes to VolumeControl. Made it thread safe. Added docs to VolumeControl and co.
removed .updateAllVolumes() in favor of auto updating inside enum instead
* Added ErrorProne for potential bugs. Fixed potential bugs.
* Small fixes.
* Removed no more needed code.
* Finished todo's
* Moved restrictedAPI to future release
* Finished todo's
* Moved restrictedAPI to future release
* Changed pom to be correct.
Fixed SnowflakeGenerator not making unique ids.
Changed naming for event implementation.
Automated id getter for events.
Added Error-Prone to all modules.
Added parents to all modules.
Added processors module.
* SoundEffectManager now generic
* Removed ResourceManager from AudioManagers
* Added linelistener to SoundEffectAsset
* commit
* commit ofzo
* Tests for SoundEffectManager
* getLegalMoves logic seems fixed //todo write better tests
* Tests toegevoegd
* punk toegevoegd
* Added shuffling on user request
* Reworked NetworkingClientManager into SRP model.
* Forgot to remove
* Improved API for dependency injection
* Some better docs.
* gui refactor
* fixed merge conflicts
* Added exceptions. Added reconnect attempts and changeable address
* Fixed event bug
* add: reversi game
* add: server chat box
* visual update
* Refactor to make Events easier to work with.
* Quick fix for closing connection.
* Documentation
* Correct client creation and user polling
* begin van audio display
* Polling music event, fires every 1 second
* Updated test.
* Updated timings
* Nuke everything on close.
* Basis Audio Display toegevoegd + standaard CSS toegevoegd
Kan nu zien hoe lang de song duurt, hoe lang ie al bezig is met draaien en de titel (-.mp3)
* Clips now also return positional information
* Skip Button
* Skip Button
* Fixes for garbage code by Omar
* Tiny fix when natural skip
* Small event fix
* Faster event schedule for PlayingMusic event
* test fix
* added method for sorting the flipped pieces by distance to Move.position
* new reversi test (both players no legal moves)
* connect4 with minimax AI
* Toegevoegd:
-Play Button + CSS + Events
-Previous Button + CSS + Events
-Changed interface for AudioResource to include a pause button which works really well with mediaplayer, however now SoundEffectAsset has an unnessescary pause
* Made it so that it indicates with the play/pause button if its paused or played
* add simple flip animations and fixed(?) server somewhat
* fixed tests
* can start game from playerlist screen
* tourney ready
* spam minder
* fixed setgamelabels
* spam minder v2
* canvas changes
* moved score out of game
* can now go to last using previous and being at the first song
* mainview false for sendchallengeview
* moved score out of game
* kleine ui fix
* updated music ma,es
* started working on the widget system
* iets met timing verkeerd temporary fix
* Fixes for garbage code by Omar
* Added replace to reduce boiler plate code
* Manually fallback to the fallback locale when a ResourceBundle is missing a resource key. Fallsback to "MISSING RESOUREC" if it's not present in the fallback.
* Removed unused import and unused parameter
* cool onhover effect for reversi
* Made the GameState enum it's own file and fixed imports
* Removed unused import
* Turned abstract methods into an interface
* Moved the Move record into it's own file, seperated from Game
* Removed unused imports
* Renamed Interface Playtable to IPlayable
* Turned Abstract Method for AI into interface
* Refactored Game to follow encapsulation principle
* Removed unused imports
* Applied encapsulation principle to TurnBasedBame.java
* Privated methods that didn't have to be public
* Reversi: made method private
* Changed checkForEarlyDraw so it doesn't need a game as input.
* Fixed warning "Warning:(27, 12) Copy constructor does not copy field 'mostRecentlyFlippedPieces'", removed unused field
* Made connect4 public method private
* half done with the widget system
* added some comments and made some methods a bit more readable
* widget system almost complete
* Functional code, is now object orientated
* Removed no more needed comments
* started a basis for the tutorials, tic tac toe is almost done with some general stuff still to do.
* rest van de tutorials toegevoegd
* resizable true
* fixed turn skip bug
fixed end score bug
now only shows legal and highlight moves when human
* Squashed commit of the following:
commit a517f2f302baa89f8ef59946a31c7bb59c56770f
Author: Stef <stbuwalda@gmail.com>
Date: Thu Nov 27 15:43:43 2025 +0100
Make it so the game shows "Waiting on ... to make their move". Styling isn't done but it is easier to see who's turn it is. There is a lot of structuring to do in the previous code...
* Fixed compilation errors
* Changed the way turns are being stored in TurnBasedGame.
* Removed views
* Added function input for enabling/disabling localization p/text
* Fix eventbus problems (#265)
* Added unsubscribe to EventFlow. ListenerHandler now functional. GlobalEventbus now user listenerHandler
* getAllListeners
* Removed nulls
* Fixed stress tests
* Added docs, no more list creation when adding events to the bus.
* Fixed unsubscribe not working.
* Moved away from deprecated functions
* moved from wildcard to typed
* Moved away from deprecated function
* Debugs for EventBus and fixed unsubscribe all (#266)
* Added unsubscribe to EventFlow. ListenerHandler now functional. GlobalEventbus now user listenerHandler
* getAllListeners
* Removed nulls
* Fixed stress tests
* Added docs, no more list creation when adding events to the bus.
* Fixed unsubscribe not working.
* Moved away from deprecated functions
* moved from wildcard to typed
* Moved away from deprecated function
* Added debugging to GlobalEventBus
* Fixed cleaning flow
* Fixed unsubscribe all
* Fixed unsubscribe all
* Removed unused import
* Fix music display not working (#267)
* Added unsubscribe to EventFlow. ListenerHandler now functional. GlobalEventbus now user listenerHandler
* getAllListeners
* Removed nulls
* Fixed stress tests
* Added docs, no more list creation when adding events to the bus.
* Fixed unsubscribe not working.
* Moved away from deprecated functions
* moved from wildcard to typed
* Moved away from deprecated function
* Added debugging to GlobalEventBus
* Fixed cleaning flow
* Fixed unsubscribe all
* Fixed unsubscribe all
* Removed unused import
* Added LoadingWidget.java for server feedback
* Replace deprecated with correct function
* Removed loading widget from Server.java
* Fixed old new EventFlow().listen() missing false as third param
* Tutorials to Dev (#264)
* Fixed garbage code
* added a pop button
* Tutorial images now use ImageAsset.java
* Added button to continue and start game. Refactors
* Refactored nextScreen runnable
* Removed unused imports
* Refactored switch statement
* Added documentation
* Removed space
* Added translations
* Added function input for enabling/disabling localization p/text
---------
Co-authored-by: ramollia <>
* Merge new framework into development (#269)
* Created a somewhat generic TurnBasedGame thread. Temporary UI that only works for TicTacToe rn. Added a LocalPlayer with the intent to add more players
* (RANDOM COMMIT) Hope it works
* Changes by bas
* Fixed dependency issues
* Fixed major issue in game deepcopy
* Merge conflict fix
* Removed unused import
* Update GTBGT branch from dev branch (#263)
* started a basis for the tutorials, tic tac toe is almost done with some general stuff still to do.
* rest van de tutorials toegevoegd
* Removed views
* Merge conflict fix
* Removed unused import
---------
Co-authored-by: michiel301b <m.brands.3@st.hanze.nl>
Co-authored-by: ramollia <>
Co-authored-by: Bas Antonius de Jong <49651652+BAFGdeJong@users.noreply.github.com>
* Revert "Update GTBGT branch from dev branch (#263)"
This reverts commit 9134d7e343.
* Fixed frontend not using GameController because of spaghetti code.
* Removed unused imports
* GameCanvas not implements a DrawPlayerMove that can be overridden for specific implementations
* Created an event that will request the controller to refresh the UI.
* ADDED DEPENDENCY. Renamed GameControllers to GameManagers, gameThread is not game controller.
* Attempt at adding an online player. I think it doesn't work because of unsubscriben after success not working
* Multiplayer is functional through OnlineThreadBehaviour. Empty slots are currently represented by -1 in the GUI.
* Removed sout spam, added logger than I can't get to work.
* Idek what these changes are
* Te lang geen commit, sorry
* Multiplayer seems to work pretty well now, hopefully I can add the other games soon.
* Added unsubscribe to EventFlow. ListenerHandler now functional. GlobalEventbus now user listenerHandler
* getAllListeners
* Removed nulls
* Inbetween commit of adding Reversi. This is a lot of spaghetti.
* Fixed stress tests
* Fixed typo in NetworkingGameClientHandler that prevented losses from being received
* Missed 2nd typo. Fixed
* Added docs, no more list creation when adding events to the bus.
* Fixed unsubscribe not working.
* Moved away from deprecated functions
* moved from wildcard to typed
* Moved away from deprecated function
* Added debugging to GlobalEventBus
* Fixed cleaning flow
* Fixed unsubscribe all
* Fixed unsubscribe all
* Removed unused import
* Works now with updated EventFlow(). Unsubscribing works. ReversiAIR has an issue where a forced move returns -1 and local play back button doesn't work properly. To be fixed
* Fixed ReversiR issue that caused skip turn desync
* Fixed color mismatch with server and online main player is now correct.
* Added a bunch of java doc and small changes
* Small changes
* Added a new Thread Behaviour to test framework.
* Fixed human error I made in TicTacToeR logic...
* Fixed broken event and wrong player being presented as winner.
* Idk changes
* Fixed PR conflicts
---------
Co-authored-by: michiel301b <m.brands.3@st.hanze.nl>
Co-authored-by: Bas Antonius de Jong <49651652+BAFGdeJong@users.noreply.github.com>
* added back button sounds because SOMEONE fucked it up.....
* 231 connecting to server feedback (#275)
* Added unsubscribe to EventFlow. ListenerHandler now functional. GlobalEventbus now user listenerHandler
* getAllListeners
* Removed nulls
* Fixed stress tests
* Added docs, no more list creation when adding events to the bus.
* Fixed unsubscribe not working.
* Moved away from deprecated functions
* moved from wildcard to typed
* Moved away from deprecated function
* Added debugging to GlobalEventBus
* Fixed cleaning flow
* Fixed unsubscribe all
* Fixed unsubscribe all
* Removed unused import
* Added LoadingWidget.java for server feedback
* Imports
* fixed loadingwidget
* Workable LoadingWidget and trying to connect to server
* Removed output
* Small bug temp fix
---------
Co-authored-by: ramollia <>
* Double loading call fix, LoadingWidget docs
* Main menu loader (#277)
* LoadingWidget main menu
* fixed garbage code
* Fixed garbage code 2
* LoadWidget fix, added loading to starting the game. Removed unnecessary console output
---------
Co-authored-by: ramollia <>
* Fixed systems starting, before assets being loaded (I am retarded)
* Added infinite boolean, fixed loading behaviour at startup
* 272 remake game framework interfaces to properly represent vmc (#278)
* Cleaned up a lot of old files and renamed/remade interfaces to better suit the framework
* Broken commit
* Fixed online play
* Better file structure and closer to MVC
* Best fix for white screen at start
* Making threads verbose regarding exceptions
* Loading circle, better loading colors.
* Event bus now testable, improved UI (#284)
* turn updates
* smalle fixes aan turn updates
* better human/ai selector with bot selection and depth on TicTacToeAIR
* depth + thinktime back to AIs, along with a a specific TicTacToeAIRSleep
* fixed overlapping back and disconnect buttons
* Changed to debug instead of info
* changed the transitionNextCustom to be easier to use
* added getAllWidgets to WidgetContainer
* Correct back view
* added replacePrevious in ViewWidget
* added removeIndexFromPreviousChain
* fixed incorrect index counting
* Fixt wrong view order
* Removed todo
* Challenge popups "Fixed"
* Popups now remove themselves
* localize the ChallengePopup text
* made the game text a header instead
* fixed getAllWidgets
* Escape popup
* fixed redundant container
* Escape remove popup
* Working escape menu
* Added find functionality
* Tutorials moved to escape menu
* Escape can't be opened in mainview now
* Can now test the event bus, created testable interfaces
* Logging errors
* Made events and handlers more generic
* Suppress
* Managers now have changeable eventbus
* Tutorials fixed
* Removed import
* Single threaded eventbus
* Fixed wrong eventbus
* Removed get
* Removed old code
* Renaming
* Optimization
* Removed useless comment
* Removed unnecessary imports
* Rename
* Renaming, refactor and type safety
* Rename
* Removed import
---------
Co-authored-by: michiel301b <m.brands.3@st.hanze.nl>
Co-authored-by: ramollia <>
* initSystems now uses latch instead of timer. Moved single threads to Executor
* Safety
* Deleted unnecessary imports
* Code cleanup
* changed "fullscreen exit key combination" from esc to F11
* shitty fix for player selector spacing issue
* shitty fix for player selector spacing issue v2
* fixed reversi colors being switched, causing multiple issues
* Merge bitboards into development (#285)
* added new classes for the games that use bitboards instead. also combined game with turnbasedgame
* (DOES NOT COMPILE) In-between commit
* turn updates
* smalle fixes aan turn updates
* Bitboard implemented with scuffed TicTacToe translation done by game. This should be done by the view.
* Almost done with implementing bitboards. Reversi is broken and artifical players don't work yet.
* better human/ai selector with bot selection and depth on TicTacToeAIR
* fixed getLegalMoves
* depth + thinktime back to AIs, along with a a specific TicTacToeAIRSleep
* fixed overlapping back and disconnect buttons
* Changed to debug instead of info
* changed the transitionNextCustom to be easier to use
* added getAllWidgets to WidgetContainer
* Correct back view
* added replacePrevious in ViewWidget
* added removeIndexFromPreviousChain
* fixed incorrect index counting
* Fixt wrong view order
* fixed? getLegalMoves
* Everything is broken
* Removed todo
* fixed getLegalMoves & getFlips
* Challenge popups "Fixed"
* Fixed local and online play for both games
* Popups now remove themselves
* Removed souts for debugging
* localize the ChallengePopup text
* made the game text a header instead
* made more classes deepClonable.
* fixed getAllWidgets
* Added comment
* Escape popup
* fixed redundant container
* Made all network events async again
* Escape remove popup
* Working escape menu
* Removed old AI and old files. Added a new generic random AI. game no longer deals with translation.
* Drawing of board on canvas is now done from bitboards rather than translating.
* Added a method getWinner() to game interface.Controller now tells gameThreads how to deal with drawing UI and sending a move to server.
* Added find functionality
* Added a ChatGPT generated MiniMaxAI based on the old MiniMaxAI but with alpha-beta pruning and heuristics for Reversi
* Removed System-Outs to clean up console
* Update BitGameCanvas.java
* Merge fixes
* Removed unused imports
---------
Co-authored-by: ramollia <>
Co-authored-by: michiel301b <m.brands.3@st.hanze.nl>
Co-authored-by: lieght <49651652+BAFGdeJong@users.noreply.github.com>
* Better limits to generic acceptance
* Will fix tests etc later
---------
Co-authored-by: ramollia <@>
Co-authored-by: Ticho Hidding <tichohidding@gmail.com>
Co-authored-by: Stef <48526421+StefBuwalda@users.noreply.github.com>
Co-authored-by: Stef <stbuwalda@gmail.com>
Co-authored-by: michiel <m.brands.3@st.hanze.nl>
Co-authored-by: ramollia <>
Co-authored-by: tichohidding <58555714+tichohidding@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
afb4844084
commit
8146be16ed
@@ -6,197 +6,198 @@ import org.apache.logging.log4j.core.LoggerContext;
|
||||
import org.apache.logging.log4j.core.config.Configuration;
|
||||
import org.apache.logging.log4j.core.config.LoggerConfig;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Utility class for configuring logging levels dynamically at runtime using Log4j 2.
|
||||
*
|
||||
* <p>Provides methods to enable or disable logs globally or per class, with support for specifying
|
||||
* log levels either via {@link Level} enums or string names.
|
||||
*/
|
||||
// Todo: refactor
|
||||
public final class Logging {
|
||||
|
||||
/** Disables all logging globally by setting the root logger level to {@link Level#OFF}. */
|
||||
public static void disableAllLogs() {
|
||||
LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
|
||||
Configuration config = ctx.getConfiguration();
|
||||
LoggerConfig rootLoggerConfig = config.getRootLogger();
|
||||
rootLoggerConfig.setLevel(Level.OFF);
|
||||
ctx.updateLoggers();
|
||||
}
|
||||
/** Disables all logging globally by setting the root logger level to {@link Level#OFF}. */
|
||||
public static void disableAllLogs() {
|
||||
LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
|
||||
Configuration config = ctx.getConfiguration();
|
||||
LoggerConfig rootLoggerConfig = config.getRootLogger();
|
||||
rootLoggerConfig.setLevel(Level.OFF);
|
||||
ctx.updateLoggers();
|
||||
}
|
||||
|
||||
/** Enables all logging globally by setting the root logger level to {@link Level#ALL}. */
|
||||
public static void enableAllLogs() {
|
||||
LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
|
||||
Configuration config = ctx.getConfiguration();
|
||||
LoggerConfig rootLoggerConfig = config.getRootLogger();
|
||||
rootLoggerConfig.setLevel(Level.ALL);
|
||||
ctx.updateLoggers();
|
||||
}
|
||||
/** Enables all logging globally by setting the root logger level to {@link Level#ALL}. */
|
||||
public static void enableAllLogs() {
|
||||
LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
|
||||
Configuration config = ctx.getConfiguration();
|
||||
LoggerConfig rootLoggerConfig = config.getRootLogger();
|
||||
rootLoggerConfig.setLevel(Level.ALL);
|
||||
ctx.updateLoggers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables global logging at a specific level by setting the root logger.
|
||||
*
|
||||
* @param level the logging level to enable for all logs
|
||||
*/
|
||||
public static void enableAllLogs(Level level) {
|
||||
LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
|
||||
Configuration config = ctx.getConfiguration();
|
||||
LoggerConfig rootLoggerConfig = config.getRootLogger();
|
||||
rootLoggerConfig.setLevel(level);
|
||||
ctx.updateLoggers();
|
||||
}
|
||||
/**
|
||||
* Enables global logging at a specific level by setting the root logger.
|
||||
*
|
||||
* @param level the logging level to enable for all logs
|
||||
*/
|
||||
public static void enableAllLogs(Level level) {
|
||||
LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
|
||||
Configuration config = ctx.getConfiguration();
|
||||
LoggerConfig rootLoggerConfig = config.getRootLogger();
|
||||
rootLoggerConfig.setLevel(level);
|
||||
ctx.updateLoggers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies whether the provided string corresponds to a valid class name.
|
||||
*
|
||||
* @param className fully-qualified class name to check
|
||||
* @return true if the class exists, false otherwise
|
||||
*/
|
||||
private static boolean verifyStringIsActualClass(String className) {
|
||||
try {
|
||||
Class.forName(className);
|
||||
return true;
|
||||
} catch (ClassNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Verifies whether the provided string corresponds to a valid class name.
|
||||
*
|
||||
* @param className fully-qualified class name to check
|
||||
* @return true if the class exists, false otherwise
|
||||
*/
|
||||
private static boolean verifyStringIsActualClass(String className) {
|
||||
try {
|
||||
Class.forName(className);
|
||||
return true;
|
||||
} catch (ClassNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal helper to disable logs for a specific class by name.
|
||||
*
|
||||
* @param className fully-qualified class name
|
||||
*/
|
||||
private static void disableLogsForClassInternal(String className) {
|
||||
LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
|
||||
Configuration config = ctx.getConfiguration();
|
||||
config.removeLogger(className);
|
||||
LoggerConfig specificConfig = new LoggerConfig(className, Level.OFF, false);
|
||||
config.addLogger(className, specificConfig);
|
||||
ctx.updateLoggers();
|
||||
}
|
||||
/**
|
||||
* Internal helper to disable logs for a specific class by name.
|
||||
*
|
||||
* @param className fully-qualified class name
|
||||
*/
|
||||
private static void disableLogsForClassInternal(String className) {
|
||||
LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
|
||||
Configuration config = ctx.getConfiguration();
|
||||
config.removeLogger(className);
|
||||
LoggerConfig specificConfig = new LoggerConfig(className, Level.OFF, false);
|
||||
config.addLogger(className, specificConfig);
|
||||
ctx.updateLoggers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables logs for a specific class.
|
||||
*
|
||||
* @param class_ the class for which logs should be disabled
|
||||
* @param <T> type of the class
|
||||
*/
|
||||
public static <T> void disableLogsForClass(Class<T> class_) {
|
||||
disableLogsForClassInternal(class_.getName());
|
||||
}
|
||||
/**
|
||||
* Disables logs for a specific class.
|
||||
*
|
||||
* @param class_ the class for which logs should be disabled
|
||||
* @param <T> type of the class
|
||||
*/
|
||||
public static <T> void disableLogsForClass(Class<T> class_) {
|
||||
disableLogsForClassInternal(class_.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables logs for a class specified by fully-qualified name, if the class exists.
|
||||
*
|
||||
* @param className fully-qualified class name
|
||||
*/
|
||||
public static void disableLogsForClass(String className) {
|
||||
if (verifyStringIsActualClass(className)) {
|
||||
disableLogsForClassInternal(className);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Disables logs for a class specified by fully-qualified name, if the class exists.
|
||||
*
|
||||
* @param className fully-qualified class name
|
||||
*/
|
||||
public static void disableLogsForClass(String className) {
|
||||
if (verifyStringIsActualClass(className)) {
|
||||
disableLogsForClassInternal(className);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal helper to enable logs for a specific class at a specific level.
|
||||
*
|
||||
* @param className fully-qualified class name
|
||||
* @param level logging level to set
|
||||
*/
|
||||
private static void enableLogsForClassInternal(String className, Level level) {
|
||||
LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
|
||||
Configuration config = ctx.getConfiguration();
|
||||
LoggerConfig loggerConfig = config.getLoggers().get(className);
|
||||
if (loggerConfig == null) {
|
||||
loggerConfig = new LoggerConfig(className, level, false);
|
||||
config.addLogger(className, loggerConfig);
|
||||
} else {
|
||||
loggerConfig.setLevel(level);
|
||||
}
|
||||
ctx.updateLoggers();
|
||||
}
|
||||
/**
|
||||
* Internal helper to enable logs for a specific class at a specific level.
|
||||
*
|
||||
* @param className fully-qualified class name
|
||||
* @param level logging level to set
|
||||
*/
|
||||
private static void enableLogsForClassInternal(String className, Level level) {
|
||||
LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
|
||||
Configuration config = ctx.getConfiguration();
|
||||
LoggerConfig loggerConfig = config.getLoggers().get(className);
|
||||
if (loggerConfig == null) {
|
||||
loggerConfig = new LoggerConfig(className, level, false);
|
||||
config.addLogger(className, loggerConfig);
|
||||
} else {
|
||||
loggerConfig.setLevel(level);
|
||||
}
|
||||
ctx.updateLoggers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables logging for a class at a specific level.
|
||||
*
|
||||
* @param class_ class to configure
|
||||
* @param levelToLog the logging level to set
|
||||
* @param <T> type of the class
|
||||
*/
|
||||
public static <T> void enableLogsForClass(Class<T> class_, Level levelToLog) {
|
||||
enableLogsForClassInternal(class_.getName(), levelToLog);
|
||||
}
|
||||
/**
|
||||
* Enables logging for a class at a specific level.
|
||||
*
|
||||
* @param class_ class to configure
|
||||
* @param levelToLog the logging level to set
|
||||
* @param <T> type of the class
|
||||
*/
|
||||
public static <T> void enableLogsForClass(Class<T> class_, Level levelToLog) {
|
||||
enableLogsForClassInternal(class_.getName(), levelToLog);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables logging for a class specified by name at a specific level, if the class exists.
|
||||
*
|
||||
* @param className fully-qualified class name
|
||||
* @param levelToLog the logging level to set
|
||||
*/
|
||||
public static void enableLogsForClass(String className, Level levelToLog) {
|
||||
if (verifyStringIsActualClass(className)) {
|
||||
enableLogsForClassInternal(className, levelToLog);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Enables logging for a class specified by name at a specific level, if the class exists.
|
||||
*
|
||||
* @param className fully-qualified class name
|
||||
* @param levelToLog the logging level to set
|
||||
*/
|
||||
public static void enableLogsForClass(String className, Level levelToLog) {
|
||||
if (verifyStringIsActualClass(className)) {
|
||||
enableLogsForClassInternal(className, levelToLog);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables logging for a class specified by name at a specific level using a string.
|
||||
*
|
||||
* @param className fully-qualified class name
|
||||
* @param levelToLog name of the logging level (e.g., "DEBUG", "INFO")
|
||||
*/
|
||||
public static void enableLogsForClass(String className, String levelToLog) {
|
||||
Level level = Level.valueOf(levelToLog.trim().toUpperCase());
|
||||
if (level != null && verifyStringIsActualClass(className)) {
|
||||
enableLogsForClassInternal(className, level);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Enables logging for a class specified by name at a specific level using a string.
|
||||
*
|
||||
* @param className fully-qualified class name
|
||||
* @param levelToLog name of the logging level (e.g., "DEBUG", "INFO")
|
||||
*/
|
||||
public static void enableLogsForClass(String className, String levelToLog) {
|
||||
Level level = Level.valueOf(levelToLog.trim().toUpperCase(Locale.ROOT));
|
||||
if (level != null && verifyStringIsActualClass(className)) {
|
||||
enableLogsForClassInternal(className, level);
|
||||
}
|
||||
}
|
||||
|
||||
/** Convenience methods for enabling logs at specific levels for classes. */
|
||||
public static <T> void enableAllLogsForClass(Class<T> class_) {
|
||||
enableLogsForClass(class_, Level.ALL);
|
||||
}
|
||||
/** Convenience methods for enabling logs at specific levels for classes. */
|
||||
public static <T> void enableAllLogsForClass(Class<T> class_) {
|
||||
enableLogsForClass(class_, Level.ALL);
|
||||
}
|
||||
|
||||
public static void enableAllLogsForClass(String className) {
|
||||
enableLogsForClass(className, Level.ALL);
|
||||
}
|
||||
public static void enableAllLogsForClass(String className) {
|
||||
enableLogsForClass(className, Level.ALL);
|
||||
}
|
||||
|
||||
public static <T> void enableDebugLogsForClass(Class<T> class_) {
|
||||
enableLogsForClass(class_, Level.DEBUG);
|
||||
}
|
||||
public static <T> void enableDebugLogsForClass(Class<T> class_) {
|
||||
enableLogsForClass(class_, Level.DEBUG);
|
||||
}
|
||||
|
||||
public static void enableDebugLogsForClass(String className) {
|
||||
enableLogsForClass(className, Level.DEBUG);
|
||||
}
|
||||
public static void enableDebugLogsForClass(String className) {
|
||||
enableLogsForClass(className, Level.DEBUG);
|
||||
}
|
||||
|
||||
public static <T> void enableErrorLogsForClass(Class<T> class_) {
|
||||
enableLogsForClass(class_, Level.ERROR);
|
||||
}
|
||||
public static <T> void enableErrorLogsForClass(Class<T> class_) {
|
||||
enableLogsForClass(class_, Level.ERROR);
|
||||
}
|
||||
|
||||
public static void enableErrorLogsForClass(String className) {
|
||||
enableLogsForClass(className, Level.ERROR);
|
||||
}
|
||||
public static void enableErrorLogsForClass(String className) {
|
||||
enableLogsForClass(className, Level.ERROR);
|
||||
}
|
||||
|
||||
public static <T> void enableFatalLogsForClass(Class<T> class_) {
|
||||
enableLogsForClass(class_, Level.FATAL);
|
||||
}
|
||||
public static <T> void enableFatalLogsForClass(Class<T> class_) {
|
||||
enableLogsForClass(class_, Level.FATAL);
|
||||
}
|
||||
|
||||
public static void enableFatalLogsForClass(String className) {
|
||||
enableLogsForClass(className, Level.FATAL);
|
||||
}
|
||||
public static void enableFatalLogsForClass(String className) {
|
||||
enableLogsForClass(className, Level.FATAL);
|
||||
}
|
||||
|
||||
public static <T> void enableInfoLogsForClass(Class<T> class_) {
|
||||
enableLogsForClass(class_, Level.INFO);
|
||||
}
|
||||
public static <T> void enableInfoLogsForClass(Class<T> class_) {
|
||||
enableLogsForClass(class_, Level.INFO);
|
||||
}
|
||||
|
||||
public static void enableInfoLogsForClass(String className) {
|
||||
enableLogsForClass(className, Level.INFO);
|
||||
}
|
||||
public static void enableInfoLogsForClass(String className) {
|
||||
enableLogsForClass(className, Level.INFO);
|
||||
}
|
||||
|
||||
public static <T> void enableTraceLogsForClass(Class<T> class_) {
|
||||
enableLogsForClass(class_, Level.TRACE);
|
||||
}
|
||||
public static <T> void enableTraceLogsForClass(Class<T> class_) {
|
||||
enableLogsForClass(class_, Level.TRACE);
|
||||
}
|
||||
|
||||
public static void enableTraceLogsForClass(String className) {
|
||||
enableLogsForClass(className, Level.TRACE);
|
||||
}
|
||||
}
|
||||
public static void enableTraceLogsForClass(String className) {
|
||||
enableLogsForClass(className, Level.TRACE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
package org.toop.framework.asset.events;
|
||||
|
||||
import org.toop.framework.eventbus.events.EventWithoutSnowflake;
|
||||
|
||||
public class AssetLoaderEvents {
|
||||
public record LoadingProgressUpdate(int hasLoadedAmount, int isLoadingAmount)
|
||||
implements EventWithoutSnowflake {}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package org.toop.framework.asset.resources;
|
||||
|
||||
import java.io.*;
|
||||
import javafx.scene.media.Media;
|
||||
import org.toop.framework.asset.types.FileExtension;
|
||||
import org.toop.framework.asset.types.LoadableResource;
|
||||
|
||||
@FileExtension({"mp3"})
|
||||
public class MusicAsset extends BaseResource implements LoadableResource {
|
||||
private Media media;
|
||||
|
||||
public MusicAsset(final File audioFile) {
|
||||
super(audioFile);
|
||||
}
|
||||
|
||||
public Media getMedia() {
|
||||
if (media == null) {
|
||||
media = new Media(file.toURI().toString());
|
||||
}
|
||||
return media;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load() {
|
||||
if (media == null) media = new Media(file.toURI().toString());
|
||||
this.isLoaded = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unload() {
|
||||
media = null;
|
||||
isLoaded = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoaded() {
|
||||
return isLoaded;
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
package org.toop.framework.asset.resources;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import javax.sound.sampled.*;
|
||||
import org.toop.framework.asset.types.FileExtension;
|
||||
import org.toop.framework.asset.types.LoadableResource;
|
||||
|
||||
@FileExtension({"wav"})
|
||||
public class SoundEffectAsset extends BaseResource implements LoadableResource {
|
||||
private byte[] rawData;
|
||||
|
||||
public SoundEffectAsset(final File audioFile) {
|
||||
super(audioFile);
|
||||
}
|
||||
|
||||
// Gets a new clip to play
|
||||
public Clip getNewClip()
|
||||
throws LineUnavailableException, UnsupportedAudioFileException, IOException {
|
||||
// Get a new clip from audio system
|
||||
Clip clip = AudioSystem.getClip();
|
||||
|
||||
// Insert a new audio stream into the clip
|
||||
AudioInputStream inputStream = this.getAudioStream();
|
||||
AudioFormat baseFormat = inputStream.getFormat();
|
||||
if (baseFormat.getSampleSizeInBits() > 16)
|
||||
inputStream = downSampleAudio(inputStream, baseFormat);
|
||||
clip.open(
|
||||
inputStream); // ^ Clip can only run 16 bit and lower, thus downsampling necessary.
|
||||
return clip;
|
||||
}
|
||||
|
||||
// Generates a new audio stream from byte array
|
||||
private AudioInputStream getAudioStream() throws UnsupportedAudioFileException, IOException {
|
||||
// Check if raw data is loaded into memory
|
||||
if (!this.isLoaded()) {
|
||||
this.load();
|
||||
}
|
||||
|
||||
// Turn rawData into an input stream and turn that into an audio input stream;
|
||||
return AudioSystem.getAudioInputStream(new ByteArrayInputStream(this.rawData));
|
||||
}
|
||||
|
||||
private AudioInputStream downSampleAudio(
|
||||
AudioInputStream audioInputStream, AudioFormat baseFormat) {
|
||||
AudioFormat decodedFormat =
|
||||
new AudioFormat(
|
||||
AudioFormat.Encoding.PCM_SIGNED,
|
||||
baseFormat.getSampleRate(),
|
||||
16, // force 16-bit
|
||||
baseFormat.getChannels(),
|
||||
baseFormat.getChannels() * 2,
|
||||
baseFormat.getSampleRate(),
|
||||
false // little-endian
|
||||
);
|
||||
|
||||
return AudioSystem.getAudioInputStream(decodedFormat, audioInputStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load() {
|
||||
try {
|
||||
this.rawData = Files.readAllBytes(file.toPath());
|
||||
this.isLoaded = true;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unload() {
|
||||
this.rawData = null;
|
||||
this.isLoaded = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoaded() {
|
||||
return this.isLoaded;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package org.toop.framework.audio;
|
||||
|
||||
import org.toop.framework.audio.events.AudioEvents;
|
||||
import org.toop.framework.audio.interfaces.MusicManager;
|
||||
import org.toop.framework.audio.interfaces.SoundEffectManager;
|
||||
import org.toop.framework.audio.interfaces.VolumeManager;
|
||||
import org.toop.framework.eventbus.EventFlow;
|
||||
import org.toop.framework.eventbus.bus.EventBus;
|
||||
import org.toop.framework.resource.types.AudioResource;
|
||||
|
||||
public class AudioEventListener<T extends AudioResource, K extends AudioResource> {
|
||||
private final EventBus eventBus;
|
||||
private final MusicManager<T> musicManager;
|
||||
private final SoundEffectManager<K> soundEffectManager;
|
||||
private final VolumeManager audioVolumeManager;
|
||||
|
||||
public AudioEventListener(
|
||||
EventBus eventBus,
|
||||
MusicManager<T> musicManager,
|
||||
SoundEffectManager<K> soundEffectManager,
|
||||
VolumeManager audioVolumeManager
|
||||
) {
|
||||
this.eventBus = eventBus;
|
||||
this.musicManager = musicManager;
|
||||
this.soundEffectManager = soundEffectManager;
|
||||
this.audioVolumeManager = audioVolumeManager;
|
||||
}
|
||||
|
||||
public AudioEventListener<?, ?> initListeners(String buttonSoundToPlay) {
|
||||
new EventFlow(eventBus)
|
||||
.listen(AudioEvents.StopAudioManager.class, this::handleStopMusicManager, false)
|
||||
.listen(AudioEvents.PlayEffect.class, this::handlePlaySound, false)
|
||||
.listen(AudioEvents.SkipMusic.class, this::handleSkipSong, false)
|
||||
.listen(AudioEvents.PauseMusic.class, this::handlePauseSong, false)
|
||||
.listen(AudioEvents.PreviousMusic.class, this::handlePreviousSong, false)
|
||||
.listen(AudioEvents.StopEffect.class, this::handleStopSound, false)
|
||||
.listen(AudioEvents.StartBackgroundMusic.class, this::handleMusicStart, false)
|
||||
.listen(AudioEvents.ChangeVolume.class, this::handleVolumeChange, false)
|
||||
.listen(AudioEvents.GetVolume.class, this::handleGetVolume,false)
|
||||
.listen(AudioEvents.ClickButton.class, _ -> soundEffectManager.play(buttonSoundToPlay, false), false);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private void handleStopMusicManager(AudioEvents.StopAudioManager event) {
|
||||
this.musicManager.stop();
|
||||
}
|
||||
|
||||
private void handlePlaySound(AudioEvents.PlayEffect event) {
|
||||
this.soundEffectManager.play(event.fileName(), event.loop());
|
||||
}
|
||||
|
||||
private void handleSkipSong(AudioEvents.SkipMusic event) {
|
||||
this.musicManager.skip();
|
||||
}
|
||||
|
||||
private void handlePauseSong(AudioEvents.PauseMusic event) {
|
||||
this.musicManager.pause();
|
||||
|
||||
}
|
||||
|
||||
private void handlePreviousSong(AudioEvents.PreviousMusic event) {
|
||||
this.musicManager.previous();
|
||||
}
|
||||
|
||||
private void handleStopSound(AudioEvents.StopEffect event) {
|
||||
this.soundEffectManager.stop(event.fileName());
|
||||
}
|
||||
|
||||
private void handleMusicStart(AudioEvents.StartBackgroundMusic event) {
|
||||
this.musicManager.play();
|
||||
}
|
||||
|
||||
private void handleVolumeChange(AudioEvents.ChangeVolume event) {
|
||||
this.audioVolumeManager.setVolume(event.newVolume() / 100, event.controlType());
|
||||
}
|
||||
|
||||
private void handleGetVolume(AudioEvents.GetVolume event) {
|
||||
eventBus.post(new AudioEvents.GetVolumeResponse(
|
||||
audioVolumeManager.getVolume(event.controlType()),
|
||||
event.identifier()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,100 +1,84 @@
|
||||
package org.toop.framework.audio;
|
||||
|
||||
import javafx.scene.media.MediaPlayer;
|
||||
import javax.sound.sampled.Clip;
|
||||
import javax.sound.sampled.FloatControl;
|
||||
import org.toop.framework.audio.events.AudioEvents;
|
||||
import org.toop.framework.eventbus.EventFlow;
|
||||
import org.toop.framework.audio.interfaces.AudioManager;
|
||||
import org.toop.framework.audio.interfaces.VolumeManager;
|
||||
import org.toop.framework.resource.types.AudioResource;
|
||||
|
||||
public class AudioVolumeManager {
|
||||
private final SoundManager sM;
|
||||
/**
|
||||
* Concrete implementation of {@link VolumeManager} that delegates volume control
|
||||
* to the {@link VolumeControl} enum.
|
||||
* <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);
|
||||
System.out.println(this.musicVolume);
|
||||
System.out.println(this.volume);
|
||||
for (MediaPlayer mediaPlayer : sM.getActiveMusic()) {
|
||||
this.updateMusicVolume(mediaPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleGetCurrentVolume(AudioEvents.GetCurrentVolume event) {
|
||||
new EventFlow()
|
||||
.addPostEvent(
|
||||
new AudioEvents.GetCurrentVolumeResponse(volume * 100, event.snowflakeId()))
|
||||
.asyncPostEvent();
|
||||
}
|
||||
|
||||
private void handleGetCurrentFxVolume(AudioEvents.GetCurrentFxVolume event) {
|
||||
new EventFlow()
|
||||
.addPostEvent(
|
||||
new AudioEvents.GetCurrentFxVolumeResponse(
|
||||
fxVolume * 100, event.snowflakeId()))
|
||||
.asyncPostEvent();
|
||||
}
|
||||
|
||||
private void handleGetCurrentMusicVolume(AudioEvents.GetCurrentMusicVolume event) {
|
||||
new EventFlow()
|
||||
.addPostEvent(
|
||||
new AudioEvents.GetCurrentMusicVolumeResponse(
|
||||
musicVolume * 100, event.snowflakeId()))
|
||||
.asyncPostEvent();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
package org.toop.framework.audio;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.toop.framework.audio.events.AudioEvents;
|
||||
import org.toop.framework.dispatch.interfaces.Dispatcher;
|
||||
import org.toop.framework.dispatch.JavaFXDispatcher;
|
||||
import org.toop.annotations.TestsOnly;
|
||||
import org.toop.framework.eventbus.bus.EventBus;
|
||||
import org.toop.framework.resource.types.AudioResource;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class MusicManager<T extends AudioResource> implements org.toop.framework.audio.interfaces.MusicManager<T> {
|
||||
private static final Logger logger = LogManager.getLogger(MusicManager.class);
|
||||
|
||||
private final EventBus eventBus;
|
||||
private final List<T> backgroundMusic = new ArrayList<>();
|
||||
private final Dispatcher dispatcher;
|
||||
private final List<T> resources;
|
||||
private int playingIndex = 0;
|
||||
private boolean playing = false;
|
||||
private long pausedPosition = 0;
|
||||
private ScheduledExecutorService scheduler;
|
||||
|
||||
|
||||
public MusicManager(EventBus eventbus, List<T> resources, boolean shuffleMusic) {
|
||||
this.eventBus = eventbus;
|
||||
this.dispatcher = new JavaFXDispatcher();
|
||||
this.resources = resources;
|
||||
// Shuffle if wanting to shuffle
|
||||
if (shuffleMusic) createShuffled();
|
||||
else backgroundMusic.addAll(resources);
|
||||
// ------------------------------
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code @TestsOnly} DO NOT USE
|
||||
*/
|
||||
@TestsOnly
|
||||
public MusicManager(EventBus eventBus, List<T> resources, Dispatcher dispatcher) {
|
||||
this.eventBus = eventBus;
|
||||
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();
|
||||
}
|
||||
|
||||
public void skip() {
|
||||
if (backgroundMusic.isEmpty()) return;
|
||||
stop();
|
||||
scheduler.shutdownNow();
|
||||
playingIndex = playingIndex + 1;
|
||||
playing = true;
|
||||
playCurrentTrack();
|
||||
}
|
||||
|
||||
// Used in testing
|
||||
void play(int index) {
|
||||
if (playing) {
|
||||
logger.warn("MusicManager is already playing.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (backgroundMusic.isEmpty()) return;
|
||||
|
||||
playingIndex = index;
|
||||
playing = true;
|
||||
playCurrentTrack();
|
||||
}
|
||||
|
||||
private void playCurrentTrack() {
|
||||
if (playingIndex >= backgroundMusic.size()) {
|
||||
playingIndex = 0;
|
||||
}
|
||||
|
||||
T current = backgroundMusic.get(playingIndex);
|
||||
|
||||
if (current == null) {
|
||||
logger.error("Current track is null!");
|
||||
return;
|
||||
}
|
||||
|
||||
dispatcher.run(() -> {
|
||||
current.play();
|
||||
|
||||
setTrackRunnable(current);
|
||||
});
|
||||
}
|
||||
|
||||
private void setTrackRunnable(T track) {
|
||||
|
||||
scheduler = Executors.newSingleThreadScheduledExecutor();
|
||||
|
||||
Runnable currentMusicTask = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
eventBus.post(new AudioEvents.PlayingMusic(track.getName(), track.currentPosition(), track.duration()));
|
||||
scheduler.schedule(this, 1, TimeUnit.SECONDS);
|
||||
}
|
||||
};
|
||||
|
||||
track.setOnEnd(() -> {
|
||||
scheduler.shutdownNow();
|
||||
playingIndex++;
|
||||
playCurrentTrack();
|
||||
});
|
||||
|
||||
track.setOnError(() -> {
|
||||
scheduler.shutdownNow();
|
||||
logger.error("Error playing track: {}", track);
|
||||
backgroundMusic.remove(track);
|
||||
|
||||
if (!backgroundMusic.isEmpty()) {
|
||||
playCurrentTrack();
|
||||
} else {
|
||||
playing = false;
|
||||
}
|
||||
});
|
||||
|
||||
scheduler.schedule(currentMusicTask, 1, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
if (!playing) return;
|
||||
|
||||
playing = false;
|
||||
dispatcher.run(() -> backgroundMusic.forEach(T::stop));
|
||||
}
|
||||
|
||||
public void pause() {
|
||||
T current = backgroundMusic.get(playingIndex);
|
||||
if (this.playing) {
|
||||
current.pause();
|
||||
playing = false;
|
||||
}
|
||||
else {
|
||||
this.playing = true;
|
||||
dispatcher.run(() -> {
|
||||
current.play();
|
||||
setTrackRunnable(current);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void previous() {
|
||||
if (backgroundMusic.isEmpty()) return;
|
||||
if (playingIndex == 0) {
|
||||
playingIndex = backgroundMusic.size();
|
||||
}
|
||||
stop();
|
||||
scheduler.shutdownNow();
|
||||
playingIndex = playingIndex - 1;
|
||||
playing = true;
|
||||
playCurrentTrack();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package org.toop.framework.audio;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.toop.framework.resource.ResourceMeta;
|
||||
import org.toop.framework.resource.resources.BaseResource;
|
||||
import org.toop.framework.resource.types.AudioResource;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class SoundEffectManager<T extends AudioResource> implements org.toop.framework.audio.interfaces.SoundEffectManager<T> {
|
||||
private static final Logger logger = LogManager.getLogger(SoundEffectManager.class);
|
||||
private final HashMap<String, T> soundEffectResources;
|
||||
|
||||
public <K extends BaseResource & AudioResource> SoundEffectManager(List<ResourceMeta<K>> resources) {
|
||||
// If there are duplicates, takes discards the first
|
||||
this.soundEffectResources = (HashMap<String, T>) resources
|
||||
.stream()
|
||||
.collect(Collectors.
|
||||
toMap(ResourceMeta::getName, ResourceMeta::getResource, (a, b) -> b, HashMap::new));
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<T> getActiveAudio() {
|
||||
return this.soundEffectResources.values();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void play(String name, boolean loop) {
|
||||
T asset = soundEffectResources.get(name);
|
||||
|
||||
if (asset == null) {
|
||||
logger.warn("Unable to load audio asset: {}", name);
|
||||
return;
|
||||
}
|
||||
|
||||
asset.play();
|
||||
|
||||
logger.debug("Playing sound: {}", asset.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop(String name){
|
||||
T asset = soundEffectResources.get(name);
|
||||
|
||||
if (asset == null) {
|
||||
logger.warn("Unable to load audio asset: {}", name);
|
||||
return;
|
||||
}
|
||||
|
||||
asset.stop();
|
||||
|
||||
logger.debug("Stopped sound: {}", asset.getName());
|
||||
}
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
package org.toop.framework.audio;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import javafx.scene.media.MediaPlayer;
|
||||
import javax.sound.sampled.*;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.toop.framework.SnowflakeGenerator;
|
||||
import org.toop.framework.asset.ResourceManager;
|
||||
import org.toop.framework.asset.ResourceMeta;
|
||||
import org.toop.framework.asset.resources.MusicAsset;
|
||||
import org.toop.framework.asset.resources.SoundEffectAsset;
|
||||
import org.toop.framework.audio.events.AudioEvents;
|
||||
import org.toop.framework.eventbus.EventFlow;
|
||||
|
||||
public class SoundManager {
|
||||
private static final Logger logger = LogManager.getLogger(SoundManager.class);
|
||||
private final List<MediaPlayer> activeMusic = new ArrayList<>();
|
||||
private final Queue<MusicAsset> backgroundMusicQueue = new LinkedList<>();
|
||||
private final Map<Long, Clip> activeSoundEffects = new HashMap<>();
|
||||
private final HashMap<String, SoundEffectAsset> audioResources = new HashMap<>();
|
||||
private final SnowflakeGenerator idGenerator =
|
||||
new SnowflakeGenerator(); // TODO: Don't create a new generator
|
||||
private final AudioVolumeManager audioVolumeManager = new AudioVolumeManager(this);
|
||||
|
||||
public SoundManager() {
|
||||
// Get all Audio Resources and add them to a list.
|
||||
for (ResourceMeta<SoundEffectAsset> asset :
|
||||
ResourceManager.getAllOfType(SoundEffectAsset.class)) {
|
||||
try {
|
||||
this.addAudioResource(asset);
|
||||
} catch (IOException | LineUnavailableException | UnsupportedAudioFileException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
new EventFlow()
|
||||
.listen(this::handlePlaySound)
|
||||
.listen(this::handleStopSound)
|
||||
.listen(this::handleMusicStart)
|
||||
.listen(
|
||||
AudioEvents.ClickButton.class,
|
||||
_ -> {
|
||||
try {
|
||||
playSound("medium-button-click.wav", false);
|
||||
} catch (UnsupportedAudioFileException
|
||||
| LineUnavailableException
|
||||
| IOException e) {
|
||||
logger.error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void handlePlaySound(AudioEvents.PlayEffect event) {
|
||||
try {
|
||||
this.playSound(event.fileName(), event.loop());
|
||||
} catch (UnsupportedAudioFileException | LineUnavailableException | IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleStopSound(AudioEvents.StopEffect event) {
|
||||
this.stopSound(event.clipId());
|
||||
}
|
||||
|
||||
private void addAudioResource(ResourceMeta<SoundEffectAsset> audioAsset)
|
||||
throws IOException, UnsupportedAudioFileException, LineUnavailableException {
|
||||
|
||||
this.audioResources.put(audioAsset.getName(), audioAsset.getResource());
|
||||
}
|
||||
|
||||
private void handleMusicStart(AudioEvents.StartBackgroundMusic e) {
|
||||
backgroundMusicQueue.clear();
|
||||
List<MusicAsset> shuffledArray =
|
||||
new ArrayList<>(
|
||||
ResourceManager.getAllOfType(MusicAsset.class).stream()
|
||||
.map(ResourceMeta::getResource)
|
||||
.toList());
|
||||
Collections.shuffle(shuffledArray);
|
||||
backgroundMusicQueue.addAll(shuffledArray);
|
||||
backgroundMusicPlayer();
|
||||
}
|
||||
|
||||
private void addBackgroundMusic(MusicAsset musicAsset) {
|
||||
backgroundMusicQueue.add(musicAsset);
|
||||
}
|
||||
|
||||
private void backgroundMusicPlayer() {
|
||||
MusicAsset ma = backgroundMusicQueue.poll();
|
||||
if (ma == null) return;
|
||||
|
||||
MediaPlayer mediaPlayer = new MediaPlayer(ma.getMedia());
|
||||
|
||||
mediaPlayer.setOnEndOfMedia(
|
||||
() -> {
|
||||
addBackgroundMusic(ma);
|
||||
activeMusic.remove(mediaPlayer);
|
||||
mediaPlayer.dispose();
|
||||
ma.unload();
|
||||
backgroundMusicPlayer(); // play next
|
||||
});
|
||||
|
||||
mediaPlayer.setOnStopped(
|
||||
() -> {
|
||||
addBackgroundMusic(ma);
|
||||
activeMusic.remove(mediaPlayer);
|
||||
ma.unload();
|
||||
});
|
||||
|
||||
mediaPlayer.setOnError(
|
||||
() -> {
|
||||
addBackgroundMusic(ma);
|
||||
activeMusic.remove(mediaPlayer);
|
||||
ma.unload();
|
||||
});
|
||||
|
||||
audioVolumeManager.updateMusicVolume(mediaPlayer);
|
||||
mediaPlayer.play();
|
||||
activeMusic.add(mediaPlayer);
|
||||
logger.info("Playing background music: {}", ma.getFile().getName());
|
||||
logger.info(
|
||||
"Background music next in line: {}",
|
||||
backgroundMusicQueue.peek().getFile().getName());
|
||||
}
|
||||
|
||||
private long playSound(String audioFileName, boolean loop)
|
||||
throws UnsupportedAudioFileException, LineUnavailableException, IOException {
|
||||
SoundEffectAsset asset = audioResources.get(audioFileName);
|
||||
|
||||
// Return -1 which indicates resource wasn't available
|
||||
if (asset == null) {
|
||||
logger.warn("Unable to load audio asset: {}", audioFileName);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Get a new clip from resource
|
||||
Clip clip = asset.getNewClip();
|
||||
|
||||
// Set volume of clip
|
||||
audioVolumeManager.updateSoundEffectVolume(clip);
|
||||
|
||||
// If supposed to loop make it loop, else just start it once
|
||||
if (loop) {
|
||||
clip.loop(Clip.LOOP_CONTINUOUSLY);
|
||||
} else {
|
||||
clip.start();
|
||||
}
|
||||
|
||||
logger.debug("Playing sound: {}", asset.getFile().getName());
|
||||
|
||||
// Generate id for clip
|
||||
long clipId = idGenerator.nextId();
|
||||
|
||||
// store it so we can stop it later
|
||||
activeSoundEffects.put(clipId, clip); // TODO: Do on snowflake for specific sound to stop
|
||||
|
||||
// remove when finished (only for non-looping sounds)
|
||||
clip.addLineListener(
|
||||
event -> {
|
||||
if (event.getType() == LineEvent.Type.STOP && !clip.isRunning()) {
|
||||
activeSoundEffects.remove(clipId);
|
||||
clip.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Return id so it can be stopped
|
||||
return clipId;
|
||||
}
|
||||
|
||||
public void stopSound(long clipId) {
|
||||
Clip clip = activeSoundEffects.get(clipId);
|
||||
|
||||
if (clip == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
clip.stop();
|
||||
clip.close();
|
||||
activeSoundEffects.remove(clipId);
|
||||
}
|
||||
|
||||
public void stopAllSounds() {
|
||||
for (Clip clip : activeSoundEffects.values()) {
|
||||
clip.stop();
|
||||
clip.close();
|
||||
}
|
||||
activeSoundEffects.clear();
|
||||
}
|
||||
|
||||
public Map<Long, Clip> getActiveSoundEffects() {
|
||||
return this.activeSoundEffects;
|
||||
}
|
||||
|
||||
public List<MediaPlayer> getActiveMusic() {
|
||||
return activeMusic;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package org.toop.framework.audio;
|
||||
|
||||
import org.toop.framework.audio.interfaces.AudioManager;
|
||||
import org.toop.framework.resource.types.AudioResource;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/**
|
||||
* Enum representing different categories of audio volume in the application.
|
||||
* <p>
|
||||
* Each volume type maintains its own volume level and a list of {@link AudioManager}s
|
||||
* that manage audio resources of that type. The enum provides methods to set, get,
|
||||
* and propagate volume changes, including master volume adjustments that automatically
|
||||
* update dependent volume types (FX and MUSIC).
|
||||
* </p>
|
||||
*
|
||||
* <p>Volume types:</p>
|
||||
* <ul>
|
||||
* <li>{@link #MASTERVOLUME}: The global/master volume that scales all other volume types.</li>
|
||||
* <li>{@link #FX}: Volume for sound effects, scaled by the master volume.</li>
|
||||
* <li>{@link #MUSIC}: Volume for music tracks, scaled by the master volume.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Key features:</p>
|
||||
* <ul>
|
||||
* <li>Thread-safe management of audio managers using {@link CopyOnWriteArrayList}.</li>
|
||||
* <li>Automatic propagation of master volume changes to dependent volume types.</li>
|
||||
* <li>Clamping volume values between 0.0 and 1.0 to ensure valid audio levels.</li>
|
||||
* <li>Dynamic registration and removal of audio managers for each volume type.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Example usage:</p>
|
||||
* <pre>{@code
|
||||
* // Add a music manager to the MUSIC volume type
|
||||
* VolumeControl.MUSIC.addManager(musicManager);
|
||||
*
|
||||
* // Set master volume to 80%
|
||||
* VolumeControl.MASTERVOLUME.setVolume(0.8, 0);
|
||||
*
|
||||
* // Set FX volume to 50% of master
|
||||
* VolumeControl.FX.setVolume(0.5, VolumeControl.MASTERVOLUME.getVolume());
|
||||
*
|
||||
* // Retrieve current music volume
|
||||
* double musicVol = VolumeControl.MUSIC.getVolume();
|
||||
* }</pre>
|
||||
*/
|
||||
public enum VolumeControl {
|
||||
MASTERVOLUME(),
|
||||
FX(),
|
||||
MUSIC();
|
||||
|
||||
@SuppressWarnings("ImmutableEnumChecker")
|
||||
private final List<AudioManager<? extends AudioResource>> managers = new CopyOnWriteArrayList<>();
|
||||
@SuppressWarnings("ImmutableEnumChecker")
|
||||
private double volume = 1.0;
|
||||
@SuppressWarnings("ImmutableEnumChecker")
|
||||
private double masterVolume = 1.0;
|
||||
|
||||
/**
|
||||
* Sets the volume for this volume type.
|
||||
* <p>
|
||||
* If this type is {@link #MASTERVOLUME}, all dependent volume types
|
||||
* (FX, MUSIC, etc.) are automatically updated to reflect the new master volume.
|
||||
* Otherwise, the volume is scaled by the provided master volume.
|
||||
*
|
||||
* @param newVolume the new volume level (0.0 to 1.0)
|
||||
* @param currentMasterVolume the current master volume for scaling non-master types
|
||||
*/
|
||||
public void setVolume(double newVolume, double currentMasterVolume) {
|
||||
this.volume = clamp(newVolume);
|
||||
|
||||
if (this == MASTERVOLUME) {
|
||||
for (VolumeControl type : VolumeControl.values()) {
|
||||
if (type != MASTERVOLUME) {
|
||||
type.masterVolume = this.volume;
|
||||
type.broadcastVolume(type.computeEffectiveVolume());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.masterVolume = clamp(currentMasterVolume);
|
||||
broadcastVolume(computeEffectiveVolume());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the effective volume for this type, taking into account
|
||||
* the master volume if this is not {@link #MASTERVOLUME}.
|
||||
*
|
||||
* @return the effective volume (0.0 to 1.0)
|
||||
*/
|
||||
private double computeEffectiveVolume() {
|
||||
return (this == MASTERVOLUME) ? volume : volume * masterVolume;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates all registered audio managers with the given effective volume.
|
||||
*
|
||||
* @param effectiveVolume the volume to apply to all active audio resources
|
||||
*/
|
||||
private void broadcastVolume(double effectiveVolume) {
|
||||
managers.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.forEach(manager -> manager.getActiveAudio()
|
||||
.forEach(aud -> aud.updateVolume(effectiveVolume)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamps a volume value to the valid range [0.0, 1.0].
|
||||
*
|
||||
* @param vol the volume to clamp
|
||||
* @return the clamped volume
|
||||
*/
|
||||
private double clamp(double vol) {
|
||||
return Math.max(0, Math.min(vol, 1.0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current volume for this type.
|
||||
*
|
||||
* @return the current volume (0.0 to 1.0)
|
||||
*/
|
||||
public double getVolume() {
|
||||
return volume;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an {@link AudioManager} to this volume type.
|
||||
* <p>
|
||||
* Duplicate managers are ignored. Managers will receive volume updates
|
||||
* when this type's volume changes.
|
||||
*
|
||||
* @param manager the audio manager to register
|
||||
*/
|
||||
public void addManager(AudioManager<? extends AudioResource> manager) {
|
||||
if (manager != null && !managers.contains(manager)) {
|
||||
managers.add(manager);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a previously registered {@link AudioManager} from this type.
|
||||
*
|
||||
* @param manager the audio manager to remove
|
||||
*/
|
||||
public void removeManager(AudioManager<? extends AudioResource> manager) {
|
||||
if (manager != null) {
|
||||
managers.remove(manager);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an unmodifiable view of all registered audio managers for this type.
|
||||
*
|
||||
* @return a list of registered audio managers
|
||||
*/
|
||||
public List<AudioManager<? extends AudioResource>> getManagers() {
|
||||
return Collections.unmodifiableList(managers);
|
||||
}
|
||||
}
|
||||
@@ -1,98 +1,40 @@
|
||||
package org.toop.framework.audio.events;
|
||||
|
||||
import java.util.Map;
|
||||
import org.toop.framework.eventbus.events.EventWithSnowflake;
|
||||
import org.toop.framework.eventbus.events.EventWithoutSnowflake;
|
||||
import org.toop.framework.eventbus.events.EventsBase;
|
||||
import org.toop.framework.audio.VolumeControl;
|
||||
import org.toop.framework.eventbus.events.*;
|
||||
|
||||
public class AudioEvents extends EventsBase {
|
||||
/** Starts playing a sound. */
|
||||
public record PlayEffect(String fileName, boolean loop) implements EventWithoutSnowflake {}
|
||||
/** Stops the audio manager. */
|
||||
public record StopAudioManager() implements GenericEvent {}
|
||||
|
||||
public record StopEffect(long clipId) implements EventWithoutSnowflake {}
|
||||
/** Start playing a sound effect. */
|
||||
public record PlayEffect(String fileName, boolean loop) implements GenericEvent {}
|
||||
|
||||
public record StartBackgroundMusic() implements EventWithoutSnowflake {}
|
||||
/** Stop playing a sound effect. */
|
||||
public record StopEffect(String fileName) implements GenericEvent {}
|
||||
|
||||
public record ChangeVolume(double newVolume) implements EventWithoutSnowflake {}
|
||||
/** Start background music. */
|
||||
public record StartBackgroundMusic() implements GenericEvent {}
|
||||
|
||||
public record ChangeFxVolume(double newVolume) implements EventWithoutSnowflake {}
|
||||
/** Gives back the name of the song, the position its currently at (in seconds) and how long it takes (in seconds) */
|
||||
public record PlayingMusic(String name, long currentPosition, long duration) implements GenericEvent {}
|
||||
|
||||
public record ChangeMusicVolume(double newVolume) implements EventWithoutSnowflake {}
|
||||
/** Skips the song to the last second of the song resulting in a skip effect */
|
||||
public record SkipMusic() implements GenericEvent {}
|
||||
|
||||
public record GetCurrentVolume(long snowflakeId) implements EventWithSnowflake {
|
||||
@Override
|
||||
public Map<String, Object> result() {
|
||||
return Map.of();
|
||||
}
|
||||
public record PreviousMusic() implements GenericEvent {}
|
||||
|
||||
@Override
|
||||
public long eventSnowflake() {
|
||||
return snowflakeId;
|
||||
}
|
||||
}
|
||||
public record PauseMusic() implements GenericEvent {}
|
||||
|
||||
public record GetCurrentVolumeResponse(double currentVolume, long snowflakeId)
|
||||
implements EventWithSnowflake {
|
||||
@Override
|
||||
public Map<String, Object> result() {
|
||||
return Map.of();
|
||||
}
|
||||
/** Change volume, choose type with {@link VolumeControl}. */
|
||||
public record ChangeVolume(double newVolume, VolumeControl controlType) implements GenericEvent {}
|
||||
|
||||
@Override
|
||||
public long eventSnowflake() {
|
||||
return snowflakeId;
|
||||
}
|
||||
}
|
||||
/** Requests the desired volume by selecting it with {@link VolumeControl}. */
|
||||
public record GetVolume(VolumeControl controlType, long identifier) implements UniqueEvent {}
|
||||
|
||||
public record GetCurrentFxVolume(long snowflakeId) implements EventWithSnowflake {
|
||||
@Override
|
||||
public Map<String, Object> result() {
|
||||
return Map.of();
|
||||
}
|
||||
/** Response to GetVolume. */
|
||||
public record GetVolumeResponse(double currentVolume, long identifier) implements ResponseToUniqueEvent {}
|
||||
|
||||
@Override
|
||||
public long eventSnowflake() {
|
||||
return this.snowflakeId;
|
||||
}
|
||||
}
|
||||
|
||||
public record GetCurrentMusicVolume(long snowflakeId) implements EventWithSnowflake {
|
||||
@Override
|
||||
public Map<String, Object> result() {
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long eventSnowflake() {
|
||||
return this.snowflakeId;
|
||||
}
|
||||
}
|
||||
|
||||
public record GetCurrentFxVolumeResponse(double currentVolume, long snowflakeId)
|
||||
implements EventWithSnowflake {
|
||||
@Override
|
||||
public Map<String, Object> result() {
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long eventSnowflake() {
|
||||
return this.snowflakeId;
|
||||
}
|
||||
}
|
||||
|
||||
public record GetCurrentMusicVolumeResponse(double currentVolume, long snowflakeId)
|
||||
implements EventWithSnowflake {
|
||||
@Override
|
||||
public Map<String, Object> result() {
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long eventSnowflake() {
|
||||
return this.snowflakeId;
|
||||
}
|
||||
}
|
||||
|
||||
public record ClickButton() implements EventWithoutSnowflake {}
|
||||
/** Plays the predetermined sound for pressing a button. */
|
||||
public record ClickButton() implements GenericEvent {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.toop.framework.audio.interfaces;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public interface AudioManager<T> {
|
||||
Collection<T> getActiveAudio();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
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();
|
||||
void skip();
|
||||
void previous();
|
||||
void pause();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.toop.framework.audio.interfaces;
|
||||
|
||||
import org.toop.framework.resource.types.AudioResource;
|
||||
|
||||
public interface SoundEffectManager<T extends AudioResource> extends AudioManager<T> {
|
||||
void play(String name, boolean loop);
|
||||
void stop(String name);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.toop.framework.audio.interfaces;
|
||||
|
||||
import org.toop.framework.audio.VolumeControl;
|
||||
|
||||
|
||||
/**
|
||||
* Interface for managing audio volumes in the application.
|
||||
* <p>
|
||||
* Implementations of this interface are responsible for controlling the volume levels
|
||||
* of different categories of audio (e.g., master volume, music, sound effects) and
|
||||
* updating the associated audio managers or resources accordingly.
|
||||
* </p>
|
||||
*
|
||||
* <p>Typical responsibilities include:</p>
|
||||
* <ul>
|
||||
* <li>Setting the volume for a specific category (master, music, FX).</li>
|
||||
* <li>Retrieving the current volume of a category.</li>
|
||||
* <li>Ensuring that changes in master volume propagate to dependent audio categories.</li>
|
||||
* <li>Interfacing with {@link org.toop.framework.audio.interfaces.AudioManager} to update active audio resources.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Example usage:</p>
|
||||
* <pre>{@code
|
||||
* VolumeManager volumeManager = ...;
|
||||
* // Set master volume to 80%
|
||||
* volumeManager.setVolume(0.8, VolumeControl.MASTERVOLUME);
|
||||
*
|
||||
* // Set music volume to 50% of master
|
||||
* volumeManager.setVolume(0.5, VolumeControl.MUSIC);
|
||||
*
|
||||
* // Retrieve current FX volume
|
||||
* double fxVolume = volumeManager.getVolume(VolumeControl.FX);
|
||||
* }</pre>
|
||||
*/
|
||||
public interface VolumeManager {
|
||||
|
||||
/**
|
||||
*
|
||||
* Sets the volume to for the specified {@link VolumeControl}.
|
||||
*
|
||||
* @param newVolume The volume to be set to.
|
||||
* @param type The type of volume to change.
|
||||
*/
|
||||
void setVolume(double newVolume, VolumeControl type);
|
||||
|
||||
/**
|
||||
* Gets the current volume for the specified {@link VolumeControl}.
|
||||
*
|
||||
* @param type the type of volume to get.
|
||||
* @return The volume as a {@link Double}
|
||||
*/
|
||||
double getVolume(VolumeControl type);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.toop.framework.dispatch;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import org.toop.framework.dispatch.interfaces.Dispatcher;
|
||||
|
||||
public class JavaFXDispatcher implements Dispatcher {
|
||||
@Override
|
||||
public void run(Runnable task) {
|
||||
Platform.runLater(task);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.toop.framework.dispatch.interfaces;
|
||||
|
||||
public interface Dispatcher {
|
||||
void run(Runnable task);
|
||||
}
|
||||
@@ -11,13 +11,18 @@ import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import org.toop.framework.SnowflakeGenerator;
|
||||
import org.toop.framework.eventbus.events.EventType;
|
||||
import org.toop.framework.eventbus.events.EventWithSnowflake;
|
||||
import org.toop.framework.eventbus.events.ResponseToUniqueEvent;
|
||||
import org.toop.framework.eventbus.events.UniqueEvent;
|
||||
import org.toop.framework.eventbus.bus.EventBus;
|
||||
import org.toop.framework.eventbus.subscriber.DefaultNamedSubscriber;
|
||||
import org.toop.framework.eventbus.subscriber.NamedSubscriber;
|
||||
import org.toop.framework.eventbus.subscriber.Subscriber;
|
||||
|
||||
/**
|
||||
* EventFlow is a utility class for creating, posting, and optionally subscribing to events in a
|
||||
* type-safe and chainable manner. It is designed to work with the {@link GlobalEventBus}.
|
||||
* type-safe and chainable manner. It is designed to work with the {@link EventBus}.
|
||||
*
|
||||
* <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,37 +35,41 @@ 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. */
|
||||
private final EventBus eventBus;
|
||||
|
||||
/** Automatically assigned UUID for {@link UniqueEvent} events. */
|
||||
private long eventSnowflake = -1;
|
||||
|
||||
/** The event instance created by this publisher. */
|
||||
private EventType event = null;
|
||||
|
||||
/** The listener returned by GlobalEventBus subscription. Used for unsubscription. */
|
||||
private final List<ListenerHandler> listeners = new ArrayList<>();
|
||||
private final List<NamedSubscriber<?>> 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() {}
|
||||
|
||||
// New: accept an event instance directly
|
||||
public EventFlow addPostEvent(EventType event) {
|
||||
this.event = event;
|
||||
return this;
|
||||
public EventFlow(EventBus eventBus) {
|
||||
this.eventBus = eventBus;
|
||||
}
|
||||
|
||||
// Optional: accept a Supplier<EventType> to defer construction
|
||||
public EventFlow addPostEvent(Supplier<? extends EventType> eventSupplier) {
|
||||
this.event = eventSupplier.get();
|
||||
return this;
|
||||
public EventFlow() {
|
||||
this.eventBus = GlobalEventBus.get();
|
||||
}
|
||||
|
||||
// Keep the old class+args version if needed
|
||||
/**
|
||||
*
|
||||
* Add an event that will be triggered when {@link #postEvent()} or {@link #asyncPostEvent()} is called.
|
||||
*
|
||||
* @param eventClass The event that will be posted.
|
||||
* @param args The event arguments, see the added event record for more information.
|
||||
* @return {@link #EventFlow}
|
||||
*
|
||||
*/
|
||||
public <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 +90,7 @@ public class EventFlow {
|
||||
int expectedParamCount = ctorHandle.type().parameterCount();
|
||||
|
||||
if (isUuidEvent && args.length < expectedParamCount) {
|
||||
this.eventSnowflake = new SnowflakeGenerator().nextId();
|
||||
this.eventSnowflake = SnowflakeGenerator.nextId();
|
||||
finalArgs = new Object[args.length + 1];
|
||||
System.arraycopy(args, 0, finalArgs, 0, args.length);
|
||||
finalArgs[args.length] = this.eventSnowflake;
|
||||
@@ -100,155 +109,403 @@ public class EventFlow {
|
||||
}
|
||||
}
|
||||
|
||||
// public EventFlow addSnowflake() {
|
||||
// this.eventSnowflake = new SnowflakeGenerator(1).nextId();
|
||||
// return this;
|
||||
// }
|
||||
|
||||
/** Subscribe by ID: only fires if UUID matches this publisher's eventId. */
|
||||
public <TT extends EventWithSnowflake> EventFlow onResponse(
|
||||
Class<TT> eventClass, Consumer<TT> action, boolean unsubscribeAfterSuccess) {
|
||||
ListenerHandler[] listenerHolder = new ListenerHandler[1];
|
||||
listenerHolder[0] =
|
||||
new ListenerHandler(
|
||||
GlobalEventBus.subscribe(
|
||||
eventClass,
|
||||
event -> {
|
||||
if (event.eventSnowflake() != this.eventSnowflake) return;
|
||||
|
||||
action.accept(event);
|
||||
|
||||
if (unsubscribeAfterSuccess && listenerHolder[0] != null) {
|
||||
GlobalEventBus.unsubscribe(listenerHolder[0]);
|
||||
this.listeners.remove(listenerHolder[0]);
|
||||
}
|
||||
|
||||
this.result = event.result();
|
||||
}));
|
||||
this.listeners.add(listenerHolder[0]);
|
||||
/**
|
||||
*
|
||||
* Add an event that will be triggered when {@link #postEvent()} or {@link #asyncPostEvent()} is called.
|
||||
*
|
||||
* @param event The event to be posted.
|
||||
* @return {@link #EventFlow}
|
||||
*
|
||||
*/
|
||||
public EventFlow addPostEvent(EventType event) {
|
||||
this.event = event;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Subscribe by ID: only fires if UUID matches this publisher's eventId. */
|
||||
public <TT extends EventWithSnowflake> EventFlow onResponse(
|
||||
Class<TT> eventClass, Consumer<TT> action) {
|
||||
return this.onResponse(eventClass, action, true);
|
||||
/**
|
||||
*
|
||||
* Add an event that will be triggered when {@link #postEvent()} or {@link #asyncPostEvent()} is called.
|
||||
*
|
||||
* @param eventSupplier The event that will be posted through a Supplier.
|
||||
* @return {@link #EventFlow}
|
||||
*
|
||||
*/
|
||||
public EventFlow addPostEvent(Supplier<? extends EventType> eventSupplier) {
|
||||
this.event = eventSupplier.get();
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Subscribe by ID without explicit class. */
|
||||
/**
|
||||
*
|
||||
* Start listening for an event and trigger when ID correlates.
|
||||
*
|
||||
* @param event The {@link ResponseToUniqueEvent} to trigger the lambda.
|
||||
* @param action The lambda to run when triggered.
|
||||
* @param unsubscribeAfterSuccess Enable/disable auto unsubscribing to event after being triggered.
|
||||
* @param name A name given to the event, can later be used to unsubscribe.
|
||||
* @return {@link #EventFlow}
|
||||
*
|
||||
*/
|
||||
public <TT extends ResponseToUniqueEvent> EventFlow onResponse(
|
||||
Class<TT> event, Consumer<TT> action, boolean unsubscribeAfterSuccess, String name
|
||||
) {
|
||||
|
||||
final long id = SnowflakeGenerator.nextId();
|
||||
|
||||
Consumer<TT> newAction = eventClass -> {
|
||||
if (eventClass.getIdentifier() != this.eventSnowflake) return;
|
||||
|
||||
action.accept(eventClass);
|
||||
|
||||
if (unsubscribeAfterSuccess) unsubscribe(String.valueOf(id));
|
||||
|
||||
this.result = eventClass.result();
|
||||
};
|
||||
|
||||
var subscriber = new DefaultNamedSubscriber<>(
|
||||
name,
|
||||
event,
|
||||
newAction
|
||||
);
|
||||
|
||||
eventBus.subscribe(subscriber);
|
||||
this.listeners.add(subscriber);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Start listening for an event and trigger when ID correlates, auto unsubscribes after being triggered and adds an empty name.
|
||||
*
|
||||
* @param event The {@link ResponseToUniqueEvent} to trigger the lambda.
|
||||
* @param action The lambda to run when triggered.
|
||||
* @return {@link #EventFlow}
|
||||
*
|
||||
*/
|
||||
public <TT extends ResponseToUniqueEvent> EventFlow onResponse(Class<TT> event, Consumer<TT> action) {
|
||||
return this.onResponse(event, action, true, "");
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Start listening for an event and trigger when ID correlates, auto adds an empty name.
|
||||
*
|
||||
* @param event The {@link ResponseToUniqueEvent} to trigger the lambda.
|
||||
* @param action The lambda to run when triggered.
|
||||
* @param unsubscribeAfterSuccess Enable/disable auto unsubscribing to event after being triggered.
|
||||
* @return {@link #EventFlow}
|
||||
*
|
||||
*/
|
||||
public <TT extends ResponseToUniqueEvent> EventFlow onResponse(Class<TT> event, Consumer<TT> action, boolean unsubscribeAfterSuccess) {
|
||||
return this.onResponse(event, action, unsubscribeAfterSuccess, "");
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Start listening for an event and trigger when ID correlates, auto unsubscribes after being triggered.
|
||||
*
|
||||
* @param event The {@link ResponseToUniqueEvent} to trigger the lambda.
|
||||
* @param action The lambda to run when triggered.
|
||||
* @param name A name given to the event, can later be used to unsubscribe.
|
||||
* @return {@link #EventFlow}
|
||||
*
|
||||
*/
|
||||
public <TT extends ResponseToUniqueEvent> EventFlow onResponse(Class<TT> event, Consumer<TT> action, String name) {
|
||||
return this.onResponse(event, action, true, name);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Subscribe by ID without explicit class.
|
||||
*
|
||||
* @param action The lambda to run when triggered.
|
||||
* @return {@link #EventFlow}
|
||||
*
|
||||
* @deprecated use {@link #onResponse(Class, Consumer, boolean, String)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("unchecked")
|
||||
public <TT extends EventWithSnowflake> 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) {
|
||||
try {
|
||||
TT typedEvent = (TT) uuidEvent;
|
||||
action.accept(typedEvent);
|
||||
if (unsubscribeAfterSuccess
|
||||
&& listenerHolder[0] != null) {
|
||||
GlobalEventBus.unsubscribe(listenerHolder[0]);
|
||||
this.listeners.remove(listenerHolder[0]);
|
||||
}
|
||||
this.result = typedEvent.result();
|
||||
} catch (ClassCastException _) {
|
||||
throw new ClassCastException(
|
||||
"Cannot cast "
|
||||
+ event.getClass().getName()
|
||||
+ " to EventWithSnowflake");
|
||||
}
|
||||
}
|
||||
}));
|
||||
this.listeners.add(listenerHolder[0]);
|
||||
public <TT extends ResponseToUniqueEvent> EventFlow onResponse(
|
||||
Consumer<TT> action, boolean unsubscribeAfterSuccess, String name) {
|
||||
|
||||
final long id = SnowflakeGenerator.nextId();
|
||||
|
||||
Consumer<TT> newAction = event -> {
|
||||
if (!(event instanceof UniqueEvent uuidEvent)) return;
|
||||
if (uuidEvent.getIdentifier() == this.eventSnowflake) {
|
||||
try {
|
||||
TT typedEvent = (TT) uuidEvent;
|
||||
action.accept(typedEvent);
|
||||
|
||||
if (unsubscribeAfterSuccess) unsubscribe(String.valueOf(id));
|
||||
|
||||
this.result = typedEvent.result();
|
||||
} catch (ClassCastException _) {
|
||||
throw new ClassCastException(
|
||||
"Cannot cast "
|
||||
+ event.getClass().getName()
|
||||
+ " to UniqueEvent");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var listener = new DefaultNamedSubscriber<>(
|
||||
name,
|
||||
(Class<TT>) action.getClass().getDeclaredMethods()[0].getParameterTypes()[0],
|
||||
newAction
|
||||
);
|
||||
|
||||
eventBus.subscribe(listener);
|
||||
this.listeners.add(listener);
|
||||
return this;
|
||||
}
|
||||
|
||||
public <TT extends EventWithSnowflake> EventFlow onResponse(Consumer<TT> action) {
|
||||
return this.onResponse(action, true);
|
||||
/**
|
||||
*
|
||||
* Subscribe by ID without explicit class.
|
||||
*
|
||||
* @param action The lambda to run when triggered.
|
||||
* @return {@link #EventFlow}
|
||||
*
|
||||
* @deprecated use {@link #onResponse(Class, Consumer)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public <TT extends ResponseToUniqueEvent> EventFlow onResponse(Consumer<TT> action) {
|
||||
return this.onResponse(action, true, "");
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Start listening for an event, and run a lambda when triggered.
|
||||
*
|
||||
* @param event The {@link EventType} to trigger the lambda.
|
||||
* @param action The lambda to run when triggered.
|
||||
* @param unsubscribeAfterSuccess Enable/disable auto unsubscribing to event after being triggered.
|
||||
* @param name A name given to the event, can later be used to unsubscribe.
|
||||
* @return {@link #EventFlow}
|
||||
*
|
||||
*/
|
||||
public <TT extends EventType> EventFlow listen(
|
||||
Class<TT> eventClass, Consumer<TT> action, boolean unsubscribeAfterSuccess) {
|
||||
ListenerHandler[] listenerHolder = new ListenerHandler[1];
|
||||
listenerHolder[0] =
|
||||
new ListenerHandler(
|
||||
GlobalEventBus.subscribe(
|
||||
eventClass,
|
||||
event -> {
|
||||
action.accept(event);
|
||||
Class<TT> event, Consumer<TT> action, boolean unsubscribeAfterSuccess, String name) {
|
||||
|
||||
if (unsubscribeAfterSuccess && listenerHolder[0] != null) {
|
||||
GlobalEventBus.unsubscribe(listenerHolder[0]);
|
||||
this.listeners.remove(listenerHolder[0]);
|
||||
}
|
||||
}));
|
||||
this.listeners.add(listenerHolder[0]);
|
||||
long id = SnowflakeGenerator.nextId();
|
||||
|
||||
Consumer<TT> newAction = eventc -> {
|
||||
action.accept(eventc);
|
||||
|
||||
if (unsubscribeAfterSuccess) unsubscribe(String.valueOf(id));
|
||||
};
|
||||
|
||||
var listener = new DefaultNamedSubscriber<>(
|
||||
name,
|
||||
event,
|
||||
newAction
|
||||
);
|
||||
|
||||
eventBus.subscribe(listener);
|
||||
this.listeners.add(listener);
|
||||
return this;
|
||||
}
|
||||
|
||||
public <TT extends EventType> EventFlow listen(Class<TT> eventClass, Consumer<TT> action) {
|
||||
return this.listen(eventClass, action, true);
|
||||
/**
|
||||
*
|
||||
* Start listening for an event, and run a lambda when triggered, auto unsubscribes.
|
||||
*
|
||||
* @param event The {@link EventType} to trigger the lambda.
|
||||
* @param action The lambda to run when triggered.
|
||||
* @param name A name given to the event, can later be used to unsubscribe.
|
||||
* @return {@link #EventFlow}
|
||||
*
|
||||
*/
|
||||
public <TT extends EventType> EventFlow listen(Class<TT> event, Consumer<TT> action, String name) {
|
||||
return this.listen(event, action, true, name);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Start listening for an event, and run a lambda when triggered, auto unsubscribe and gives it an empty name.
|
||||
*
|
||||
* @param event The {@link EventType} to trigger the lambda.
|
||||
* @param action The lambda to run when triggered.
|
||||
* @return {@link #EventFlow}
|
||||
*
|
||||
*/
|
||||
public <TT extends EventType> EventFlow listen(Class<TT> event, Consumer<TT> action) {
|
||||
return this.listen(event, action, true, "");
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Start listening for an event, and run a lambda when triggered, adds an empty name.
|
||||
*
|
||||
* @param event The {@link EventType} to trigger the lambda.
|
||||
* @param action The lambda to run when triggered.
|
||||
* @param unsubscribeAfterSuccess Enable/disable auto unsubscribing to event after being triggered.
|
||||
* @return {@link #EventFlow}
|
||||
*
|
||||
*/
|
||||
public <TT extends EventType> EventFlow listen(Class<TT> event, Consumer<TT> action, boolean unsubscribeAfterSuccess) {
|
||||
return this.listen(event, action, unsubscribeAfterSuccess, "");
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Start listening to an event.
|
||||
*
|
||||
* @param action The lambda to run when triggered.
|
||||
* @return {@link EventFlow}
|
||||
*
|
||||
* @deprecated use {@link #listen(Class, Consumer, boolean, String)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("unchecked")
|
||||
public <TT extends EventType> EventFlow listen(
|
||||
Consumer<TT> action, boolean unsubscribeAfterSuccess) {
|
||||
ListenerHandler[] listenerHolder = new ListenerHandler[1];
|
||||
listenerHolder[0] =
|
||||
new ListenerHandler(
|
||||
GlobalEventBus.subscribe(
|
||||
event -> {
|
||||
if (!(event instanceof EventType nonUuidEvent)) return;
|
||||
try {
|
||||
TT typedEvent = (TT) nonUuidEvent;
|
||||
action.accept(typedEvent);
|
||||
if (unsubscribeAfterSuccess && listenerHolder[0] != null) {
|
||||
GlobalEventBus.unsubscribe(listenerHolder[0]);
|
||||
this.listeners.remove(listenerHolder[0]);
|
||||
}
|
||||
} catch (ClassCastException _) {
|
||||
throw new ClassCastException(
|
||||
"Cannot cast "
|
||||
+ event.getClass().getName()
|
||||
+ " to EventWithSnowflake");
|
||||
}
|
||||
}));
|
||||
this.listeners.add(listenerHolder[0]);
|
||||
Consumer<TT> action, boolean unsubscribeAfterSuccess, String name) {
|
||||
long id = SnowflakeGenerator.nextId();
|
||||
|
||||
Class<TT> eventClass = (Class<TT>) action.getClass().getDeclaredMethods()[0].getParameterTypes()[0];
|
||||
|
||||
Consumer<TT> newAction = event -> {
|
||||
if (!(event instanceof EventType nonUuidEvent)) return;
|
||||
try {
|
||||
TT typedEvent = (TT) nonUuidEvent;
|
||||
action.accept(typedEvent);
|
||||
if (unsubscribeAfterSuccess) unsubscribe(String.valueOf(id));
|
||||
} catch (ClassCastException _) {
|
||||
throw new ClassCastException(
|
||||
"Cannot cast "
|
||||
+ event.getClass().getName()
|
||||
+ " to UniqueEvent");
|
||||
}
|
||||
};
|
||||
|
||||
var listener = new DefaultNamedSubscriber<>(
|
||||
name,
|
||||
eventClass,
|
||||
newAction
|
||||
);
|
||||
|
||||
eventBus.subscribe(listener);
|
||||
this.listeners.add(listener);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Start listening to an event.
|
||||
*
|
||||
* @param action The lambda to run when triggered.
|
||||
* @return {@link EventFlow}
|
||||
*
|
||||
* @deprecated use {@link #listen(Class, Consumer)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public <TT extends EventType> EventFlow listen(Consumer<TT> action) {
|
||||
return this.listen(action, true);
|
||||
return this.listen(action, true, "");
|
||||
}
|
||||
|
||||
/** Post synchronously */
|
||||
/**
|
||||
* Posts the event added through {@link #addPostEvent}.
|
||||
*/
|
||||
public EventFlow postEvent() {
|
||||
GlobalEventBus.post(this.event);
|
||||
eventBus.post(this.event);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Post asynchronously */
|
||||
/**
|
||||
* Posts the event added through {@link #addPostEvent} asynchronously.
|
||||
*
|
||||
* @deprecated use {@link #postEvent()} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public EventFlow asyncPostEvent() {
|
||||
GlobalEventBus.postAsync(this.event);
|
||||
eventBus.post(this.event);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Map<String, Object> getResult() {
|
||||
/**
|
||||
*
|
||||
* Unsubscribe from an event.
|
||||
*
|
||||
* @param action The listener object to remove and unsubscribe.
|
||||
*/
|
||||
public void unsubscribe(Consumer<?> action) {
|
||||
this.listeners.removeIf(handler -> {
|
||||
if (handler.handler().equals(action)) {
|
||||
eventBus.unsubscribe(handler);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from an event.
|
||||
*
|
||||
* @param name The name given to the listener.
|
||||
*/
|
||||
public void unsubscribe(String name) {
|
||||
this.listeners.removeIf(handler -> {
|
||||
if (handler.id().equals(name)) {
|
||||
eventBus.unsubscribe(handler);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe all events.
|
||||
*/
|
||||
public void unsubscribeAll() {
|
||||
listeners.removeIf(handler -> {
|
||||
eventBus.unsubscribe(handler);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean and remove everything inside {@link EventFlow}.
|
||||
*/
|
||||
private void clean() {
|
||||
unsubscribeAll();
|
||||
this.event = null;
|
||||
this.result = null;
|
||||
} // TODO
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @return TODO
|
||||
*/
|
||||
public Map<String, ?> getResult() {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @return TODO
|
||||
*/
|
||||
public EventType getEvent() {
|
||||
return event;
|
||||
}
|
||||
|
||||
public ListenerHandler[] getListeners() {
|
||||
return listeners.toArray(new ListenerHandler[0]);
|
||||
/**
|
||||
*
|
||||
* Returns a copy of the list of listeners.
|
||||
*
|
||||
* @return Copy of the list of listeners.
|
||||
*/
|
||||
public Subscriber<?>[] getListeners() {
|
||||
return listeners.toArray(new Subscriber[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the generated snowflake for the {@link EventFlow}
|
||||
*
|
||||
* @return The generated snowflake for this {@link EventFlow}
|
||||
*/
|
||||
public long getEventSnowflake() {
|
||||
return eventSnowflake;
|
||||
}
|
||||
|
||||
@@ -1,183 +1,46 @@
|
||||
package org.toop.framework.eventbus;
|
||||
|
||||
import com.lmax.disruptor.*;
|
||||
import com.lmax.disruptor.dsl.Disruptor;
|
||||
import com.lmax.disruptor.dsl.ProducerType;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.function.Consumer;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.toop.framework.eventbus.bus.DisruptorEventBus;
|
||||
import org.toop.framework.eventbus.bus.EventBus;
|
||||
import org.toop.framework.eventbus.events.EventType;
|
||||
import org.toop.framework.eventbus.events.EventWithSnowflake;
|
||||
import org.toop.framework.eventbus.store.DefaultSubscriberStore;
|
||||
import org.toop.framework.eventbus.subscriber.Subscriber;
|
||||
|
||||
/**
|
||||
* GlobalEventBus backed by the LMAX Disruptor for ultra-low latency, high-throughput event
|
||||
* publishing.
|
||||
*/
|
||||
public final class GlobalEventBus {
|
||||
public class GlobalEventBus implements EventBus {
|
||||
private static final EventBus INSTANCE = new DisruptorEventBus(
|
||||
LogManager.getLogger(DisruptorEventBus.class),
|
||||
new DefaultSubscriberStore()
|
||||
);
|
||||
|
||||
/** Map of event class to type-specific listeners. */
|
||||
private static final Map<Class<?>, CopyOnWriteArrayList<Consumer<? super EventType>>>
|
||||
LISTENERS = new ConcurrentHashMap<>();
|
||||
|
||||
/** Map of event class to Snowflake-ID-specific listeners. */
|
||||
private static final Map<
|
||||
Class<?>, ConcurrentHashMap<Long, Consumer<? extends EventWithSnowflake>>>
|
||||
UUID_LISTENERS = new ConcurrentHashMap<>();
|
||||
|
||||
/** Disruptor ring buffer size (must be power of two). */
|
||||
private static final int RING_BUFFER_SIZE = 1024 * 64;
|
||||
|
||||
/** Disruptor instance. */
|
||||
private static final Disruptor<EventHolder> DISRUPTOR;
|
||||
|
||||
/** Ring buffer used for publishing events. */
|
||||
private static final RingBuffer<EventHolder> RING_BUFFER;
|
||||
|
||||
static {
|
||||
ThreadFactory threadFactory =
|
||||
r -> {
|
||||
Thread t = new Thread(r, "EventBus-Disruptor");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
};
|
||||
|
||||
DISRUPTOR =
|
||||
new Disruptor<>(
|
||||
EventHolder::new,
|
||||
RING_BUFFER_SIZE,
|
||||
threadFactory,
|
||||
ProducerType.MULTI,
|
||||
new BusySpinWaitStrategy());
|
||||
|
||||
// Single consumer that dispatches to subscribers
|
||||
DISRUPTOR.handleEventsWith(
|
||||
(holder, seq, endOfBatch) -> {
|
||||
if (holder.event != null) {
|
||||
dispatchEvent(holder.event);
|
||||
holder.event = null;
|
||||
}
|
||||
});
|
||||
|
||||
DISRUPTOR.start();
|
||||
RING_BUFFER = DISRUPTOR.getRingBuffer();
|
||||
}
|
||||
|
||||
/** Prevent instantiation. */
|
||||
private GlobalEventBus() {}
|
||||
|
||||
/** Wrapper used inside the ring buffer. */
|
||||
private static class EventHolder {
|
||||
EventType event;
|
||||
public static EventBus get() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Subscription
|
||||
// ------------------------------------------------------------------------
|
||||
public static <T extends EventType> Consumer<? super EventType> subscribe(
|
||||
Class<T> eventClass, Consumer<T> listener) {
|
||||
|
||||
CopyOnWriteArrayList<Consumer<? super EventType>> list =
|
||||
LISTENERS.computeIfAbsent(eventClass, k -> new CopyOnWriteArrayList<>());
|
||||
|
||||
Consumer<? super EventType> wrapper = event -> listener.accept(eventClass.cast(event));
|
||||
list.add(wrapper);
|
||||
return wrapper;
|
||||
@Override
|
||||
public void subscribe(Subscriber<? extends EventType> listener) {
|
||||
INSTANCE.subscribe(listener);
|
||||
}
|
||||
|
||||
public static Consumer<? super EventType> subscribe(Consumer<Object> listener) {
|
||||
Consumer<? super EventType> wrapper = event -> listener.accept(event);
|
||||
LISTENERS.computeIfAbsent(Object.class, _ -> new CopyOnWriteArrayList<>()).add(wrapper);
|
||||
return wrapper;
|
||||
@Override
|
||||
public void unsubscribe(Subscriber<? extends EventType> listener) {
|
||||
INSTANCE.unsubscribe(listener);
|
||||
}
|
||||
|
||||
public static <T extends EventWithSnowflake> void subscribeById(
|
||||
Class<T> eventClass, long eventId, Consumer<T> listener) {
|
||||
UUID_LISTENERS
|
||||
.computeIfAbsent(eventClass, _ -> new ConcurrentHashMap<>())
|
||||
.put(eventId, listener);
|
||||
@Override
|
||||
public <T extends EventType> void post(T event) {
|
||||
INSTANCE.post(event);
|
||||
}
|
||||
|
||||
public static void unsubscribe(Object listener) {
|
||||
LISTENERS.values().forEach(list -> list.remove(listener));
|
||||
@Override
|
||||
public void shutdown() {
|
||||
INSTANCE.shutdown();
|
||||
}
|
||||
|
||||
public static <T extends EventWithSnowflake> void unsubscribeById(
|
||||
Class<T> eventClass, long eventId) {
|
||||
Map<Long, Consumer<? extends EventWithSnowflake>> map = UUID_LISTENERS.get(eventClass);
|
||||
if (map != null) map.remove(eventId);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Posting
|
||||
// ------------------------------------------------------------------------
|
||||
public static <T extends EventType> void post(T event) {
|
||||
dispatchEvent(event); // synchronous
|
||||
}
|
||||
|
||||
public static <T extends EventType> void postAsync(T event) {
|
||||
long seq = RING_BUFFER.next();
|
||||
try {
|
||||
EventHolder holder = RING_BUFFER.get(seq);
|
||||
holder.event = event;
|
||||
} finally {
|
||||
RING_BUFFER.publish(seq);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void dispatchEvent(EventType event) {
|
||||
Class<?> clazz = event.getClass();
|
||||
|
||||
// class-specific listeners
|
||||
CopyOnWriteArrayList<Consumer<? super EventType>> classListeners = LISTENERS.get(clazz);
|
||||
if (classListeners != null) {
|
||||
for (Consumer<? super EventType> listener : classListeners) {
|
||||
try {
|
||||
listener.accept(event);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// generic listeners
|
||||
CopyOnWriteArrayList<Consumer<? super EventType>> genericListeners =
|
||||
LISTENERS.get(Object.class);
|
||||
if (genericListeners != null) {
|
||||
for (Consumer<? super EventType> listener : genericListeners) {
|
||||
try {
|
||||
listener.accept(event);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// snowflake listeners
|
||||
if (event instanceof EventWithSnowflake snowflakeEvent) {
|
||||
Map<Long, Consumer<? extends EventWithSnowflake>> map = UUID_LISTENERS.get(clazz);
|
||||
if (map != null) {
|
||||
Consumer<EventWithSnowflake> listener =
|
||||
(Consumer<EventWithSnowflake>) map.remove(snowflakeEvent.eventSnowflake());
|
||||
if (listener != null) {
|
||||
try {
|
||||
listener.accept(snowflakeEvent);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Lifecycle
|
||||
// ------------------------------------------------------------------------
|
||||
public static void shutdown() {
|
||||
DISRUPTOR.shutdown();
|
||||
LISTENERS.clear();
|
||||
UUID_LISTENERS.clear();
|
||||
}
|
||||
|
||||
public static void reset() {
|
||||
LISTENERS.clear();
|
||||
UUID_LISTENERS.clear();
|
||||
@Override
|
||||
public void reset() {
|
||||
INSTANCE.reset();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package org.toop.framework.eventbus;
|
||||
|
||||
public class ListenerHandler {
|
||||
private Object listener = null;
|
||||
|
||||
// private boolean unsubscribeAfterSuccess = true;
|
||||
|
||||
// public ListenerHandler(Object listener, boolean unsubAfterSuccess) {
|
||||
// this.listener = listener;
|
||||
// this.unsubscribeAfterSuccess = unsubAfterSuccess;
|
||||
// }
|
||||
|
||||
public ListenerHandler(Object listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public Object getListener() {
|
||||
return this.listener;
|
||||
}
|
||||
|
||||
// public boolean isUnsubscribeAfterSuccess() {
|
||||
// return this.unsubscribeAfterSuccess;
|
||||
// }
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.toop.framework.eventbus.bus;
|
||||
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.toop.framework.eventbus.events.EventType;
|
||||
import org.toop.framework.eventbus.store.SubscriberStore;
|
||||
import org.toop.framework.eventbus.subscriber.Subscriber;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class DefaultEventBus implements EventBus {
|
||||
private final Logger logger;
|
||||
private final SubscriberStore eventsHolder;
|
||||
|
||||
public DefaultEventBus(Logger logger, SubscriberStore eventsHolder) {
|
||||
this.logger = logger;
|
||||
this.eventsHolder = eventsHolder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void subscribe(Subscriber<? extends EventType> subscriber) {
|
||||
eventsHolder.add(subscriber);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unsubscribe(Subscriber<? extends EventType> subscriber) {
|
||||
eventsHolder.remove(subscriber);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends EventType> void post(T event) {
|
||||
Class<T> eventType = (Class<T>) event.getClass();
|
||||
var subs = eventsHolder.get(eventType);
|
||||
if (subs != null) {
|
||||
for (Subscriber<?> subscriber : subs) {
|
||||
Class<T> eventClass = (Class<T>) subscriber.event();
|
||||
Consumer<EventType> action = (Consumer<EventType>) subscriber.handler();
|
||||
|
||||
action.accept((EventType) eventClass.cast(event));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
eventsHolder.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
eventsHolder.reset();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package org.toop.framework.eventbus.bus;
|
||||
|
||||
import com.lmax.disruptor.BusySpinWaitStrategy;
|
||||
import com.lmax.disruptor.RingBuffer;
|
||||
import com.lmax.disruptor.dsl.Disruptor;
|
||||
import com.lmax.disruptor.dsl.ProducerType;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.toop.framework.eventbus.subscriber.Subscriber;
|
||||
import org.toop.framework.eventbus.events.EventType;
|
||||
import org.toop.framework.eventbus.store.SubscriberStore;
|
||||
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class DisruptorEventBus implements EventBus {
|
||||
/** Wrapper used inside the ring buffer. */
|
||||
private static class EventHolder<T> {
|
||||
T event;
|
||||
}
|
||||
|
||||
private final Logger logger;
|
||||
private final SubscriberStore eventsHolder;
|
||||
|
||||
private final Disruptor<EventHolder<? extends EventType>> disruptor;
|
||||
private final RingBuffer<EventHolder<? extends EventType>> ringBuffer;
|
||||
|
||||
public DisruptorEventBus(Logger logger, SubscriberStore eventsHolder) {
|
||||
this.logger = logger;
|
||||
this.eventsHolder = eventsHolder;
|
||||
|
||||
ThreadFactory threadFactory =
|
||||
r -> {
|
||||
Thread t = new Thread(r, "EventBus-Disruptor");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
};
|
||||
|
||||
disruptor = getEventHolderDisruptor(threadFactory);
|
||||
|
||||
disruptor.start();
|
||||
this.ringBuffer = disruptor.getRingBuffer();
|
||||
}
|
||||
|
||||
private Disruptor<EventHolder<? extends EventType>> getEventHolderDisruptor(ThreadFactory threadFactory) {
|
||||
int RING_BUFFER_SIZE = 1024 * 64;
|
||||
Disruptor<EventHolder<? extends EventType>> disruptor = new Disruptor<>(
|
||||
EventHolder::new,
|
||||
RING_BUFFER_SIZE,
|
||||
threadFactory,
|
||||
ProducerType.MULTI,
|
||||
new BusySpinWaitStrategy());
|
||||
|
||||
disruptor.handleEventsWith(
|
||||
(holder, _, _) -> {
|
||||
if (holder.event != null) {
|
||||
dispatchEvent(holder.event);
|
||||
holder.event = null;
|
||||
}
|
||||
});
|
||||
return disruptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void subscribe(Subscriber<? extends EventType> listener) {
|
||||
eventsHolder.add(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unsubscribe(Subscriber<? extends EventType> listener) {
|
||||
eventsHolder.remove(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends EventType> void post(T event) {
|
||||
long seq = ringBuffer.next();
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
EventHolder<T> holder = (EventHolder<T>) ringBuffer.get(seq);
|
||||
holder.event = event;
|
||||
} finally {
|
||||
ringBuffer.publish(seq);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
disruptor.shutdown();
|
||||
eventsHolder.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
eventsHolder.reset();
|
||||
}
|
||||
|
||||
private <T extends EventType> void dispatchEvent(T event) {
|
||||
var classListeners = eventsHolder.get(event.getClass());
|
||||
if (classListeners != null) {
|
||||
for (Subscriber<?> listener : classListeners) {
|
||||
try {
|
||||
callListener(listener, event);
|
||||
} catch (Throwable e) {
|
||||
logger.warn("Exception while handling event: {}", event, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> void callListener(Subscriber<?> subscriber, T event) {
|
||||
Class<T> eventClass = (Class<T>) subscriber.event();
|
||||
Consumer<EventType> action = (Consumer<EventType>) subscriber.handler();
|
||||
|
||||
action.accept((EventType) eventClass.cast(event));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.toop.framework.eventbus.bus;
|
||||
|
||||
import org.toop.framework.eventbus.events.EventType;
|
||||
import org.toop.framework.eventbus.subscriber.Subscriber;
|
||||
|
||||
public interface EventBus {
|
||||
void subscribe(Subscriber<? extends EventType> subscriber);
|
||||
void unsubscribe(Subscriber<? extends EventType> subscriber);
|
||||
<T extends EventType> void post(T event);
|
||||
void shutdown();
|
||||
void reset();
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package org.toop.framework.eventbus.events;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public interface EventWithSnowflake extends EventType {
|
||||
Map<String, Object> result();
|
||||
long eventSnowflake();
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
package org.toop.framework.eventbus.events;
|
||||
|
||||
public interface EventWithoutSnowflake extends EventType {}
|
||||
@@ -1,69 +1,4 @@
|
||||
package org.toop.framework.eventbus.events;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.Arrays;
|
||||
|
||||
/** Events that are used in the GlobalEventBus class. */
|
||||
public class EventsBase {
|
||||
|
||||
/**
|
||||
* WIP, DO NOT USE!
|
||||
*
|
||||
* @param eventName
|
||||
* @param args
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
public static Object get(String eventName, Object... args) throws Exception {
|
||||
Class<?> clazz = Class.forName("org.toop.framework.eventbus.events.Events$ServerEvents$" + eventName);
|
||||
Class<?>[] paramTypes = Arrays.stream(args).map(Object::getClass).toArray(Class<?>[]::new);
|
||||
Constructor<?> constructor = clazz.getConstructor(paramTypes);
|
||||
return constructor.newInstance(args);
|
||||
}
|
||||
|
||||
/**
|
||||
* WIP, DO NOT USE!
|
||||
*
|
||||
* @param eventCategory
|
||||
* @param eventName
|
||||
* @param args
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
public static Object get(String eventCategory, String eventName, Object... args)
|
||||
throws Exception {
|
||||
Class<?> clazz =
|
||||
Class.forName("org.toop.framework.eventbus.events.Events$" + eventCategory + "$" + eventName);
|
||||
Class<?>[] paramTypes = Arrays.stream(args).map(Object::getClass).toArray(Class<?>[]::new);
|
||||
Constructor<?> constructor = clazz.getConstructor(paramTypes);
|
||||
return constructor.newInstance(args);
|
||||
}
|
||||
|
||||
/**
|
||||
* WIP, DO NOT USE!
|
||||
*
|
||||
* @param eventName
|
||||
* @param args
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
public static Object get2(String eventName, Object... args) throws Exception {
|
||||
// Fully qualified class name
|
||||
String className = "org.toop.server.backend.Events$ServerEvents$" + eventName;
|
||||
|
||||
// Load the class
|
||||
Class<?> clazz = Class.forName(className);
|
||||
|
||||
// Build array of argument types
|
||||
Class<?>[] paramTypes = new Class[args.length];
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
paramTypes[i] = args[i].getClass();
|
||||
}
|
||||
|
||||
// Get the constructor
|
||||
Constructor<?> constructor = clazz.getConstructor(paramTypes);
|
||||
|
||||
// Create a new instance
|
||||
return constructor.newInstance(args);
|
||||
}
|
||||
}
|
||||
public class EventsBase {}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package org.toop.framework.eventbus.events;
|
||||
|
||||
public interface GenericEvent extends EventType {}
|
||||
@@ -0,0 +1,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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package org.toop.framework.eventbus.store;
|
||||
|
||||
import org.toop.framework.eventbus.events.EventType;
|
||||
import org.toop.framework.eventbus.subscriber.Subscriber;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
||||
public class AsyncSubscriberStore implements SubscriberStore {
|
||||
private final ConcurrentHashMap<Class<? extends EventType>, ConcurrentLinkedQueue<Subscriber<? extends EventType>>> queues = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Class<? extends EventType>, Subscriber<? extends EventType>[]> snapshots = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void add(Subscriber<? extends EventType> sub) {
|
||||
queues.computeIfAbsent(sub.event(), _ -> new ConcurrentLinkedQueue<>()).add(sub);
|
||||
rebuildSnapshot(sub.event());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(Subscriber<? extends EventType> sub) {
|
||||
ConcurrentLinkedQueue<Subscriber<?>> queue = queues.get(sub.event());
|
||||
if (queue != null) {
|
||||
queue.remove(sub);
|
||||
rebuildSnapshot(sub.event());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Subscriber<? extends EventType>[] get(Class<? extends EventType> event) {
|
||||
return snapshots.getOrDefault(event, new Subscriber<?>[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
queues.clear();
|
||||
snapshots.clear();
|
||||
}
|
||||
|
||||
private void rebuildSnapshot(Class<? extends EventType> event) {
|
||||
ConcurrentLinkedQueue<Subscriber<?>> queue = queues.get(event);
|
||||
if (queue != null) {
|
||||
snapshots.put(event, queue.toArray(new Subscriber<?>[0]));
|
||||
} else {
|
||||
snapshots.put(event, new Subscriber<?>[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package org.toop.framework.eventbus.store;
|
||||
|
||||
import org.toop.framework.eventbus.events.EventType;
|
||||
import org.toop.framework.eventbus.subscriber.NamedSubscriber;
|
||||
import org.toop.framework.eventbus.subscriber.Subscriber;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class DefaultSubscriberStore implements SubscriberStore {
|
||||
|
||||
private static final Subscriber<? extends EventType>[] EMPTY = new Subscriber<?>[0];
|
||||
|
||||
private final ConcurrentHashMap<Class<? extends EventType>, Subscriber<? extends EventType>[]>
|
||||
listeners = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void add(Subscriber<? extends EventType> sub) {
|
||||
listeners.compute(sub.event(), (_, arr) -> {
|
||||
if (arr == null || arr.length == 0) {
|
||||
return new Subscriber<?>[]{sub};
|
||||
}
|
||||
|
||||
int len = arr.length;
|
||||
Subscriber<?>[] newArr = new Subscriber[len + 1];
|
||||
System.arraycopy(arr, 0, newArr, 0, len);
|
||||
newArr[len] = sub;
|
||||
return newArr;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(Subscriber<? extends EventType> sub) {
|
||||
listeners.computeIfPresent(sub.event(), (_, arr) -> {
|
||||
int len = arr.length;
|
||||
|
||||
if (len == 1) {
|
||||
return arr[0].equals(sub) ? null : arr;
|
||||
}
|
||||
|
||||
int keep = 0;
|
||||
for (Subscriber<?> s : arr) {
|
||||
if (!s.equals(sub)) keep++;
|
||||
}
|
||||
|
||||
if (keep == len) {
|
||||
return arr;
|
||||
}
|
||||
if (keep == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Subscriber<?>[] newArr = new Subscriber[keep];
|
||||
int i = 0;
|
||||
for (Subscriber<?> s : arr) {
|
||||
if (!s.equals(sub)) {
|
||||
newArr[i++] = s;
|
||||
}
|
||||
}
|
||||
|
||||
return newArr;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Subscriber<? extends EventType>[] get(Class<? extends EventType> event) {
|
||||
return listeners.getOrDefault(event, EMPTY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
listeners.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.toop.framework.eventbus.store;
|
||||
|
||||
import org.toop.framework.eventbus.events.EventType;
|
||||
import org.toop.framework.eventbus.subscriber.Subscriber;
|
||||
|
||||
public interface SubscriberStore {
|
||||
void add(Subscriber<? extends EventType> subscriber);
|
||||
void remove(Subscriber<? extends EventType> subscriber);
|
||||
Subscriber<? extends EventType>[] get(Class<? extends EventType> event);
|
||||
void reset();
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.toop.framework.eventbus.store;
|
||||
|
||||
import org.toop.framework.eventbus.events.EventType;
|
||||
import org.toop.framework.eventbus.subscriber.Subscriber;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class SyncSubscriberStore implements SubscriberStore {
|
||||
private final Map<Class<? extends EventType>, List<Subscriber<? extends EventType>>> LISTENERS = new ConcurrentHashMap<>();
|
||||
private static final Subscriber<? extends EventType>[] EMPTY = new Subscriber<?>[0];
|
||||
|
||||
@Override
|
||||
public void add(Subscriber<? extends EventType> sub) {
|
||||
LISTENERS.computeIfAbsent(sub.event(), _ -> new ArrayList<>()).add(sub);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(Subscriber<? extends EventType> sub) {
|
||||
LISTENERS.getOrDefault(sub.event(), new ArrayList<>()).remove(sub);
|
||||
LISTENERS.entrySet().removeIf(entry -> entry.getValue().isEmpty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Subscriber<? extends EventType>[] get(Class<? extends EventType> event) {
|
||||
List<Subscriber<? extends EventType>> list = LISTENERS.get(event);
|
||||
if (list == null || list.isEmpty()) return EMPTY;
|
||||
return list.toArray(EMPTY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
LISTENERS.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.toop.framework.eventbus.subscriber;
|
||||
|
||||
import org.toop.framework.eventbus.events.EventType;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public record DefaultNamedSubscriber<K extends EventType>(String id, Class<K> event, Consumer<K> handler)
|
||||
implements NamedSubscriber<K> {}
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.toop.framework.eventbus.subscriber;
|
||||
|
||||
import org.toop.framework.eventbus.events.EventType;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public record DefaultSubscriber<K extends EventType>(Class<K> event, Consumer<K> handler) implements Subscriber<K> {}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.toop.framework.eventbus.subscriber;
|
||||
|
||||
public interface HasId<ID> {
|
||||
ID id();
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.toop.framework.eventbus.subscriber;
|
||||
|
||||
import org.toop.framework.eventbus.events.EventType;
|
||||
|
||||
public interface IdSubscriber<K extends EventType> extends Subscriber<K>, HasId<Long> {}
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.toop.framework.eventbus.subscriber;
|
||||
|
||||
import org.toop.framework.eventbus.events.EventType;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public record LongIdSubscriber<K extends EventType>(Long id, Class<K> event, Consumer<K> handler)
|
||||
implements IdSubscriber<K> {}
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.toop.framework.eventbus.subscriber;
|
||||
|
||||
import org.toop.framework.eventbus.events.EventType;
|
||||
|
||||
public interface NamedSubscriber<K extends EventType> extends Subscriber<K>, HasId<String> {}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.toop.framework.eventbus.subscriber;
|
||||
|
||||
import org.toop.framework.eventbus.events.EventType;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public interface Subscriber<K extends EventType> {
|
||||
Class<K> event();
|
||||
Consumer<K> handler();
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.toop.framework.gameFramework;
|
||||
|
||||
/**
|
||||
* Represents the current state of a turn-based game.
|
||||
*/
|
||||
public enum GameState {
|
||||
/** Game is ongoing and no special condition applies. */
|
||||
NORMAL,
|
||||
|
||||
/** Game ended in a draw. */
|
||||
DRAW,
|
||||
|
||||
/** Game ended with a win for a player. */
|
||||
WIN,
|
||||
|
||||
/** Next player's turn was skipped. */
|
||||
TURN_SKIPPED,
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package org.toop.framework.gameFramework;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface LongPairConsumer {
|
||||
void accept(long a, long b);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.toop.framework.gameFramework.controller;
|
||||
|
||||
import org.toop.framework.gameFramework.model.game.SupportsOnlinePlay;
|
||||
import org.toop.framework.gameFramework.model.game.threadBehaviour.Controllable;
|
||||
import org.toop.framework.networking.events.NetworkEvents;
|
||||
|
||||
public interface GameController extends Controllable, UpdatesGameUI {
|
||||
/** Called when it is this player's turn to make a move. */
|
||||
void onYourTurn(NetworkEvents.YourTurnResponse event);
|
||||
|
||||
/** Called when a move from another player is received. */
|
||||
void onMoveReceived(NetworkEvents.GameMoveResponse event);
|
||||
|
||||
/** Called when the game has finished, with the final result. */
|
||||
void gameFinished(NetworkEvents.GameResultResponse event);
|
||||
|
||||
void sendMove(long clientId, long move);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.toop.framework.gameFramework.controller;
|
||||
|
||||
/**
|
||||
* Interface for classes that can trigger a UI update.
|
||||
*/
|
||||
public interface UpdatesGameUI {
|
||||
|
||||
/** Called to refresh or update the game UI. */
|
||||
void updateUI();
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.toop.framework.gameFramework.model.game;
|
||||
|
||||
public interface BoardProvider {
|
||||
long[] getBoard();
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.toop.framework.gameFramework.model.game;
|
||||
|
||||
public interface DeepCopyable<T> {
|
||||
T deepCopy();
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.toop.framework.gameFramework.model.game;
|
||||
|
||||
import org.toop.framework.gameFramework.GameState;
|
||||
|
||||
/**
|
||||
* Represents the result of a move in a turn-based game.
|
||||
*
|
||||
* @param state the resulting {@link GameState} after the move
|
||||
* @param player the index of the player associated with the result (winner or relevant player)
|
||||
*/
|
||||
public record PlayResult(GameState state, int player) {
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.toop.framework.gameFramework.model.game;
|
||||
|
||||
import org.toop.framework.gameFramework.GameState;
|
||||
|
||||
/**
|
||||
* Interface for turn-based games that can be played and queried for legal moves.
|
||||
*/
|
||||
public interface Playable {
|
||||
|
||||
/**
|
||||
* Returns the moves that are currently valid in the game.
|
||||
*
|
||||
* @return an array of integers representing legal moves
|
||||
*/
|
||||
long getLegalMoves();
|
||||
|
||||
/**
|
||||
* Plays the given move and returns the resulting game state.
|
||||
*
|
||||
* @param move the move to apply
|
||||
* @return the {@link GameState} and additional info after the move
|
||||
*/
|
||||
PlayResult play(long move);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.toop.framework.gameFramework.model.game;
|
||||
|
||||
import org.toop.framework.gameFramework.model.player.Player;
|
||||
|
||||
public interface PlayerProvider<T extends TurnBasedGame<T>> {
|
||||
Player<T> getPlayer(int index);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.toop.framework.gameFramework.model.game;
|
||||
|
||||
import org.toop.framework.networking.events.NetworkEvents;
|
||||
|
||||
/**
|
||||
* Interface for games that support online multiplayer play.
|
||||
* <p>
|
||||
* Methods are called in response to network events from the server.
|
||||
*/
|
||||
public interface SupportsOnlinePlay {
|
||||
|
||||
/** Called when it is this player's turn to make a move. */
|
||||
void onYourTurn(long clientId);
|
||||
|
||||
/** Called when a move from another player is received. */
|
||||
void onMoveReceived(long move);
|
||||
|
||||
/** Called when the game has finished, with the final result. */
|
||||
void gameFinished(String condition);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.toop.framework.gameFramework.model.game;
|
||||
|
||||
public interface TurnBasedGame<T extends TurnBasedGame<T>> extends Playable, DeepCopyable<T>, PlayerProvider<T>, BoardProvider {
|
||||
int getCurrentTurn();
|
||||
int getPlayerCount();
|
||||
int getWinner();
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package org.toop.framework.gameFramework.model.game.threadBehaviour;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.toop.framework.gameFramework.LongPairConsumer;
|
||||
import org.toop.framework.gameFramework.controller.GameController;
|
||||
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Base class for thread-based game behaviours.
|
||||
* <p>
|
||||
* Provides common functionality for managing game state and execution:
|
||||
* a running flag, a game reference, and a logger.
|
||||
* Subclasses implement the actual game-loop logic.
|
||||
*/
|
||||
public abstract class AbstractThreadBehaviour<T extends TurnBasedGame<T>> implements ThreadBehaviour {
|
||||
private LongPairConsumer onSendMove;
|
||||
private Runnable onUpdateUI;
|
||||
/** Indicates whether the game loop or event processing is active. */
|
||||
protected final AtomicBoolean isRunning = new AtomicBoolean();
|
||||
|
||||
/** The game instance controlled by this behaviour. */
|
||||
protected final T game;
|
||||
|
||||
/** Logger for the subclass to report errors or debug info. */
|
||||
protected final Logger logger = LogManager.getLogger(this.getClass());
|
||||
|
||||
/**
|
||||
* Creates a new base behaviour for the specified game.
|
||||
*
|
||||
* @param game the turn-based game to control
|
||||
*/
|
||||
public AbstractThreadBehaviour(T game) {
|
||||
this.game = game;
|
||||
}
|
||||
|
||||
protected void updateUI(){
|
||||
if (onUpdateUI != null) {
|
||||
onUpdateUI.run();
|
||||
}
|
||||
}
|
||||
|
||||
protected void sendMove(long clientId, long move){
|
||||
if (onSendMove != null) {
|
||||
onSendMove.accept(clientId, move);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnUpdateUI(Runnable onUpdateUI) {
|
||||
this.onUpdateUI = onUpdateUI;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnSendMove(LongPairConsumer onSendMove) {
|
||||
this.onSendMove = onSendMove;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.toop.framework.gameFramework.model.game.threadBehaviour;
|
||||
|
||||
public interface Controllable {
|
||||
void start();
|
||||
|
||||
void stop();
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.toop.framework.gameFramework.model.game.threadBehaviour;
|
||||
|
||||
import org.toop.framework.gameFramework.LongPairConsumer;
|
||||
import org.toop.framework.gameFramework.controller.GameController;
|
||||
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Strategy interface for controlling game thread behavior.
|
||||
* <p>
|
||||
* Defines how a game's execution is started, stopped, and which player is active.
|
||||
*/
|
||||
public interface ThreadBehaviour extends Controllable {
|
||||
void setOnUpdateUI(Runnable onUpdateUI);
|
||||
void setOnSendMove(LongPairConsumer onSendMove);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.toop.framework.gameFramework.model.player;
|
||||
|
||||
import org.toop.framework.gameFramework.model.game.DeepCopyable;
|
||||
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
|
||||
|
||||
public interface AI<T extends TurnBasedGame<T>> extends MoveProvider<T>, DeepCopyable<AI<T>> {
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.toop.framework.gameFramework.model.player;
|
||||
|
||||
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
|
||||
|
||||
/**
|
||||
* Abstract base class for AI implementations for games extending {@link GameR}.
|
||||
* <p>
|
||||
* Provides a common superclass for specific AI algorithms. Concrete subclasses
|
||||
* must implement the {@link #findBestMove(GameR, int)} method defined by
|
||||
* {@link IAIMoveR} to determine the best move given a game state and a search depth.
|
||||
* </p>
|
||||
*
|
||||
* @param <T> the specific type of game this AI can play, extending {@link GameR}
|
||||
*/
|
||||
public abstract class AbstractAI<T extends TurnBasedGame<T>> implements AI<T> {
|
||||
// Concrete AI implementations should override findBestMove(T game, int depth)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.toop.framework.gameFramework.model.player;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
|
||||
|
||||
/**
|
||||
* Abstract class representing a player in a game.
|
||||
* <p>
|
||||
* Players are entities that can make moves based on the current state of a game.
|
||||
* player types, such as human players or AI players.
|
||||
* </p>
|
||||
* <p>
|
||||
* Subclasses should override the {@link #getMove(GameR)} method to provide
|
||||
* specific move logic.
|
||||
* </p>
|
||||
*/
|
||||
public abstract class AbstractPlayer<T extends TurnBasedGame<T>> implements Player<T> {
|
||||
private final Logger logger = LogManager.getLogger(this.getClass());
|
||||
|
||||
private final String name;
|
||||
|
||||
protected AbstractPlayer(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
protected AbstractPlayer(AbstractPlayer<T> other) {
|
||||
this.name = other.name;
|
||||
}
|
||||
/**
|
||||
* Determines the next move based on the provided game state.
|
||||
* <p>
|
||||
* The default implementation throws an {@link UnsupportedOperationException},
|
||||
* indicating that concrete subclasses must override this method to provide
|
||||
* actual move logic.
|
||||
* </p>
|
||||
*
|
||||
* @param gameCopy a snapshot of the current game state
|
||||
* @return an integer representing the chosen move
|
||||
* @throws UnsupportedOperationException if the method is not overridden
|
||||
*/
|
||||
public long getMove(T gameCopy) {
|
||||
logger.error("Method getMove not implemented.");
|
||||
throw new UnsupportedOperationException("Not supported yet.");
|
||||
}
|
||||
|
||||
public String getName(){
|
||||
return this.name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.toop.framework.gameFramework.model.player;
|
||||
|
||||
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
|
||||
|
||||
public interface MoveProvider<T extends TurnBasedGame<T>> {
|
||||
long getMove(T game);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.toop.framework.gameFramework.model.player;
|
||||
|
||||
public interface NameProvider {
|
||||
String getName();
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.toop.framework.gameFramework.model.player;
|
||||
|
||||
import org.toop.framework.gameFramework.model.game.DeepCopyable;
|
||||
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
|
||||
|
||||
public interface Player<T extends TurnBasedGame<T>> extends NameProvider, MoveProvider<T>, DeepCopyable<Player<T>> {
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package org.toop.framework.gameFramework.view;
|
||||
|
||||
import org.toop.framework.eventbus.events.EventsBase;
|
||||
import org.toop.framework.eventbus.events.GenericEvent;
|
||||
|
||||
/**
|
||||
* Defines GUI-related events for the event bus.
|
||||
* <p>
|
||||
* These events notify the UI about updates such as game progress,
|
||||
* player actions, and game completion.
|
||||
*/
|
||||
public class GUIEvents extends EventsBase {
|
||||
/**
|
||||
* Event indicating the game has ended.
|
||||
*
|
||||
* @param winOrTie true if the game ended in a win, false for a draw
|
||||
* @param winner the index of the winning player, or -1 if no winner
|
||||
*/
|
||||
public record GameEnded(boolean winOrTie, int winner) implements GenericEvent {}
|
||||
|
||||
/** Event indicating a player has attempted a move. */
|
||||
public record PlayerAttemptedMove(long move) implements GenericEvent {}
|
||||
|
||||
/** Event indicating a player is hovering over a move (for UI feedback). */
|
||||
public record PlayerMoveHovered(long move) implements GenericEvent {}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
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.eventbus.bus.EventBus;
|
||||
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(EventBus eventBus, NetworkingClientManager clientManager) {
|
||||
this.clientManager = clientManager;
|
||||
new EventFlow(eventBus)
|
||||
.listen(NetworkEvents.StartClient.class, this::handleStartClient, false)
|
||||
.listen(NetworkEvents.SendCommand.class, this::handleCommand, false)
|
||||
.listen(NetworkEvents.SendLogin.class, this::handleSendLogin, false)
|
||||
.listen(NetworkEvents.SendLogout.class, this::handleSendLogout, false)
|
||||
.listen(NetworkEvents.SendGetPlayerlist.class, this::handleSendGetPlayerlist, false)
|
||||
.listen(NetworkEvents.SendGetGamelist.class, this::handleSendGetGamelist, false)
|
||||
.listen(NetworkEvents.SendSubscribe.class, this::handleSendSubscribe, false)
|
||||
.listen(NetworkEvents.SendMove.class, this::handleSendMove, false)
|
||||
.listen(NetworkEvents.SendChallenge.class, this::handleSendChallenge, false)
|
||||
.listen(NetworkEvents.SendAcceptChallenge.class, this::handleSendAcceptChallenge, false)
|
||||
.listen(NetworkEvents.SendForfeit.class, this::handleSendForfeit, false)
|
||||
.listen(NetworkEvents.SendMessage.class, this::handleSendMessage, false)
|
||||
.listen(NetworkEvents.SendHelp.class, this::handleSendHelp, false)
|
||||
.listen(NetworkEvents.SendHelpForCommand.class, this::handleSendHelpForCommand, false)
|
||||
.listen(NetworkEvents.CloseClient.class, this::handleCloseClient, false)
|
||||
.listen(NetworkEvents.Reconnect.class, this::handleReconnect, false)
|
||||
.listen(NetworkEvents.ChangeAddress.class, this::handleChangeAddress, false)
|
||||
.listen(NetworkEvents.RequestsAllClients.class, this::handleGetAllConnections, false)
|
||||
.listen(NetworkEvents.ForceCloseAllClients.class, this::handleShutdownAll, false);
|
||||
}
|
||||
|
||||
void handleStartClient(NetworkEvents.StartClient event) {
|
||||
long clientId = SnowflakeGenerator.nextId();
|
||||
new EventFlow().addPostEvent(new NetworkEvents.CreatedIdForClient(clientId, event.identifier())).postEvent();
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -2,202 +2,139 @@ 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.eventbus.bus.EventBus;
|
||||
import org.toop.framework.networking.events.NetworkEvents;
|
||||
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 {
|
||||
|
||||
public class NetworkingClientManager implements org.toop.framework.networking.interfaces.NetworkingClientManager {
|
||||
private static final Logger logger = LogManager.getLogger(NetworkingClientManager.class);
|
||||
|
||||
/** Map of serverId -> Server instances */
|
||||
final Map<Long, NetworkingClient> networkClients = new ConcurrentHashMap<>();
|
||||
private final EventBus eventBus;
|
||||
private final Map<Long, NetworkingClient> networkClients = new ConcurrentHashMap<>();
|
||||
|
||||
/** 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;
|
||||
public NetworkingClientManager(EventBus eventBus) {
|
||||
this.eventBus = eventBus;
|
||||
}
|
||||
|
||||
private void connectHelper(
|
||||
long id,
|
||||
NetworkingClient nClient,
|
||||
NetworkingConnector nConnector,
|
||||
Runnable onSuccess,
|
||||
Runnable onFailure
|
||||
) {
|
||||
|
||||
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
|
||||
|
||||
EventFlow closeEvent = new EventFlow()
|
||||
.listen(
|
||||
NetworkEvents.CloseClient.class,
|
||||
e -> {
|
||||
if (e.clientId() == id) scheduler.shutdownNow();
|
||||
}, "close");
|
||||
|
||||
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());
|
||||
eventBus.post(new NetworkEvents.ConnectTry(id, attempts, nConnector.reconnectAttempts(), true));
|
||||
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());
|
||||
eventBus.post(new NetworkEvents.ConnectTry(id, attempts, nConnector.reconnectAttempts(), false));
|
||||
scheduler.schedule(this, nConnector.timeout(), nConnector.timeUnit());
|
||||
} else {
|
||||
logger.error("Failed to start client for {}:{} after {} attempts", nConnector.host(), nConnector.port(), attempts);
|
||||
eventBus.post(new NetworkEvents.ConnectTry(id, -1, nConnector.reconnectAttempts(), false));
|
||||
onFailure.run();
|
||||
scheduler.shutdown();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Unexpected exception during startClient", e);
|
||||
eventBus.post(new NetworkEvents.ConnectTry(id, -1, nConnector.reconnectAttempts(), false));
|
||||
onFailure.run();
|
||||
scheduler.shutdown();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
scheduler.schedule(connectTask, 0, TimeUnit.MILLISECONDS);
|
||||
//
|
||||
// closeEvent.unsubscribe("close");
|
||||
}
|
||||
|
||||
@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(); // TODO: Maybe use the one generated
|
||||
try { // With EventFlow
|
||||
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(
|
||||
() -> {
|
||||
try {
|
||||
Thread.sleep(100); // TODO: Is this a good idea?
|
||||
new EventFlow()
|
||||
.addPostEvent(
|
||||
NetworkEvents.StartClientResponse.class,
|
||||
id,
|
||||
event.eventSnowflake())
|
||||
.asyncPostEvent();
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
})
|
||||
.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(); // TODO: Check if not blocking, what if error, mb not remove?
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,32 @@ 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.eventbus.bus.EventBus;
|
||||
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 final EventBus eventBus;
|
||||
private Channel channel;
|
||||
private NetworkingGameClientHandler handler;
|
||||
|
||||
public NetworkingClient(
|
||||
Supplier<NetworkingGameClientHandler> handlerFactory,
|
||||
String host,
|
||||
int port,
|
||||
long connectionId) {
|
||||
this.connectionId = connectionId;
|
||||
public TournamentNetworkingClient(EventBus eventBus) {
|
||||
this.eventBus = eventBus;
|
||||
}
|
||||
|
||||
@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 +45,7 @@ public class NetworkingClient {
|
||||
new ChannelInitializer<SocketChannel>() {
|
||||
@Override
|
||||
public void initChannel(SocketChannel ch) {
|
||||
handler = handlerFactory.get();
|
||||
NetworkingGameClientHandler handler = new NetworkingGameClientHandler(eventBus, clientId);
|
||||
|
||||
ChannelPipeline pipeline = ch.pipeline();
|
||||
pipeline.addLast(new LineBasedFrameDecoder(1024)); // split at \n
|
||||
@@ -52,53 +57,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);
|
||||
logger.warn("Cannot send message: '{}', connection inactive. ", literalMsg);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void closeConnection() {
|
||||
if (this.channel != null && this.channel.isActive()) {
|
||||
this.channel
|
||||
@@ -109,11 +89,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 +98,4 @@ public class NetworkingClient {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return this.connectionId;
|
||||
}
|
||||
}
|
||||
@@ -1,213 +1,222 @@
|
||||
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.GlobalEventBus;
|
||||
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 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 {}
|
||||
|
||||
public record CreatedIdForClient(long clientId, long identifier) implements ResponseToUniqueEvent {}
|
||||
|
||||
public record ConnectTry(long clientId, int amount, int maxAmount, boolean success) implements GenericEvent {}
|
||||
|
||||
/**
|
||||
* 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 {}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
@@ -1,20 +1,24 @@
|
||||
package org.toop.framework.networking;
|
||||
package org.toop.framework.networking.handlers;
|
||||
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
|
||||
import java.util.regex.MatchResult;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.toop.framework.eventbus.EventFlow;
|
||||
import org.toop.framework.eventbus.bus.EventBus;
|
||||
import org.toop.framework.networking.events.NetworkEvents;
|
||||
|
||||
public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
|
||||
private static final Logger logger = LogManager.getLogger(NetworkingGameClientHandler.class);
|
||||
|
||||
private final EventBus eventBus;
|
||||
private final long connectionId;
|
||||
|
||||
public NetworkingGameClientHandler(long connectionId) {
|
||||
public NetworkingGameClientHandler(EventBus eventBus, long connectionId) {
|
||||
this.eventBus = eventBus;
|
||||
this.connectionId = connectionId;
|
||||
}
|
||||
|
||||
@@ -38,9 +42,7 @@ public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
|
||||
"Received SVR message from server-{}, data: {}",
|
||||
ctx.channel().remoteAddress(),
|
||||
msg);
|
||||
new EventFlow()
|
||||
.addPostEvent(new NetworkEvents.ServerResponse(this.connectionId))
|
||||
.asyncPostEvent();
|
||||
eventBus.post(new NetworkEvents.ServerResponse(this.connectionId));
|
||||
parseServerReturn(rec);
|
||||
return;
|
||||
}
|
||||
@@ -70,11 +72,11 @@ public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
|
||||
case "CHALLENGE":
|
||||
gameChallengeHandler(recSrvRemoved);
|
||||
return;
|
||||
case "WIN", "DRAW", "LOSE":
|
||||
case "WIN", "DRAW", "LOSS":
|
||||
gameWinConditionHandler(recSrvRemoved);
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
// return
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -93,10 +95,10 @@ public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
|
||||
helpHandler(recSrvRemoved);
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
// return
|
||||
}
|
||||
} else {
|
||||
return; // TODO: Should be an error.
|
||||
logger.error("Could not parse: {}", rec);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,24 +113,18 @@ public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
|
||||
.map(m -> m.group(1).trim())
|
||||
.toArray(String[]::new);
|
||||
|
||||
new EventFlow()
|
||||
.addPostEvent(
|
||||
new NetworkEvents.GameMoveResponse(
|
||||
this.connectionId, msg[0], msg[1], msg[2]))
|
||||
.asyncPostEvent();
|
||||
eventBus.post(new NetworkEvents.GameMoveResponse(this.connectionId, msg[0], msg[1], msg[2]));
|
||||
}
|
||||
|
||||
private void gameWinConditionHandler(String rec) {
|
||||
String condition =
|
||||
Pattern.compile("\\b(win|draw|lose)\\b", Pattern.CASE_INSENSITIVE)
|
||||
.matcher(rec)
|
||||
.results()
|
||||
.toString()
|
||||
.trim();
|
||||
String condition = Pattern.compile("\\b(win|draw|loss)\\b", Pattern.CASE_INSENSITIVE)
|
||||
.matcher(rec)
|
||||
.results()
|
||||
.map(MatchResult::group)
|
||||
.findFirst()
|
||||
.orElse("");
|
||||
|
||||
new EventFlow()
|
||||
.addPostEvent(new NetworkEvents.GameResultResponse(this.connectionId, condition))
|
||||
.asyncPostEvent();
|
||||
eventBus.post(new NetworkEvents.GameResultResponse(this.connectionId, condition));
|
||||
}
|
||||
|
||||
private void gameChallengeHandler(String rec) {
|
||||
@@ -136,24 +132,16 @@ public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
|
||||
try {
|
||||
String[] msg =
|
||||
Pattern.compile(
|
||||
"(?:CHALLENGER|GAMETYPE|CHALLENGENUMBER):\\s*\"?(.*?)\"?\\s*(?:,|})")
|
||||
"(?:CHALLENGER|GAMETYPE|CHALLENGENUMBER):\\s*\"?(.*?)\"?\\s*[,}]")
|
||||
.matcher(rec)
|
||||
.results()
|
||||
.map(m -> m.group().trim())
|
||||
.toArray(String[]::new);
|
||||
|
||||
if (isCancelled)
|
||||
new EventFlow()
|
||||
.addPostEvent(
|
||||
new NetworkEvents.ChallengeCancelledResponse(
|
||||
this.connectionId, msg[0]))
|
||||
.asyncPostEvent();
|
||||
eventBus.post(new NetworkEvents.GameResultResponse(this.connectionId, msg[0]));
|
||||
else
|
||||
new EventFlow()
|
||||
.addPostEvent(
|
||||
new NetworkEvents.ChallengeResponse(
|
||||
this.connectionId, msg[0], msg[1], msg[2]))
|
||||
.asyncPostEvent();
|
||||
eventBus.post(new NetworkEvents.ChallengeResponse(this.connectionId, msg[0], msg[1], msg[2]));
|
||||
} catch (ArrayIndexOutOfBoundsException e) {
|
||||
logger.error("Array out of bounds for: {}", rec, e);
|
||||
}
|
||||
@@ -169,17 +157,14 @@ public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
|
||||
.toArray(String[]::new);
|
||||
|
||||
// [0] playerToMove, [1] gameType, [2] opponent
|
||||
new EventFlow()
|
||||
.addPostEvent(
|
||||
new NetworkEvents.GameMatchResponse(
|
||||
this.connectionId, msg[0], msg[1], msg[2]))
|
||||
.asyncPostEvent();
|
||||
eventBus.post(new NetworkEvents.GameMatchResponse(this.connectionId, msg[0], msg[1], msg[2]));
|
||||
} catch (ArrayIndexOutOfBoundsException e) {
|
||||
logger.error("Array out of bounds for: {}", rec, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void gameYourTurnHandler(String rec) {
|
||||
@SuppressWarnings("StreamToString")
|
||||
String msg =
|
||||
Pattern.compile("TURNMESSAGE:\\s*\"([^\"]*)\"")
|
||||
.matcher(rec)
|
||||
@@ -187,9 +172,7 @@ public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
|
||||
.toString()
|
||||
.trim();
|
||||
|
||||
new EventFlow()
|
||||
.addPostEvent(new NetworkEvents.YourTurnResponse(this.connectionId, msg))
|
||||
.asyncPostEvent();
|
||||
eventBus.post(new NetworkEvents.YourTurnResponse(this.connectionId, msg));
|
||||
}
|
||||
|
||||
private void playerlistHandler(String rec) {
|
||||
@@ -200,9 +183,7 @@ public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
|
||||
.map(m -> m.group(1).trim())
|
||||
.toArray(String[]::new);
|
||||
|
||||
new EventFlow()
|
||||
.addPostEvent(new NetworkEvents.PlayerlistResponse(this.connectionId, players))
|
||||
.asyncPostEvent();
|
||||
eventBus.post(new NetworkEvents.PlayerlistResponse(this.connectionId, players));
|
||||
}
|
||||
|
||||
private void gamelistHandler(String rec) {
|
||||
@@ -213,9 +194,7 @@ public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
|
||||
.map(m -> m.group(1).trim())
|
||||
.toArray(String[]::new);
|
||||
|
||||
new EventFlow()
|
||||
.addPostEvent(new NetworkEvents.GamelistResponse(this.connectionId, gameTypes))
|
||||
.asyncPostEvent();
|
||||
eventBus.post(new NetworkEvents.GamelistResponse(this.connectionId, gameTypes));
|
||||
}
|
||||
|
||||
private void helpHandler(String rec) {
|
||||
@@ -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);
|
||||
//
|
||||
//
|
||||
//}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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) {}
|
||||
@@ -0,0 +1,3 @@
|
||||
package org.toop.framework.networking.types;
|
||||
|
||||
public record ServerCommand(long clientId, String command) {}
|
||||
@@ -0,0 +1,3 @@
|
||||
package org.toop.framework.networking.types;
|
||||
|
||||
public record ServerMessage(String message) {}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.toop.framework.asset;
|
||||
package org.toop.framework.resource;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.*;
|
||||
@@ -9,12 +9,13 @@ import java.util.function.Function;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.reflections.Reflections;
|
||||
import org.toop.framework.asset.events.AssetLoaderEvents;
|
||||
import org.toop.framework.asset.resources.*;
|
||||
import org.toop.framework.asset.types.BundledResource;
|
||||
import org.toop.framework.asset.types.FileExtension;
|
||||
import org.toop.framework.asset.types.PreloadResource;
|
||||
import org.toop.framework.eventbus.EventFlow;
|
||||
import org.toop.framework.resource.events.AssetLoaderEvents;
|
||||
import org.toop.framework.resource.exceptions.CouldNotCreateResourceFactoryException;
|
||||
import org.toop.framework.resource.resources.*;
|
||||
import org.toop.framework.resource.types.BundledResource;
|
||||
import org.toop.framework.resource.types.FileExtension;
|
||||
import org.toop.framework.resource.types.PreloadResource;
|
||||
|
||||
/**
|
||||
* Responsible for loading assets from a file system directory into memory.
|
||||
@@ -141,19 +142,24 @@ public class ResourceLoader {
|
||||
}
|
||||
|
||||
/** Maps a file to a resource instance based on its extension and registered factories. */
|
||||
private <T extends BaseResource> T resourceMapper(File file, Class<T> type) {
|
||||
private <T extends BaseResource> T resourceMapper(File file)
|
||||
throws CouldNotCreateResourceFactoryException, IllegalArgumentException {
|
||||
String ext = getExtension(file.getName());
|
||||
Function<File, ? extends BaseResource> factory = registry.get(ext);
|
||||
if (factory == null) return null;
|
||||
if (factory == null)
|
||||
throw new CouldNotCreateResourceFactoryException(registry, file.getName());
|
||||
|
||||
BaseResource resource = factory.apply(file);
|
||||
|
||||
if (!type.isInstance(resource)) {
|
||||
if (resource == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"File " + file.getName() + " is not of type " + type.getSimpleName());
|
||||
"File "
|
||||
+ file.getName()
|
||||
+ " is not of type "
|
||||
+ BaseResource.class.getSimpleName());
|
||||
}
|
||||
|
||||
return type.cast(resource);
|
||||
return ((Class<T>) BaseResource.class).cast(resource);
|
||||
}
|
||||
|
||||
/** Loads the given list of files into assets, handling bundled and preload resources. */
|
||||
@@ -162,7 +168,14 @@ public class ResourceLoader {
|
||||
|
||||
for (File file : files) {
|
||||
boolean skipAdd = false;
|
||||
BaseResource resource = resourceMapper(file, BaseResource.class);
|
||||
BaseResource resource = null;
|
||||
try {
|
||||
resource = resourceMapper(file);
|
||||
} catch (CouldNotCreateResourceFactoryException _) {
|
||||
logger.warn("Could not create resource for: {}", file);
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.error(e);
|
||||
}
|
||||
switch (resource) {
|
||||
case null -> {
|
||||
continue;
|
||||
@@ -216,7 +229,7 @@ public class ResourceLoader {
|
||||
* {@link FileExtension}.
|
||||
*/
|
||||
private void autoRegisterResources() {
|
||||
Reflections reflections = new Reflections("org.toop.framework.asset.resources");
|
||||
Reflections reflections = new Reflections("org.toop.framework.resource.resources");
|
||||
Set<Class<? extends BaseResource>> classes = reflections.getSubTypesOf(BaseResource.class);
|
||||
|
||||
for (Class<? extends BaseResource> cls : classes) {
|
||||
@@ -1,16 +1,18 @@
|
||||
package org.toop.framework.asset;
|
||||
package org.toop.framework.resource;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.toop.framework.asset.resources.*;
|
||||
import org.toop.framework.resource.exceptions.ResourceNotFoundException;
|
||||
import org.toop.framework.resource.resources.*;
|
||||
|
||||
/**
|
||||
* Centralized manager for all loaded assets in the application.
|
||||
*
|
||||
* <p>{@code ResourceManager} maintains a thread-safe registry of {@link Asset} objects and provides
|
||||
* utility methods to retrieve assets by name, ID, or type. It works together with {@link
|
||||
* <p>{@code ResourceManager} maintains a thread-safe registry of {@link ResourceMeta} objects and
|
||||
* provides utility methods to retrieve assets by name, ID, or type. It works together with {@link
|
||||
* ResourceLoader} to register assets automatically when they are loaded from the file system.
|
||||
*
|
||||
* <p>Key responsibilities:
|
||||
@@ -50,19 +52,17 @@ import org.toop.framework.asset.resources.*;
|
||||
*/
|
||||
public class ResourceManager {
|
||||
private static final Logger logger = LogManager.getLogger(ResourceManager.class);
|
||||
private static final ResourceManager INSTANCE = new ResourceManager();
|
||||
private static final Map<String, ResourceMeta<? extends BaseResource>> assets =
|
||||
new ConcurrentHashMap<>();
|
||||
private static ResourceManager instance;
|
||||
|
||||
private ResourceManager() {}
|
||||
|
||||
/**
|
||||
* Returns the singleton instance of {@code ResourceManager}.
|
||||
*
|
||||
* @return the shared instance
|
||||
*/
|
||||
public static ResourceManager getInstance() {
|
||||
return INSTANCE;
|
||||
if (instance == null) {
|
||||
instance = new ResourceManager();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,7 +71,7 @@ public class ResourceManager {
|
||||
* @param loader the loader that has already loaded assets
|
||||
*/
|
||||
public static synchronized void loadAssets(ResourceLoader loader) {
|
||||
for (var asset : loader.getAssets()) {
|
||||
for (ResourceMeta<? extends BaseResource> asset : loader.getAssets()) {
|
||||
assets.put(asset.getName(), asset);
|
||||
}
|
||||
}
|
||||
@@ -87,21 +87,11 @@ public class ResourceManager {
|
||||
public static <T extends BaseResource> T get(String name) {
|
||||
ResourceMeta<T> asset = (ResourceMeta<T>) assets.get(name);
|
||||
if (asset == null) {
|
||||
throw new TypeNotPresentException(
|
||||
name,
|
||||
new RuntimeException(
|
||||
String.format(
|
||||
"Type %s not present",
|
||||
name))); // TODO: Create own exception, BAM
|
||||
throw new ResourceNotFoundException(name);
|
||||
}
|
||||
return asset.getResource();
|
||||
}
|
||||
|
||||
// @SuppressWarnings("unchecked")
|
||||
// public static <T extends BaseResource> ArrayList<ResourceMeta<T>> getAllOfType() {
|
||||
// return (ArrayList<ResourceMeta<T>>) (ArrayList<?>) new ArrayList<>(assets.values());
|
||||
// }
|
||||
|
||||
/**
|
||||
* Retrieve all assets of a specific resource type.
|
||||
*
|
||||
@@ -109,16 +99,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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,5 +149,6 @@ public class ResourceManager {
|
||||
*/
|
||||
public static void addAsset(ResourceMeta<? extends BaseResource> asset) {
|
||||
assets.put(asset.getName(), asset);
|
||||
logger.info("Successfully added asset: {}, to the asset list", asset.getName());
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.toop.framework.asset;
|
||||
package org.toop.framework.resource;
|
||||
|
||||
import org.toop.framework.SnowflakeGenerator;
|
||||
import org.toop.framework.asset.resources.BaseResource;
|
||||
import org.toop.framework.resource.resources.BaseResource;
|
||||
|
||||
public class ResourceMeta<T extends BaseResource> {
|
||||
private final Long id;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.toop.framework.resource.events;
|
||||
|
||||
import org.toop.framework.eventbus.events.GenericEvent;
|
||||
|
||||
public class AssetLoaderEvents {
|
||||
public record LoadingProgressUpdate(int hasLoadedAmount, int isLoadingAmount)
|
||||
implements GenericEvent {}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.toop.framework.resource.exceptions;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class CouldNotCreateResourceFactoryException extends RuntimeException {
|
||||
public CouldNotCreateResourceFactoryException(Map<?, ?> registry, String fileName) {
|
||||
super(
|
||||
String.format(
|
||||
"Could not create resource factory for: %s, isRegistryEmpty: %b",
|
||||
fileName, registry.isEmpty()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.toop.framework.resource.exceptions;
|
||||
|
||||
public class IsNotAResourceException extends RuntimeException {
|
||||
public <T> IsNotAResourceException(Class<T> clazz, String message) {
|
||||
super(clazz.getName() + " does not implement BaseResource");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.toop.framework.resource.exceptions;
|
||||
|
||||
public class ResourceNotFoundException extends RuntimeException {
|
||||
public ResourceNotFoundException(String name) {
|
||||
super("Could not find resource: " + name);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.toop.framework.asset.resources;
|
||||
package org.toop.framework.resource.resources;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.toop.framework.asset.resources;
|
||||
package org.toop.framework.resource.resources;
|
||||
|
||||
import java.io.File;
|
||||
import org.toop.framework.asset.types.FileExtension;
|
||||
import org.toop.framework.resource.types.FileExtension;
|
||||
|
||||
@FileExtension({"css"})
|
||||
public class CssAsset extends BaseResource {
|
||||
@@ -1,11 +1,11 @@
|
||||
package org.toop.framework.asset.resources;
|
||||
package org.toop.framework.resource.resources;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import javafx.scene.text.Font;
|
||||
import org.toop.framework.asset.types.FileExtension;
|
||||
import org.toop.framework.asset.types.PreloadResource;
|
||||
import org.toop.framework.resource.types.FileExtension;
|
||||
import org.toop.framework.resource.types.PreloadResource;
|
||||
|
||||
@FileExtension({"ttf", "otf"})
|
||||
public class FontAsset extends BaseResource implements PreloadResource {
|
||||
@@ -1,14 +1,14 @@
|
||||
package org.toop.framework.asset.resources;
|
||||
package org.toop.framework.resource.resources;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import javafx.scene.image.Image;
|
||||
import org.toop.framework.asset.types.FileExtension;
|
||||
import org.toop.framework.asset.types.LoadableResource;
|
||||
import org.toop.framework.resource.types.FileExtension;
|
||||
import org.toop.framework.resource.types.LoadableResource;
|
||||
|
||||
@FileExtension({"png", "jpg", "jpeg"})
|
||||
public class ImageAsset extends BaseResource implements LoadableResource {
|
||||
private Image image;
|
||||
private Image image = null;
|
||||
|
||||
public ImageAsset(final File file) {
|
||||
super(file);
|
||||
@@ -40,8 +40,7 @@ public class ImageAsset extends BaseResource implements LoadableResource {
|
||||
public Image getImage() {
|
||||
if (!this.isLoaded) {
|
||||
this.load();
|
||||
return image;
|
||||
}
|
||||
return null;
|
||||
return image;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.toop.framework.asset.resources;
|
||||
package org.toop.framework.resource.resources;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
@@ -6,15 +6,15 @@ import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import org.toop.framework.asset.types.FileExtension;
|
||||
import org.toop.framework.asset.types.LoadableResource;
|
||||
import org.toop.framework.resource.types.FileExtension;
|
||||
import org.toop.framework.resource.types.LoadableResource;
|
||||
|
||||
@FileExtension({"json"})
|
||||
public class JsonAsset<T> extends BaseResource implements LoadableResource {
|
||||
|
||||
private T content;
|
||||
private Class<T> type;
|
||||
private final Gson gson = new GsonBuilder().setPrettyPrinting().create();
|
||||
private final Gson gson = new GsonBuilder().serializeNulls().setPrettyPrinting().create();
|
||||
|
||||
public JsonAsset(File file, Class<T> type) {
|
||||
super(file);
|
||||
@@ -61,7 +61,8 @@ public class JsonAsset<T> extends BaseResource implements LoadableResource {
|
||||
File file = getFile();
|
||||
File parent = file.getParentFile();
|
||||
if (parent != null && !parent.exists()) {
|
||||
parent.mkdirs();
|
||||
boolean isDirectoryMade = parent.mkdirs();
|
||||
assert isDirectoryMade;
|
||||
}
|
||||
try (FileWriter writer = new FileWriter(file)) {
|
||||
gson.toJson(content, writer);
|
||||
@@ -1,11 +1,11 @@
|
||||
package org.toop.framework.asset.resources;
|
||||
package org.toop.framework.resource.resources;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import org.toop.framework.asset.types.BundledResource;
|
||||
import org.toop.framework.asset.types.FileExtension;
|
||||
import org.toop.framework.asset.types.LoadableResource;
|
||||
import org.toop.framework.resource.types.BundledResource;
|
||||
import org.toop.framework.resource.types.FileExtension;
|
||||
import org.toop.framework.resource.types.LoadableResource;
|
||||
|
||||
/**
|
||||
* Represents a localization resource asset that loads and manages property files containing
|
||||
@@ -32,13 +32,13 @@ 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";
|
||||
|
||||
/** Fallback locale used when no matching locale is found. */
|
||||
private final Locale fallback = Locale.forLanguageTag("en_US");
|
||||
private final Locale fallback = Locale.forLanguageTag("en");
|
||||
|
||||
/**
|
||||
* Constructs a new LocalizationAsset for the specified file.
|
||||
@@ -53,16 +53,19 @@ 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;
|
||||
}
|
||||
|
||||
/** Returns the fallback locale used when locale is missing argument*/
|
||||
public Locale getFallback() {return this.fallback;}
|
||||
|
||||
/**
|
||||
* Checks whether this asset has been loaded.
|
||||
*
|
||||
@@ -70,7 +73,7 @@ public class LocalizationAsset extends BaseResource implements LoadableResource,
|
||||
*/
|
||||
@Override
|
||||
public boolean isLoaded() {
|
||||
return isLoaded;
|
||||
return loaded;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -126,12 +129,12 @@ public class LocalizationAsset extends BaseResource implements LoadableResource,
|
||||
public void loadFile(File file) {
|
||||
try (InputStreamReader reader =
|
||||
new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)) {
|
||||
Locale locale = extractLocale(file.getName(), baseName);
|
||||
Locale locale = extractLocale(file.getName());
|
||||
bundles.put(locale, new PropertyResourceBundle(reader));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to load localization file: " + file, e);
|
||||
}
|
||||
isLoaded = true;
|
||||
loaded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -144,32 +147,13 @@ public class LocalizationAsset extends BaseResource implements LoadableResource,
|
||||
return this.baseName;
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Extracts the base name from a file name.
|
||||
// *
|
||||
// * @param fileName the file name
|
||||
// * @return base name without locale or extension
|
||||
// */
|
||||
// private String getBaseName(String fileName) {
|
||||
// int dotIndex = fileName.lastIndexOf('.');
|
||||
// String nameWithoutExtension = (dotIndex > 0) ? fileName.substring(0, dotIndex) :
|
||||
// fileName;
|
||||
//
|
||||
// int underscoreIndex = nameWithoutExtension.indexOf('_');
|
||||
// if (underscoreIndex > 0) {
|
||||
// return nameWithoutExtension.substring(0, underscoreIndex);
|
||||
// }
|
||||
// return nameWithoutExtension;
|
||||
// }
|
||||
|
||||
/**
|
||||
* Extracts a locale from a file name based on the pattern "base_LOCALE.properties".
|
||||
*
|
||||
* @param fileName the file name
|
||||
* @param baseName the base name
|
||||
* @return extracted locale, or fallback if none found
|
||||
*/
|
||||
private Locale extractLocale(String fileName, String baseName) {
|
||||
private Locale extractLocale(String fileName) {
|
||||
int underscoreIndex = fileName.indexOf('_');
|
||||
int dotIndex = fileName.lastIndexOf('.');
|
||||
if (underscoreIndex > 0 && dotIndex > underscoreIndex) {
|
||||
@@ -0,0 +1,106 @@
|
||||
package org.toop.framework.resource.resources;
|
||||
|
||||
import java.io.*;
|
||||
import javafx.scene.media.Media;
|
||||
import javafx.scene.media.MediaPlayer;
|
||||
import org.toop.framework.resource.types.AudioResource;
|
||||
import org.toop.framework.resource.types.FileExtension;
|
||||
import org.toop.framework.resource.types.LoadableResource;
|
||||
|
||||
@FileExtension({"mp3"})
|
||||
public class MusicAsset extends BaseResource implements LoadableResource, AudioResource {
|
||||
private MediaPlayer mediaPlayer;
|
||||
private double volume;
|
||||
|
||||
public MusicAsset(final File audioFile) {
|
||||
super(audioFile);
|
||||
}
|
||||
|
||||
public MediaPlayer getMediaPlayer() {
|
||||
load();
|
||||
return mediaPlayer;
|
||||
}
|
||||
|
||||
private void initPlayer() {
|
||||
mediaPlayer.setOnEndOfMedia(this::stop);
|
||||
mediaPlayer.setOnError(this::stop);
|
||||
mediaPlayer.setOnStopped(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load() {
|
||||
if (mediaPlayer == null) {
|
||||
mediaPlayer = new MediaPlayer(new Media(file.toURI().toString()));
|
||||
initPlayer();
|
||||
mediaPlayer.setVolume(volume);
|
||||
}
|
||||
this.isLoaded = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unload() {
|
||||
if (mediaPlayer != null) {
|
||||
mediaPlayer.stop();
|
||||
mediaPlayer.dispose();
|
||||
mediaPlayer = null;
|
||||
}
|
||||
isLoaded = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoaded() {
|
||||
return isLoaded;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateVolume(double volume) {
|
||||
if (mediaPlayer != null) {
|
||||
mediaPlayer.setVolume(volume);
|
||||
}
|
||||
this.volume = volume;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() { return super.getFile().getName(); }
|
||||
|
||||
@Override
|
||||
public void setOnEnd(Runnable run) {
|
||||
mediaPlayer.setOnEndOfMedia(run);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnError(Runnable run) {
|
||||
mediaPlayer.setOnError(run);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void play() {
|
||||
getMediaPlayer().play();
|
||||
}
|
||||
|
||||
public void pause() {
|
||||
getMediaPlayer().pause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
getMediaPlayer().stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long duration() {
|
||||
if (mediaPlayer != null) {
|
||||
return (long) this.mediaPlayer.getTotalDuration().toSeconds(); // Why is this a double? TODO: Fix cast
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long currentPosition() {
|
||||
if (mediaPlayer != null) {
|
||||
return (long) this.mediaPlayer.getCurrentTime().toSeconds(); // Same here. TODO: Fix cast
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.toop.framework.asset.resources;
|
||||
package org.toop.framework.resource.resources;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Locale;
|
||||
@@ -38,6 +38,22 @@ public class SettingsAsset extends JsonAsset<Settings> {
|
||||
return getContent().layoutSize;
|
||||
}
|
||||
|
||||
public Boolean getTutorialFlag() {
|
||||
return getContent().showTutorials;
|
||||
}
|
||||
|
||||
public Boolean getFirstTTT() {
|
||||
return getContent().firstTTT;
|
||||
}
|
||||
|
||||
public Boolean getFirstConnect4() {
|
||||
return getContent().firstConnect4;
|
||||
}
|
||||
|
||||
public Boolean getFirstReversi() {
|
||||
return getContent().firstReversi;
|
||||
}
|
||||
|
||||
public void setVolume(int volume) {
|
||||
getContent().volume = volume;
|
||||
save();
|
||||
@@ -72,4 +88,24 @@ public class SettingsAsset extends JsonAsset<Settings> {
|
||||
getContent().layoutSize = layoutSize;
|
||||
save();
|
||||
}
|
||||
|
||||
public void setTutorialFlag(boolean tutorialFlag) {
|
||||
getContent().showTutorials = tutorialFlag;
|
||||
save();
|
||||
}
|
||||
|
||||
public void setFirstTTT(boolean firstTTT) {
|
||||
getContent().firstTTT = firstTTT;
|
||||
save();
|
||||
}
|
||||
|
||||
public void setFirstConnect4(boolean firstConnect4) {
|
||||
getContent().firstConnect4 = firstConnect4;
|
||||
save();
|
||||
}
|
||||
|
||||
public void setFirstReversi(boolean firstReversi) {
|
||||
getContent().firstReversi = firstReversi;
|
||||
save();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package org.toop.framework.resource.resources;
|
||||
|
||||
import java.io.*;
|
||||
import javax.sound.sampled.*;
|
||||
|
||||
import org.toop.framework.resource.types.AudioResource;
|
||||
import org.toop.framework.resource.types.FileExtension;
|
||||
import org.toop.framework.resource.types.LoadableResource;
|
||||
|
||||
@FileExtension({"wav"})
|
||||
public class SoundEffectAsset extends BaseResource implements LoadableResource, AudioResource {
|
||||
private final Clip clip = AudioSystem.getClip();
|
||||
|
||||
private LineListener onEnd = null;
|
||||
private LineListener onError = null;
|
||||
|
||||
private double volume = 100; // TODO: Find a better way to set volume on clip load
|
||||
|
||||
public SoundEffectAsset(final File audioFile) throws LineUnavailableException {
|
||||
super(audioFile);
|
||||
}
|
||||
|
||||
// Gets a new clip to play
|
||||
public Clip getClip() {
|
||||
if (!this.isLoaded()) {this.load();} return this.clip;
|
||||
}
|
||||
|
||||
private AudioInputStream downSampleAudio(AudioInputStream audioInputStream, AudioFormat baseFormat) {
|
||||
AudioFormat decodedFormat =
|
||||
new AudioFormat(
|
||||
AudioFormat.Encoding.PCM_SIGNED,
|
||||
baseFormat.getSampleRate(),
|
||||
16, // force 16-bit
|
||||
baseFormat.getChannels(),
|
||||
baseFormat.getChannels() * 2,
|
||||
baseFormat.getSampleRate(),
|
||||
false // little-endian
|
||||
);
|
||||
|
||||
return AudioSystem.getAudioInputStream(decodedFormat, audioInputStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load() {
|
||||
try {
|
||||
if (this.isLoaded){
|
||||
return; // Return if it is already loaded
|
||||
}
|
||||
|
||||
// Insert a new audio stream into the clip
|
||||
AudioInputStream inputStream = AudioSystem.getAudioInputStream(new BufferedInputStream(new FileInputStream(this.getFile())));
|
||||
AudioFormat baseFormat = inputStream.getFormat();
|
||||
if (baseFormat.getSampleSizeInBits() > 16)
|
||||
inputStream = downSampleAudio(inputStream, baseFormat);
|
||||
this.clip.open(inputStream); // ^ Clip can only run 16 bit and lower, thus downsampling necessary.
|
||||
this.updateVolume(this.volume);
|
||||
this.isLoaded = true;
|
||||
} catch (LineUnavailableException | UnsupportedAudioFileException | IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unload() {
|
||||
if (!this.isLoaded) return; // Return if already unloaded
|
||||
|
||||
if (clip.isRunning()) clip.stop(); // Stops playback of the clip
|
||||
|
||||
clip.close(); // Releases native resources (empties buffer)
|
||||
|
||||
this.getClip().removeLineListener(this.onEnd);
|
||||
this.getClip().removeLineListener(this.onError);
|
||||
|
||||
this.onEnd = null;
|
||||
this.onError = null;
|
||||
|
||||
this.isLoaded = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoaded() {
|
||||
return this.isLoaded;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateVolume(double volume) {
|
||||
{
|
||||
this.volume = volume;
|
||||
if (clip.isControlSupported(FloatControl.Type.MASTER_GAIN)) {
|
||||
FloatControl volumeControl =
|
||||
(FloatControl) clip.getControl(FloatControl.Type.MASTER_GAIN);
|
||||
float min = volumeControl.getMinimum();
|
||||
float max = volumeControl.getMaximum();
|
||||
float dB =
|
||||
(float)
|
||||
(Math.log10(Math.max(volume, 0.0001))
|
||||
* 20.0); // convert linear to dB
|
||||
dB = Math.max(min, Math.min(max, dB));
|
||||
volumeControl.setValue(dB);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.getFile().getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnEnd(Runnable run) {
|
||||
this.onEnd = event -> {
|
||||
if (event.getType() == LineEvent.Type.STOP) {
|
||||
run.run();
|
||||
}
|
||||
};
|
||||
|
||||
this.getClip().addLineListener(this.onEnd);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnError(Runnable run) {
|
||||
// this.onError = event -> {
|
||||
// if (event.getType() == LineEvent.Type.STOP) {
|
||||
// run.run();
|
||||
// }
|
||||
// }; TODO
|
||||
//
|
||||
// this.getClip().addLineListener(this.onEnd);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void play() {
|
||||
if (!isLoaded()) load();
|
||||
|
||||
this.clip.setFramePosition(0); // rewind to the start
|
||||
this.clip.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
if (this.clip.isRunning()) this.clip.stop();
|
||||
}
|
||||
|
||||
// had to be implemented for mediaplayer.pause() to work so:
|
||||
// TODO: get this removed, somehow OR get a clip.pause which I have no idea why you would ever use
|
||||
public void pause() {
|
||||
this.clip.stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long duration() {
|
||||
if (clip != null) {
|
||||
return (long) (clip.getMicrosecondLength() / 1_000_000.0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long currentPosition() {
|
||||
if (clip != null) {
|
||||
return (long) (clip.getMicrosecondPosition() / 1_000_000.0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package org.toop.framework.asset.resources;
|
||||
package org.toop.framework.resource.resources;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import org.toop.framework.asset.types.FileExtension;
|
||||
import org.toop.framework.asset.types.LoadableResource;
|
||||
import org.toop.framework.resource.types.FileExtension;
|
||||
import org.toop.framework.resource.types.LoadableResource;
|
||||
|
||||
@FileExtension({"txt", "json", "xml"})
|
||||
public class TextAsset extends BaseResource implements LoadableResource {
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.toop.framework.resource.types;
|
||||
|
||||
public interface AudioResource {
|
||||
String getName();
|
||||
void updateVolume(double volume);
|
||||
void play();
|
||||
void stop();
|
||||
long duration();
|
||||
long currentPosition();
|
||||
void setOnEnd(Runnable run);
|
||||
void setOnError(Runnable run);
|
||||
void pause();
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.toop.framework.asset.types;
|
||||
package org.toop.framework.resource.types;
|
||||
|
||||
import java.io.File;
|
||||
import org.toop.framework.asset.ResourceLoader;
|
||||
import org.toop.framework.resource.ResourceLoader;
|
||||
|
||||
/**
|
||||
* Represents a resource that can be composed of multiple files, or "bundled" together under a
|
||||
@@ -69,9 +69,4 @@ public interface BundledResource {
|
||||
* @return the base name used to identify this bundled resource
|
||||
*/
|
||||
String getBaseName();
|
||||
|
||||
// /**
|
||||
// Returns the name
|
||||
// */
|
||||
// String getDefaultName();
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package org.toop.framework.asset.types;
|
||||
package org.toop.framework.resource.types;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import org.toop.framework.asset.ResourceLoader;
|
||||
import org.toop.framework.asset.resources.BaseResource;
|
||||
import org.toop.framework.resource.ResourceLoader;
|
||||
import org.toop.framework.resource.resources.BaseResource;
|
||||
|
||||
/**
|
||||
* Annotation to declare which file extensions a {@link BaseResource} subclass can handle.
|
||||
@@ -1,6 +1,6 @@
|
||||
package org.toop.framework.asset.types;
|
||||
package org.toop.framework.resource.types;
|
||||
|
||||
import org.toop.framework.asset.ResourceLoader;
|
||||
import org.toop.framework.resource.ResourceLoader;
|
||||
|
||||
/**
|
||||
* Represents a resource that can be explicitly loaded and unloaded.
|
||||
@@ -1,6 +1,6 @@
|
||||
package org.toop.framework.asset.types;
|
||||
package org.toop.framework.resource.types;
|
||||
|
||||
import org.toop.framework.asset.ResourceLoader;
|
||||
import org.toop.framework.resource.ResourceLoader;
|
||||
|
||||
/**
|
||||
* Marker interface for resources that should be **automatically loaded** by the {@link
|
||||
@@ -8,4 +8,9 @@ public class Settings {
|
||||
public int volume = 100;
|
||||
public int fxVolume = 20;
|
||||
public int musicVolume = 15;
|
||||
public Boolean showTutorials;
|
||||
public Boolean firstReversi;
|
||||
public Boolean firstTTT;
|
||||
public Boolean firstConnect4;
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user