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

# Conflicts:
#	app/src/main/java/org/toop/app/App.java
#	app/src/main/java/org/toop/app/GameInformation.java
#	app/src/main/java/org/toop/app/canvas/GameCanvas.java
#	app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java
#	app/src/main/java/org/toop/app/layer/Container.java
#	app/src/main/java/org/toop/app/layer/Layer.java
#	app/src/main/java/org/toop/app/layer/NodeBuilder.java
#	app/src/main/java/org/toop/app/layer/Popup.java
#	app/src/main/java/org/toop/app/layer/containers/HorizontalContainer.java
#	app/src/main/java/org/toop/app/layer/containers/VerticalContainer.java
#	app/src/main/java/org/toop/app/layer/layers/ConnectedLayer.java
#	app/src/main/java/org/toop/app/layer/layers/CreditsPopup.java
#	app/src/main/java/org/toop/app/layer/layers/MainLayer.java
#	app/src/main/java/org/toop/app/layer/layers/MultiplayerLayer.java
#	app/src/main/java/org/toop/app/layer/layers/OptionsPopup.java
#	app/src/main/java/org/toop/app/layer/layers/QuitPopup.java
#	app/src/main/java/org/toop/app/layer/layers/game/GameFinishedPopup.java
#	app/src/main/java/org/toop/app/layer/layers/game/TicTacToeLayer.java
#	app/src/main/java/org/toop/local/AppSettings.java
This commit is contained in:
ramollia
2025-10-15 00:17:17 +02:00
50 changed files with 991 additions and 954 deletions

4
.gitignore vendored
View File

@@ -48,6 +48,8 @@ shelf/
*.ipr
*.iws
misc.xml
uiDesigner.xml
##############################
## Eclipse
@@ -76,6 +78,8 @@ dist/
nbdist/
nbactions.xml
nb-configuration.xml
misc.xml
compiler.xml
##############################
## Visual Studio Code

22
.idea/compiler.xml generated
View File

@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="pism_framework" />
<module name="pism_game" />
<module name="pism_app" />
</profile>
</annotationProcessing>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="pism_app" options="" />
<module name="pism_framework" options="" />
<module name="pism_game" options="" />
</option>
</component>
</project>

View File

@@ -13,6 +13,7 @@
<w>toop</w>
<w>vmoptions</w>
<w>xplugin</w>
<w>yourturn</w>
</words>
</dictionary>
</component>

View File

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

19
.idea/misc.xml generated
View File

@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EntryPointsManager">
<list size="1">
<item index="0" class="java.lang.String" itemvalue="com.google.common.eventbus.Subscribe" />
</list>
</component>
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" default="true" project-jdk-name="openjdk-25" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View File

