diff --git a/.idea/misc.xml b/.idea/misc.xml index 64c32f6..72be14a 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -13,7 +13,7 @@ - + \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/App.java b/app/src/main/java/org/toop/app/App.java index db9683d..931bd2c 100644 --- a/app/src/main/java/org/toop/app/App.java +++ b/app/src/main/java/org/toop/app/App.java @@ -8,6 +8,8 @@ import javafx.application.Application; import javafx.scene.layout.StackPane; import javafx.scene.Scene; import javafx.stage.Stage; +import org.toop.framework.asset.AssetManager; +import org.toop.framework.asset.resources.LocalizationAsset; import org.toop.local.AppContext; import java.util.Locale; @@ -18,8 +20,8 @@ public class App extends Application { private static Scene scene; private static StackPane root; - private Locale currentLocale = AppContext.getLocale(); - private ResourceBundle resourceBundle = ResourceBundle.getBundle("Localization", currentLocale); + private Locale currentLocale = AppContext.getLocale(); + private LocalizationAsset loc = AssetManager.get("localization.properties"); public static void run(String[] args) { launch(args); @@ -30,7 +32,7 @@ public class App extends Application { final StackPane root = new StackPane(new MainMenu().getPane()); final Scene scene = new Scene(root); - stage.setTitle(resourceBundle.getString("windowTitle")); + stage.setTitle(loc.getString("windowTitle", currentLocale)); stage.setMinWidth(1080); stage.setMinHeight(720); diff --git a/app/src/main/java/org/toop/app/menu/CreditsMenu.java b/app/src/main/java/org/toop/app/menu/CreditsMenu.java index 385bf42..e2f0b5f 100644 --- a/app/src/main/java/org/toop/app/menu/CreditsMenu.java +++ b/app/src/main/java/org/toop/app/menu/CreditsMenu.java @@ -1,5 +1,7 @@ package org.toop.app.menu; +import org.toop.framework.asset.AssetManager; +import org.toop.framework.asset.resources.LocalizationAsset; import org.toop.local.AppContext; import java.util.Locale; @@ -7,7 +9,7 @@ import java.util.ResourceBundle; public final class CreditsMenu extends Menu { private Locale currentLocale = AppContext.getLocale(); - private ResourceBundle resourceBundle = ResourceBundle.getBundle("Localization", currentLocale); + private LocalizationAsset loc = AssetManager.get("localization.properties"); public CreditsMenu() { } } \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/menu/MainMenu.java b/app/src/main/java/org/toop/app/menu/MainMenu.java index 3ac4955..7d1d2d0 100644 --- a/app/src/main/java/org/toop/app/menu/MainMenu.java +++ b/app/src/main/java/org/toop/app/menu/MainMenu.java @@ -4,31 +4,33 @@ import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.image.ImageView; import javafx.scene.layout.*; -import org.toop.local.AppContext; +import javafx.scene.text.Font; +import org.toop.framework.asset.resources.FontAsset; +import org.toop.framework.asset.resources.LocalizationAsset; import java.util.Locale; -import java.util.ResourceBundle; import org.toop.framework.asset.AssetManager; import org.toop.framework.asset.resources.CssAsset; import org.toop.framework.asset.resources.ImageAsset; public final class MainMenu extends Menu { - private Locale currentLocale = AppContext.getLocale(); - private ResourceBundle resourceBundle = ResourceBundle.getBundle("Localization", currentLocale); + private final Locale currentLocale = Locale.of("nl"); + private final LocalizationAsset loc = AssetManager.get("localization.properties"); public MainMenu() { - final Button tictactoe = createButton(resourceBundle.getString("mainMenuSelectTicTacToe"), () -> {}); - final Button reversi = createButton(resourceBundle.getString("mainMenuSelectReversi"), () -> {}); - final Button sudoku = createButton(resourceBundle.getString("mainMenuSelectSudoku"), () -> {}); - final Button battleship = createButton(resourceBundle.getString("mainMenuSelectBattleship"), () -> {}); - final Button other = createButton(resourceBundle.getString("mainMenuSelectOther"), () -> {}); + + final Button tictactoe = createButton(loc.getString("mainMenuSelectTicTacToe", currentLocale), () -> {}); + final Button reversi = createButton(loc.getString("mainMenuSelectReversi", currentLocale), () -> {}); + final Button sudoku = createButton(loc.getString("mainMenuSelectSudoku", currentLocale), () -> {}); + final Button battleship = createButton(loc.getString("mainMenuSelectBattleship", currentLocale), () -> {}); + final Button other = createButton(loc.getString("mainMenuSelectOther", currentLocale), () -> {}); final VBox gamesBox = new VBox(tictactoe, reversi, sudoku, battleship, other); gamesBox.setAlignment(Pos.TOP_CENTER); - final Button credits = createButton(resourceBundle.getString("mainMenuSelectCredits"), () -> {}); - final Button options = createButton(resourceBundle.getString("mainMenuSelectOptions"), () -> {}); - final Button quit = createButton(resourceBundle.getString("mainMenuSelectQuit"), () -> {}); + final Button credits = createButton(loc.getString("mainMenuSelectCredits", currentLocale), () -> {}); + final Button options = createButton(loc.getString("mainMenuSelectOptions", currentLocale), () -> {}); + final Button quit = createButton(loc.getString("mainMenuSelectQuit", currentLocale), () -> {}); final VBox creditsBox = new VBox(credits, options, quit); creditsBox.setAlignment(Pos.BOTTOM_CENTER); @@ -43,7 +45,7 @@ public final class MainMenu extends Menu { background.fitHeightProperty().bind(grid.heightProperty()); pane = new StackPane(background, grid); - CssAsset css = (CssAsset) AssetManager.getByName("main.css").getResource(); + CssAsset css = AssetManager.get("main.css"); pane.getStylesheets().add(css.getUrl()); } } \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/menu/Menu.java b/app/src/main/java/org/toop/app/menu/Menu.java index fba8208..8a75209 100644 --- a/app/src/main/java/org/toop/app/menu/Menu.java +++ b/app/src/main/java/org/toop/app/menu/Menu.java @@ -6,16 +6,19 @@ import javafx.animation.FadeTransition; import javafx.scene.control.Button; import javafx.scene.layout.Pane; import javafx.util.Duration; +import org.toop.framework.asset.Asset; +import org.toop.framework.asset.AssetManager; +import org.toop.framework.asset.resources.LocalizationAsset; import org.toop.local.AppContext; +import java.util.ArrayList; import java.util.Locale; -import java.util.ResourceBundle; public abstract class Menu { protected Pane pane; public Pane getPane() { return pane; } - private Locale currentLocale = AppContext.getLocale(); - private ResourceBundle resourceBundle = ResourceBundle.getBundle("Localization", currentLocale); + private Locale currentLocale = AppContext.getLocale(); + private LocalizationAsset loc = AssetManager.get("localization.properties"); public void fadeBackgroundImage(String imagePath, float from, float to, float milliseconds) { final FadeTransition fade = new FadeTransition(Duration.millis(milliseconds), App.getRoot()); diff --git a/app/src/main/java/org/toop/app/menu/OptionsMenu.java b/app/src/main/java/org/toop/app/menu/OptionsMenu.java index 2d2bd64..ea21dcb 100644 --- a/app/src/main/java/org/toop/app/menu/OptionsMenu.java +++ b/app/src/main/java/org/toop/app/menu/OptionsMenu.java @@ -1,5 +1,7 @@ package org.toop.app.menu; +import org.toop.framework.asset.AssetManager; +import org.toop.framework.asset.resources.LocalizationAsset; import org.toop.local.AppContext; import java.util.Locale; @@ -7,7 +9,7 @@ import java.util.ResourceBundle; public final class OptionsMenu extends Menu { private Locale currentLocale = AppContext.getLocale(); - private ResourceBundle resourceBundle = ResourceBundle.getBundle("Localization", currentLocale); + private LocalizationAsset loc = AssetManager.get("localization.properties"); public OptionsMenu() { } } \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/menu/QuitMenu.java b/app/src/main/java/org/toop/app/menu/QuitMenu.java index 150c687..3916dfc 100644 --- a/app/src/main/java/org/toop/app/menu/QuitMenu.java +++ b/app/src/main/java/org/toop/app/menu/QuitMenu.java @@ -10,29 +10,30 @@ import javafx.scene.text.Text; import org.toop.app.App; import org.toop.framework.asset.AssetManager; import org.toop.framework.asset.resources.CssAsset; +import org.toop.framework.asset.resources.LocalizationAsset; import org.toop.local.AppContext; import java.util.Locale; import java.util.ResourceBundle; public final class QuitMenu extends Menu { - private Locale currentLocale = AppContext.getLocale(); - private ResourceBundle resourceBundle = ResourceBundle.getBundle("Localization", currentLocale); + private Locale currentLocale = AppContext.getLocale(); + private LocalizationAsset loc = AssetManager.get("localization.properties"); public QuitMenu() { final Region background = new Region(); background.getStyleClass().add("quit-background"); background.setPrefSize(Double.MAX_VALUE, Double.MAX_VALUE); - final Text sure = new Text(resourceBundle.getString("quitMenuTextSure")); + final Text sure = new Text(loc.getString("quitMenuTextSure", currentLocale)); sure.getStyleClass().add("quit-text"); - final Button yes = new Button(resourceBundle.getString("quitMenuButtonYes")); + final Button yes = new Button(loc.getString("quitMenuButtonYes", currentLocale)); yes.getStyleClass().add("quit-button"); yes.setOnAction(_ -> { App.quit(); }); - final Button no = new Button(resourceBundle.getString("quitMenuButtonNo")); + final Button no = new Button(loc.getString("quitMenuButtonNo", currentLocale)); no.getStyleClass().add("quit-button"); no.setOnAction(_ -> { App.pop(); @@ -51,7 +52,7 @@ public final class QuitMenu extends Menu { StackPane.setAlignment(box, Pos.CENTER); pane = modalContainer; - CssAsset css = (CssAsset) AssetManager.getByName("quit.css").getResource(); + CssAsset css = AssetManager.get("quit.css"); pane.getStylesheets().add(css.getUrl()); } } \ No newline at end of file diff --git a/app/src/main/java/org/toop/tictactoe/LocalTicTacToe.java b/app/src/main/java/org/toop/tictactoe/LocalTicTacToe.java deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/org/toop/tictactoe/gui/UIGameBoard.java b/app/src/main/java/org/toop/tictactoe/gui/UIGameBoard.java deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/resources/assets/fonts/GroovyManiac.ttf b/app/src/main/resources/assets/fonts/GroovyManiac.ttf new file mode 100644 index 0000000..4202757 Binary files /dev/null and b/app/src/main/resources/assets/fonts/GroovyManiac.ttf differ diff --git a/app/src/main/resources/assets/fonts/Roboto-Regular.ttf b/app/src/main/resources/assets/fonts/Roboto-Regular.ttf new file mode 100644 index 0000000..7e3bb2f Binary files /dev/null and b/app/src/main/resources/assets/fonts/Roboto-Regular.ttf differ diff --git a/app/src/main/resources/assets/localization/localization.properties b/app/src/main/resources/assets/localization/localization.properties new file mode 100644 index 0000000..9718bf0 --- /dev/null +++ b/app/src/main/resources/assets/localization/localization.properties @@ -0,0 +1,17 @@ +# Window title +windowTitle=ISY Games Selector + +# Main Menu buttons +mainMenuSelectTicTacToe=Tic Tac Toe +mainMenuSelectReversi=Reversi +mainMenuSelectSudoku=Sudoku +mainMenuSelectBattleship=Battleship +mainMenuSelectOther=Other +mainMenuSelectCredits=Credits +mainMenuSelectOptions=Options +mainMenuSelectQuit=Quit + +# Quit Menu text and buttons +quitMenuTextSure=Are you sure? +quitMenuButtonYes=Yes +quitMenuButtonNo=No \ No newline at end of file diff --git a/app/src/main/resources/assets/localization/localization_nl.properties b/app/src/main/resources/assets/localization/localization_nl.properties new file mode 100644 index 0000000..4c3eb30 --- /dev/null +++ b/app/src/main/resources/assets/localization/localization_nl.properties @@ -0,0 +1,17 @@ +# Window title +windowTitle=ISY Spellen Kiezer + +# Main Menu buttons +mainMenuSelectTicTacToe=Boter Kaas En Eieren +mainMenuSelectReversi=Reversi +mainMenuSelectSudoku=Sudoku +mainMenuSelectBattleship=Zeeslag +mainMenuSelectOther=Anders +mainMenuSelectCredits=Credits +mainMenuSelectOptions=Opties +mainMenuSelectQuit=Afsluiten + +# Quit Menu text and buttons +quitMenuTextSure=Weet je het zeker? +quitMenuButtonYes=Ja +quitMenuButtonNo=Nee \ No newline at end of file diff --git a/app/src/main/resources/assets/style/main.css b/app/src/main/resources/assets/style/main.css index 99e8087..b6abd24 100644 --- a/app/src/main/resources/assets/style/main.css +++ b/app/src/main/resources/assets/style/main.css @@ -1,6 +1,5 @@ .main-button { -fx-background-color: transparent; - -fx-background-image: url("card-default.jpg"); /* fallback image */ -fx-background-size: cover; -fx-background-position: center; -fx-pref-width: 250px; diff --git a/app/src/main/resources/assets/style/quit.css b/app/src/main/resources/assets/style/quit.css index 59c72a3..212734d 100644 --- a/app/src/main/resources/assets/style/quit.css +++ b/app/src/main/resources/assets/style/quit.css @@ -13,7 +13,7 @@ -fx-fill: white; -fx-font-size: 28px; -fx-font-weight: 600; - -fx-font-family: "Segoe UI", sans-serif; + -fx-font-family: "Groovy Maniac Demo", sans-serif; } .quit-button { diff --git a/app/src/main/resources/image/game/battleship.png b/app/src/main/resources/image/game/battleship.png deleted file mode 100644 index 813893f..0000000 Binary files a/app/src/main/resources/image/game/battleship.png and /dev/null differ diff --git a/app/src/main/resources/image/game/other.png b/app/src/main/resources/image/game/other.png deleted file mode 100644 index 6bd4167..0000000 Binary files a/app/src/main/resources/image/game/other.png and /dev/null differ diff --git a/app/src/main/resources/image/game/reversi.png b/app/src/main/resources/image/game/reversi.png deleted file mode 100644 index bd9252f..0000000 Binary files a/app/src/main/resources/image/game/reversi.png and /dev/null differ diff --git a/app/src/main/resources/image/game/sudoku.png b/app/src/main/resources/image/game/sudoku.png deleted file mode 100644 index ec88234..0000000 Binary files a/app/src/main/resources/image/game/sudoku.png and /dev/null differ diff --git a/app/src/main/resources/image/game/tictactoe.png b/app/src/main/resources/image/game/tictactoe.png deleted file mode 100644 index 2a81e05..0000000 Binary files a/app/src/main/resources/image/game/tictactoe.png and /dev/null differ diff --git a/app/src/main/resources/style/main.css b/app/src/main/resources/style/main.css deleted file mode 100644 index 99e8087..0000000 --- a/app/src/main/resources/style/main.css +++ /dev/null @@ -1,33 +0,0 @@ -.main-button { - -fx-background-color: transparent; - -fx-background-image: url("card-default.jpg"); /* fallback image */ - -fx-background-size: cover; - -fx-background-position: center; - -fx-pref-width: 250px; - -fx-pref-height: 180px; - -fx-border-radius: 15; - -fx-background-radius: 15; - -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.3), 15, 0.4, 0, 4); - -fx-cursor: hand; - -fx-padding: 0; -} - -.card-label { - -fx-background-color: rgba(0, 0, 0, 0.5); - -fx-font-size: 20px; - -fx-font-weight: bold; - -fx-text-fill: white; - -fx-padding: 10px; - -fx-alignment: top-center; - -fx-background-radius: 15 15 0 0; - -fx-opacity: 0; - -fx-transition: all 0.3s ease; -} - -.main-button:hover { - -fx-effect: dropshadow(gaussian, #00ffff, 15, 0.5, 0, 0); -} - -.main-button:hover .card-label { - -fx-opacity: 1; -} \ No newline at end of file diff --git a/app/src/main/resources/style/quit.css b/app/src/main/resources/style/quit.css deleted file mode 100644 index 59c72a3..0000000 --- a/app/src/main/resources/style/quit.css +++ /dev/null @@ -1,33 +0,0 @@ -.quit-background { - -fx-background-color: rgba(0, 0, 0, 0.6); -} - -.quit-box { - -fx-background-color: rgba(30, 30, 30, 0.95); - -fx-background-radius: 15; - -fx-padding: 30; - -fx-effect: dropshadow(gaussian, black, 20, 0.6, 0, 4); -} - -.quit-text { - -fx-fill: white; - -fx-font-size: 28px; - -fx-font-weight: 600; - -fx-font-family: "Segoe UI", sans-serif; -} - -.quit-button { - -fx-font-size: 16px; - -fx-text-fill: white; - -fx-background-color: transparent; - -fx-border-color: white; - -fx-border-radius: 5; - -fx-padding: 8 20; - -fx-cursor: hand; -} - -.quit-button:hover { - -fx-text-fill: #00ffff; - -fx-border-color: #00ffff; - -fx-effect: dropshadow(gaussian, #00ffff, 8, 0.5, 0, 0); -} \ No newline at end of file diff --git a/app/src/main/resources/style/style.css b/app/src/main/resources/style/style.css deleted file mode 100644 index c09d516..0000000 --- a/app/src/main/resources/style/style.css +++ /dev/null @@ -1,20 +0,0 @@ -.root { - -fx-background-color: #2d2d2d; - - -fx-font-size: 28px; - -fx-font-weight: 600; - -fx-font-family: "Segoe UI", sans-serif; -} - -.button { - -fx-background-color: transparent; - -fx-text-fill: white; - -fx-border-color: transparent; - -fx-padding: 10 20; - -fx-cursor: hand; - -fx-effect: null; -} - -.button:hover { - -fx-effect: dropshadow(gaussian, #00ffff, 10, 0.3, 0, 0); -} \ No newline at end of file diff --git a/framework/src/main/java/org/toop/framework/SnowflakeGenerator.java b/framework/src/main/java/org/toop/framework/SnowflakeGenerator.java index a48a8a7..7fbb946 100644 --- a/framework/src/main/java/org/toop/framework/SnowflakeGenerator.java +++ b/framework/src/main/java/org/toop/framework/SnowflakeGenerator.java @@ -5,7 +5,38 @@ import java.time.Instant; import java.util.Collections; import java.util.concurrent.atomic.AtomicLong; +/** + * A thread-safe, distributed unique ID generator following the Snowflake pattern. + *

+ * Each generated 64-bit ID encodes: + *

+ *

+ * + *

This implementation ensures: + *

+ *

+ * + *

Custom epoch is set to {@code 2025-01-01T00:00:00Z}.

+ * + *

Usage example:

+ *
{@code
+ * SnowflakeGenerator generator = new SnowflakeGenerator();
+ * long id = generator.nextId();
+ * }
+ */ public class SnowflakeGenerator { + + /** + * Custom epoch in milliseconds (2025-01-01T00:00:00Z). + */ private static final long EPOCH = Instant.parse("2025-01-01T00:00:00Z").toEpochMilli(); // Bit allocations @@ -13,19 +44,27 @@ public class SnowflakeGenerator { private static final long MACHINE_BITS = 10; private static final long SEQUENCE_BITS = 12; - // Max values + // Maximum values for each component 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 + // Bit shifts for composing the ID 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(); + private final AtomicLong lastTimestamp = new AtomicLong(-1L); private 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. + */ private static long genMachineId() { try { StringBuilder sb = new StringBuilder(); @@ -35,17 +74,25 @@ public class SnowflakeGenerator { for (byte b : mac) sb.append(String.format("%02X", b)); } } - // limit to 10 bits (0–1023) - return sb.toString().hashCode() & 0x3FF; + return sb.toString().hashCode() & 0x3FF; // limit to 10 bits } catch (Exception e) { return (long) (Math.random() * 1024); // fallback } } + /** + * 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( @@ -53,6 +100,16 @@ public class SnowflakeGenerator { } } + /** + * Generates the next unique ID. + *

+ * If multiple IDs are generated in the same millisecond, a sequence number + * is incremented. If the sequence overflows, waits until the next millisecond. + *

+ * + * @return a unique 64-bit ID + * @throws IllegalStateException if clock moves backwards or timestamp exceeds 41-bit limit + */ public synchronized long nextId() { long currentTimestamp = timestamp(); @@ -67,7 +124,6 @@ public class SnowflakeGenerator { if (currentTimestamp == lastTimestamp.get()) { sequence = (sequence + 1) & MAX_SEQUENCE; if (sequence == 0) { - // Sequence overflow, wait for next millisecond currentTimestamp = waitNextMillis(currentTimestamp); } } else { @@ -81,6 +137,11 @@ public class SnowflakeGenerator { | sequence; } + /** + * Waits until the next millisecond if sequence overflows. + * @param lastTimestamp previous timestamp + * @return new timestamp + */ private long waitNextMillis(long lastTimestamp) { long ts = timestamp(); while (ts <= lastTimestamp) { @@ -89,6 +150,9 @@ public class SnowflakeGenerator { return ts; } + /** + * Returns current system timestamp in milliseconds. + */ private long timestamp() { return System.currentTimeMillis(); } diff --git a/framework/src/main/java/org/toop/framework/asset/AssetLoader.java b/framework/src/main/java/org/toop/framework/asset/AssetLoader.java index 6d632fc..cfc04d6 100644 --- a/framework/src/main/java/org/toop/framework/asset/AssetLoader.java +++ b/framework/src/main/java/org/toop/framework/asset/AssetLoader.java @@ -2,65 +2,123 @@ package org.toop.framework.asset; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.toop.framework.asset.events.AssetEvents; -import org.toop.framework.asset.resources.BaseResource; +import org.toop.framework.asset.events.AssetLoaderEvents; +import org.toop.framework.asset.resources.*; +import org.toop.framework.eventbus.EventFlow; +import org.reflections.Reflections; import java.io.File; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; -import org.reflections.Reflections; -import org.toop.framework.asset.resources.FileExtension; -import org.toop.framework.asset.resources.FontAsset; -import org.toop.framework.eventbus.EventFlow; - +/** + * Responsible for loading assets from a file system directory into memory. + *

+ * The {@code AssetLoader} scans a root folder recursively, identifies files, + * and maps them to registered resource types based on file extensions and + * {@link FileExtension} annotations. + * It supports multiple resource types including {@link PreloadResource} (automatically loaded) + * and {@link BundledResource} (merged across multiple files). + *

+ * + *

Assets are stored in a static, thread-safe list and can be retrieved + * through {@link AssetManager}.

+ * + *

Features:

+ * + * + *

Usage example:

+ *
{@code
+ * AssetLoader loader = new AssetLoader("assets");
+ * double progress = loader.getProgress();
+ * List> loadedAssets = loader.getAssets();
+ * }
+ */ public class AssetLoader { private static final Logger logger = LogManager.getLogger(AssetLoader.class); - private final List> assets = new CopyOnWriteArrayList<>(); + private static final List> assets = new CopyOnWriteArrayList<>(); private final Map> registry = new ConcurrentHashMap<>(); - private volatile int loadedCount = 0; - private volatile int totalCount = 0; + private final AtomicInteger loadedCount = new AtomicInteger(0); + private int totalCount = 0; + /** + * Constructs an AssetLoader and loads assets from the given root folder. + * @param rootFolder the folder containing asset files + */ public AssetLoader(File rootFolder) { - autoRegisterResources(); // make sure resources are registered! - + autoRegisterResources(); List foundFiles = new ArrayList<>(); fileSearcher(rootFolder, foundFiles); this.totalCount = foundFiles.size(); loader(foundFiles); } + /** + * Constructs an AssetLoader from a folder path. + * @param rootFolder the folder path containing assets + */ public AssetLoader(String rootFolder) { this(new File(rootFolder)); } + /** + * Returns the current progress of loading assets (0.0 to 1.0). + * @return progress as a double + */ public double getProgress() { - return (this.totalCount == 0) ? 1.0 : (this.loadedCount / (double) this.totalCount); + return (totalCount == 0) ? 1.0 : (loadedCount.get() / (double) totalCount); } + /** + * Returns the number of assets loaded so far. + * @return loaded count + */ public int getLoadedCount() { - return this.loadedCount; + return loadedCount.get(); } + /** + * Returns the total number of files found to load. + * @return total asset count + */ public int getTotalCount() { - return this.totalCount; + return totalCount; } + /** + * Returns a snapshot list of all assets loaded by this loader. + * @return list of loaded assets + */ public List> getAssets() { - return new ArrayList<>(this.assets); + return new ArrayList<>(assets); } + /** + * Registers a factory for a specific file extension. + * @param extension the file extension (without dot) + * @param factory a function mapping a File to a resource instance + * @param the type of resource + */ public void register(String extension, Function factory) { this.registry.put(extension, factory); } + /** + * Maps a file to a resource instance based on its extension and registered factories. + */ private T resourceMapper(File file, Class type) { String ext = getExtension(file.getName()); - Function factory = this.registry.get(ext); - + Function factory = registry.get(ext); if (factory == null) return null; BaseResource resource = factory.apply(file); @@ -70,32 +128,54 @@ public class AssetLoader { "File " + file.getName() + " is not of type " + type.getSimpleName() ); } - return type.cast(resource); } + /** + * Loads the given list of files into assets, handling bundled and preload resources. + */ private void loader(List files) { + Map bundledResources = new HashMap<>(); + for (File file : files) { BaseResource resource = resourceMapper(file, BaseResource.class); - if (resource != null) { - Asset asset = new Asset<>(file.getName(), resource); - this.assets.add(asset); - - if (resource instanceof FontAsset fontAsset) { - fontAsset.load(); + switch (resource) { + case null -> { + continue; + } + case BundledResource br -> { + String key = resource.getClass().getName() + ":" + br.getBaseName(); + if (bundledResources.containsKey(key)) { + bundledResources.get(key).loadFile(file); + resource = (BaseResource) bundledResources.get(key); + } else { + br.loadFile(file); + bundledResources.put(key, br); + } + } + case PreloadResource pr -> pr.load(); + default -> { } - - logger.info("Loaded {} from {}", resource.getClass().getSimpleName(), file.getAbsolutePath()); - - this.loadedCount++; // TODO: Fix non atmomic operation - new EventFlow() - .addPostEvent(new AssetEvents.LoadingProgressUpdate(this.loadedCount, this.totalCount)) - .postEvent(); } + + BaseResource finalResource = resource; + boolean alreadyAdded = assets.stream() + .anyMatch(a -> a.getResource() == finalResource); + if (!alreadyAdded) { + assets.add(new Asset<>(file.getName(), resource)); + } + + logger.info("Loaded {} from {}", resource.getClass().getSimpleName(), file.getAbsolutePath()); + loadedCount.incrementAndGet(); + new EventFlow() + .addPostEvent(new AssetLoaderEvents.LoadingProgressUpdate(loadedCount.get(), totalCount)) + .postEvent(); } - logger.info("Loaded {} assets", files.size()); } + /** + * Recursively searches a folder and adds all files to the foundFiles list. + */ private void fileSearcher(final File folder, List foundFiles) { for (File fileEntry : Objects.requireNonNull(folder.listFiles())) { if (fileEntry.isDirectory()) { @@ -106,6 +186,10 @@ public class AssetLoader { } } + /** + * Uses reflection to automatically register all {@link BaseResource} subclasses + * annotated with {@link FileExtension}. + */ private void autoRegisterResources() { Reflections reflections = new Reflections("org.toop.framework.asset.resources"); Set> classes = reflections.getSubTypesOf(BaseResource.class); @@ -114,7 +198,7 @@ public class AssetLoader { if (!cls.isAnnotationPresent(FileExtension.class)) continue; FileExtension annotation = cls.getAnnotation(FileExtension.class); for (String ext : annotation.value()) { - this.registry.put(ext, file -> { + registry.put(ext, file -> { try { return cls.getConstructor(File.class).newInstance(file); } catch (Exception e) { @@ -125,6 +209,19 @@ public class AssetLoader { } } + /** + * Extracts the base name from a file name, used for bundling multiple files. + */ + private static String getBaseName(String fileName) { + int underscoreIndex = fileName.indexOf('_'); + int dotIndex = fileName.lastIndexOf('.'); + if (underscoreIndex > 0) return fileName.substring(0, underscoreIndex); + return fileName.substring(0, dotIndex); + } + + /** + * Returns the file extension of a given file name (without dot). + */ public static String getExtension(String name) { int i = name.lastIndexOf('.'); return (i > 0) ? name.substring(i + 1) : ""; diff --git a/framework/src/main/java/org/toop/framework/asset/AssetManager.java b/framework/src/main/java/org/toop/framework/asset/AssetManager.java index e53aac0..1630ae0 100644 --- a/framework/src/main/java/org/toop/framework/asset/AssetManager.java +++ b/framework/src/main/java/org/toop/framework/asset/AssetManager.java @@ -5,25 +5,96 @@ import org.toop.framework.asset.resources.*; import java.util.*; import java.util.concurrent.ConcurrentHashMap; +/** + * Centralized manager for all loaded assets in the application. + *

+ * {@code AssetManager} 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 AssetLoader} to register assets automatically + * when they are loaded from the file system. + *

+ * + *

Key responsibilities:

+ *
    + *
  • Storing all loaded assets in a concurrent map.
  • + *
  • Providing typed access to asset resources.
  • + *
  • Allowing lookup by asset name or ID.
  • + *
  • Supporting retrieval of all assets of a specific {@link BaseResource} subclass.
  • + *
+ * + *

Example usage:

+ *
{@code
+ * // Load assets from a loader
+ * AssetLoader loader = new AssetLoader(new File("RootFolder"));
+ * AssetManager.loadAssets(loader);
+ *
+ * // Retrieve a single resource
+ * ImageAsset background = AssetManager.get("background.jpg");
+ *
+ * // Retrieve all fonts
+ * List> fonts = AssetManager.getAllOfType(FontAsset.class);
+ *
+ * // Retrieve by asset name or optional lookup
+ * Optional> maybeAsset = AssetManager.findByName("menu.css");
+ * }
+ * + *

Notes:

+ *
    + *
  • All retrieval methods are static and thread-safe.
  • + *
  • The {@link #get(String)} method may require casting if the asset type is not known at compile time.
  • + *
  • Assets should be loaded via {@link AssetLoader} before retrieval.
  • + *
+ */ public class AssetManager { private static final AssetManager INSTANCE = new AssetManager(); private static final Map> assets = new ConcurrentHashMap<>(); private AssetManager() {} + /** + * Returns the singleton instance of {@code AssetManager}. + * + * @return the shared instance + */ public static AssetManager getInstance() { return INSTANCE; } + /** + * Loads all assets from a given {@link AssetLoader} into the manager. + * + * @param loader the loader that has already loaded assets + */ public synchronized static void loadAssets(AssetLoader loader) { for (var asset : loader.getAssets()) { assets.put(asset.getName(), asset); } } + /** + * Retrieve the resource of a given name, cast to the expected type. + * + * @param name the asset name + * @param the expected resource type + * @return the resource, or null if not found + */ + @SuppressWarnings("unchecked") + public static T get(String name) { + Asset asset = (Asset) assets.get(name); + if (asset == null) return null; + return asset.getResource(); + } + + /** + * Retrieve all assets of a specific resource type. + * + * @param type the class type to filter + * @param the resource type + * @return a list of assets matching the type + */ public static ArrayList> getAllOfType(Class type) { ArrayList> list = new ArrayList<>(); - for (Asset asset : assets.values()) { // <-- use .values() + for (Asset asset : assets.values()) { if (type.isInstance(asset.getResource())) { @SuppressWarnings("unchecked") Asset typed = (Asset) asset; @@ -33,6 +104,12 @@ public class AssetManager { return list; } + /** + * Retrieve an asset by its unique ID. + * + * @param id the asset ID + * @return the asset, or null if not found + */ public static Asset getById(String id) { for (Asset asset : assets.values()) { if (asset.getId().toString().equals(id)) { @@ -42,16 +119,32 @@ public class AssetManager { return null; } + /** + * Retrieve an asset by its name. + * + * @param name the asset name + * @return the asset, or null if not found + */ public static Asset getByName(String name) { return assets.get(name); } + /** + * Attempt to find an asset by name, returning an {@link Optional}. + * + * @param name the asset name + * @return an Optional containing the asset if found + */ public static Optional> findByName(String name) { return Optional.ofNullable(assets.get(name)); } + /** + * Add a new asset to the manager. + * + * @param asset the asset to add + */ public static void addAsset(Asset asset) { assets.put(asset.getName(), asset); } - -} \ No newline at end of file +} diff --git a/framework/src/main/java/org/toop/framework/asset/events/AssetEvents.java b/framework/src/main/java/org/toop/framework/asset/events/AssetLoaderEvents.java similarity index 87% rename from framework/src/main/java/org/toop/framework/asset/events/AssetEvents.java rename to framework/src/main/java/org/toop/framework/asset/events/AssetLoaderEvents.java index faf8bfb..91a296e 100644 --- a/framework/src/main/java/org/toop/framework/asset/events/AssetEvents.java +++ b/framework/src/main/java/org/toop/framework/asset/events/AssetLoaderEvents.java @@ -2,6 +2,6 @@ package org.toop.framework.asset.events; import org.toop.framework.eventbus.events.EventWithoutSnowflake; -public class AssetEvents { +public class AssetLoaderEvents { public record LoadingProgressUpdate(int hasLoadedAmount, int isLoadingAmount) implements EventWithoutSnowflake {} } diff --git a/framework/src/main/java/org/toop/framework/asset/resources/BundledResource.java b/framework/src/main/java/org/toop/framework/asset/resources/BundledResource.java new file mode 100644 index 0000000..b6559b8 --- /dev/null +++ b/framework/src/main/java/org/toop/framework/asset/resources/BundledResource.java @@ -0,0 +1,67 @@ +package org.toop.framework.asset.resources; + +import java.io.File; + +/** + * Represents a resource that can be composed of multiple files, or "bundled" together + * under a common base name. + * + *

Implementing classes allow an {@link org.toop.framework.asset.AssetLoader} + * to automatically merge multiple related files into a single resource instance.

+ * + *

Typical use cases include:

+ *
    + *
  • Localization assets, where multiple `.properties` files (e.g., `messages_en.properties`, + * `messages_nl.properties`) are grouped under the same logical resource.
  • + *
  • Sprite sheets, tile sets, or other multi-file resources that logically belong together.
  • + *
+ * + *

Implementing classes must provide:

+ *
    + *
  • {@link #loadFile(File)}: Logic to load or merge an individual file into the resource.
  • + *
  • {@link #getBaseName()}: A consistent base name used to group multiple files into this resource.
  • + *
+ * + *

Example usage:

+ *
{@code
+ * public class LocalizationAsset extends BaseResource implements BundledResource {
+ *     private final String baseName;
+ *
+ *     public LocalizationAsset(File file) {
+ *         super(file);
+ *         this.baseName = extractBaseName(file.getName());
+ *         loadFile(file);
+ *     }
+ *
+ *     @Override
+ *     public void loadFile(File file) {
+ *         // merge file into existing bundles
+ *     }
+ *
+ *     @Override
+ *     public String getBaseName() {
+ *         return baseName;
+ *     }
+ * }
+ * }
+ * + *

When used with an asset loader, all files sharing the same base name are + * automatically merged into a single resource instance.

+ */ +public interface BundledResource { + + /** + * Load or merge an additional file into this resource. + * + * @param file the file to load or merge + */ + void loadFile(File file); + + /** + * Return a base name for grouping multiple files into this single resource. + * Files with the same base name are automatically merged by the loader. + * + * @return the base name used to identify this bundled resource + */ + String getBaseName(); +} \ No newline at end of file diff --git a/framework/src/main/java/org/toop/framework/asset/resources/FileExtension.java b/framework/src/main/java/org/toop/framework/asset/resources/FileExtension.java index 4aceb00..1beb405 100644 --- a/framework/src/main/java/org/toop/framework/asset/resources/FileExtension.java +++ b/framework/src/main/java/org/toop/framework/asset/resources/FileExtension.java @@ -5,8 +5,37 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.annotation.ElementType; +/** + * Annotation to declare which file extensions a {@link BaseResource} subclass + * can handle. + * + *

This annotation is processed by the {@link org.toop.framework.asset.AssetLoader} + * to automatically register resource types for specific file extensions. + * Each extension listed will be mapped to the annotated resource class, + * allowing the loader to instantiate the correct type when scanning files.

+ * + *

Usage example:

+ *
{@code
+ * @FileExtension({"png", "jpg"})
+ * public class ImageAsset extends BaseResource implements LoadableResource {
+ *     ...
+ * }
+ * }
+ * + *

Key points:

+ *
    + *
  • The annotation is retained at runtime for reflection-based registration.
  • + *
  • Can only be applied to types (classes) that extend {@link BaseResource}.
  • + *
  • Multiple extensions can be specified in the {@code value()} array.
  • + *
+ */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface FileExtension { + /** + * The list of file extensions (without leading dot) that the annotated resource class can handle. + * + * @return array of file extensions + */ String[] value(); -} \ No newline at end of file +} diff --git a/framework/src/main/java/org/toop/framework/asset/resources/FontAsset.java b/framework/src/main/java/org/toop/framework/asset/resources/FontAsset.java index 16570de..4835f0f 100644 --- a/framework/src/main/java/org/toop/framework/asset/resources/FontAsset.java +++ b/framework/src/main/java/org/toop/framework/asset/resources/FontAsset.java @@ -6,7 +6,7 @@ import java.io.FileInputStream; import java.io.IOException; @FileExtension({"ttf", "otf"}) -public class FontAsset extends BaseResource implements LoadableResource { +public class FontAsset extends BaseResource implements PreloadResource { private String family; public FontAsset(final File fontFile) { diff --git a/framework/src/main/java/org/toop/framework/asset/resources/LoadableResource.java b/framework/src/main/java/org/toop/framework/asset/resources/LoadableResource.java index 4b94092..69374f7 100644 --- a/framework/src/main/java/org/toop/framework/asset/resources/LoadableResource.java +++ b/framework/src/main/java/org/toop/framework/asset/resources/LoadableResource.java @@ -1,9 +1,64 @@ package org.toop.framework.asset.resources; -import java.io.FileNotFoundException; - +/** + * Represents a resource that can be explicitly loaded and unloaded. + *

+ * Any class implementing {@code LoadableResource} is responsible for managing its own + * loading and unloading logic, such as reading files, initializing data structures, + * or allocating external resources. + *

+ * + *

Implementing classes must define the following behaviors:

+ *
    + *
  • {@link #load()}: Load the resource into memory or perform necessary initialization.
  • + *
  • {@link #unload()}: Release any held resources or memory when the resource is no longer needed.
  • + *
  • {@link #isLoaded()}: Return {@code true} if the resource has been successfully loaded and is ready for use, {@code false} otherwise.
  • + *
+ * + *

Typical usage:

+ *
{@code
+ * public class MyFontAsset extends BaseResource implements LoadableResource {
+ *     private boolean loaded = false;
+ *
+ *     @Override
+ *     public void load() {
+ *         // Load font file into memory
+ *         loaded = true;
+ *     }
+ *
+ *     @Override
+ *     public void unload() {
+ *         // Release resources if needed
+ *         loaded = false;
+ *     }
+ *
+ *     @Override
+ *     public boolean isLoaded() {
+ *         return loaded;
+ *     }
+ * }
+ * }
+ * + *

This interface is commonly used with {@link PreloadResource} to allow automatic + * loading by an {@link org.toop.framework.asset.AssetLoader} if desired.

+ */ public interface LoadableResource { + /** + * Load the resource into memory or initialize it. + * This method may throw runtime exceptions if loading fails. + */ void load(); + + /** + * Unload the resource and free any associated resources. + * After this call, {@link #isLoaded()} should return false. + */ void unload(); + + /** + * Check whether the resource has been successfully loaded. + * + * @return true if the resource is loaded and ready for use, false otherwise + */ boolean isLoaded(); } diff --git a/framework/src/main/java/org/toop/framework/asset/resources/LocalizationAsset.java b/framework/src/main/java/org/toop/framework/asset/resources/LocalizationAsset.java index 27d9b83..39862c4 100644 --- a/framework/src/main/java/org/toop/framework/asset/resources/LocalizationAsset.java +++ b/framework/src/main/java/org/toop/framework/asset/resources/LocalizationAsset.java @@ -5,9 +5,10 @@ import java.nio.charset.StandardCharsets; import java.util.*; @FileExtension({"properties"}) -public class LocalizationAsset extends BaseResource implements LoadableResource { +public class LocalizationAsset extends BaseResource implements LoadableResource, BundledResource { private final Map bundles = new HashMap<>(); private boolean isLoaded = false; + private final Locale fallback = Locale.forLanguageTag(""); public LocalizationAsset(File file) { super(file); @@ -15,53 +16,57 @@ public class LocalizationAsset extends BaseResource implements LoadableResource @Override public void load() { - // Convention: file names like messages_en.properties, ui_de.properties, etc. - String baseName = getBaseName(getFile().getName()); - - // Scan the parent folder for all matching *.properties with same basename - File folder = getFile().getParentFile(); - File[] files = folder.listFiles((dir, name) -> - name.startsWith(baseName) && name.endsWith(".properties")); - - if (files != null) { - for (File f : files) { - Locale locale = extractLocale(f.getName(), baseName); - try (InputStreamReader reader = - new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8)) { - this.bundles.put(locale, new PropertyResourceBundle(reader)); - } catch (IOException e) { - throw new RuntimeException("Failed to load localization file: " + f, e); - } - } - } - - this.isLoaded = true; + loadFile(getFile()); + isLoaded = true; } @Override public void unload() { - this.bundles.clear(); - this.isLoaded = false; + bundles.clear(); + isLoaded = false; } @Override public boolean isLoaded() { - return this.isLoaded; + return isLoaded; } public String getString(String key, Locale locale) { - ResourceBundle bundle = this.bundles.get(locale); + Locale target = findBestLocale(locale); + ResourceBundle bundle = bundles.get(target); if (bundle == null) throw new MissingResourceException( - "No bundle for locale: " + locale, getClass().getName(), key); + "No bundle for locale: " + target, getClass().getName(), key); return bundle.getString(key); } - public boolean hasLocale(Locale locale) { - return this.bundles.containsKey(locale); + private Locale findBestLocale(Locale locale) { + if (bundles.containsKey(locale)) return locale; + for (Locale l : bundles.keySet()) { + if (l.getLanguage().equals(locale.getLanguage())) return l; + } + return fallback; } public Set getAvailableLocales() { - return Collections.unmodifiableSet(this.bundles.keySet()); + return Collections.unmodifiableSet(bundles.keySet()); + } + + @Override + public void loadFile(File file) { + String baseName = getBaseName(file.getName()); + try (InputStreamReader reader = + new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)) { + Locale locale = extractLocale(file.getName(), baseName); + bundles.put(locale, new PropertyResourceBundle(reader)); + } catch (IOException e) { + throw new RuntimeException("Failed to load localization file: " + file, e); + } + isLoaded = true; + } + + @Override + public String getBaseName() { + return getBaseName(getFile().getName()); } private String getBaseName(String fileName) { @@ -80,6 +85,6 @@ public class LocalizationAsset extends BaseResource implements LoadableResource String localePart = fileName.substring(underscoreIndex + 1, dotIndex); return Locale.forLanguageTag(localePart.replace('_', '-')); } - return Locale.getDefault(); // fallback + return fallback; } -} \ No newline at end of file +} diff --git a/framework/src/main/java/org/toop/framework/asset/resources/PreloadResource.java b/framework/src/main/java/org/toop/framework/asset/resources/PreloadResource.java new file mode 100644 index 0000000..6458751 --- /dev/null +++ b/framework/src/main/java/org/toop/framework/asset/resources/PreloadResource.java @@ -0,0 +1,37 @@ +package org.toop.framework.asset.resources; + +/** + * Marker interface for resources that should be **automatically loaded** by the {@link org.toop.framework.asset.AssetLoader}. + * + *

Extends {@link LoadableResource}, so any implementing class must provide the standard + * {@link LoadableResource#load()} and {@link LoadableResource#unload()} methods, as well as the + * {@link LoadableResource#isLoaded()} check.

+ * + *

When a resource implements {@code PreloadResource}, the {@code AssetLoader} will invoke + * {@link LoadableResource#load()} automatically after the resource is discovered and instantiated, + * without requiring manual loading by the user.

+ * + *

Typical usage:

+ *
{@code
+ * public class MyFontAsset extends BaseResource implements PreloadResource {
+ *     @Override
+ *     public void load() {
+ *         // load the font into memory
+ *     }
+ *
+ *     @Override
+ *     public void unload() {
+ *         // release resources if needed
+ *     }
+ *
+ *     @Override
+ *     public boolean isLoaded() {
+ *         return loaded;
+ *     }
+ * }
+ * }
+ * + *

Note: Only use this interface for resources that are safe to load at startup, as it may + * increase memory usage or startup time.

+ */ +public interface PreloadResource extends LoadableResource {}