feat: Started implementing screen rendering
And some minor design changes and stuff like that.
This commit is contained in:
@@ -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,22 +26,24 @@ 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.getTerminal().addResizeListener((_, terminalSize) -> eventManager.emitEvent(new TerminalResizeEvent(terminalSize)));
|
||||
|
||||
RunningState runningState = stateManager.getOrThrow(RunningState.class);
|
||||
while (runningState.isRunning()) {
|
||||
KeyStroke keyStroke = terminal.pollInput();
|
||||
KeyStroke keyStroke = terminal.readInput();
|
||||
if (keyStroke != null) {
|
||||
if (keyStroke instanceof com.googlecode.lanterna.input.MouseAction mouse) {
|
||||
eventManager.emitEvent(new MouseAction(mouse));
|
||||
@@ -45,9 +52,12 @@ public class Cli implements Runnable {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
10
src/main/java/cz/jzitnik/config/EventThreadPoolConfig.java
Normal file
10
src/main/java/cz/jzitnik/config/EventThreadPoolConfig.java
Normal file
@@ -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;
|
||||
}
|
||||
28
src/main/java/cz/jzitnik/events/RerenderScreen.java
Normal file
28
src/main/java/cz/jzitnik/events/RerenderScreen.java
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
12
src/main/java/cz/jzitnik/events/TerminalResizeEvent.java
Normal file
12
src/main/java/cz/jzitnik/events/TerminalResizeEvent.java
Normal file
@@ -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;
|
||||
}
|
||||
54
src/main/java/cz/jzitnik/events/handlers/CliHandler.java
Normal file
54
src/main/java/cz/jzitnik/events/handlers/CliHandler.java
Normal file
@@ -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<RerenderScreen> {
|
||||
@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()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,14 +24,9 @@ public class KeyboardPressEventHandler extends AbstractEventHandler<KeyboardPres
|
||||
case Escape:
|
||||
eventManager.emitEvent(new ExitEvent());
|
||||
break;
|
||||
case ArrowUp:
|
||||
System.out.println("Up arrow pressed");
|
||||
break;
|
||||
case Character:
|
||||
System.out.println("Key pressed: " + keyStroke.getCharacter());
|
||||
break;
|
||||
default:
|
||||
System.out.println("IDK: " + keyStroke.getKeyType());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,5 @@ public class MouseActionEventHandler extends AbstractEventHandler<MouseAction> {
|
||||
|
||||
@Override
|
||||
public void handle(MouseAction event) {
|
||||
System.out.println("ACTION: " + event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<TerminalResizeEvent> {
|
||||
public TerminalResizeEventHandler(DependencyManager dm) {
|
||||
super(dm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(TerminalResizeEvent event) {
|
||||
System.out.println("NEWSIZE: " + event.getNewSize());
|
||||
}
|
||||
}
|
||||
76
src/main/java/cz/jzitnik/sound/SoundPlayer.java
Normal file
76
src/main/java/cz/jzitnik/sound/SoundPlayer.java
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/main/java/cz/jzitnik/states/ScreenBuffer.java
Normal file
11
src/main/java/cz/jzitnik/states/ScreenBuffer.java
Normal file
@@ -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;
|
||||
}
|
||||
9
src/main/java/cz/jzitnik/ui/pixels/ColoredPixel.java
Normal file
9
src/main/java/cz/jzitnik/ui/pixels/ColoredPixel.java
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
7
src/main/java/cz/jzitnik/ui/pixels/Empty.java
Normal file
7
src/main/java/cz/jzitnik/ui/pixels/Empty.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package cz.jzitnik.ui.pixels;
|
||||
|
||||
public final class Empty extends Pixel {
|
||||
public Empty() {
|
||||
super(null);
|
||||
}
|
||||
}
|
||||
11
src/main/java/cz/jzitnik/ui/pixels/Pixel.java
Normal file
11
src/main/java/cz/jzitnik/ui/pixels/Pixel.java
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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<Class<?>> 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;
|
||||
|
||||
|
||||
@@ -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<Class<?>, 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 <T> Optional<T> get(Class<T> clazz) {
|
||||
return Optional.ofNullable((T) data.get(clazz));
|
||||
|
||||
@@ -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<Class<? extends Event>, AbstractEventHandler<? extends Event>> handlers = new HashMap<>();
|
||||
private final BlockingQueue<Event> 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 <T extends Event> void handleEvent(Event event) {
|
||||
T typedEvent = (T) event; // safe because handler is keyed by event.getClass()
|
||||
T typedEvent = (T) event;
|
||||
AbstractEventHandler<T> handler = getHandler((Class<T>) event.getClass());
|
||||
Thread thread = new Thread(() -> handler.handle(typedEvent));
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
|
||||
if (handler != null) {
|
||||
eventExecutor.submit(() -> handler.handle(typedEvent));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user