@@ -1,14 +1,14 @@
package org.toop;
import org.toop.app.App;
import org.toop.framework.asset.ResourceLoader;
import org.toop.framework.asset.ResourceManager;
import org.toop.framework.audio.SoundManager;
import org.toop.framework.networking.NetworkingClientManager;
import org.toop.framework.networking.NetworkingInitializationException;
import org.toop.framework.resource.ResourceLoader;
import org.toop.framework.resource.ResourceManager;
public final class Main {
public static void main(String[] args) {
static void main(String[] args) {
initSystems();
App.run(args);
}

View File

@@ -1,9 +1,8 @@
package org.toop.local;
import org.toop.framework.asset.ResourceManager;
import org.toop.framework.asset.resources.LocalizationAsset;
import java.util.Locale;
import org.toop.framework.resource.ResourceManager;
import org.toop.framework.resource.resources.LocalizationAsset;
public class AppContext {
private static final LocalizationAsset localization = ResourceManager.get("localization");

View File

@@ -1,9 +1,14 @@
package org.toop.local;
import java.io.File;
import java.util.Locale;
import org.toop.app.App;
import org.toop.framework.asset.resources.SettingsAsset;
import org.toop.framework.audio.events.AudioEvents;
import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.resource.ResourceManager;
import org.toop.framework.resource.ResourceMeta;
import org.toop.framework.resource.resources.SettingsAsset;
import org.toop.framework.settings.Settings;
import java.io.File;
@@ -12,18 +17,31 @@ import java.util.Locale;
public class AppSettings {
private static SettingsAsset settingsAsset;
private SettingsAsset settingsAsset;
public void applySettings() {
this.settingsAsset = getPath();
if (!this.settingsAsset.isLoaded()) {
this.settingsAsset.load();
public static void applySettings() {
SettingsAsset settings = getPath();
if (!settings.isLoaded()) {
settings.load();
}
Settings settingsData = settings.getContent();
Settings settingsData = this.settingsAsset.getContent();
AppContext.setLocale(Locale.of(settingsData.locale));
App.setFullscreen(settingsData.fullScreen);
new EventFlow().addPostEvent(new AudioEvents.ChangeVolume(settingsData.volume)).asyncPostEvent();
new EventFlow().addPostEvent(new AudioEvents.ChangeFxVolume(settingsData.fxVolume)).asyncPostEvent();
new EventFlow().addPostEvent(new AudioEvents.ChangeMusicVolume(settingsData.musicVolume)).asyncPostEvent();
new EventFlow()
.addPostEvent(new AudioEvents.ChangeVolume(settingsData.volume))
.asyncPostEvent();
new EventFlow()
.addPostEvent(new AudioEvents.ChangeFxVolume(settingsData.fxVolume))
.asyncPostEvent();
new EventFlow()
.addPostEvent(new AudioEvents.ChangeMusicVolume(settingsData.musicVolume))
.asyncPostEvent();
App.setStyle(settingsAsset.getTheme(), settingsAsset.getLayoutSize());
}
@@ -43,10 +61,12 @@ public class AppSettings {
basePath = System.getProperty("user.home") + "/.config";
}
File settingsFile = new File(basePath + File.separator + "ISY1" + File.separator + "settings.json");
settingsAsset = new SettingsAsset(settingsFile);
File settingsFile =
new File(basePath + File.separator + "ISY1" + File.separator + "settings.json");
// this.settingsAsset = new SettingsAsset(settingsFile);
ResourceManager.addAsset(new ResourceMeta<>("settings.json", new SettingsAsset(settingsFile)));
}
return settingsAsset;
return ResourceManager.get("settings.json");
}
public static SettingsAsset getSettings() {

View File

@@ -12,7 +12,6 @@ import org.apache.logging.log4j.core.config.LoggerConfig;
* <p>Provides methods to enable or disable logs globally or per class, with support for specifying
* log levels either via {@link Level} enums or string names.
*/
// Todo: refactor
public final class Logging {
/** Disables all logging globally by setting the root logger level to {@link Level#OFF}. */

View File

@@ -7,26 +7,27 @@ import java.util.concurrent.atomic.AtomicLong;
/**
* A thread-safe, distributed unique ID generator following the Snowflake pattern.
* <p>
* Each generated 64-bit ID encodes:
*
* <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>
* <li>41-bit timestamp (milliseconds since custom epoch)
* <li>10-bit machine identifier
* <li>12-bit sequence number for IDs generated in the same millisecond
* </ul>
* </p>
*
* <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>
* <li>IDs are unique per machine.
* <li>Monotonicity within the same machine.
* <li>Safe concurrent generation via synchronized {@link #nextId()}.
* </ul>
* </p>
*
* <p>Custom epoch is set to {@code 2025-01-01T00:00:00Z}.</p>
* <p>Custom epoch is set to {@code 2025-01-01T00:00:00Z}.
*
* <p>Usage example:
*
* <p>Usage example:</p>
* <pre>{@code
* SnowflakeGenerator generator = new SnowflakeGenerator();
* long id = generator.nextId();
@@ -34,9 +35,7 @@ import java.util.concurrent.atomic.AtomicLong;
*/
public class SnowflakeGenerator {
/**
* Custom epoch in milliseconds (2025-01-01T00:00:00Z).
*/
/** Custom epoch in milliseconds (2025-01-01T00:00:00Z). */
private static final long EPOCH = Instant.parse("2025-01-01T00:00:00Z").toEpochMilli();
// Bit allocations
@@ -53,17 +52,15 @@ public class SnowflakeGenerator {
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).
*/
/** 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.
* 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 {
@@ -82,6 +79,7 @@ public class SnowflakeGenerator {
/**
* For testing: manually set the last generated timestamp.
*
* @param l timestamp in milliseconds
*/
void setTime(long l) {
@@ -89,8 +87,8 @@ public class SnowflakeGenerator {
}
/**
* Constructs a SnowflakeGenerator.
* Validates that the machine ID is within allowed range.
* Constructs a SnowflakeGenerator. Validates that the machine ID is within allowed range.
*
* @throws IllegalArgumentException if machine ID is invalid
*/
public SnowflakeGenerator() {
@@ -102,10 +100,9 @@ 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>
*
* <p>If multiple IDs are generated in the same millisecond, a sequence number is incremented.
* If the sequence overflows, waits until the next millisecond.
*
* @return a unique 64-bit ID
* @throws IllegalStateException if clock moves backwards or timestamp exceeds 41-bit limit
@@ -139,6 +136,7 @@ public class SnowflakeGenerator {
/**
* Waits until the next millisecond if sequence overflows.
*
* @param lastTimestamp previous timestamp
* @return new timestamp
*/
@@ -150,9 +148,7 @@ public class SnowflakeGenerator {
return ts;
}
/**
* Returns current system timestamp in milliseconds.
*/
/** Returns current system timestamp in milliseconds. */
private long timestamp() {
return System.currentTimeMillis();
}

View File

@@ -1,74 +0,0 @@
package org.toop.framework.asset.types;
import org.toop.framework.asset.ResourceLoader;
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 ResourceLoader}
* 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();
// /**
// Returns the name
// */
// String getDefaultName();
}

View File

@@ -1,44 +0,0 @@
package org.toop.framework.asset.types;
import org.toop.framework.asset.ResourceLoader;
import org.toop.framework.asset.resources.BaseResource;
import java.lang.annotation.Retention;
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.
*
* <p>This annotation is processed by the {@link ResourceLoader}
* 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)
@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();
}

View File

@@ -1,66 +0,0 @@
package org.toop.framework.asset.types;
import org.toop.framework.asset.ResourceLoader;
/**
* 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 ResourceLoader} if desired.</p>
*/
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();
}

View File

@@ -1,12 +1,10 @@
package org.toop.framework.audio;
import com.sun.scenario.Settings;
import javafx.scene.media.MediaPlayer;
import org.toop.framework.audio.events.AudioEvents;
import org.toop.framework.eventbus.EventFlow;
import javax.sound.sampled.Clip;
import javax.sound.sampled.FloatControl;
import org.toop.framework.audio.events.AudioEvents;
import org.toop.framework.eventbus.EventFlow;
public class AudioVolumeManager {
private final SoundManager sM;
@@ -15,7 +13,7 @@ public class AudioVolumeManager {
private double fxVolume = 1.0;
private double musicVolume = 1.0;
public AudioVolumeManager(SoundManager soundManager){
public AudioVolumeManager(SoundManager soundManager) {
this.sM = soundManager;
new EventFlow()
@@ -25,19 +23,22 @@ public class AudioVolumeManager {
.listen(this::handleGetCurrentVolume)
.listen(this::handleGetCurrentFxVolume)
.listen(this::handleGetCurrentMusicVolume);
}
public void updateMusicVolume(MediaPlayer mediaPlayer){
public void updateMusicVolume(MediaPlayer mediaPlayer) {
mediaPlayer.setVolume(this.musicVolume * this.volume);
}
public void updateSoundEffectVolume(Clip clip){
if (clip.isControlSupported(FloatControl.Type.MASTER_GAIN)){
FloatControl volumeControl = (FloatControl) clip.getControl(FloatControl.Type.MASTER_GAIN);
public void updateSoundEffectVolume(Clip clip) {
if (clip.isControlSupported(FloatControl.Type.MASTER_GAIN)) {
FloatControl volumeControl =
(FloatControl) clip.getControl(FloatControl.Type.MASTER_GAIN);
float min = volumeControl.getMinimum();
float max = volumeControl.getMaximum();
float dB = (float) (Math.log10(Math.max(this.fxVolume * this.volume, 0.0001)) * 20.0); // convert linear to dB
float dB =
(float)
(Math.log10(Math.max(this.fxVolume * this.volume, 0.0001))
* 20.0); // convert linear to dB
dB = Math.max(min, Math.min(max, dB));
volumeControl.setValue(dB);
}
@@ -50,7 +51,7 @@ public class AudioVolumeManager {
private void handleFxVolumeChange(AudioEvents.ChangeFxVolume event) {
this.fxVolume = limitVolume(event.newVolume() / 100);
for (Clip clip : sM.getActiveSoundEffects().values()){
for (Clip clip : sM.getActiveSoundEffects().values()) {
updateSoundEffectVolume(clip);
}
}
@@ -60,32 +61,38 @@ public class AudioVolumeManager {
for (MediaPlayer mediaPlayer : sM.getActiveMusic()) {
this.updateMusicVolume(mediaPlayer);
}
for (Clip clip : sM.getActiveSoundEffects().values()){
for (Clip clip : sM.getActiveSoundEffects().values()) {
updateSoundEffectVolume(clip);
}
}
private void handleMusicVolumeChange(AudioEvents.ChangeMusicVolume event){
private void handleMusicVolumeChange(AudioEvents.ChangeMusicVolume event) {
this.musicVolume = limitVolume(event.newVolume() / 100);
System.out.println(this.musicVolume);
System.out.println(this.volume);
for (MediaPlayer mediaPlayer : sM.getActiveMusic()){
for (MediaPlayer mediaPlayer : sM.getActiveMusic()) {
this.updateMusicVolume(mediaPlayer);
}
}
private void handleGetCurrentVolume(AudioEvents.GetCurrentVolume event) {
new EventFlow().addPostEvent(new AudioEvents.GetCurrentVolumeResponse(volume * 100, event.snowflakeId()))
new EventFlow()
.addPostEvent(
new AudioEvents.GetCurrentVolumeResponse(volume * 100, event.snowflakeId()))
.asyncPostEvent();
}
private void handleGetCurrentFxVolume(AudioEvents.GetCurrentFxVolume event) {
new EventFlow().addPostEvent(new AudioEvents.GetCurrentFxVolumeResponse(fxVolume * 100, event.snowflakeId()))
new EventFlow()
.addPostEvent(
new AudioEvents.GetCurrentFxVolumeResponse(
fxVolume * 100, event.snowflakeId()))
.asyncPostEvent();
}
private void handleGetCurrentMusicVolume(AudioEvents.GetCurrentMusicVolume event){
new EventFlow().addPostEvent(new AudioEvents.GetCurrentMusicVolumeResponse(musicVolume * 100, event.snowflakeId()))
private void handleGetCurrentMusicVolume(AudioEvents.GetCurrentMusicVolume event) {
new EventFlow()
.addPostEvent(
new AudioEvents.GetCurrentMusicVolumeResponse(
musicVolume * 100, event.snowflakeId()))
.asyncPostEvent();
}
}

View File

@@ -1,20 +1,18 @@
package org.toop.framework.audio;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.toop.framework.SnowflakeGenerator;
import org.toop.framework.asset.ResourceManager;
import org.toop.framework.asset.ResourceMeta;
import org.toop.framework.asset.resources.MusicAsset;
import org.toop.framework.asset.resources.SoundEffectAsset;
import org.toop.framework.audio.events.AudioEvents;
import org.toop.framework.eventbus.EventFlow;
import javafx.scene.media.MediaPlayer;
import java.io.*;
import java.util.*;
import javafx.scene.media.MediaPlayer;
import javax.sound.sampled.*;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.toop.framework.SnowflakeGenerator;
import org.toop.framework.audio.events.AudioEvents;
import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.resource.ResourceManager;
import org.toop.framework.resource.ResourceMeta;
import org.toop.framework.resource.resources.MusicAsset;
import org.toop.framework.resource.resources.SoundEffectAsset;
public class SoundManager {
private static final Logger logger = LogManager.getLogger(SoundManager.class);
@@ -22,13 +20,12 @@ public class SoundManager {
private final Queue<MusicAsset> backgroundMusicQueue = new LinkedList<>();
private final Map<Long, Clip> activeSoundEffects = new HashMap<>();
private final HashMap<String, SoundEffectAsset> audioResources = new HashMap<>();
private final SnowflakeGenerator idGenerator = new SnowflakeGenerator(); // TODO: Don't create a new generator
private final AudioVolumeManager audioVolumeManager = new AudioVolumeManager(this);
public SoundManager() {
// Get all Audio Resources and add them to a list.
for (ResourceMeta<SoundEffectAsset> asset : ResourceManager.getAllOfType(SoundEffectAsset.class)) {
for (ResourceMeta<SoundEffectAsset> asset :
ResourceManager.getAllOfType(SoundEffectAsset.class)) {
try {
this.addAudioResource(asset);
} catch (IOException | LineUnavailableException | UnsupportedAudioFileException e) {
@@ -39,10 +36,14 @@ public class SoundManager {
.listen(this::handlePlaySound)
.listen(this::handleStopSound)
.listen(this::handleMusicStart)
.listen(AudioEvents.ClickButton.class, _ -> {
.listen(
AudioEvents.ClickButton.class,
_ -> {
try {
playSound("medium-button-click.wav", false);
} catch (UnsupportedAudioFileException | LineUnavailableException | IOException e) {
} catch (UnsupportedAudioFileException
| LineUnavailableException
| IOException e) {
logger.error(e);
}
});
@@ -68,14 +69,13 @@ public class SoundManager {
private void handleMusicStart(AudioEvents.StartBackgroundMusic e) {
backgroundMusicQueue.clear();
List<MusicAsset> shuffledArray = new ArrayList<>(ResourceManager.getAllOfType(MusicAsset.class)
.stream()
List<MusicAsset> shuffledArray =
new ArrayList<>(
ResourceManager.getAllOfType(MusicAsset.class).stream()
.map(ResourceMeta::getResource)
.toList());
Collections.shuffle(shuffledArray);
backgroundMusicQueue.addAll(
shuffledArray
);
backgroundMusicQueue.addAll(shuffledArray);
backgroundMusicPlayer();
}
@@ -89,7 +89,8 @@ public class SoundManager {
MediaPlayer mediaPlayer = new MediaPlayer(ma.getMedia());
mediaPlayer.setOnEndOfMedia(() -> {
mediaPlayer.setOnEndOfMedia(
() -> {
addBackgroundMusic(ma);
activeMusic.remove(mediaPlayer);
mediaPlayer.dispose();
@@ -97,13 +98,15 @@ public class SoundManager {
backgroundMusicPlayer(); // play next
});
mediaPlayer.setOnStopped(() -> {
mediaPlayer.setOnStopped(
() -> {
addBackgroundMusic(ma);
activeMusic.remove(mediaPlayer);
ma.unload();
});
mediaPlayer.setOnError(() -> {
mediaPlayer.setOnError(
() -> {
addBackgroundMusic(ma);
activeMusic.remove(mediaPlayer);
ma.unload();
@@ -113,10 +116,15 @@ public class SoundManager {
mediaPlayer.play();
activeMusic.add(mediaPlayer);
logger.info("Playing background music: {}", ma.getFile().getName());
logger.info("Background music next in line: {}", backgroundMusicQueue.peek().getFile().getName());
logger.info(
"Background music next in line: {}",
backgroundMusicQueue.peek() != null
? backgroundMusicQueue.peek().getFile().getName()
: null);
}
private long playSound(String audioFileName, boolean loop) throws UnsupportedAudioFileException, LineUnavailableException, IOException {
private long playSound(String audioFileName, boolean loop)
throws UnsupportedAudioFileException, LineUnavailableException, IOException {
SoundEffectAsset asset = audioResources.get(audioFileName);
// Return -1 which indicates resource wasn't available
@@ -134,21 +142,21 @@ public class SoundManager {
// If supposed to loop make it loop, else just start it once
if (loop) {
clip.loop(Clip.LOOP_CONTINUOUSLY);
}
else {
} else {
clip.start();
}
logger.debug("Playing sound: {}", asset.getFile().getName());
// Generate id for clip
long clipId = idGenerator.nextId();
long clipId = new SnowflakeGenerator().nextId();
// store it so we can stop it later
activeSoundEffects.put(clipId, clip); // TODO: Do on snowflake for specific sound to stop
activeSoundEffects.put(clipId, clip);
// remove when finished (only for non-looping sounds)
clip.addLineListener(event -> {
clip.addLineListener(
event -> {
if (event.getType() == LineEvent.Type.STOP && !clip.isRunning()) {
activeSoundEffects.remove(clipId);
clip.close();
@@ -179,7 +187,11 @@ public class SoundManager {
activeSoundEffects.clear();
}
public Map<Long, Clip> getActiveSoundEffects(){ return this.activeSoundEffects; }
public Map<Long, Clip> getActiveSoundEffects() {
return this.activeSoundEffects;
}
public List<MediaPlayer> getActiveMusic() { return activeMusic; }
public List<MediaPlayer> getActiveMusic() {
return activeMusic;
}
}

View File

@@ -1,21 +1,22 @@
package org.toop.framework.audio.events;
import java.util.Map;
import org.toop.framework.eventbus.events.EventWithSnowflake;
import org.toop.framework.eventbus.events.EventWithoutSnowflake;
import org.toop.framework.eventbus.events.EventsBase;
import java.util.Map;
public class AudioEvents extends EventsBase {
/** Starts playing a sound. */
public record PlayEffect(String fileName, boolean loop)
implements EventWithoutSnowflake {}
public record PlayEffect(String fileName, boolean loop) implements EventWithoutSnowflake {}
public record StopEffect(long clipId) implements EventWithoutSnowflake {}
public record StartBackgroundMusic() implements EventWithoutSnowflake {}
public record ChangeVolume(double newVolume) implements EventWithoutSnowflake {}
public record ChangeFxVolume(double newVolume) implements EventWithoutSnowflake {}
public record ChangeMusicVolume(double newVolume) implements EventWithoutSnowflake {}
public record GetCurrentVolume(long snowflakeId) implements EventWithSnowflake {
@@ -29,7 +30,9 @@ public class AudioEvents extends EventsBase {
return snowflakeId;
}
}
public record GetCurrentVolumeResponse(double currentVolume, long snowflakeId) implements EventWithSnowflake {
public record GetCurrentVolumeResponse(double currentVolume, long snowflakeId)
implements EventWithSnowflake {
@Override
public Map<String, Object> result() {
return Map.of();
@@ -65,7 +68,8 @@ public class AudioEvents extends EventsBase {
}
}
public record GetCurrentFxVolumeResponse(double currentVolume, long snowflakeId) implements EventWithSnowflake {
public record GetCurrentFxVolumeResponse(double currentVolume, long snowflakeId)
implements EventWithSnowflake {
@Override
public Map<String, Object> result() {
return Map.of();
@@ -77,7 +81,8 @@ public class AudioEvents extends EventsBase {
}
}
public record GetCurrentMusicVolumeResponse(double currentVolume, long snowflakeId) implements EventWithSnowflake {
public record GetCurrentMusicVolumeResponse(double currentVolume, long snowflakeId)
implements EventWithSnowflake {
@Override
public Map<String, Object> result() {
return Map.of();
@@ -90,5 +95,4 @@ public class AudioEvents extends EventsBase {
}
public record ClickButton() implements EventWithoutSnowflake {}
}
}

View File

@@ -1,7 +1,7 @@
package org.toop.framework.eventbus;
public class ListenerHandler {
private Object listener = null;
private Object listener;
// private boolean unsubscribeAfterSuccess = true;

View File

@@ -84,9 +84,9 @@ public class NetworkingClient {
if (isChannelActive()) {
this.channel.writeAndFlush(msg);
logger.info(
"Connection {} sent message: '{}'", this.channel.remoteAddress(), literalMsg);
"Connection {} sent message: '{}' ", this.channel.remoteAddress(), literalMsg);
} else {
logger.warn("Cannot send message: '{}', connection inactive.", literalMsg);
logger.warn("Cannot send message: '{}', connection inactive. ", literalMsg);
}
}

View File

@@ -45,8 +45,8 @@ public class NetworkingClientManager {
}
long startClientRequest(String ip, int port) {
long connectionId = new SnowflakeGenerator().nextId(); // TODO: Maybe use the one generated
try { // With EventFlow
long connectionId = new SnowflakeGenerator().nextId();
try {
NetworkingClient client =
new NetworkingClient(
() -> new NetworkingGameClientHandler(connectionId),
@@ -81,19 +81,13 @@ public class NetworkingClientManager {
void handleStartClient(NetworkEvents.StartClient event) {
long id = this.startClientRequest(event.ip(), event.port());
new Thread(
() -> {
try {
Thread.sleep(100); // TODO: Is this a good idea?
() ->
new EventFlow()
.addPostEvent(
NetworkEvents.StartClientResponse.class,
id,
event.eventSnowflake())
.asyncPostEvent();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
})
.asyncPostEvent())
.start();
}
@@ -185,7 +179,7 @@ public class NetworkingClientManager {
void handleCloseClient(NetworkEvents.CloseClient event) {
NetworkingClient client = this.networkClients.get(event.clientId());
client.closeConnection(); // TODO: Check if not blocking, what if error, mb not remove?
client.closeConnection();
this.networkClients.remove(event.clientId());
logger.info("Client {} closed successfully.", event.clientId());
}

View File

@@ -74,7 +74,7 @@ public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
gameWinConditionHandler(recSrvRemoved);
return;
default:
return;
// return
}
} else {
@@ -93,10 +93,10 @@ public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
helpHandler(recSrvRemoved);
return;
default:
return;
// return
}
} else {
return; // TODO: Should be an error.
logger.error("Could not parse: {}", rec);
}
}
}
@@ -136,7 +136,7 @@ public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
try {
String[] msg =
Pattern.compile(
"(?:CHALLENGER|GAMETYPE|CHALLENGENUMBER):\\s*\"?(.*?)\"?\\s*(?:,|})")
"(?:CHALLENGER|GAMETYPE|CHALLENGENUMBER):\\s*\"?(.*?)\"?\\s*[,}]")
.matcher(rec)
.results()
.map(m -> m.group().trim())

View File

@@ -1,14 +1,4 @@
package org.toop.framework.asset;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.toop.framework.asset.events.AssetLoaderEvents;
import org.toop.framework.asset.resources.*;
import org.toop.framework.asset.types.BundledResource;
import org.toop.framework.asset.types.FileExtension;
import org.toop.framework.asset.types.PreloadResource;
import org.toop.framework.eventbus.EventFlow;
import org.reflections.Reflections;
package org.toop.framework.resource;
import java.io.File;
import java.util.*;
@@ -16,30 +6,40 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.reflections.Reflections;
import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.resource.events.AssetLoaderEvents;
import org.toop.framework.resource.exceptions.CouldNotCreateResourceFactoryException;
import org.toop.framework.resource.resources.*;
import org.toop.framework.resource.types.BundledResource;
import org.toop.framework.resource.types.FileExtension;
import org.toop.framework.resource.types.PreloadResource;
/**
* Responsible for loading assets from a file system directory into memory.
* <p>
* The {@code ResourceLoader} 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 ResourceManager}.</p>
* <p>The {@code ResourceLoader} 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>Assets are stored in a static, thread-safe list and can be retrieved through {@link
* ResourceManager}.
*
* <p>Features:
*
* <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>
* <li>Recursive directory scanning for assets.
* <li>Automatic registration of resource classes via reflection.
* <li>Bundled resource support: multiple files merged into a single resource instance.
* <li>Preload resources automatically invoke {@link PreloadResource#load()}.
* <li>Progress tracking via {@link AssetLoaderEvents.LoadingProgressUpdate} events.
* </ul>
*
* <p>Usage example:</p>
* <p>Usage example:
*
* <pre>{@code
* ResourceLoader loader = new ResourceLoader("assets");
* double progress = loader.getProgress();
@@ -48,14 +48,17 @@ import java.util.function.Function;
*/
public class ResourceLoader {
private static final Logger logger = LogManager.getLogger(ResourceLoader.class);
private static final List<ResourceMeta<? extends BaseResource>> assets = new CopyOnWriteArrayList<>();
private final Map<String, Function<File, ? extends BaseResource>> registry = new ConcurrentHashMap<>();
private static final List<ResourceMeta<? extends BaseResource>> assets =
new CopyOnWriteArrayList<>();
private final Map<String, Function<File, ? extends BaseResource>> registry =
new ConcurrentHashMap<>();
private final AtomicInteger loadedCount = new AtomicInteger(0);
private int totalCount = 0;
/**
* Constructs an ResourceLoader and loads assets from the given root folder.
*
* @param rootFolder the folder containing asset files
*/
public ResourceLoader(File rootFolder) {
@@ -84,6 +87,7 @@ public class ResourceLoader {
/**
* Constructs an ResourceLoader from a folder path.
*
* @param rootFolder the folder path containing assets
*/
public ResourceLoader(String rootFolder) {
@@ -92,6 +96,7 @@ public class ResourceLoader {
/**
* Returns the current progress of loading assets (0.0 to 1.0).
*
* @return progress as a double
*/
public double getProgress() {
@@ -100,6 +105,7 @@ public class ResourceLoader {
/**
* Returns the number of assets loaded so far.
*
* @return loaded count
*/
public int getLoadedCount() {
@@ -108,6 +114,7 @@ public class ResourceLoader {
/**
* Returns the total number of files found to load.
*
* @return total asset count
*/
public int getTotalCount() {
@@ -116,6 +123,7 @@ public class ResourceLoader {
/**
* Returns a snapshot list of all assets loaded by this loader.
*
* @return list of loaded assets
*/
public List<ResourceMeta<? extends BaseResource>> getAssets() {
@@ -124,6 +132,7 @@ public class ResourceLoader {
/**
* 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
@@ -132,69 +141,79 @@ public class ResourceLoader {
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) {
/** Maps a file to a resource instance based on its extension and registered factories. */
private <T extends BaseResource> T resourceMapper(File file)
throws CouldNotCreateResourceFactoryException, IllegalArgumentException {
String ext = getExtension(file.getName());
Function<File, ? extends BaseResource> factory = registry.get(ext);
if (factory == null) return null;
if (factory == null)
throw new CouldNotCreateResourceFactoryException(registry, file.getName());
BaseResource resource = factory.apply(file);
if (!type.isInstance(resource)) {
if (resource == null) {
throw new IllegalArgumentException(
"File " + file.getName() + " is not of type " + type.getSimpleName()
);
"File "
+ file.getName()
+ " is not of type "
+ BaseResource.class.getSimpleName());
}
return type.cast(resource);
return ((Class<T>) BaseResource.class).cast(resource);
}
/**
* Loads the given list of files into assets, handling bundled and preload resources.
*/
/** Loads the given list of files into assets, handling bundled and preload resources. */
private void loader(List<File> files) {
Map<String, BundledResource> bundledResources = new HashMap<>();
for (File file : files) {
boolean skipAdd = false;
BaseResource resource = resourceMapper(file, BaseResource.class);
BaseResource resource = null;
try {
resource = resourceMapper(file);
} catch (CouldNotCreateResourceFactoryException _) {
logger.warn("Could not create resource for: {}", file);
} catch (IllegalArgumentException e) {
logger.error(e);
}
switch (resource) {
case null -> {
continue;
}
case BundledResource br -> {
String key = resource.getClass().getName() + ":" + br.getBaseName();
if (!bundledResources.containsKey(key)) {bundledResources.put(key, br);}
if (!bundledResources.containsKey(key)) {
bundledResources.put(key, br);
}
bundledResources.get(key).loadFile(file);
resource = (BaseResource) bundledResources.get(key);
assets.add(new ResourceMeta<>(br.getBaseName(), resource));
skipAdd = true;
}
case PreloadResource pr -> pr.load();
default -> {
}
default -> {}
}
BaseResource finalResource = resource;
boolean alreadyAdded = assets.stream()
.anyMatch(a -> a.getResource() == finalResource);
boolean alreadyAdded = assets.stream().anyMatch(a -> a.getResource() == finalResource);
if (!alreadyAdded && !skipAdd) {
assets.add(new ResourceMeta<>(file.getName(), resource));
}
logger.info("Loaded {} from {}", resource.getClass().getSimpleName(), file.getAbsolutePath());
logger.info(
"Loaded {} from {}",
resource.getClass().getSimpleName(),
file.getAbsolutePath());
loadedCount.incrementAndGet();
new EventFlow()
.addPostEvent(new AssetLoaderEvents.LoadingProgressUpdate(loadedCount.get(), totalCount))
.addPostEvent(
new AssetLoaderEvents.LoadingProgressUpdate(
loadedCount.get(), totalCount))
.postEvent();
}
}
/**
* Recursively searches a folder and adds all files to the foundFiles list.
*/
/** Recursively searches a folder and adds all files to the foundFiles list. */
private void fileSearcher(final File folder, List<File> foundFiles) {
for (File fileEntry : Objects.requireNonNull(folder.listFiles())) {
if (fileEntry.isDirectory()) {
@@ -206,18 +225,20 @@ public class ResourceLoader {
}
/**
* Uses reflection to automatically register all {@link BaseResource} subclasses
* annotated with {@link FileExtension}.
* 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");
Reflections reflections = new Reflections("org.toop.framework.resource.resources");
Set<Class<? extends BaseResource>> classes = reflections.getSubTypesOf(BaseResource.class);
for (Class<? extends BaseResource> cls : classes) {
if (!cls.isAnnotationPresent(FileExtension.class)) continue;
FileExtension annotation = cls.getAnnotation(FileExtension.class);
for (String ext : annotation.value()) {
registry.put(ext, file -> {
registry.put(
ext,
file -> {
try {
return cls.getConstructor(File.class).newInstance(file);
} catch (Exception e) {
@@ -228,9 +249,7 @@ public class ResourceLoader {
}
}
/**
* Extracts the base name from a file name, used for bundling multiple files.
*/
/** 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('.');
@@ -238,9 +257,7 @@ public class ResourceLoader {
return fileName.substring(0, dotIndex);
}
/**
* Returns the file extension of a given file name (without dot).
*/
/** 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) : "";

View File

@@ -1,30 +1,31 @@
package org.toop.framework.asset;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.toop.framework.asset.resources.*;
package org.toop.framework.resource;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.toop.framework.resource.exceptions.ResourceNotFoundException;
import org.toop.framework.resource.resources.*;
/**
* Centralized manager for all loaded assets in the application.
* <p>
* {@code ResourceManager} maintains a thread-safe registry of {@link Asset} objects
* and provides utility methods to retrieve assets by name, ID, or type.
* It works together with {@link ResourceLoader} to register assets automatically
* when they are loaded from the file system.
* </p>
*
* <p>Key responsibilities:</p>
* <p>{@code ResourceManager} maintains a thread-safe registry of {@link ResourceMeta} objects and
* provides utility methods to retrieve assets by name, ID, or type. It works together with {@link
* ResourceLoader} to register assets automatically when they are loaded from the file system.
*
* <p>Key responsibilities:
*
* <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>
* <li>Storing all loaded assets in a concurrent map.
* <li>Providing typed access to asset resources.
* <li>Allowing lookup by asset name or ID.
* <li>Supporting retrieval of all assets of a specific {@link BaseResource} subclass.
* </ul>
*
* <p>Example usage:</p>
* <p>Example usage:
*
* <pre>{@code
* // Load assets from a loader
* ResourceLoader loader = new ResourceLoader(new File("RootFolder"));
@@ -40,36 +41,29 @@ import java.util.concurrent.ConcurrentHashMap;
* Optional<Asset<? extends BaseResource>> maybeAsset = ResourceManager.findByName("menu.css");
* }</pre>
*
* <p>Notes:</p>
* <p>Notes:
*
* <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 ResourceLoader} before retrieval.</li>
* <li>All retrieval methods are static and thread-safe.
* <li>The {@link #get(String)} method may require casting if the asset type is not known at
* compile time.
* <li>Assets should be loaded via {@link ResourceLoader} before retrieval.
* </ul>
*/
public class ResourceManager {
private static final Logger logger = LogManager.getLogger(ResourceManager.class);
private static final ResourceManager INSTANCE = new ResourceManager();
private static final Map<String, ResourceMeta<? extends BaseResource>> assets = new ConcurrentHashMap<>();
private static final Map<String, ResourceMeta<? extends BaseResource>> assets =
new ConcurrentHashMap<>();
private ResourceManager() {}
/**
* Returns the singleton instance of {@code ResourceManager}.
*
* @return the shared instance
*/
public static ResourceManager getInstance() {
return INSTANCE;
}
/**
* Loads all assets from a given {@link ResourceLoader} into the manager.
*
* @param loader the loader that has already loaded assets
*/
public synchronized static void loadAssets(ResourceLoader loader) {
for (var asset : loader.getAssets()) {
public static synchronized void loadAssets(ResourceLoader loader) {
for (ResourceMeta<? extends BaseResource> asset : loader.getAssets()) {
assets.put(asset.getName(), asset);
}
}
@@ -85,15 +79,15 @@ public class ResourceManager {
public static <T extends BaseResource> T get(String name) {
ResourceMeta<T> asset = (ResourceMeta<T>) assets.get(name);
if (asset == null) {
throw new TypeNotPresentException(name, new RuntimeException(String.format("Type %s not present", name))); // TODO: Create own exception, BAM
throw new ResourceNotFoundException(name);
}
return asset.getResource();
}
// @SuppressWarnings("unchecked")
// public static <T extends BaseResource> ArrayList<ResourceMeta<T>> getAllOfType() {
// return (ArrayList<ResourceMeta<T>>) (ArrayList<?>) new ArrayList<>(assets.values());
// }
// @SuppressWarnings("unchecked")
// public static <T extends BaseResource> ArrayList<ResourceMeta<T>> getAllOfType() {
// return (ArrayList<ResourceMeta<T>>) (ArrayList<?>) new ArrayList<>(assets.values());
// }
/**
* Retrieve all assets of a specific resource type.
@@ -136,5 +130,6 @@ public class ResourceManager {
*/
public static void addAsset(ResourceMeta<? extends BaseResource> asset) {
assets.put(asset.getName(), asset);
logger.info("Successfully added asset: {}, to the asset list", asset.getName());
}
}

View File

@@ -1,7 +1,7 @@
package org.toop.framework.asset;
package org.toop.framework.resource;
import org.toop.framework.SnowflakeGenerator;
import org.toop.framework.asset.resources.BaseResource;
import org.toop.framework.resource.resources.BaseResource;
public class ResourceMeta<T extends BaseResource> {
private final Long id;
@@ -25,5 +25,4 @@ public class ResourceMeta<T extends BaseResource> {
public T getResource() {
return this.resource;
}
}

View File

@@ -1,7 +1,8 @@
package org.toop.framework.asset.events;
package org.toop.framework.resource.events;
import org.toop.framework.eventbus.events.EventWithoutSnowflake;
public class AssetLoaderEvents {
public record LoadingProgressUpdate(int hasLoadedAmount, int isLoadingAmount) implements EventWithoutSnowflake {}
public record LoadingProgressUpdate(int hasLoadedAmount, int isLoadingAmount)
implements EventWithoutSnowflake {}
}

View File

@@ -0,0 +1,12 @@
package org.toop.framework.resource.exceptions;
import java.util.Map;
public class CouldNotCreateResourceFactoryException extends RuntimeException {
public CouldNotCreateResourceFactoryException(Map<?, ?> registry, String fileName) {
super(
String.format(
"Could not create resource factory for: %s, isRegistryEmpty: %b",
fileName, registry.isEmpty()));
}
}

View File

@@ -0,0 +1,7 @@
package org.toop.framework.resource.exceptions;
public class IsNotAResourceException extends RuntimeException {
public <T> IsNotAResourceException(Class<T> clazz, String message) {
super(clazz.getName() + " does not implement BaseResource");
}
}

View File

@@ -0,0 +1,7 @@
package org.toop.framework.resource.exceptions;
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String name) {
super("Could not find resource: " + name);
}
}

View File

@@ -1,4 +1,4 @@
package org.toop.framework.asset.resources;
package org.toop.framework.resource.resources;
import java.io.*;
@@ -14,5 +14,4 @@ public abstract class BaseResource {
public File getFile() {
return this.file;
}
}

View File

@@ -1,8 +1,7 @@
package org.toop.framework.asset.resources;
import org.toop.framework.asset.types.FileExtension;
package org.toop.framework.resource.resources;
import java.io.File;
import org.toop.framework.resource.types.FileExtension;
@FileExtension({"css"})
public class CssAsset extends BaseResource {

View File

@@ -1,12 +1,11 @@
package org.toop.framework.asset.resources;
import javafx.scene.text.Font;
import org.toop.framework.asset.types.FileExtension;
import org.toop.framework.asset.types.PreloadResource;
package org.toop.framework.resource.resources;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import javafx.scene.text.Font;
import org.toop.framework.resource.types.FileExtension;
import org.toop.framework.resource.types.PreloadResource;
@FileExtension({"ttf", "otf"})
public class FontAsset extends BaseResource implements PreloadResource {

View File

@@ -1,11 +1,10 @@
package org.toop.framework.asset.resources;
import javafx.scene.image.Image;
import org.toop.framework.asset.types.FileExtension;
import org.toop.framework.asset.types.LoadableResource;
package org.toop.framework.resource.resources;
import java.io.File;
import java.io.FileInputStream;
import javafx.scene.image.Image;
import org.toop.framework.resource.types.FileExtension;
import org.toop.framework.resource.types.LoadableResource;
@FileExtension({"png", "jpg", "jpeg"})
public class ImageAsset extends BaseResource implements LoadableResource {

View File

@@ -1,12 +1,13 @@
package org.toop.framework.asset.resources;
package org.toop.framework.resource.resources;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.toop.framework.asset.types.FileExtension;
import org.toop.framework.asset.types.LoadableResource;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import org.toop.framework.resource.types.FileExtension;
import org.toop.framework.resource.types.LoadableResource;
@FileExtension({"json"})
public class JsonAsset<T> extends BaseResource implements LoadableResource {
@@ -25,7 +26,8 @@ public class JsonAsset<T> extends BaseResource implements LoadableResource {
File file = getFile();
if (!file.exists()) {
try {
// make a new file with the declared constructor (example: settings) if it doesn't exist
// make a new file with the declared constructor (example: settings) if it doesn't
// exist
content = type.getDeclaredConstructor().newInstance();
save();
} catch (Exception e) {
@@ -36,7 +38,7 @@ public class JsonAsset<T> extends BaseResource implements LoadableResource {
try (FileReader reader = new FileReader(file)) {
content = gson.fromJson(reader, type);
this.isLoaded = true;
} catch(Exception e) {
} catch (Exception e) {
throw new RuntimeException("Failed to load JSON asset" + getFile(), e);
}
}
@@ -59,7 +61,8 @@ public class JsonAsset<T> extends BaseResource implements LoadableResource {
File file = getFile();
File parent = file.getParentFile();
if (parent != null && !parent.exists()) {
parent.mkdirs();
boolean isDirectoryMade = parent.mkdirs();
assert isDirectoryMade;
}
try (FileWriter writer = new FileWriter(file)) {
gson.toJson(content, writer);

View File

@@ -1,34 +1,29 @@
package org.toop.framework.asset.resources;
import org.toop.framework.asset.types.BundledResource;
import org.toop.framework.asset.types.FileExtension;
import org.toop.framework.asset.types.LoadableResource;
package org.toop.framework.resource.resources;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import org.toop.framework.resource.types.BundledResource;
import org.toop.framework.resource.types.FileExtension;
import org.toop.framework.resource.types.LoadableResource;
/**
* Represents a localization resource asset that loads and manages property files
* containing key-value pairs for different locales.
* <p>
* This class implements {@link LoadableResource} to support loading/unloading
* and {@link BundledResource} to represent resources that can contain multiple
* localized bundles.
* </p>
* <p>
* Files handled by this class must have the {@code .properties} extension,
* optionally with a locale suffix, e.g., {@code messages_en_US.properties}.
* </p>
* Represents a localization resource asset that loads and manages property files containing
* key-value pairs for different locales.
*
* <p>This class implements {@link LoadableResource} to support loading/unloading and {@link
* BundledResource} to represent resources that can contain multiple localized bundles.
*
* <p>Files handled by this class must have the {@code .properties} extension, optionally with a
* locale suffix, e.g., {@code messages_en_US.properties}.
*
* <p>Example usage:
*
* <p>
* Example usage:
* <pre>{@code
* LocalizationAsset asset = new LocalizationAsset(new File("messages_en_US.properties"));
* asset.load();
* String greeting = asset.getString("hello", Locale.US);
* }</pre>
* </p>
*/
@FileExtension({"properties"})
public class LocalizationAsset extends BaseResource implements LoadableResource, BundledResource {
@@ -43,7 +38,7 @@ public class LocalizationAsset extends BaseResource implements LoadableResource,
private final String baseName = "localization";
/** Fallback locale used when no matching locale is found. */
private final Locale fallback = Locale.forLanguageTag("en_US");
private final Locale fallback = Locale.forLanguageTag("en");
/**
* Constructs a new LocalizationAsset for the specified file.
@@ -54,18 +49,14 @@ public class LocalizationAsset extends BaseResource implements LoadableResource,
super(file);
}
/**
* Loads the resource file into memory and prepares localized bundles.
*/
/** Loads the resource file into memory and prepares localized bundles. */
@Override
public void load() {
loadFile(getFile());
isLoaded = true;
}
/**
* Unloads all loaded resource bundles, freeing memory.
*/
/** Unloads all loaded resource bundles, freeing memory. */
@Override
public void unload() {
bundles.clear();
@@ -83,9 +74,8 @@ public class LocalizationAsset extends BaseResource implements LoadableResource,
}
/**
* Retrieves a localized string for the given key and locale.
* If an exact match for the locale is not found, a fallback
* matching the language or the default locale will be used.
* Retrieves a localized string for the given key and locale. If an exact match for the locale
* is not found, a fallback matching the language or the default locale will be used.
*
* @param key the key of the string
* @param locale the desired locale
@@ -95,14 +85,15 @@ public class LocalizationAsset extends BaseResource implements LoadableResource,
public String getString(String key, Locale 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: " + target, getClass().getName(), key);
return bundle.getString(key);
}
/**
* Finds the best matching locale among loaded bundles.
* Prefers an exact match, then language-only match, then fallback.
* Finds the best matching locale among loaded bundles. Prefers an exact match, then
* language-only match, then fallback.
*
* @param locale the desired locale
* @return the best matching locale
@@ -125,8 +116,8 @@ public class LocalizationAsset extends BaseResource implements LoadableResource,
}
/**
* Loads a specific property file as a resource bundle.
* The locale is extracted from the file name if present.
* Loads a specific property file as a resource bundle. The locale is extracted from the file
* name if present.
*
* @param file the property file to load
* @throws RuntimeException if the file cannot be read
@@ -153,22 +144,23 @@ public class LocalizationAsset extends BaseResource implements LoadableResource,
return this.baseName;
}
// /**
// * Extracts the base name from a file name.
// *
// * @param fileName the file name
// * @return base name without locale or extension
// */
// private String getBaseName(String fileName) {
// int dotIndex = fileName.lastIndexOf('.');
// String nameWithoutExtension = (dotIndex > 0) ? fileName.substring(0, dotIndex) : fileName;
//
// int underscoreIndex = nameWithoutExtension.indexOf('_');
// if (underscoreIndex > 0) {
// return nameWithoutExtension.substring(0, underscoreIndex);
// }
// return nameWithoutExtension;
// }
// /**
// * Extracts the base name from a file name.
// *
// * @param fileName the file name
// * @return base name without locale or extension
// */
// private String getBaseName(String fileName) {
// int dotIndex = fileName.lastIndexOf('.');
// String nameWithoutExtension = (dotIndex > 0) ? fileName.substring(0, dotIndex) :
// fileName;
//
// int underscoreIndex = nameWithoutExtension.indexOf('_');
// if (underscoreIndex > 0) {
// return nameWithoutExtension.substring(0, underscoreIndex);
// }
// return nameWithoutExtension;
// }
/**
* Extracts a locale from a file name based on the pattern "base_LOCALE.properties".

View File

@@ -1,10 +1,9 @@
package org.toop.framework.asset.resources;
import javafx.scene.media.Media;
import org.toop.framework.asset.types.FileExtension;
import org.toop.framework.asset.types.LoadableResource;
package org.toop.framework.resource.resources;
import java.io.*;
import javafx.scene.media.Media;
import org.toop.framework.resource.types.FileExtension;
import org.toop.framework.resource.types.LoadableResource;
@FileExtension({"mp3"})
public class MusicAsset extends BaseResource implements LoadableResource {

View File

@@ -1,10 +1,8 @@
package org.toop.framework.asset.resources;
import org.toop.framework.settings.Settings;
package org.toop.framework.resource.resources;
import java.io.File;
import java.util.Locale;
import org.toop.framework.settings.Settings;
public class SettingsAsset extends JsonAsset<Settings> {

View File

@@ -1,11 +1,10 @@
package org.toop.framework.asset.resources;
package org.toop.framework.resource.resources;
import org.toop.framework.asset.types.FileExtension;
import org.toop.framework.asset.types.LoadableResource;
import javax.sound.sampled.*;
import java.io.*;
import java.nio.file.Files;
import javax.sound.sampled.*;
import org.toop.framework.resource.types.FileExtension;
import org.toop.framework.resource.types.LoadableResource;
@FileExtension({"wav"})
public class SoundEffectAsset extends BaseResource implements LoadableResource {
@@ -16,22 +15,25 @@ public class SoundEffectAsset extends BaseResource implements LoadableResource {
}
// Gets a new clip to play
public Clip getNewClip() throws LineUnavailableException, UnsupportedAudioFileException, IOException {
public Clip getNewClip()
throws LineUnavailableException, UnsupportedAudioFileException, IOException {
// Get a new clip from audio system
Clip clip = AudioSystem.getClip();
// Insert a new audio stream into the clip
AudioInputStream inputStream = this.getAudioStream();
AudioFormat baseFormat = inputStream.getFormat();
if (baseFormat.getSampleSizeInBits() > 16) inputStream = downSampleAudio(inputStream, baseFormat);
clip.open(inputStream); // ^ Clip can only run 16 bit and lower, thus downsampling necessary.
if (baseFormat.getSampleSizeInBits() > 16)
inputStream = downSampleAudio(inputStream, baseFormat);
clip.open(
inputStream); // ^ Clip can only run 16 bit and lower, thus downsampling necessary.
return clip;
}
// Generates a new audio stream from byte array
private AudioInputStream getAudioStream() throws UnsupportedAudioFileException, IOException {
// Check if raw data is loaded into memory
if(!this.isLoaded()){
if (!this.isLoaded()) {
this.load();
}
@@ -39,8 +41,10 @@ public class SoundEffectAsset extends BaseResource implements LoadableResource {
return AudioSystem.getAudioInputStream(new ByteArrayInputStream(this.rawData));
}
private AudioInputStream downSampleAudio(AudioInputStream audioInputStream, AudioFormat baseFormat) {
AudioFormat decodedFormat = new AudioFormat(
private AudioInputStream downSampleAudio(
AudioInputStream audioInputStream, AudioFormat baseFormat) {
AudioFormat decodedFormat =
new AudioFormat(
AudioFormat.Encoding.PCM_SIGNED,
baseFormat.getSampleRate(),
16, // force 16-bit

View File

@@ -1,12 +1,11 @@
package org.toop.framework.asset.resources;
import org.toop.framework.asset.types.FileExtension;
import org.toop.framework.asset.types.LoadableResource;
package org.toop.framework.resource.resources;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import org.toop.framework.resource.types.FileExtension;
import org.toop.framework.resource.types.LoadableResource;
@FileExtension({"txt", "json", "xml"})
public class TextAsset extends BaseResource implements LoadableResource {

View File

@@ -0,0 +1,77 @@
package org.toop.framework.resource.types;
import java.io.File;
import org.toop.framework.resource.ResourceLoader;
/**
* Represents a resource that can be composed of multiple files, or "bundled" together under a
* common base name.
*
* <p>Implementing classes allow an {@link ResourceLoader} to automatically merge multiple related
* files into a single resource instance.
*
* <p>Typical use cases include:
*
* <ul>
* <li>Localization assets, where multiple `.properties` files (e.g., `messages_en.properties`,
* `messages_nl.properties`) are grouped under the same logical resource.
* <li>Sprite sheets, tile sets, or other multi-file resources that logically belong together.
* </ul>
*
* <p>Implementing classes must provide:
*
* <ul>
* <li>{@link #loadFile(File)}: Logic to load or merge an individual file into the resource.
* <li>{@link #getBaseName()}: A consistent base name used to group multiple files into this
* resource.
* </ul>
*
* <p>Example usage:
*
* <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.
*/
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();
// /**
// Returns the name
// */
// String getDefaultName();
}

View File

@@ -0,0 +1,44 @@
package org.toop.framework.resource.types;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.toop.framework.resource.ResourceLoader;
import org.toop.framework.resource.resources.BaseResource;
/**
* Annotation to declare which file extensions a {@link BaseResource} subclass can handle.
*
* <p>This annotation is processed by the {@link ResourceLoader} 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>Usage example:
*
* <pre>{@code
* @FileExtension({"png", "jpg"})
* public class ImageAsset extends BaseResource implements LoadableResource {
* ...
* }
* }</pre>
*
* <p>Key points:
*
* <ul>
* <li>The annotation is retained at runtime for reflection-based registration.
* <li>Can only be applied to types (classes) that extend {@link BaseResource}.
* <li>Multiple extensions can be specified in the {@code value()} array.
* </ul>
*/
@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();
}

View File

@@ -0,0 +1,69 @@
package org.toop.framework.resource.types;
import org.toop.framework.resource.ResourceLoader;
/**
* 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>Implementing classes must define the following behaviors:
*
* <ul>
* <li>{@link #load()}: Load the resource into memory or perform necessary initialization.
* <li>{@link #unload()}: Release any held resources or memory when the resource is no longer
* needed.
* <li>{@link #isLoaded()}: Return {@code true} if the resource has been successfully loaded and
* is ready for use, {@code false} otherwise.
* </ul>
*
* <p>Typical usage:
*
* <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 ResourceLoader} 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();
}

View File

@@ -1,19 +1,21 @@
package org.toop.framework.asset.types;
package org.toop.framework.resource.types;
import org.toop.framework.asset.ResourceLoader;
import org.toop.framework.resource.ResourceLoader;
/**
* Marker interface for resources that should be **automatically loaded** by the {@link ResourceLoader}.
* Marker interface for resources that should be **automatically loaded** by the {@link
* ResourceLoader}.
*
* <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>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>When a resource implements {@code PreloadResource}, the {@code ResourceLoader} will invoke
* {@link LoadableResource#load()} automatically after the resource is discovered and instantiated,
* without requiring manual loading by the user.</p>
* without requiring manual loading by the user.
*
* <p>Typical usage:
*
* <p>Typical usage:</p>
* <pre>{@code
* public class MyFontAsset extends BaseResource implements PreloadResource {
* @Override
@@ -34,6 +36,6 @@ import org.toop.framework.asset.ResourceLoader;
* }</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>
* increase memory usage or startup time.
*/
public interface PreloadResource extends LoadableResource {}

View File

@@ -35,10 +35,10 @@ class NetworkEventsTest {
@Test
void testChallengeResponse() {
NetworkEvents.ChallengeResponse event =
new NetworkEvents.ChallengeResponse(1L, "Alice", "Chess", "ch001");
assertEquals("Alice", event.challengerName());
assertEquals("Chess", event.gameType());
assertEquals("ch001", event.challengeId());
new NetworkEvents.ChallengeResponse(1L, "John", "1", "tic-tac-toe");
assertEquals("John", event.challengerName());
assertEquals("1", event.challengeId());
assertEquals("tic-tac-toe", event.gameType());
}
@Test

View File

@@ -4,12 +4,14 @@ import java.util.Arrays;
public abstract class Game {
public enum State {
NORMAL, DRAW, WIN,
NORMAL,
DRAW,
WIN,
}
public record Move(int position, char value) {}
public static final char EMPTY = (char)0;
public static final char EMPTY = (char) 0;
public final int rowSize;
public final int columnSize;
@@ -32,5 +34,6 @@ public abstract class Game {
}
public abstract Move[] getLegalMoves();
public abstract State play(Move move);
}

View File

@@ -21,5 +21,7 @@ public abstract class TurnBasedGame extends Game {
currentTurn = (currentTurn + 1) % turns;
}
public int getCurrentTurn() { return currentTurn; }
public int getCurrentTurn() {
return currentTurn;
}
}

View File

@@ -1,8 +1,7 @@
package org.toop.game.tictactoe;
import org.toop.game.TurnBasedGame;
import java.util.ArrayList;
import org.toop.game.TurnBasedGame;
public final class TicTacToe extends TurnBasedGame {
private int movesLeft;
@@ -60,7 +59,9 @@ public final class TicTacToe extends TurnBasedGame {
for (int i = 0; i < 3; i++) {
final int index = i * 3;
if (board[index] != EMPTY && board[index] == board[index + 1] && board[index] == board[index + 2]) {
if (board[index] != EMPTY
&& board[index] == board[index + 1]
&& board[index] == board[index + 2]) {
return true;
}
}
@@ -94,6 +95,6 @@ public final class TicTacToe extends TurnBasedGame {
}
private char getCurrentValue() {
return currentTurn == 0? 'X' : 'O';
return currentTurn == 0 ? 'X' : 'O';
}
}

View File

@@ -1,13 +1,11 @@
package org.toop.game.tictactoe;
import org.toop.game.Game;
import static org.junit.jupiter.api.Assertions.*;
import java.util.Set;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import org.toop.game.Game;
class TicTacToeAITest {
private TicTacToe game;