Merge remote-tracking branch 'refs/remotes/origin/Development' into UI

# Conflicts:
#	app/src/main/java/org/toop/app/App.java
#	framework/src/main/java/org/toop/framework/asset/AssetLoader.java
#	framework/src/main/java/org/toop/framework/asset/AssetManager.java
This commit is contained in:
lieght
2025-10-02 19:22:39 +02:00
20 changed files with 165 additions and 36 deletions

View File

@@ -10,10 +10,11 @@ import javafx.scene.Scene;
import javafx.stage.Stage; import javafx.stage.Stage;
import org.toop.framework.asset.AssetManager; import org.toop.framework.asset.AssetManager;
import org.toop.framework.asset.resources.LocalizationAsset; import org.toop.framework.asset.resources.LocalizationAsset;
import org.toop.framework.audio.events.AudioEvents;
import org.toop.framework.eventbus.EventFlow;
import org.toop.local.AppContext; import org.toop.local.AppContext;
import java.util.Locale; import java.util.Locale;
import java.util.ResourceBundle;
public class App extends Application { public class App extends Application {
private static Stage stage; private static Stage stage;
@@ -49,6 +50,9 @@ public class App extends Application {
App.stage = stage; App.stage = stage;
App.scene = scene; App.scene = scene;
App.root = root; App.root = root;
new EventFlow().addPostEvent(new AudioEvents.StartBackgroundMusic()).postEvent();
new EventFlow().addPostEvent(new AudioEvents.ChangeVolume(0.3)).postEvent();
} }
public static void activate(Menu menu) { public static void activate(Menu menu) {

Binary file not shown.

Binary file not shown.

View File

@@ -102,6 +102,14 @@
<version>25</version> <version>25</version>
</dependency> </dependency>
<!-- JavaFX Media -->
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-media</artifactId>
<version>25</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.reflections/reflections --> <!-- https://mvnrepository.com/artifact/org.reflections/reflections -->
<dependency> <dependency>
<groupId>org.reflections</groupId> <groupId>org.reflections</groupId>
@@ -122,24 +130,6 @@
<target>25</target> <target>25</target>
<release>25</release> <release>25</release>
<encoding>UTF-8</encoding> <encoding>UTF-8</encoding>
<!-- <compilerArgs>-->
<!-- <arg>-XDcompilePolicy=simple</arg>-->
<!-- <arg>&#45;&#45;should-stop=ifError=FLOW</arg>-->
<!-- <arg>-Xplugin:ErrorProne</arg>-->
<!-- </compilerArgs>-->
<!-- <annotationProcessorPaths>-->
<!-- <path>-->
<!-- <groupId>com.google.errorprone</groupId>-->
<!-- <artifactId>error_prone_core</artifactId>-->
<!-- <version>2.42.0</version>-->
<!-- </path>-->
<!-- &lt;!&ndash; Other annotation processors go here.-->
<!-- If 'annotationProcessorPaths' is set, processors will no longer be-->
<!-- discovered on the regular -classpath; see also 'Using Error Prone-->
<!-- together with other annotation processors' below. &ndash;&gt;-->
<!-- </annotationProcessorPaths>-->
<!-- <fork>true</fork>-->
</configuration> </configuration>
</plugin> </plugin>
<plugin> <plugin>

View File

@@ -60,7 +60,23 @@ public class AssetLoader {
List<File> foundFiles = new ArrayList<>(); List<File> foundFiles = new ArrayList<>();
fileSearcher(rootFolder, foundFiles); fileSearcher(rootFolder, foundFiles);
this.totalCount = foundFiles.size(); this.totalCount = foundFiles.size();
// measure memory before loading
long before = getUsedMemory();
loader(foundFiles); loader(foundFiles);
// ~measure memory after loading
long after = getUsedMemory();
long used = after - before;
logger.info("Total files loaded: {}", this.totalCount);
logger.info("Memory used by assets: ~{} MB", used / (1024 * 1024));
}
private static long getUsedMemory() {
Runtime runtime = Runtime.getRuntime();
return runtime.totalMemory() - runtime.freeMemory();
} }
/** /**
@@ -128,6 +144,7 @@ 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);
} }

View File

@@ -0,0 +1,38 @@
package org.toop.framework.asset.resources;
import javafx.scene.media.Media;
import java.io.*;
@FileExtension({"mp3"})
public class MusicAsset extends BaseResource implements LoadableResource {
private Media media;
public MusicAsset(final File audioFile) {
super(audioFile);
}
public Media getMedia() {
if (media == null) {
media = new Media(file.toURI().toString());
}
return media;
}
@Override
public void load() {
if (media == null) media = new Media(file.toURI().toString());
this.isLoaded = true;
}
@Override
public void unload() {
media = null;
isLoaded = false;
}
@Override
public boolean isLoaded() {
return isLoaded;
}
}

View File

@@ -1,12 +1,15 @@
package org.toop.framework.asset.resources; package org.toop.framework.asset.resources;
import javafx.scene.media.Media;
import javax.sound.sampled.*; import javax.sound.sampled.*;
import java.io.*; import java.io.*;
import java.net.URI;
@FileExtension({"wav"}) @FileExtension({"wav"})
public class AudioAsset extends BaseResource implements LoadableResource { public class SoundEffectAsset extends BaseResource implements LoadableResource {
public AudioAsset(final File audioFile) { public SoundEffectAsset(final File audioFile) {
super(audioFile); super(audioFile);
} }

View File

@@ -1,9 +1,12 @@
package org.toop.framework.audio; package org.toop.framework.audio;
import javafx.application.Platform;
import javafx.scene.media.MediaPlayer;
import org.toop.framework.SnowflakeGenerator; import org.toop.framework.SnowflakeGenerator;
import org.toop.framework.asset.Asset; import org.toop.framework.asset.Asset;
import org.toop.framework.asset.AssetManager; import org.toop.framework.asset.AssetManager;
import org.toop.framework.asset.resources.AudioAsset; 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.audio.events.AudioEvents;
import org.toop.framework.eventbus.EventFlow; import org.toop.framework.eventbus.EventFlow;
@@ -12,13 +15,17 @@ import java.util.*;
import javax.sound.sampled.*; import javax.sound.sampled.*;
public class SoundManager { public class SoundManager {
private final Map<Long, Clip> activeClips = new HashMap<>(); private final List<MediaPlayer> activeMusic = new ArrayList<>();
private final HashMap<String, AudioAsset> audioResources = new HashMap<>(); 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 SnowflakeGenerator idGenerator = new SnowflakeGenerator(); // TODO: Don't create a new generator
private double volume = 1.0;
public SoundManager() { public SoundManager() {
// Get all Audio Resources and add them to a list. // Get all Audio Resources and add them to a list.
for (Asset<AudioAsset> asset : AssetManager.getAllOfType(AudioAsset.class)) { for (Asset<SoundEffectAsset> asset : AssetManager.getAllOfType(SoundEffectAsset.class)) {
try { try {
this.addAudioResource(asset); this.addAudioResource(asset);
} catch (IOException | LineUnavailableException | UnsupportedAudioFileException e) { } catch (IOException | LineUnavailableException | UnsupportedAudioFileException e) {
@@ -27,12 +34,21 @@ public class SoundManager {
} }
new EventFlow() new EventFlow()
.listen(this::handlePlaySound) .listen(this::handlePlaySound)
.listen(this::handleStopSound); .listen(this::handleStopSound)
.listen(this::handleMusicStart)
.listen(this::handleVolumeChange)
.listen(AudioEvents.playOnClickButton.class, _ -> {
try {
playSound("hitsound0.wav", false);
} catch (UnsupportedAudioFileException | LineUnavailableException | IOException e) {
throw new RuntimeException(e);
}
});
} }
private void handlePlaySound(AudioEvents.PlayAudio event) { private void handlePlaySound(AudioEvents.PlayAudio event) {
try { try {
this.playSound(event.fileNameNoExtensionAndNoDirectory(), event.loop()); this.playSound(event.fileName(), event.loop());
} catch (UnsupportedAudioFileException | LineUnavailableException | IOException e) { } catch (UnsupportedAudioFileException | LineUnavailableException | IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
@@ -42,14 +58,70 @@ public class SoundManager {
this.stopSound(event.clipId()); this.stopSound(event.clipId());
} }
private void addAudioResource(Asset<AudioAsset> audioAsset) private void addAudioResource(Asset<SoundEffectAsset> audioAsset)
throws IOException, UnsupportedAudioFileException, LineUnavailableException { throws IOException, UnsupportedAudioFileException, LineUnavailableException {
this.audioResources.put(audioAsset.getName(), audioAsset.getResource()); this.audioResources.put(audioAsset.getName(), audioAsset.getResource());
} }
private void handleVolumeChange(AudioEvents.ChangeVolume event) {
if (event.newVolume() > 1.0) this.volume = 1.0;
else this.volume = Math.max(event.newVolume(), 0.0);
for (MediaPlayer mediaPlayer : this.activeMusic) {
mediaPlayer.setVolume(this.volume);
}
}
private void handleMusicStart(AudioEvents.StartBackgroundMusic e) {
backgroundMusicQueue.clear();
Platform.runLater(() -> {
backgroundMusicQueue.addAll(
AssetManager.getAllOfType(MusicAsset.class).stream()
.map(Asset::getResource)
.toList()
);
backgroundMusicPlayer();
});
}
private void addBackgroundMusic(MusicAsset musicAsset) {
backgroundMusicQueue.add(musicAsset);
}
private void backgroundMusicPlayer() {
MusicAsset ma = backgroundMusicQueue.poll();
if (ma == null) return;
MediaPlayer mediaPlayer = new MediaPlayer(ma.getMedia());
mediaPlayer.setOnEndOfMedia(() -> {
addBackgroundMusic(ma);
activeMusic.remove(mediaPlayer);
mediaPlayer.dispose();
ma.unload();
backgroundMusicPlayer(); // play next
});
mediaPlayer.setOnStopped(() -> {
addBackgroundMusic(ma);
activeMusic.remove(mediaPlayer);
ma.unload();
});
mediaPlayer.setOnError(() -> {
addBackgroundMusic(ma);
activeMusic.remove(mediaPlayer);
ma.unload();
});
mediaPlayer.setVolume(this.volume);
mediaPlayer.play();
activeMusic.add(mediaPlayer);
}
private long playSound(String audioFileName, boolean loop) throws UnsupportedAudioFileException, LineUnavailableException, IOException { private long playSound(String audioFileName, boolean loop) throws UnsupportedAudioFileException, LineUnavailableException, IOException {
AudioAsset asset = audioResources.get(audioFileName); SoundEffectAsset asset = audioResources.get(audioFileName);
// Return -1 which indicates resource wasn't available // Return -1 which indicates resource wasn't available
if (asset == null){ if (asset == null){
@@ -71,12 +143,12 @@ public class SoundManager {
long clipId = idGenerator.nextId(); long clipId = idGenerator.nextId();
// store it so we can stop it later // store it so we can stop it later
activeClips.put(clipId, clip); // TODO: Do on snowflake for specific sound to stop activeSoundEffects.put(clipId, clip); // TODO: Do on snowflake for specific sound to stop
// remove when finished (only for non-looping sounds) // remove when finished (only for non-looping sounds)
clip.addLineListener(event -> { clip.addLineListener(event -> {
if (event.getType() == LineEvent.Type.STOP && !clip.isRunning()) { if (event.getType() == LineEvent.Type.STOP && !clip.isRunning()) {
activeClips.remove(clipId); activeSoundEffects.remove(clipId);
clip.close(); clip.close();
} }
}); });
@@ -86,7 +158,7 @@ public class SoundManager {
} }
public void stopSound(long clipId) { public void stopSound(long clipId) {
Clip clip = activeClips.get(clipId); Clip clip = activeSoundEffects.get(clipId);
if (clip == null) { if (clip == null) {
return; return;
@@ -94,14 +166,14 @@ public class SoundManager {
clip.stop(); clip.stop();
clip.close(); clip.close();
activeClips.remove(clipId); activeSoundEffects.remove(clipId);
} }
public void stopAllSounds() { public void stopAllSounds() {
for (Clip clip : activeClips.values()) { for (Clip clip : activeSoundEffects.values()) {
clip.stop(); clip.stop();
clip.close(); clip.close();
} }
activeClips.clear(); activeSoundEffects.clear();
} }
} }

View File

@@ -1,12 +1,17 @@
package org.toop.framework.audio.events; package org.toop.framework.audio.events;
import org.toop.framework.asset.resources.MusicAsset;
import org.toop.framework.eventbus.events.EventWithoutSnowflake; import org.toop.framework.eventbus.events.EventWithoutSnowflake;
import org.toop.framework.eventbus.events.EventsBase; import org.toop.framework.eventbus.events.EventsBase;
public class AudioEvents extends EventsBase { public class AudioEvents extends EventsBase {
/** Starts playing a sound. */ /** Starts playing a sound. */
public record PlayAudio(String fileNameNoExtensionAndNoDirectory, boolean loop) public record PlayAudio(String fileName, boolean loop)
implements EventWithoutSnowflake {} implements EventWithoutSnowflake {}
public record StopAudio(long clipId) implements EventWithoutSnowflake {} public record StopAudio(long clipId) implements EventWithoutSnowflake {}
public record StartBackgroundMusic() implements EventWithoutSnowflake {}
public record ChangeVolume(double newVolume) implements EventWithoutSnowflake {}
public record playOnClickButton() implements EventWithoutSnowflake {}
} }