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:
+ *
+ * - 41-bit timestamp (milliseconds since custom epoch)
+ * - 10-bit machine identifier
+ * - 12-bit sequence number for IDs generated in the same millisecond
+ *
+ *
+ *
+ * This implementation ensures:
+ *
+ * - IDs are unique per machine.
+ * - Monotonicity within the same machine.
+ * - Safe concurrent generation via synchronized {@link #nextId()}.
+ *
+ *
+ *
+ * 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:
+ *
+ * - Recursive directory scanning for assets.
+ * - Automatic registration of resource classes via reflection.
+ * - Bundled resource support: multiple files merged into a single resource instance.
+ * - Preload resources automatically invoke {@link PreloadResource#load()}.
+ * - Progress tracking via {@link AssetLoaderEvents.LoadingProgressUpdate} events.
+ *
+ *
+ * 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 extends BaseResource> 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 extends BaseResource> asset : assets.values()) { // <-- use .values()
+ for (Asset extends BaseResource> 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 extends BaseResource> getById(String id) {
for (Asset extends BaseResource> 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 extends BaseResource> 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 extends BaseResource> 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 {}