mirror of
https://github.com/2OOP/pism.git
synced 2026-02-04 10:54:51 +00:00
UI (#96)
* added localization options //todo add all the strings * broken push * merge to UI * broken push * Alpha rebase complete, added asset loader for UI branch * merge to UI * UI now uses assetmanager * added NL and EN for all strings currently in UI * fix small merge error * Removed no more needed files. * JDK25 * Removed files no longer in use * Removed need for manual typecast * Added ability to load in BundledResource for localization * Docs and easy font loading. Added interfaces for PreloadResource's --------- Co-authored-by: Ticho Hidding <tichohidding@gmail.com> Co-authored-by: ramollia <@>
This commit is contained in:
committed by
GitHub
parent
9b17a4ba7f
commit
258adbb627
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -13,7 +13,7 @@
|
|||||||
</list>
|
</list>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" default="true" project-jdk-name="openjdk-25" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_X" default="true" project-jdk-name="openjdk-25" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/out" />
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -8,6 +8,8 @@ import javafx.application.Application;
|
|||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
|
import org.toop.framework.asset.AssetManager;
|
||||||
|
import org.toop.framework.asset.resources.LocalizationAsset;
|
||||||
import org.toop.local.AppContext;
|
import org.toop.local.AppContext;
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
@@ -18,8 +20,8 @@ public class App extends Application {
|
|||||||
private static Scene scene;
|
private static Scene scene;
|
||||||
private static StackPane root;
|
private static StackPane root;
|
||||||
|
|
||||||
private Locale currentLocale = AppContext.getLocale();
|
private Locale currentLocale = AppContext.getLocale();
|
||||||
private ResourceBundle resourceBundle = ResourceBundle.getBundle("Localization", currentLocale);
|
private LocalizationAsset loc = AssetManager.get("localization.properties");
|
||||||
|
|
||||||
public static void run(String[] args) {
|
public static void run(String[] args) {
|
||||||
launch(args);
|
launch(args);
|
||||||
@@ -30,7 +32,7 @@ public class App extends Application {
|
|||||||
final StackPane root = new StackPane(new MainMenu().getPane());
|
final StackPane root = new StackPane(new MainMenu().getPane());
|
||||||
final Scene scene = new Scene(root);
|
final Scene scene = new Scene(root);
|
||||||
|
|
||||||
stage.setTitle(resourceBundle.getString("windowTitle"));
|
stage.setTitle(loc.getString("windowTitle", currentLocale));
|
||||||
stage.setMinWidth(1080);
|
stage.setMinWidth(1080);
|
||||||
stage.setMinHeight(720);
|
stage.setMinHeight(720);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package org.toop.app.menu;
|
package org.toop.app.menu;
|
||||||
|
|
||||||
|
import org.toop.framework.asset.AssetManager;
|
||||||
|
import org.toop.framework.asset.resources.LocalizationAsset;
|
||||||
import org.toop.local.AppContext;
|
import org.toop.local.AppContext;
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
@@ -7,7 +9,7 @@ import java.util.ResourceBundle;
|
|||||||
|
|
||||||
public final class CreditsMenu extends Menu {
|
public final class CreditsMenu extends Menu {
|
||||||
private Locale currentLocale = AppContext.getLocale();
|
private Locale currentLocale = AppContext.getLocale();
|
||||||
private ResourceBundle resourceBundle = ResourceBundle.getBundle("Localization", currentLocale);
|
private LocalizationAsset loc = AssetManager.get("localization.properties");
|
||||||
public CreditsMenu() {
|
public CreditsMenu() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,31 +4,33 @@ import javafx.geometry.Pos;
|
|||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.image.ImageView;
|
import javafx.scene.image.ImageView;
|
||||||
import javafx.scene.layout.*;
|
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.Locale;
|
||||||
import java.util.ResourceBundle;
|
|
||||||
import org.toop.framework.asset.AssetManager;
|
import org.toop.framework.asset.AssetManager;
|
||||||
import org.toop.framework.asset.resources.CssAsset;
|
import org.toop.framework.asset.resources.CssAsset;
|
||||||
import org.toop.framework.asset.resources.ImageAsset;
|
import org.toop.framework.asset.resources.ImageAsset;
|
||||||
|
|
||||||
public final class MainMenu extends Menu {
|
public final class MainMenu extends Menu {
|
||||||
private Locale currentLocale = AppContext.getLocale();
|
private final Locale currentLocale = Locale.of("nl");
|
||||||
private ResourceBundle resourceBundle = ResourceBundle.getBundle("Localization", currentLocale);
|
private final LocalizationAsset loc = AssetManager.get("localization.properties");
|
||||||
|
|
||||||
public MainMenu() {
|
public MainMenu() {
|
||||||
final Button tictactoe = createButton(resourceBundle.getString("mainMenuSelectTicTacToe"), () -> {});
|
|
||||||
final Button reversi = createButton(resourceBundle.getString("mainMenuSelectReversi"), () -> {});
|
final Button tictactoe = createButton(loc.getString("mainMenuSelectTicTacToe", currentLocale), () -> {});
|
||||||
final Button sudoku = createButton(resourceBundle.getString("mainMenuSelectSudoku"), () -> {});
|
final Button reversi = createButton(loc.getString("mainMenuSelectReversi", currentLocale), () -> {});
|
||||||
final Button battleship = createButton(resourceBundle.getString("mainMenuSelectBattleship"), () -> {});
|
final Button sudoku = createButton(loc.getString("mainMenuSelectSudoku", currentLocale), () -> {});
|
||||||
final Button other = createButton(resourceBundle.getString("mainMenuSelectOther"), () -> {});
|
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);
|
final VBox gamesBox = new VBox(tictactoe, reversi, sudoku, battleship, other);
|
||||||
gamesBox.setAlignment(Pos.TOP_CENTER);
|
gamesBox.setAlignment(Pos.TOP_CENTER);
|
||||||
|
|
||||||
final Button credits = createButton(resourceBundle.getString("mainMenuSelectCredits"), () -> {});
|
final Button credits = createButton(loc.getString("mainMenuSelectCredits", currentLocale), () -> {});
|
||||||
final Button options = createButton(resourceBundle.getString("mainMenuSelectOptions"), () -> {});
|
final Button options = createButton(loc.getString("mainMenuSelectOptions", currentLocale), () -> {});
|
||||||
final Button quit = createButton(resourceBundle.getString("mainMenuSelectQuit"), () -> {});
|
final Button quit = createButton(loc.getString("mainMenuSelectQuit", currentLocale), () -> {});
|
||||||
|
|
||||||
final VBox creditsBox = new VBox(credits, options, quit);
|
final VBox creditsBox = new VBox(credits, options, quit);
|
||||||
creditsBox.setAlignment(Pos.BOTTOM_CENTER);
|
creditsBox.setAlignment(Pos.BOTTOM_CENTER);
|
||||||
@@ -43,7 +45,7 @@ public final class MainMenu extends Menu {
|
|||||||
background.fitHeightProperty().bind(grid.heightProperty());
|
background.fitHeightProperty().bind(grid.heightProperty());
|
||||||
|
|
||||||
pane = new StackPane(background, grid);
|
pane = new StackPane(background, grid);
|
||||||
CssAsset css = (CssAsset) AssetManager.getByName("main.css").getResource();
|
CssAsset css = AssetManager.get("main.css");
|
||||||
pane.getStylesheets().add(css.getUrl());
|
pane.getStylesheets().add(css.getUrl());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,16 +6,19 @@ import javafx.animation.FadeTransition;
|
|||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.layout.Pane;
|
import javafx.scene.layout.Pane;
|
||||||
import javafx.util.Duration;
|
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 org.toop.local.AppContext;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.ResourceBundle;
|
|
||||||
|
|
||||||
public abstract class Menu {
|
public abstract class Menu {
|
||||||
protected Pane pane;
|
protected Pane pane;
|
||||||
public Pane getPane() { return pane; }
|
public Pane getPane() { return pane; }
|
||||||
private Locale currentLocale = AppContext.getLocale();
|
private Locale currentLocale = AppContext.getLocale();
|
||||||
private ResourceBundle resourceBundle = ResourceBundle.getBundle("Localization", currentLocale);
|
private LocalizationAsset loc = AssetManager.get("localization.properties");
|
||||||
|
|
||||||
public void fadeBackgroundImage(String imagePath, float from, float to, float milliseconds) {
|
public void fadeBackgroundImage(String imagePath, float from, float to, float milliseconds) {
|
||||||
final FadeTransition fade = new FadeTransition(Duration.millis(milliseconds), App.getRoot());
|
final FadeTransition fade = new FadeTransition(Duration.millis(milliseconds), App.getRoot());
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package org.toop.app.menu;
|
package org.toop.app.menu;
|
||||||
|
|
||||||
|
import org.toop.framework.asset.AssetManager;
|
||||||
|
import org.toop.framework.asset.resources.LocalizationAsset;
|
||||||
import org.toop.local.AppContext;
|
import org.toop.local.AppContext;
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
@@ -7,7 +9,7 @@ import java.util.ResourceBundle;
|
|||||||
|
|
||||||
public final class OptionsMenu extends Menu {
|
public final class OptionsMenu extends Menu {
|
||||||
private Locale currentLocale = AppContext.getLocale();
|
private Locale currentLocale = AppContext.getLocale();
|
||||||
private ResourceBundle resourceBundle = ResourceBundle.getBundle("Localization", currentLocale);
|
private LocalizationAsset loc = AssetManager.get("localization.properties");
|
||||||
public OptionsMenu() {
|
public OptionsMenu() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,29 +10,30 @@ import javafx.scene.text.Text;
|
|||||||
import org.toop.app.App;
|
import org.toop.app.App;
|
||||||
import org.toop.framework.asset.AssetManager;
|
import org.toop.framework.asset.AssetManager;
|
||||||
import org.toop.framework.asset.resources.CssAsset;
|
import org.toop.framework.asset.resources.CssAsset;
|
||||||
|
import org.toop.framework.asset.resources.LocalizationAsset;
|
||||||
import org.toop.local.AppContext;
|
import org.toop.local.AppContext;
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.ResourceBundle;
|
import java.util.ResourceBundle;
|
||||||
|
|
||||||
public final class QuitMenu extends Menu {
|
public final class QuitMenu extends Menu {
|
||||||
private Locale currentLocale = AppContext.getLocale();
|
private Locale currentLocale = AppContext.getLocale();
|
||||||
private ResourceBundle resourceBundle = ResourceBundle.getBundle("Localization", currentLocale);
|
private LocalizationAsset loc = AssetManager.get("localization.properties");
|
||||||
public QuitMenu() {
|
public QuitMenu() {
|
||||||
final Region background = new Region();
|
final Region background = new Region();
|
||||||
background.getStyleClass().add("quit-background");
|
background.getStyleClass().add("quit-background");
|
||||||
background.setPrefSize(Double.MAX_VALUE, Double.MAX_VALUE);
|
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");
|
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.getStyleClass().add("quit-button");
|
||||||
yes.setOnAction(_ -> {
|
yes.setOnAction(_ -> {
|
||||||
App.quit();
|
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.getStyleClass().add("quit-button");
|
||||||
no.setOnAction(_ -> {
|
no.setOnAction(_ -> {
|
||||||
App.pop();
|
App.pop();
|
||||||
@@ -51,7 +52,7 @@ public final class QuitMenu extends Menu {
|
|||||||
StackPane.setAlignment(box, Pos.CENTER);
|
StackPane.setAlignment(box, Pos.CENTER);
|
||||||
|
|
||||||
pane = modalContainer;
|
pane = modalContainer;
|
||||||
CssAsset css = (CssAsset) AssetManager.getByName("quit.css").getResource();
|
CssAsset css = AssetManager.get("quit.css");
|
||||||
pane.getStylesheets().add(css.getUrl());
|
pane.getStylesheets().add(css.getUrl());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BIN
app/src/main/resources/assets/fonts/GroovyManiac.ttf
Normal file
BIN
app/src/main/resources/assets/fonts/GroovyManiac.ttf
Normal file
Binary file not shown.
BIN
app/src/main/resources/assets/fonts/Roboto-Regular.ttf
Normal file
BIN
app/src/main/resources/assets/fonts/Roboto-Regular.ttf
Normal file
Binary file not shown.
@@ -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
|
||||||
@@ -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
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
.main-button {
|
.main-button {
|
||||||
-fx-background-color: transparent;
|
-fx-background-color: transparent;
|
||||||
-fx-background-image: url("card-default.jpg"); /* fallback image */
|
|
||||||
-fx-background-size: cover;
|
-fx-background-size: cover;
|
||||||
-fx-background-position: center;
|
-fx-background-position: center;
|
||||||
-fx-pref-width: 250px;
|
-fx-pref-width: 250px;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
-fx-fill: white;
|
-fx-fill: white;
|
||||||
-fx-font-size: 28px;
|
-fx-font-size: 28px;
|
||||||
-fx-font-weight: 600;
|
-fx-font-weight: 600;
|
||||||
-fx-font-family: "Segoe UI", sans-serif;
|
-fx-font-family: "Groovy Maniac Demo", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quit-button {
|
.quit-button {
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.9 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 189 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 15 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.0 KiB |
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,38 @@ import java.time.Instant;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A thread-safe, distributed unique ID generator following the Snowflake pattern.
|
||||||
|
* <p>
|
||||||
|
* Each generated 64-bit ID encodes:
|
||||||
|
* <ul>
|
||||||
|
* <li>41-bit timestamp (milliseconds since custom epoch)</li>
|
||||||
|
* <li>10-bit machine identifier</li>
|
||||||
|
* <li>12-bit sequence number for IDs generated in the same millisecond</li>
|
||||||
|
* </ul>
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>This implementation ensures:
|
||||||
|
* <ul>
|
||||||
|
* <li>IDs are unique per machine.</li>
|
||||||
|
* <li>Monotonicity within the same machine.</li>
|
||||||
|
* <li>Safe concurrent generation via synchronized {@link #nextId()}.</li>
|
||||||
|
* </ul>
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>Custom epoch is set to {@code 2025-01-01T00:00:00Z}.</p>
|
||||||
|
*
|
||||||
|
* <p>Usage example:</p>
|
||||||
|
* <pre>{@code
|
||||||
|
* SnowflakeGenerator generator = new SnowflakeGenerator();
|
||||||
|
* long id = generator.nextId();
|
||||||
|
* }</pre>
|
||||||
|
*/
|
||||||
public class SnowflakeGenerator {
|
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();
|
private static final long EPOCH = Instant.parse("2025-01-01T00:00:00Z").toEpochMilli();
|
||||||
|
|
||||||
// Bit allocations
|
// Bit allocations
|
||||||
@@ -13,19 +44,27 @@ public class SnowflakeGenerator {
|
|||||||
private static final long MACHINE_BITS = 10;
|
private static final long MACHINE_BITS = 10;
|
||||||
private static final long SEQUENCE_BITS = 12;
|
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_MACHINE_ID = (1L << MACHINE_BITS) - 1;
|
||||||
private static final long MAX_SEQUENCE = (1L << SEQUENCE_BITS) - 1;
|
private static final long MAX_SEQUENCE = (1L << SEQUENCE_BITS) - 1;
|
||||||
private static final long MAX_TIMESTAMP = (1L << TIMESTAMP_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 MACHINE_SHIFT = SEQUENCE_BITS;
|
||||||
private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + MACHINE_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 static final long machineId = SnowflakeGenerator.genMachineId();
|
||||||
|
|
||||||
private final AtomicLong lastTimestamp = new AtomicLong(-1L);
|
private final AtomicLong lastTimestamp = new AtomicLong(-1L);
|
||||||
private long sequence = 0L;
|
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() {
|
private static long genMachineId() {
|
||||||
try {
|
try {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
@@ -35,17 +74,25 @@ public class SnowflakeGenerator {
|
|||||||
for (byte b : mac) sb.append(String.format("%02X", b));
|
for (byte b : mac) sb.append(String.format("%02X", b));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// limit to 10 bits (0–1023)
|
return sb.toString().hashCode() & 0x3FF; // limit to 10 bits
|
||||||
return sb.toString().hashCode() & 0x3FF;
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return (long) (Math.random() * 1024); // fallback
|
return (long) (Math.random() * 1024); // fallback
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For testing: manually set the last generated timestamp.
|
||||||
|
* @param l timestamp in milliseconds
|
||||||
|
*/
|
||||||
void setTime(long l) {
|
void setTime(long l) {
|
||||||
this.lastTimestamp.set(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() {
|
public SnowflakeGenerator() {
|
||||||
if (machineId < 0 || machineId > MAX_MACHINE_ID) {
|
if (machineId < 0 || machineId > MAX_MACHINE_ID) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
@@ -53,6 +100,16 @@ public class SnowflakeGenerator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the next unique ID.
|
||||||
|
* <p>
|
||||||
|
* If multiple IDs are generated in the same millisecond, a sequence number
|
||||||
|
* is incremented. If the sequence overflows, waits until the next millisecond.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @return a unique 64-bit ID
|
||||||
|
* @throws IllegalStateException if clock moves backwards or timestamp exceeds 41-bit limit
|
||||||
|
*/
|
||||||
public synchronized long nextId() {
|
public synchronized long nextId() {
|
||||||
long currentTimestamp = timestamp();
|
long currentTimestamp = timestamp();
|
||||||
|
|
||||||
@@ -67,7 +124,6 @@ public class SnowflakeGenerator {
|
|||||||
if (currentTimestamp == lastTimestamp.get()) {
|
if (currentTimestamp == lastTimestamp.get()) {
|
||||||
sequence = (sequence + 1) & MAX_SEQUENCE;
|
sequence = (sequence + 1) & MAX_SEQUENCE;
|
||||||
if (sequence == 0) {
|
if (sequence == 0) {
|
||||||
// Sequence overflow, wait for next millisecond
|
|
||||||
currentTimestamp = waitNextMillis(currentTimestamp);
|
currentTimestamp = waitNextMillis(currentTimestamp);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -81,6 +137,11 @@ public class SnowflakeGenerator {
|
|||||||
| sequence;
|
| sequence;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits until the next millisecond if sequence overflows.
|
||||||
|
* @param lastTimestamp previous timestamp
|
||||||
|
* @return new timestamp
|
||||||
|
*/
|
||||||
private long waitNextMillis(long lastTimestamp) {
|
private long waitNextMillis(long lastTimestamp) {
|
||||||
long ts = timestamp();
|
long ts = timestamp();
|
||||||
while (ts <= lastTimestamp) {
|
while (ts <= lastTimestamp) {
|
||||||
@@ -89,6 +150,9 @@ public class SnowflakeGenerator {
|
|||||||
return ts;
|
return ts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns current system timestamp in milliseconds.
|
||||||
|
*/
|
||||||
private long timestamp() {
|
private long timestamp() {
|
||||||
return System.currentTimeMillis();
|
return System.currentTimeMillis();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,65 +2,123 @@ package org.toop.framework.asset;
|
|||||||
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.toop.framework.asset.events.AssetEvents;
|
import org.toop.framework.asset.events.AssetLoaderEvents;
|
||||||
import org.toop.framework.asset.resources.BaseResource;
|
import org.toop.framework.asset.resources.*;
|
||||||
|
import org.toop.framework.eventbus.EventFlow;
|
||||||
|
import org.reflections.Reflections;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
import org.reflections.Reflections;
|
/**
|
||||||
import org.toop.framework.asset.resources.FileExtension;
|
* Responsible for loading assets from a file system directory into memory.
|
||||||
import org.toop.framework.asset.resources.FontAsset;
|
* <p>
|
||||||
import org.toop.framework.eventbus.EventFlow;
|
* 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).
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>Assets are stored in a static, thread-safe list and can be retrieved
|
||||||
|
* through {@link AssetManager}.</p>
|
||||||
|
*
|
||||||
|
* <p>Features:</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>Recursive directory scanning for assets.</li>
|
||||||
|
* <li>Automatic registration of resource classes via reflection.</li>
|
||||||
|
* <li>Bundled resource support: multiple files merged into a single resource instance.</li>
|
||||||
|
* <li>Preload resources automatically invoke {@link PreloadResource#load()}.</li>
|
||||||
|
* <li>Progress tracking via {@link AssetLoaderEvents.LoadingProgressUpdate} events.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Usage example:</p>
|
||||||
|
* <pre>{@code
|
||||||
|
* AssetLoader loader = new AssetLoader("assets");
|
||||||
|
* double progress = loader.getProgress();
|
||||||
|
* List<Asset<? extends BaseResource>> loadedAssets = loader.getAssets();
|
||||||
|
* }</pre>
|
||||||
|
*/
|
||||||
public class AssetLoader {
|
public class AssetLoader {
|
||||||
private static final Logger logger = LogManager.getLogger(AssetLoader.class);
|
private static final Logger logger = LogManager.getLogger(AssetLoader.class);
|
||||||
private final List<Asset<? extends BaseResource>> assets = new CopyOnWriteArrayList<>();
|
private static final List<Asset<? extends BaseResource>> assets = new CopyOnWriteArrayList<>();
|
||||||
private final Map<String, Function<File, ? extends BaseResource>> registry = new ConcurrentHashMap<>();
|
private final Map<String, Function<File, ? extends BaseResource>> registry = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
private volatile int loadedCount = 0;
|
private final AtomicInteger loadedCount = new AtomicInteger(0);
|
||||||
private volatile int totalCount = 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) {
|
public AssetLoader(File rootFolder) {
|
||||||
autoRegisterResources(); // make sure resources are registered!
|
autoRegisterResources();
|
||||||
|
|
||||||
List<File> foundFiles = new ArrayList<>();
|
List<File> foundFiles = new ArrayList<>();
|
||||||
fileSearcher(rootFolder, foundFiles);
|
fileSearcher(rootFolder, foundFiles);
|
||||||
this.totalCount = foundFiles.size();
|
this.totalCount = foundFiles.size();
|
||||||
loader(foundFiles);
|
loader(foundFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an AssetLoader from a folder path.
|
||||||
|
* @param rootFolder the folder path containing assets
|
||||||
|
*/
|
||||||
public AssetLoader(String rootFolder) {
|
public AssetLoader(String rootFolder) {
|
||||||
this(new File(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() {
|
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() {
|
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() {
|
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<Asset<? extends BaseResource>> getAssets() {
|
public List<Asset<? extends BaseResource>> 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 <T> the type of resource
|
||||||
|
*/
|
||||||
public <T extends BaseResource> void register(String extension, Function<File, T> factory) {
|
public <T extends BaseResource> void register(String extension, Function<File, T> factory) {
|
||||||
this.registry.put(extension, factory);
|
this.registry.put(extension, factory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps a file to a resource instance based on its extension and registered factories.
|
||||||
|
*/
|
||||||
private <T extends BaseResource> T resourceMapper(File file, Class<T> type) {
|
private <T extends BaseResource> T resourceMapper(File file, Class<T> type) {
|
||||||
String ext = getExtension(file.getName());
|
String ext = getExtension(file.getName());
|
||||||
Function<File, ? extends BaseResource> factory = this.registry.get(ext);
|
Function<File, ? extends BaseResource> factory = registry.get(ext);
|
||||||
|
|
||||||
if (factory == null) return null;
|
if (factory == null) return null;
|
||||||
|
|
||||||
BaseResource resource = factory.apply(file);
|
BaseResource resource = factory.apply(file);
|
||||||
@@ -70,32 +128,54 @@ public class AssetLoader {
|
|||||||
"File " + file.getName() + " is not of type " + type.getSimpleName()
|
"File " + file.getName() + " is not of type " + type.getSimpleName()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return type.cast(resource);
|
return type.cast(resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the given list of files into assets, handling bundled and preload resources.
|
||||||
|
*/
|
||||||
private void loader(List<File> files) {
|
private void loader(List<File> files) {
|
||||||
|
Map<String, BundledResource> bundledResources = new HashMap<>();
|
||||||
|
|
||||||
for (File file : files) {
|
for (File file : files) {
|
||||||
BaseResource resource = resourceMapper(file, BaseResource.class);
|
BaseResource resource = resourceMapper(file, BaseResource.class);
|
||||||
if (resource != null) {
|
switch (resource) {
|
||||||
Asset<? extends BaseResource> asset = new Asset<>(file.getName(), resource);
|
case null -> {
|
||||||
this.assets.add(asset);
|
continue;
|
||||||
|
}
|
||||||
if (resource instanceof FontAsset fontAsset) {
|
case BundledResource br -> {
|
||||||
fontAsset.load();
|
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<File> foundFiles) {
|
private void fileSearcher(final File folder, List<File> foundFiles) {
|
||||||
for (File fileEntry : Objects.requireNonNull(folder.listFiles())) {
|
for (File fileEntry : Objects.requireNonNull(folder.listFiles())) {
|
||||||
if (fileEntry.isDirectory()) {
|
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() {
|
private void autoRegisterResources() {
|
||||||
Reflections reflections = new Reflections("org.toop.framework.asset.resources");
|
Reflections reflections = new Reflections("org.toop.framework.asset.resources");
|
||||||
Set<Class<? extends BaseResource>> classes = reflections.getSubTypesOf(BaseResource.class);
|
Set<Class<? extends BaseResource>> classes = reflections.getSubTypesOf(BaseResource.class);
|
||||||
@@ -114,7 +198,7 @@ public class AssetLoader {
|
|||||||
if (!cls.isAnnotationPresent(FileExtension.class)) continue;
|
if (!cls.isAnnotationPresent(FileExtension.class)) continue;
|
||||||
FileExtension annotation = cls.getAnnotation(FileExtension.class);
|
FileExtension annotation = cls.getAnnotation(FileExtension.class);
|
||||||
for (String ext : annotation.value()) {
|
for (String ext : annotation.value()) {
|
||||||
this.registry.put(ext, file -> {
|
registry.put(ext, file -> {
|
||||||
try {
|
try {
|
||||||
return cls.getConstructor(File.class).newInstance(file);
|
return cls.getConstructor(File.class).newInstance(file);
|
||||||
} catch (Exception e) {
|
} 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) {
|
public static String getExtension(String name) {
|
||||||
int i = name.lastIndexOf('.');
|
int i = name.lastIndexOf('.');
|
||||||
return (i > 0) ? name.substring(i + 1) : "";
|
return (i > 0) ? name.substring(i + 1) : "";
|
||||||
|
|||||||
@@ -5,25 +5,96 @@ import org.toop.framework.asset.resources.*;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centralized manager for all loaded assets in the application.
|
||||||
|
* <p>
|
||||||
|
* {@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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>Key responsibilities:</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>Storing all loaded assets in a concurrent map.</li>
|
||||||
|
* <li>Providing typed access to asset resources.</li>
|
||||||
|
* <li>Allowing lookup by asset name or ID.</li>
|
||||||
|
* <li>Supporting retrieval of all assets of a specific {@link BaseResource} subclass.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Example usage:</p>
|
||||||
|
* <pre>{@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<Asset<FontAsset>> fonts = AssetManager.getAllOfType(FontAsset.class);
|
||||||
|
*
|
||||||
|
* // Retrieve by asset name or optional lookup
|
||||||
|
* Optional<Asset<? extends BaseResource>> maybeAsset = AssetManager.findByName("menu.css");
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>Notes:</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>All retrieval methods are static and thread-safe.</li>
|
||||||
|
* <li>The {@link #get(String)} method may require casting if the asset type is not known at compile time.</li>
|
||||||
|
* <li>Assets should be loaded via {@link AssetLoader} before retrieval.</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
public class AssetManager {
|
public class AssetManager {
|
||||||
private static final AssetManager INSTANCE = new AssetManager();
|
private static final AssetManager INSTANCE = new AssetManager();
|
||||||
private static final Map<String, Asset<? extends BaseResource>> assets = new ConcurrentHashMap<>();
|
private static final Map<String, Asset<? extends BaseResource>> assets = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
private AssetManager() {}
|
private AssetManager() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the singleton instance of {@code AssetManager}.
|
||||||
|
*
|
||||||
|
* @return the shared instance
|
||||||
|
*/
|
||||||
public static AssetManager getInstance() {
|
public static AssetManager getInstance() {
|
||||||
return INSTANCE;
|
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) {
|
public synchronized static void loadAssets(AssetLoader loader) {
|
||||||
for (var asset : loader.getAssets()) {
|
for (var asset : loader.getAssets()) {
|
||||||
assets.put(asset.getName(), asset);
|
assets.put(asset.getName(), asset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the resource of a given name, cast to the expected type.
|
||||||
|
*
|
||||||
|
* @param name the asset name
|
||||||
|
* @param <T> the expected resource type
|
||||||
|
* @return the resource, or null if not found
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static <T extends BaseResource> T get(String name) {
|
||||||
|
Asset<T> asset = (Asset<T>) 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 <T> the resource type
|
||||||
|
* @return a list of assets matching the type
|
||||||
|
*/
|
||||||
public static <T extends BaseResource> ArrayList<Asset<T>> getAllOfType(Class<T> type) {
|
public static <T extends BaseResource> ArrayList<Asset<T>> getAllOfType(Class<T> type) {
|
||||||
ArrayList<Asset<T>> list = new ArrayList<>();
|
ArrayList<Asset<T>> list = new ArrayList<>();
|
||||||
for (Asset<? extends BaseResource> asset : assets.values()) { // <-- use .values()
|
for (Asset<? extends BaseResource> asset : assets.values()) {
|
||||||
if (type.isInstance(asset.getResource())) {
|
if (type.isInstance(asset.getResource())) {
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
Asset<T> typed = (Asset<T>) asset;
|
Asset<T> typed = (Asset<T>) asset;
|
||||||
@@ -33,6 +104,12 @@ public class AssetManager {
|
|||||||
return list;
|
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) {
|
public static Asset<? extends BaseResource> getById(String id) {
|
||||||
for (Asset<? extends BaseResource> asset : assets.values()) {
|
for (Asset<? extends BaseResource> asset : assets.values()) {
|
||||||
if (asset.getId().toString().equals(id)) {
|
if (asset.getId().toString().equals(id)) {
|
||||||
@@ -42,16 +119,32 @@ public class AssetManager {
|
|||||||
return null;
|
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) {
|
public static Asset<? extends BaseResource> getByName(String name) {
|
||||||
return assets.get(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<Asset<? extends BaseResource>> findByName(String name) {
|
public static Optional<Asset<? extends BaseResource>> findByName(String name) {
|
||||||
return Optional.ofNullable(assets.get(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) {
|
public static void addAsset(Asset<? extends BaseResource> asset) {
|
||||||
assets.put(asset.getName(), asset);
|
assets.put(asset.getName(), asset);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ package org.toop.framework.asset.events;
|
|||||||
|
|
||||||
import org.toop.framework.eventbus.events.EventWithoutSnowflake;
|
import org.toop.framework.eventbus.events.EventWithoutSnowflake;
|
||||||
|
|
||||||
public class AssetEvents {
|
public class AssetLoaderEvents {
|
||||||
public record LoadingProgressUpdate(int hasLoadedAmount, int isLoadingAmount) implements EventWithoutSnowflake {}
|
public record LoadingProgressUpdate(int hasLoadedAmount, int isLoadingAmount) implements EventWithoutSnowflake {}
|
||||||
}
|
}
|
||||||
@@ -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.
|
||||||
|
*
|
||||||
|
* <p>Implementing classes allow an {@link org.toop.framework.asset.AssetLoader}
|
||||||
|
* to automatically merge multiple related files into a single resource instance.</p>
|
||||||
|
*
|
||||||
|
* <p>Typical use cases include:</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>Localization assets, where multiple `.properties` files (e.g., `messages_en.properties`,
|
||||||
|
* `messages_nl.properties`) are grouped under the same logical resource.</li>
|
||||||
|
* <li>Sprite sheets, tile sets, or other multi-file resources that logically belong together.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Implementing classes must provide:</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #loadFile(File)}: Logic to load or merge an individual file into the resource.</li>
|
||||||
|
* <li>{@link #getBaseName()}: A consistent base name used to group multiple files into this resource.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Example usage:</p>
|
||||||
|
* <pre>{@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;
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>When used with an asset loader, all files sharing the same base name are
|
||||||
|
* automatically merged into a single resource instance.</p>
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
}
|
||||||
@@ -5,8 +5,37 @@ import java.lang.annotation.RetentionPolicy;
|
|||||||
import java.lang.annotation.Target;
|
import java.lang.annotation.Target;
|
||||||
import java.lang.annotation.ElementType;
|
import java.lang.annotation.ElementType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annotation to declare which file extensions a {@link BaseResource} subclass
|
||||||
|
* can handle.
|
||||||
|
*
|
||||||
|
* <p>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.</p>
|
||||||
|
*
|
||||||
|
* <p>Usage example:</p>
|
||||||
|
* <pre>{@code
|
||||||
|
* @FileExtension({"png", "jpg"})
|
||||||
|
* public class ImageAsset extends BaseResource implements LoadableResource {
|
||||||
|
* ...
|
||||||
|
* }
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>Key points:</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>The annotation is retained at runtime for reflection-based registration.</li>
|
||||||
|
* <li>Can only be applied to types (classes) that extend {@link BaseResource}.</li>
|
||||||
|
* <li>Multiple extensions can be specified in the {@code value()} array.</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@Target(ElementType.TYPE)
|
@Target(ElementType.TYPE)
|
||||||
public @interface FileExtension {
|
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();
|
String[] value();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import java.io.FileInputStream;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
@FileExtension({"ttf", "otf"})
|
@FileExtension({"ttf", "otf"})
|
||||||
public class FontAsset extends BaseResource implements LoadableResource {
|
public class FontAsset extends BaseResource implements PreloadResource {
|
||||||
private String family;
|
private String family;
|
||||||
|
|
||||||
public FontAsset(final File fontFile) {
|
public FontAsset(final File fontFile) {
|
||||||
|
|||||||
@@ -1,9 +1,64 @@
|
|||||||
package org.toop.framework.asset.resources;
|
package org.toop.framework.asset.resources;
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
/**
|
||||||
|
* Represents a resource that can be explicitly loaded and unloaded.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>Implementing classes must define the following behaviors:</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #load()}: Load the resource into memory or perform necessary initialization.</li>
|
||||||
|
* <li>{@link #unload()}: Release any held resources or memory when the resource is no longer needed.</li>
|
||||||
|
* <li>{@link #isLoaded()}: Return {@code true} if the resource has been successfully loaded and is ready for use, {@code false} otherwise.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Typical usage:</p>
|
||||||
|
* <pre>{@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;
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>This interface is commonly used with {@link PreloadResource} to allow automatic
|
||||||
|
* loading by an {@link org.toop.framework.asset.AssetLoader} if desired.</p>
|
||||||
|
*/
|
||||||
public interface LoadableResource {
|
public interface LoadableResource {
|
||||||
|
/**
|
||||||
|
* Load the resource into memory or initialize it.
|
||||||
|
* This method may throw runtime exceptions if loading fails.
|
||||||
|
*/
|
||||||
void load();
|
void load();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unload the resource and free any associated resources.
|
||||||
|
* After this call, {@link #isLoaded()} should return false.
|
||||||
|
*/
|
||||||
void unload();
|
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();
|
boolean isLoaded();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import java.nio.charset.StandardCharsets;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
@FileExtension({"properties"})
|
@FileExtension({"properties"})
|
||||||
public class LocalizationAsset extends BaseResource implements LoadableResource {
|
public class LocalizationAsset extends BaseResource implements LoadableResource, BundledResource {
|
||||||
private final Map<Locale, ResourceBundle> bundles = new HashMap<>();
|
private final Map<Locale, ResourceBundle> bundles = new HashMap<>();
|
||||||
private boolean isLoaded = false;
|
private boolean isLoaded = false;
|
||||||
|
private final Locale fallback = Locale.forLanguageTag("");
|
||||||
|
|
||||||
public LocalizationAsset(File file) {
|
public LocalizationAsset(File file) {
|
||||||
super(file);
|
super(file);
|
||||||
@@ -15,53 +16,57 @@ public class LocalizationAsset extends BaseResource implements LoadableResource
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void load() {
|
public void load() {
|
||||||
// Convention: file names like messages_en.properties, ui_de.properties, etc.
|
loadFile(getFile());
|
||||||
String baseName = getBaseName(getFile().getName());
|
isLoaded = true;
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void unload() {
|
public void unload() {
|
||||||
this.bundles.clear();
|
bundles.clear();
|
||||||
this.isLoaded = false;
|
isLoaded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isLoaded() {
|
public boolean isLoaded() {
|
||||||
return this.isLoaded;
|
return isLoaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getString(String key, Locale locale) {
|
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(
|
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);
|
return bundle.getString(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasLocale(Locale locale) {
|
private Locale findBestLocale(Locale locale) {
|
||||||
return this.bundles.containsKey(locale);
|
if (bundles.containsKey(locale)) return locale;
|
||||||
|
for (Locale l : bundles.keySet()) {
|
||||||
|
if (l.getLanguage().equals(locale.getLanguage())) return l;
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Set<Locale> getAvailableLocales() {
|
public Set<Locale> 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) {
|
private String getBaseName(String fileName) {
|
||||||
@@ -80,6 +85,6 @@ public class LocalizationAsset extends BaseResource implements LoadableResource
|
|||||||
String localePart = fileName.substring(underscoreIndex + 1, dotIndex);
|
String localePart = fileName.substring(underscoreIndex + 1, dotIndex);
|
||||||
return Locale.forLanguageTag(localePart.replace('_', '-'));
|
return Locale.forLanguageTag(localePart.replace('_', '-'));
|
||||||
}
|
}
|
||||||
return Locale.getDefault(); // fallback
|
return fallback;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}.
|
||||||
|
*
|
||||||
|
* <p>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.</p>
|
||||||
|
*
|
||||||
|
* <p>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.</p>
|
||||||
|
*
|
||||||
|
* <p>Typical usage:</p>
|
||||||
|
* <pre>{@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;
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>Note: Only use this interface for resources that are safe to load at startup, as it may
|
||||||
|
* increase memory usage or startup time.</p>
|
||||||
|
*/
|
||||||
|
public interface PreloadResource extends LoadableResource {}
|
||||||
Reference in New Issue
Block a user