diff --git a/pom.xml b/pom.xml
index f547023..a815d6b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -139,5 +139,17 @@
lanterna
3.1.3
+
+
+ org.bytedeco
+ javacv-platform
+ 1.5.10
+
+
+
+ org.bytedeco
+ ffmpeg-platform
+ 6.1.1-1.5.10
+
\ No newline at end of file
diff --git a/src/main/java/cz/jzitnik/Game.java b/src/main/java/cz/jzitnik/Game.java
index 8c236f5..cb4934f 100644
--- a/src/main/java/cz/jzitnik/Game.java
+++ b/src/main/java/cz/jzitnik/Game.java
@@ -1,6 +1,6 @@
package cz.jzitnik;
-import cz.jzitnik.game.GameSetup;
+import cz.jzitnik.game.setup.GameSetup;
import cz.jzitnik.utils.DependencyManager;
import org.reflections.Reflections;
diff --git a/src/main/java/cz/jzitnik/events/PlayVideo.java b/src/main/java/cz/jzitnik/events/PlayVideo.java
new file mode 100644
index 0000000..dc2f5c1
--- /dev/null
+++ b/src/main/java/cz/jzitnik/events/PlayVideo.java
@@ -0,0 +1,11 @@
+package cz.jzitnik.events;
+
+import cz.jzitnik.utils.events.Event;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public class PlayVideo implements Event {
+ private String fileName;
+}
diff --git a/src/main/java/cz/jzitnik/events/handlers/PlayVideoHandler.java b/src/main/java/cz/jzitnik/events/handlers/PlayVideoHandler.java
new file mode 100644
index 0000000..da2c168
--- /dev/null
+++ b/src/main/java/cz/jzitnik/events/handlers/PlayVideoHandler.java
@@ -0,0 +1,48 @@
+package cz.jzitnik.events.handlers;
+
+import com.googlecode.lanterna.TextColor;
+import com.googlecode.lanterna.screen.Screen;
+import com.googlecode.lanterna.screen.TerminalScreen;
+import cz.jzitnik.annotations.EventHandler;
+import cz.jzitnik.annotations.injectors.InjectDependency;
+import cz.jzitnik.annotations.injectors.InjectState;
+import cz.jzitnik.events.PlayVideo;
+import cz.jzitnik.game.ResourceManager;
+import cz.jzitnik.states.TerminalState;
+import cz.jzitnik.utils.DependencyManager;
+import cz.jzitnik.utils.events.AbstractEventHandler;
+import org.bytedeco.javacv.FFmpegFrameGrabber;
+import org.bytedeco.javacv.Frame;
+import org.bytedeco.javacv.Java2DFrameConverter;
+
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+
+@EventHandler(PlayVideo.class)
+public class PlayVideoHandler extends AbstractEventHandler {
+ public PlayVideoHandler(DependencyManager dm) {
+ super(dm);
+ }
+
+ @InjectDependency
+ private ResourceManager resourceManager;
+
+ @InjectState
+ private TerminalState terminalState;
+
+ @Override
+ public void handle(PlayVideo event) {
+ }
+
+
+ private static TextColor.RGB toColor(int rgb) {
+ int r = (rgb >> 16) & 0xFF;
+ int g = (rgb >> 8) & 0xFF;
+ int b = rgb & 0xFF;
+ return new TextColor.RGB(r, g, b);
+ }
+}
diff --git a/src/main/java/cz/jzitnik/events/handlers/TerminalResizeEventHandler.java b/src/main/java/cz/jzitnik/events/handlers/TerminalResizeEventHandler.java
index 0163060..c876b3f 100644
--- a/src/main/java/cz/jzitnik/events/handlers/TerminalResizeEventHandler.java
+++ b/src/main/java/cz/jzitnik/events/handlers/TerminalResizeEventHandler.java
@@ -5,6 +5,7 @@ import cz.jzitnik.annotations.EventHandler;
import cz.jzitnik.annotations.injectors.InjectDependency;
import cz.jzitnik.annotations.injectors.InjectState;
import cz.jzitnik.events.FullRoomDraw;
+import cz.jzitnik.events.PlayVideo;
import cz.jzitnik.events.TerminalResizeEvent;
import cz.jzitnik.game.GameState;
import cz.jzitnik.states.ScreenBuffer;
diff --git a/src/main/java/cz/jzitnik/game/GameState.java b/src/main/java/cz/jzitnik/game/GameState.java
index a28f231..d2577f6 100644
--- a/src/main/java/cz/jzitnik/game/GameState.java
+++ b/src/main/java/cz/jzitnik/game/GameState.java
@@ -3,16 +3,34 @@ package cz.jzitnik.game;
import cz.jzitnik.annotations.State;
import cz.jzitnik.game.objects.Interactable;
import cz.jzitnik.screens.Screen;
+import cz.jzitnik.utils.DependencyManager;
import lombok.Getter;
+import lombok.RequiredArgsConstructor;
import lombok.Setter;
+@RequiredArgsConstructor
@State
-@Getter
-@Setter
public class GameState {
+ private final DependencyManager dependencyManager;
+
+ @Getter
+ @Setter
private GameRoom currentRoom;
+ @Getter
+ @Setter
private Player player;
+ @Getter
+ @Setter
private Interactable interacting;
+
+ @Getter
private Screen screen;
+
+ public void setScreen(Screen screen) {
+ if (screen != null) {
+ dependencyManager.inject(screen);
+ }
+ this.screen = screen;
+ }
}
diff --git a/src/main/java/cz/jzitnik/game/ResourceManager.java b/src/main/java/cz/jzitnik/game/ResourceManager.java
index 8fbaf6b..2507bce 100644
--- a/src/main/java/cz/jzitnik/game/ResourceManager.java
+++ b/src/main/java/cz/jzitnik/game/ResourceManager.java
@@ -59,4 +59,8 @@ public class ResourceManager {
throw new RuntimeException(e);
}
}
+
+ public InputStream getResourceAsStream(String path) {
+ return classLoader.getResourceAsStream(path);
+ }
}
diff --git a/src/main/java/cz/jzitnik/game/GameSetup.java b/src/main/java/cz/jzitnik/game/setup/GameSetup.java
similarity index 82%
rename from src/main/java/cz/jzitnik/game/GameSetup.java
rename to src/main/java/cz/jzitnik/game/setup/GameSetup.java
index fdb4f6c..9c75690 100644
--- a/src/main/java/cz/jzitnik/game/GameSetup.java
+++ b/src/main/java/cz/jzitnik/game/setup/GameSetup.java
@@ -1,12 +1,17 @@
-package cz.jzitnik.game;
+package cz.jzitnik.game.setup;
import cz.jzitnik.annotations.Dependency;
import cz.jzitnik.annotations.injectors.InjectDependency;
import cz.jzitnik.annotations.injectors.InjectState;
+import cz.jzitnik.game.GameRoom;
+import cz.jzitnik.game.GameState;
+import cz.jzitnik.game.Player;
+import cz.jzitnik.game.ResourceManager;
import cz.jzitnik.game.items.GameItem;
import cz.jzitnik.game.items.WoodenSword;
import cz.jzitnik.game.objects.Chest;
import cz.jzitnik.game.utils.RoomCords;
+import cz.jzitnik.screens.scenes.IntroScene;
import cz.jzitnik.utils.DependencyManager;
@Dependency
@@ -21,6 +26,8 @@ public class GameSetup {
private DependencyManager dependencyManager;
public void setup() {
+ gameState.setScreen(new IntroScene(dependencyManager));
+
GameRoom mainRoom = new GameRoom(ResourceManager.Resource.ROOM1);
GameRoom rightRoom = new GameRoom(ResourceManager.Resource.ROOM2);
GameRoom topRightRoom = new GameRoom(ResourceManager.Resource.ROOM3);
diff --git a/src/main/java/cz/jzitnik/screens/VideoPlayScreen.java b/src/main/java/cz/jzitnik/screens/VideoPlayScreen.java
new file mode 100644
index 0000000..fe83b09
--- /dev/null
+++ b/src/main/java/cz/jzitnik/screens/VideoPlayScreen.java
@@ -0,0 +1,152 @@
+package cz.jzitnik.screens;
+
+import com.googlecode.lanterna.TextColor;
+import com.googlecode.lanterna.screen.TerminalScreen;
+import cz.jzitnik.annotations.injectors.InjectDependency;
+import cz.jzitnik.annotations.injectors.InjectState;
+import cz.jzitnik.game.ResourceManager;
+import cz.jzitnik.states.TerminalState;
+import lombok.extern.slf4j.Slf4j;
+import org.bytedeco.javacv.FFmpegFrameGrabber;
+import org.bytedeco.javacv.Frame;
+import org.bytedeco.javacv.Java2DFrameConverter;
+import com.googlecode.lanterna.screen.Screen.RefreshType;
+
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+
+@Slf4j
+public abstract class VideoPlayScreen extends Screen {
+ @InjectDependency
+ private ResourceManager resourceManager;
+
+ @InjectState
+ private TerminalState terminalState;
+
+ private final String videoPath;
+ protected boolean isRenderedAlready;
+
+ public VideoPlayScreen(String videoPath) {
+ this.videoPath = videoPath;
+ }
+
+ @Override
+ public void fullRender() {
+ if (!isRenderedAlready) {
+ isRenderedAlready = true;
+ render();
+ }
+ }
+
+ protected void render() {
+ File tempVideo = null;
+ try (InputStream resource = resourceManager.getResourceAsStream(videoPath)) {
+
+ TerminalScreen screen = terminalState.getTerminalScreen();
+ var tg = terminalState.getTextGraphics();
+
+ tempVideo = File.createTempFile("lanterna-video", ".mp4");
+ tempVideo.deleteOnExit();
+ Files.copy(resource, tempVideo.toPath(), StandardCopyOption.REPLACE_EXISTING);
+
+ try (FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(tempVideo);
+ Java2DFrameConverter converter = new Java2DFrameConverter()) {
+
+ grabber.start();
+
+ double fps = grabber.getFrameRate();
+ long frameDelayNs = (long) (1_000_000_000 / fps);
+
+ long videoStartTime = System.nanoTime();
+ long frameIndex = 0;
+
+ // Track previous terminal size
+ int prevTermWidth = screen.getTerminalSize().getColumns();
+ int prevTermHeight = screen.getTerminalSize().getRows();
+
+ Frame frame;
+ while ((frame = grabber.grabImage()) != null) {
+
+ long now = System.nanoTime();
+ long expectedTime = videoStartTime + frameIndex * frameDelayNs;
+
+ // Skip frames if we're behind schedule
+ while (now > expectedTime) {
+ frame = grabber.grabImage();
+ if (frame == null) break;
+ frameIndex++;
+ expectedTime = videoStartTime + frameIndex * frameDelayNs;
+ now = System.nanoTime();
+ }
+
+ if (frame == null) break;
+
+ BufferedImage img = converter.convert(frame);
+ if (img == null) continue;
+
+ int termWidth = screen.getTerminalSize().getColumns();
+ int termHeight = screen.getTerminalSize().getRows();
+ int targetWidth = termWidth;
+ int targetHeight = termHeight * 2;
+
+ BufferedImage scaledImg = img;
+ if (img.getWidth() != targetWidth || img.getHeight() != targetHeight) {
+ scaledImg = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB);
+ scaledImg.getGraphics().drawImage(img, 0, 0, targetWidth, targetHeight, null);
+ }
+
+ for (int y = 0; y < termHeight; y++) {
+ for (int x = 0; x < termWidth; x++) {
+ int topPixel = scaledImg.getRGB(x, y * 2);
+ int bottomPixel = scaledImg.getRGB(x, y * 2 + 1);
+
+ TextColor.RGB bg = toColor(topPixel);
+ TextColor.RGB fg = toColor(bottomPixel);
+
+ tg.setBackgroundColor(bg);
+ tg.setForegroundColor(fg);
+ tg.setCharacter(x, y, '▄');
+ }
+ }
+
+ RefreshType refreshType;
+ if (termWidth != prevTermWidth || termHeight != prevTermHeight) {
+ refreshType = RefreshType.COMPLETE;
+ prevTermWidth = termWidth;
+ prevTermHeight = termHeight;
+ } else {
+ refreshType = RefreshType.DELTA;
+ }
+
+ screen.refresh(refreshType);
+
+ frameIndex++;
+ long nextFrameTime = videoStartTime + frameIndex * frameDelayNs;
+ long sleepTime = nextFrameTime - System.nanoTime();
+ if (sleepTime > 0) {
+ Thread.sleep(sleepTime / 1_000_000, (int) (sleepTime % 1_000_000));
+ }
+ }
+
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to play video", e);
+ } finally {
+ if (tempVideo != null && tempVideo.exists()) {
+ if (!tempVideo.delete()) {
+ System.err.println("Warning: failed to delete temp video file " + tempVideo.getAbsolutePath());
+ }
+ }
+ }
+ }
+
+ private TextColor.RGB toColor(int rgb) {
+ int r = (rgb >> 16) & 0xFF;
+ int g = (rgb >> 8) & 0xFF;
+ int b = rgb & 0xFF;
+ return new TextColor.RGB(r, g, b);
+ }
+}
diff --git a/src/main/java/cz/jzitnik/screens/scenes/BasicVideoScene.java b/src/main/java/cz/jzitnik/screens/scenes/BasicVideoScene.java
new file mode 100644
index 0000000..2895646
--- /dev/null
+++ b/src/main/java/cz/jzitnik/screens/scenes/BasicVideoScene.java
@@ -0,0 +1,19 @@
+package cz.jzitnik.screens.scenes;
+
+import cz.jzitnik.events.KeyboardPressEvent;
+import cz.jzitnik.events.MouseAction;
+import cz.jzitnik.screens.VideoPlayScreen;
+
+public final class BasicVideoScene extends VideoPlayScreen {
+ public BasicVideoScene(String videoPath) {
+ super(videoPath);
+ }
+
+ @Override
+ public void handleMouseAction(MouseAction event) {
+ }
+
+ @Override
+ public void handleKeyboardAction(KeyboardPressEvent event) {
+ }
+}
diff --git a/src/main/java/cz/jzitnik/screens/scenes/IntroScene.java b/src/main/java/cz/jzitnik/screens/scenes/IntroScene.java
new file mode 100644
index 0000000..ee1c3be
--- /dev/null
+++ b/src/main/java/cz/jzitnik/screens/scenes/IntroScene.java
@@ -0,0 +1,16 @@
+package cz.jzitnik.screens.scenes;
+
+import cz.jzitnik.screens.Screen;
+import cz.jzitnik.utils.DependencyManager;
+
+public class IntroScene extends Scene {
+ public IntroScene(DependencyManager dependencyManager) {
+ super(
+ new Screen[]{
+ new VideoSceneWithAudio("video.mp4", "audio.ogg")
+ },
+ OnEndAction.SWITCH_TO_GAME
+ );
+ dependencyManager.inject(this);
+ }
+}
diff --git a/src/main/java/cz/jzitnik/screens/scenes/Scene.java b/src/main/java/cz/jzitnik/screens/scenes/Scene.java
new file mode 100644
index 0000000..3245e3f
--- /dev/null
+++ b/src/main/java/cz/jzitnik/screens/scenes/Scene.java
@@ -0,0 +1,76 @@
+package cz.jzitnik.screens.scenes;
+
+import cz.jzitnik.annotations.injectors.InjectDependency;
+import cz.jzitnik.annotations.injectors.InjectState;
+import cz.jzitnik.events.FullRoomDraw;
+import cz.jzitnik.events.KeyboardPressEvent;
+import cz.jzitnik.events.MouseAction;
+import cz.jzitnik.game.GameState;
+import cz.jzitnik.screens.Screen;
+import cz.jzitnik.utils.DependencyManager;
+import cz.jzitnik.utils.events.EventManager;
+
+public abstract class Scene extends Screen {
+ private final Screen[] parts;
+ private Screen currentPart;
+ private int currentIndex = 0;
+ private boolean isRenderedAlready = false;
+ private final OnEndAction onEndAction;
+
+ @InjectDependency
+ private DependencyManager dependencyManager;
+
+ @InjectState
+ private GameState gameState;
+
+ @InjectDependency
+ private EventManager eventManager;
+
+ public enum OnEndAction {
+ SWITCH_TO_GAME
+ }
+
+ public Scene(Screen[] parts, OnEndAction onEndAction) {
+ this.parts = parts;
+ this.currentPart = parts[0];
+ this.onEndAction = onEndAction;
+ }
+
+ @Override
+ public void fullRender() {
+ if (!isRenderedAlready) {
+ isRenderedAlready = true;
+ render();
+ }
+ }
+
+ private void render() {
+ while (currentPart != null) {
+ dependencyManager.inject(currentPart);
+ currentPart.fullRender();
+
+ try {
+ currentPart = parts[++currentIndex];
+ } catch (ArrayIndexOutOfBoundsException e) {
+ currentPart = null;
+ }
+ }
+
+ switch (onEndAction) {
+ case SWITCH_TO_GAME -> {
+ gameState.setScreen(null);
+ eventManager.emitEvent(new FullRoomDraw(true));
+ }
+ }
+ }
+
+ @Override
+ public void handleMouseAction(MouseAction event) {
+ currentPart.handleMouseAction(event);
+ }
+
+ @Override
+ public void handleKeyboardAction(KeyboardPressEvent event) {
+ currentPart.handleKeyboardAction(event);
+ }
+}
diff --git a/src/main/java/cz/jzitnik/screens/scenes/VideoSceneWithAudio.java b/src/main/java/cz/jzitnik/screens/scenes/VideoSceneWithAudio.java
new file mode 100644
index 0000000..5aa73fd
--- /dev/null
+++ b/src/main/java/cz/jzitnik/screens/scenes/VideoSceneWithAudio.java
@@ -0,0 +1,42 @@
+package cz.jzitnik.screens.scenes;
+
+import cz.jzitnik.events.KeyboardPressEvent;
+import cz.jzitnik.events.MouseAction;
+import cz.jzitnik.screens.VideoPlayScreen;
+import cz.jzitnik.sound.SoundPlayer;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public final class VideoSceneWithAudio extends VideoPlayScreen {
+ private final SoundPlayer soundPlayer = new SoundPlayer();
+ private final String audioPath;
+
+ public VideoSceneWithAudio(String videoPath, String audioPath) {
+ super(videoPath);
+ this.audioPath = audioPath;
+ }
+
+ @Override
+ public void fullRender() {
+ if (!isRenderedAlready) {
+ isRenderedAlready = true;
+ playSound();
+ render();
+ this.soundPlayer.stopCurrentSound();
+ }
+ }
+
+ private void playSound() {
+ new Thread(() -> soundPlayer.playSound(audioPath, 100, 100)).start();
+ }
+
+ @Override
+ public void handleMouseAction(MouseAction event) {
+
+ }
+
+ @Override
+ public void handleKeyboardAction(KeyboardPressEvent event) {
+
+ }
+}
diff --git a/src/main/java/cz/jzitnik/sound/SoundPlayer.java b/src/main/java/cz/jzitnik/sound/SoundPlayer.java
index 8ee094d..71b481a 100644
--- a/src/main/java/cz/jzitnik/sound/SoundPlayer.java
+++ b/src/main/java/cz/jzitnik/sound/SoundPlayer.java
@@ -1,76 +1,107 @@
package cz.jzitnik.sound;
-import cz.jzitnik.annotations.Dependency;
-import cz.jzitnik.annotations.injectors.InjectDependency;
-import lombok.extern.slf4j.Slf4j;
+import javax.sound.sampled.*;
import javax.sound.sampled.*;
import java.io.IOException;
+import java.util.concurrent.atomic.AtomicReference;
+
+import lombok.extern.slf4j.Slf4j;
@Slf4j
-@Dependency
public class SoundPlayer {
- @InjectDependency
- private ClassLoader classLoader;
+ private final AtomicReference currentLine = new AtomicReference<>();
+
+ public void playSound(String filePath, int backendVolume, int masterVolume) {
+ long threadId = Thread.currentThread().getId();
- public void playSound(String filePath, int backendVolume, int masterVolume)
- throws LineUnavailableException, IOException, UnsupportedAudioFileException {
if (!filePath.endsWith(".ogg") || masterVolume == 0) {
- return; // No sound if master volume is 0
- }
-
- log.info("Loading resource: {}", "sounds/" + filePath);
- var file = classLoader.getResourceAsStream("sounds/" + filePath);
- if (file == null) {
return;
}
- AudioInputStream audioStream = AudioSystem.getAudioInputStream(file);
- AudioFormat baseFormat = audioStream.getFormat();
-
- AudioFormat targetFormat = new AudioFormat(
- AudioFormat.Encoding.PCM_SIGNED,
- baseFormat.getSampleRate(),
- 16,
- baseFormat.getChannels(),
- baseFormat.getChannels() * 2,
- baseFormat.getSampleRate(),
- false
- );
-
- AudioInputStream dataIn = AudioSystem.getAudioInputStream(targetFormat, audioStream);
-
- byte[] buffer = new byte[8192];
- DataLine.Info info = new DataLine.Info(SourceDataLine.class, targetFormat);
-
- try (SourceDataLine line = (SourceDataLine) AudioSystem.getLine(info)) {
- if (line != null) {
- line.open(targetFormat);
- line.start();
-
- float finalVolume = (backendVolume / 100.0f) * (masterVolume / 100.0f);
- log.info("Applying volume: {} (backend: {}, master: {})", finalVolume, backendVolume, masterVolume);
-
- int bytesRead;
- while ((bytesRead = dataIn.read(buffer, 0, buffer.length)) != -1) {
- applyVolume(buffer, bytesRead, finalVolume);
- line.write(buffer, 0, bytesRead);
- }
-
- line.drain();
+ try (var fileStream = getClass().getClassLoader().getResourceAsStream(filePath)) {
+ if (fileStream == null) {
+ return;
}
- }
- dataIn.close();
- audioStream.close();
+ try (AudioInputStream audioStream = AudioSystem.getAudioInputStream(fileStream)) {
+ AudioFormat baseFormat = audioStream.getFormat();
+ AudioFormat targetFormat = new AudioFormat(
+ AudioFormat.Encoding.PCM_SIGNED,
+ baseFormat.getSampleRate(),
+ 16,
+ baseFormat.getChannels(),
+ baseFormat.getChannels() * 2,
+ baseFormat.getSampleRate(),
+ false
+ );
+
+ try (AudioInputStream dataIn = AudioSystem.getAudioInputStream(targetFormat, audioStream)) {
+ DataLine.Info info = new DataLine.Info(SourceDataLine.class, targetFormat);
+
+ SourceDataLine line = (SourceDataLine) AudioSystem.getLine(info);
+ currentLine.set(line);
+
+ if (line != null) {
+ line.open(targetFormat);
+
+ if (line.isControlSupported(FloatControl.Type.MASTER_GAIN)) {
+ FloatControl gainControl = (FloatControl) line.getControl(FloatControl.Type.MASTER_GAIN);
+ float finalVolume = (backendVolume / 100.0f) * (masterVolume / 100.0f);
+ float dB = (float) (Math.log10(Math.max(finalVolume, 0.0001f)) * 20.0f);
+ gainControl.setValue(dB);
+ }
+
+ line.start();
+ log.info("[{}] Line started.", threadId);
+
+ byte[] buffer = new byte[4096];
+ int bytesRead;
+
+ while ((bytesRead = dataIn.read(buffer, 0, buffer.length)) != -1) {
+ if (Thread.currentThread().isInterrupted() || !line.isOpen()) {
+ log.info("[{}] Loop interrupted.", threadId);
+ break;
+ }
+
+ try {
+ line.write(buffer, 0, bytesRead);
+ } catch (Exception e) {
+ break;
+ }
+ }
+
+ // Cleanup (only if not already closed)
+ if (line.isOpen()) {
+ line.drain();
+ line.stop();
+ line.close();
+ }
+ }
+ } catch (LineUnavailableException | IOException e) {
+ throw new RuntimeException(e);
+ }
+ } catch (UnsupportedAudioFileException e) {
+ throw new RuntimeException(e);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ } finally {
+ currentLine.set(null);
+ }
}
- private static void applyVolume(byte[] buffer, int bytesRead, float volume) {
- for (int i = 0; i < bytesRead; i += 2) { // 16-bit PCM samples are 2 bytes each
- int sample = (buffer[i] & 0xFF) | (buffer[i + 1] << 8);
- sample = (int) (sample * volume);
- buffer[i] = (byte) (sample & 0xFF);
- buffer[i + 1] = (byte) ((sample >> 8) & 0xFF);
+ /**
+ * Call this method from your Main Thread / VideoSceneWithAudio
+ * to stop the sound INSTANTLY.
+ */
+ public void stopCurrentSound() {
+ SourceDataLine line = currentLine.get();
+ if (line != null && line.isOpen()) {
+ log.info("Force stopping sound...");
+ line.flush();
+ line.stop();
+ line.close();
}
}
}
diff --git a/src/main/java/cz/jzitnik/states/PlayerConfig.java b/src/main/java/cz/jzitnik/states/PlayerConfig.java
new file mode 100644
index 0000000..b44ceb1
--- /dev/null
+++ b/src/main/java/cz/jzitnik/states/PlayerConfig.java
@@ -0,0 +1,8 @@
+package cz.jzitnik.states;
+
+import cz.jzitnik.annotations.State;
+
+@State
+public class PlayerConfig {
+ private int masterVolume = 100;
+}
diff --git a/src/main/java/cz/jzitnik/utils/DependencyManager.java b/src/main/java/cz/jzitnik/utils/DependencyManager.java
index f52d206..3b23497 100644
--- a/src/main/java/cz/jzitnik/utils/DependencyManager.java
+++ b/src/main/java/cz/jzitnik/utils/DependencyManager.java
@@ -16,8 +16,7 @@ import org.reflections.Reflections;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
-import java.util.Optional;
-import java.util.Set;
+import java.util.*;
@Slf4j
public class DependencyManager {
@@ -105,13 +104,20 @@ public class DependencyManager {
public void inject(Object instance) {
StateManager stateManager = (StateManager) data.get(StateManager.class);
- Class> clazz = instance.getClass();
- for (Field field : clazz.getDeclaredFields()) {
+ List allFields = new ArrayList<>();
+ Class> current = instance.getClass();
+
+ while (current != null && current != Object.class) {
+ allFields.addAll(Arrays.asList(current.getDeclaredFields()));
+ current = current.getSuperclass();
+ }
+
+ for (Field field : allFields) {
if (field.isAnnotationPresent(InjectDependency.class)) {
field.setAccessible(true);
- if (!data.containsKey(field.getType())) continue;
+ if (!data.containsKey(field.getType()) && field.getType() != getClass()) continue;
Object dependency = field.getType() == getClass() ? this : data.get(field.getType());
diff --git a/src/main/java/cz/jzitnik/utils/StateManager.java b/src/main/java/cz/jzitnik/utils/StateManager.java
index a63be43..72aa72b 100644
--- a/src/main/java/cz/jzitnik/utils/StateManager.java
+++ b/src/main/java/cz/jzitnik/utils/StateManager.java
@@ -3,6 +3,7 @@ package cz.jzitnik.utils;
import cz.jzitnik.annotations.Dependency;
import cz.jzitnik.annotations.State;
+import cz.jzitnik.annotations.injectors.InjectDependency;
import lombok.extern.slf4j.Slf4j;
import org.reflections.Reflections;
@@ -15,15 +16,22 @@ import java.util.Optional;
public class StateManager {
private final HashMap, Object> data = new HashMap<>();
- public StateManager(Reflections reflections) {
+ public StateManager(Reflections reflections, DependencyManager dependencyManager) {
var classes = reflections.getTypesAnnotatedWith(State.class);
for (Class> clazz : classes) {
try {
var instance = clazz.getDeclaredConstructor().newInstance();
data.put(clazz, instance);
- } catch (InstantiationException | IllegalAccessException | InvocationTargetException |
- NoSuchMethodException e) {
+ } catch (NoSuchMethodException e) {
+ try {
+ var instance = clazz.getDeclaredConstructor(DependencyManager.class).newInstance(dependencyManager);
+ data.put(clazz, instance);
+ } catch (NoSuchMethodException | InvocationTargetException | InstantiationException |
+ IllegalAccessException ex) {
+ throw new RuntimeException(ex);
+ }
+ } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
log.error("Failed to instantiate state class: {}", clazz.getName(), e);
}
}
diff --git a/src/main/resources/audio.ogg b/src/main/resources/audio.ogg
new file mode 100644
index 0000000..dd2c03f
Binary files /dev/null and b/src/main/resources/audio.ogg differ
diff --git a/src/main/resources/video.mp4 b/src/main/resources/video.mp4
new file mode 100644
index 0000000..31bb0fa
Binary files /dev/null and b/src/main/resources/video.mp4 differ