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