diff --git a/src/main/java/cz/jzitnik/Cli.java b/src/main/java/cz/jzitnik/Cli.java index 121b049..f2ce5d3 100644 --- a/src/main/java/cz/jzitnik/Cli.java +++ b/src/main/java/cz/jzitnik/Cli.java @@ -8,11 +8,16 @@ import cz.jzitnik.annotations.Dependency; import cz.jzitnik.annotations.injectors.InjectDependency; import cz.jzitnik.events.KeyboardPressEvent; import cz.jzitnik.events.MouseAction; +import cz.jzitnik.events.RerenderScreen; +import cz.jzitnik.events.TerminalResizeEvent; import cz.jzitnik.states.RunningState; import cz.jzitnik.utils.StateManager; import cz.jzitnik.utils.events.EventManager; -import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import java.io.IOException; + +@Slf4j @Dependency public class Cli implements Runnable { @InjectDependency @@ -21,33 +26,38 @@ public class Cli implements Runnable { @InjectDependency private StateManager stateManager; - @SneakyThrows // I know deal with it @Override public void run() { eventManager.start(); // Start event manager thread - TerminalScreen terminal = new DefaultTerminalFactory() + try (TerminalScreen terminal = new DefaultTerminalFactory() .setMouseCaptureMode(MouseCaptureMode.CLICK_RELEASE_DRAG_MOVE) - .createScreen(); + .createScreen()) { + stateManager.registerManually(terminal); - terminal.setCursorPosition(null); - terminal.doResizeIfNecessary(); - terminal.getTerminal().enterPrivateMode(); + terminal.setCursorPosition(null); + terminal.doResizeIfNecessary(); + terminal.getTerminal().enterPrivateMode(); - RunningState runningState = stateManager.getOrThrow(RunningState.class); - while (runningState.isRunning()) { - KeyStroke keyStroke = terminal.pollInput(); - if (keyStroke != null) { - if (keyStroke instanceof com.googlecode.lanterna.input.MouseAction mouse) { - eventManager.emitEvent(new MouseAction(mouse)); - continue; + terminal.getTerminal().addResizeListener((_, terminalSize) -> eventManager.emitEvent(new TerminalResizeEvent(terminalSize))); + + RunningState runningState = stateManager.getOrThrow(RunningState.class); + while (runningState.isRunning()) { + KeyStroke keyStroke = terminal.readInput(); + if (keyStroke != null) { + if (keyStroke instanceof com.googlecode.lanterna.input.MouseAction mouse) { + eventManager.emitEvent(new MouseAction(mouse)); + continue; + } + + eventManager.emitEvent(new KeyboardPressEvent(keyStroke)); } - - eventManager.emitEvent(new KeyboardPressEvent(keyStroke)); } - Thread.sleep(50); - } - terminal.close(); + eventManager.emitEvent(RerenderScreen.full(terminal.getTerminalSize())); + } catch (IOException e) { + log.error("Terminal error occurred, shutting down CLI thread.", e); + throw new RuntimeException(e); + } } } diff --git a/src/main/java/cz/jzitnik/config/EventThreadPoolConfig.java b/src/main/java/cz/jzitnik/config/EventThreadPoolConfig.java new file mode 100644 index 0000000..6532f6b --- /dev/null +++ b/src/main/java/cz/jzitnik/config/EventThreadPoolConfig.java @@ -0,0 +1,10 @@ +package cz.jzitnik.config; + +import cz.jzitnik.annotations.Config; +import lombok.Getter; + +@Getter +@Config +public class EventThreadPoolConfig { + private final int threadCount = 8; +} diff --git a/src/main/java/cz/jzitnik/events/RerenderScreen.java b/src/main/java/cz/jzitnik/events/RerenderScreen.java new file mode 100644 index 0000000..acbc85f --- /dev/null +++ b/src/main/java/cz/jzitnik/events/RerenderScreen.java @@ -0,0 +1,28 @@ +package cz.jzitnik.events; + +import com.googlecode.lanterna.TerminalSize; +import cz.jzitnik.utils.events.Event; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.awt.*; + +public record RerenderScreen(ScreenPart[] parts) implements Event { + public static RerenderScreen full(TerminalSize terminalSize) { + return new RerenderScreen( + new ScreenPart[]{ + new ScreenPart( + new Point(0, 0), + new Point(terminalSize.getRows() - 1, terminalSize.getColumns() - 1) + ) + } + ); + } + + @Data + @AllArgsConstructor + public static class ScreenPart { + private Point start; + private Point end; + } +} diff --git a/src/main/java/cz/jzitnik/events/TerminalResizeEvent.java b/src/main/java/cz/jzitnik/events/TerminalResizeEvent.java new file mode 100644 index 0000000..f5749d1 --- /dev/null +++ b/src/main/java/cz/jzitnik/events/TerminalResizeEvent.java @@ -0,0 +1,12 @@ +package cz.jzitnik.events; + +import com.googlecode.lanterna.TerminalSize; +import cz.jzitnik.utils.events.Event; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class TerminalResizeEvent implements Event { + private TerminalSize newSize; +} diff --git a/src/main/java/cz/jzitnik/events/handlers/CliHandler.java b/src/main/java/cz/jzitnik/events/handlers/CliHandler.java new file mode 100644 index 0000000..c971e9e --- /dev/null +++ b/src/main/java/cz/jzitnik/events/handlers/CliHandler.java @@ -0,0 +1,54 @@ +package cz.jzitnik.events.handlers; + +import com.googlecode.lanterna.TextCharacter; +import com.googlecode.lanterna.TextColor; +import com.googlecode.lanterna.screen.TerminalScreen; +import cz.jzitnik.annotations.EventHandler; +import cz.jzitnik.annotations.injectors.InjectState; +import cz.jzitnik.events.RerenderScreen; +import cz.jzitnik.states.ScreenBuffer; +import cz.jzitnik.ui.pixels.Empty; +import cz.jzitnik.ui.pixels.Pixel; +import cz.jzitnik.utils.DependencyManager; +import cz.jzitnik.utils.events.AbstractEventHandler; + +import java.awt.*; + +@EventHandler(RerenderScreen.class) +public class CliHandler extends AbstractEventHandler { + @InjectState + private TerminalScreen terminalScreen; + + @InjectState + private ScreenBuffer screenBuffer; + + public CliHandler(DependencyManager dm) { + super(dm); + } + + @Override + public void handle(RerenderScreen event) { + var parts = event.parts(); + var buffer = screenBuffer.getBuffer(); + + for (RerenderScreen.ScreenPart part : parts) { + Point a = part.getStart(); + Point b = part.getEnd(); + + for (double y = a.getY(); y < b.getY(); y++) { + for (double x = a.getX(); x < b.getX(); x++) { + Pixel pixel = buffer[(int) y][(int) x]; + terminalScreen.setCharacter( + (int) x, + (int) y, + new TextCharacter( + ' ', + TextColor.ANSI.DEFAULT, + pixel.getClass().equals(Empty.class) ? TextColor.ANSI.DEFAULT : pixel.getColor() + ) + ); + } + } + } + } +} diff --git a/src/main/java/cz/jzitnik/events/handlers/KeyboardPressEventHandler.java b/src/main/java/cz/jzitnik/events/handlers/KeyboardPressEventHandler.java index ea6f0ef..50b1ac8 100644 --- a/src/main/java/cz/jzitnik/events/handlers/KeyboardPressEventHandler.java +++ b/src/main/java/cz/jzitnik/events/handlers/KeyboardPressEventHandler.java @@ -24,14 +24,9 @@ public class KeyboardPressEventHandler extends AbstractEventHandler { @Override public void handle(MouseAction event) { - System.out.println("ACTION: " + event); } } diff --git a/src/main/java/cz/jzitnik/events/handlers/TerminalResizeEventHandler.java b/src/main/java/cz/jzitnik/events/handlers/TerminalResizeEventHandler.java new file mode 100644 index 0000000..3ae3e85 --- /dev/null +++ b/src/main/java/cz/jzitnik/events/handlers/TerminalResizeEventHandler.java @@ -0,0 +1,18 @@ +package cz.jzitnik.events.handlers; + +import cz.jzitnik.annotations.EventHandler; +import cz.jzitnik.events.TerminalResizeEvent; +import cz.jzitnik.utils.DependencyManager; +import cz.jzitnik.utils.events.AbstractEventHandler; + +@EventHandler(TerminalResizeEvent.class) +public class TerminalResizeEventHandler extends AbstractEventHandler { + public TerminalResizeEventHandler(DependencyManager dm) { + super(dm); + } + + @Override + public void handle(TerminalResizeEvent event) { + System.out.println("NEWSIZE: " + event.getNewSize()); + } +} diff --git a/src/main/java/cz/jzitnik/sound/SoundPlayer.java b/src/main/java/cz/jzitnik/sound/SoundPlayer.java new file mode 100644 index 0000000..8ee094d --- /dev/null +++ b/src/main/java/cz/jzitnik/sound/SoundPlayer.java @@ -0,0 +1,76 @@ +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 java.io.IOException; + +@Slf4j +@Dependency +public class SoundPlayer { + @InjectDependency + private ClassLoader classLoader; + + 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(); + } + } + + dataIn.close(); + audioStream.close(); + } + + 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); + } + } +} diff --git a/src/main/java/cz/jzitnik/states/ScreenBuffer.java b/src/main/java/cz/jzitnik/states/ScreenBuffer.java new file mode 100644 index 0000000..bba4772 --- /dev/null +++ b/src/main/java/cz/jzitnik/states/ScreenBuffer.java @@ -0,0 +1,11 @@ +package cz.jzitnik.states; + +import cz.jzitnik.annotations.State; +import cz.jzitnik.ui.pixels.Pixel; +import lombok.Data; + +@Data +@State +public class ScreenBuffer { + private Pixel[][] buffer; +} diff --git a/src/main/java/cz/jzitnik/ui/pixels/ColoredPixel.java b/src/main/java/cz/jzitnik/ui/pixels/ColoredPixel.java new file mode 100644 index 0000000..e089a3e --- /dev/null +++ b/src/main/java/cz/jzitnik/ui/pixels/ColoredPixel.java @@ -0,0 +1,9 @@ +package cz.jzitnik.ui.pixels; + +import com.googlecode.lanterna.TextColor; + +public final class ColoredPixel extends Pixel { + public ColoredPixel(TextColor color) { + super(color); + } +} diff --git a/src/main/java/cz/jzitnik/ui/pixels/Empty.java b/src/main/java/cz/jzitnik/ui/pixels/Empty.java new file mode 100644 index 0000000..0c9cd34 --- /dev/null +++ b/src/main/java/cz/jzitnik/ui/pixels/Empty.java @@ -0,0 +1,7 @@ +package cz.jzitnik.ui.pixels; + +public final class Empty extends Pixel { + public Empty() { + super(null); + } +} diff --git a/src/main/java/cz/jzitnik/ui/pixels/Pixel.java b/src/main/java/cz/jzitnik/ui/pixels/Pixel.java new file mode 100644 index 0000000..50e2d0a --- /dev/null +++ b/src/main/java/cz/jzitnik/ui/pixels/Pixel.java @@ -0,0 +1,11 @@ +package cz.jzitnik.ui.pixels; + +import com.googlecode.lanterna.TextColor; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public sealed abstract class Pixel permits Empty, ColoredPixel { + protected TextColor color; +} diff --git a/src/main/java/cz/jzitnik/utils/DependencyManager.java b/src/main/java/cz/jzitnik/utils/DependencyManager.java index 1ff7234..4130f78 100644 --- a/src/main/java/cz/jzitnik/utils/DependencyManager.java +++ b/src/main/java/cz/jzitnik/utils/DependencyManager.java @@ -1,5 +1,8 @@ package cz.jzitnik.utils; +// Don't blame me im using field injection instead of construction injection. I just like it more leave me alone. +// Yes I know I'll suffer in the unit tests. + import cz.jzitnik.annotations.Config; import cz.jzitnik.annotations.Dependency; import cz.jzitnik.annotations.injectors.InjectConfig; @@ -47,10 +50,12 @@ public class DependencyManager { var instance = constructor.newInstance(); configs.put(configClass, instance); } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | - IllegalAccessException _) { + IllegalAccessException e) { + log.error("Failed to instantiate config class: {}", configClass.getName(), e); } } + data.put(ClassLoader.class, DependencyManager.class.getClassLoader()); Set> classes = reflections.getTypesAnnotatedWith(Dependency.class); // Construct all classes @@ -62,9 +67,7 @@ public class DependencyManager { for (int i = 0; i < paramTypes.length; i++) { Class type = paramTypes[i]; - if (configs.containsKey(type)) - params[i] = configs.get(type); - else if (type == getClass()) + if (type == getClass()) params[i] = this; else if (type == Reflections.class) params[i] = reflections; @@ -103,7 +106,7 @@ public class DependencyManager { if (!data.containsKey(field.getType())) continue; - Object dependency = data.get(field.getType()); + Object dependency = field.getType() == getClass() ? this : data.get(field.getType()); if (!field.getType().isAssignableFrom(dependency.getClass())) continue; diff --git a/src/main/java/cz/jzitnik/utils/StateManager.java b/src/main/java/cz/jzitnik/utils/StateManager.java index 9bd10e5..a63be43 100644 --- a/src/main/java/cz/jzitnik/utils/StateManager.java +++ b/src/main/java/cz/jzitnik/utils/StateManager.java @@ -3,12 +3,14 @@ package cz.jzitnik.utils; import cz.jzitnik.annotations.Dependency; import cz.jzitnik.annotations.State; +import lombok.extern.slf4j.Slf4j; import org.reflections.Reflections; import java.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.Optional; +@Slf4j @Dependency public class StateManager { private final HashMap, Object> data = new HashMap<>(); @@ -21,11 +23,16 @@ public class StateManager { var instance = clazz.getDeclaredConstructor().newInstance(); data.put(clazz, instance); } catch (InstantiationException | IllegalAccessException | InvocationTargetException | - NoSuchMethodException _) { + NoSuchMethodException e) { + log.error("Failed to instantiate state class: {}", clazz.getName(), e); } } } + public void registerManually(Object instance) { + data.put(instance.getClass(), instance); + } + @SuppressWarnings("unchecked") public Optional get(Class clazz) { return Optional.ofNullable((T) data.get(clazz)); diff --git a/src/main/java/cz/jzitnik/utils/events/EventManager.java b/src/main/java/cz/jzitnik/utils/events/EventManager.java index cb5dce3..61617a9 100644 --- a/src/main/java/cz/jzitnik/utils/events/EventManager.java +++ b/src/main/java/cz/jzitnik/utils/events/EventManager.java @@ -2,21 +2,28 @@ package cz.jzitnik.utils.events; import cz.jzitnik.annotations.Dependency; import cz.jzitnik.annotations.EventHandler; +import cz.jzitnik.annotations.injectors.InjectConfig; import cz.jzitnik.annotations.injectors.InjectState; +import cz.jzitnik.config.EventThreadPoolConfig; import cz.jzitnik.states.RunningState; import cz.jzitnik.utils.DependencyManager; +import lombok.extern.slf4j.Slf4j; import org.reflections.Reflections; +import org.w3c.dom.events.EventException; import java.lang.reflect.InvocationTargetException; import java.util.HashMap; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.*; +@Slf4j @Dependency public class EventManager extends Thread { @InjectState private RunningState runningState; + @InjectConfig + private EventThreadPoolConfig eventThreadPoolConfig; + private ExecutorService eventExecutor; private final HashMap, AbstractEventHandler> handlers = new HashMap<>(); private final BlockingQueue eventQueue = new LinkedBlockingQueue<>(); @@ -29,6 +36,7 @@ public class EventManager extends Thread { eventQueue.add(event); } + @SuppressWarnings("unchecked") public EventManager(Reflections reflections, DependencyManager dependencyManager) { setDaemon(true); @@ -41,34 +49,34 @@ public class EventManager extends Thread { handlers.put(eventHandler.value(), instance); } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + log.error("Failed to instantiate event handler: {}", clazz.getName(), e); } } - - try { - Thread.sleep(3000); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } } @Override public void run() { + eventExecutor = Executors.newFixedThreadPool(eventThreadPoolConfig.getThreadCount()); while (runningState.isRunning()) { try { Event event = eventQueue.take(); handleEvent(event); } catch (InterruptedException e) { - // Program stops + // The game is shutting down. + eventExecutor.shutdownNow(); + Thread.currentThread().interrupt(); } } + eventExecutor.shutdown(); } @SuppressWarnings("unchecked") private void handleEvent(Event event) { - T typedEvent = (T) event; // safe because handler is keyed by event.getClass() + T typedEvent = (T) event; AbstractEventHandler handler = getHandler((Class) event.getClass()); - Thread thread = new Thread(() -> handler.handle(typedEvent)); - thread.setDaemon(true); - thread.start(); + + if (handler != null) { + eventExecutor.submit(() -> handler.handle(typedEvent)); + } } }