chore: Scaffolding of the project

This commit is contained in:
2025-12-11 16:38:08 +01:00
commit 10a2e402b7
28 changed files with 760 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
package cz.jzitnik;
import com.googlecode.lanterna.input.KeyStroke;
import com.googlecode.lanterna.screen.TerminalScreen;
import com.googlecode.lanterna.terminal.DefaultTerminalFactory;
import com.googlecode.lanterna.terminal.MouseCaptureMode;
import cz.jzitnik.annotations.Dependency;
import cz.jzitnik.annotations.injectors.InjectDependency;
import cz.jzitnik.events.KeyboardPressEvent;
import cz.jzitnik.events.MouseAction;
import cz.jzitnik.states.RunningState;
import cz.jzitnik.utils.StateManager;
import cz.jzitnik.utils.events.EventManager;
import lombok.SneakyThrows;
@Dependency
public class Cli implements Runnable {
@InjectDependency
private EventManager eventManager;
@InjectDependency
private StateManager stateManager;
@SneakyThrows // I know deal with it
@Override
public void run() {
eventManager.start(); // Start event manager thread
TerminalScreen terminal = new DefaultTerminalFactory()
.setMouseCaptureMode(MouseCaptureMode.CLICK_RELEASE_DRAG_MOVE)
.createScreen();
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;
}
eventManager.emitEvent(new KeyboardPressEvent(keyStroke));
}
Thread.sleep(50);
}
terminal.close();
}
}

View File

@@ -0,0 +1,13 @@
package cz.jzitnik;
import cz.jzitnik.utils.DependencyManager;
import org.reflections.Reflections;
public class Game {
private final DependencyManager dependencyManager = new DependencyManager(new Reflections("cz.jzitnik"));
public void start() {
Cli cli = dependencyManager.getDependencyOrThrow(Cli.class);
cli.run();
}
}

View File

@@ -0,0 +1,7 @@
package cz.jzitnik;
public class Main {
public static void main(String[] args) {
new Game().start();
}
}

View File

@@ -0,0 +1,11 @@
package cz.jzitnik.annotations;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Config {
}

View File

@@ -0,0 +1,13 @@
package cz.jzitnik.annotations;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Dependency {
/** Custom alias **/
Class<?> value() default Object.class;
}

View File

@@ -0,0 +1,14 @@
package cz.jzitnik.annotations;
import cz.jzitnik.utils.events.Event;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface EventHandler {
Class<? extends Event> value();
}

View File

@@ -0,0 +1,11 @@
package cz.jzitnik.annotations;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface State {
}

View File

@@ -0,0 +1,11 @@
package cz.jzitnik.annotations.injectors;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface InjectConfig {
}

View File

@@ -0,0 +1,11 @@
package cz.jzitnik.annotations.injectors;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface InjectDependency {
}

View File

@@ -0,0 +1,11 @@
package cz.jzitnik.annotations.injectors;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface InjectState {
}

View File

@@ -0,0 +1,7 @@
package cz.jzitnik.events;
import cz.jzitnik.utils.events.Event;
/** Custom event without any handler **/
public class ExitEvent implements Event {
}

View File

@@ -0,0 +1,12 @@
package cz.jzitnik.events;
import com.googlecode.lanterna.input.KeyStroke;
import cz.jzitnik.utils.events.Event;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class KeyboardPressEvent implements Event {
private KeyStroke keyStroke;
}

View File

@@ -0,0 +1,15 @@
package cz.jzitnik.events;
import com.googlecode.lanterna.TerminalPosition;
import com.googlecode.lanterna.input.MouseActionType;
import cz.jzitnik.utils.events.Event;
public class MouseAction extends com.googlecode.lanterna.input.MouseAction implements Event {
public MouseAction(MouseActionType actionType, int button, TerminalPosition position) {
super(actionType, button, position);
}
public MouseAction(com.googlecode.lanterna.input.MouseAction mouseAction) {
this(mouseAction.getActionType(), mouseAction.getButton(), mouseAction.getPosition());
}
}

View File

@@ -0,0 +1,22 @@
package cz.jzitnik.events.handlers;
import cz.jzitnik.annotations.EventHandler;
import cz.jzitnik.events.ExitEvent;
import cz.jzitnik.states.RunningState;
import cz.jzitnik.utils.DependencyManager;
import cz.jzitnik.utils.StateManager;
import cz.jzitnik.utils.events.AbstractEventHandler;
@EventHandler(ExitEvent.class)
public class ExitEventHandler extends AbstractEventHandler<ExitEvent> {
public ExitEventHandler(DependencyManager dm) {
super(dm);
}
@Override
public void handle(ExitEvent event) {
StateManager stateManager = dm.getDependencyOrThrow(StateManager.class);
RunningState runningState = stateManager.getOrThrow(RunningState.class);
runningState.setRunning(false);
}
}

View File

@@ -0,0 +1,38 @@
package cz.jzitnik.events.handlers;
import com.googlecode.lanterna.input.KeyStroke;
import cz.jzitnik.annotations.EventHandler;
import cz.jzitnik.events.ExitEvent;
import cz.jzitnik.events.KeyboardPressEvent;
import cz.jzitnik.utils.DependencyManager;
import cz.jzitnik.utils.events.AbstractEventHandler;
import cz.jzitnik.utils.events.EventManager;
@EventHandler(KeyboardPressEvent.class)
public class KeyboardPressEventHandler extends AbstractEventHandler<KeyboardPressEvent> {
public KeyboardPressEventHandler(DependencyManager dm) {
super(dm);
}
@Override
public void handle(KeyboardPressEvent event) {
EventManager eventManager = dm.getDependencyOrThrow(EventManager.class);
KeyStroke keyStroke = event.getKeyStroke();
switch (keyStroke.getKeyType()) {
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;
}
}
}

View File

@@ -0,0 +1,18 @@
package cz.jzitnik.events.handlers;
import cz.jzitnik.annotations.EventHandler;
import cz.jzitnik.events.MouseAction;
import cz.jzitnik.utils.DependencyManager;
import cz.jzitnik.utils.events.AbstractEventHandler;
@EventHandler(MouseAction.class)
public class MouseActionEventHandler extends AbstractEventHandler<MouseAction> {
public MouseActionEventHandler(DependencyManager dm) {
super(dm);
}
@Override
public void handle(MouseAction event) {
System.out.println("ACTION: " + event);
}
}

View File

@@ -0,0 +1,10 @@
package cz.jzitnik.states;
import cz.jzitnik.annotations.State;
import lombok.Data;
@Data
@State
public class RunningState {
private boolean running = true;
}

View File

@@ -0,0 +1,142 @@
package cz.jzitnik.utils;
import cz.jzitnik.annotations.Config;
import cz.jzitnik.annotations.Dependency;
import cz.jzitnik.annotations.injectors.InjectConfig;
import cz.jzitnik.annotations.injectors.InjectDependency;
import cz.jzitnik.annotations.injectors.InjectState;
import lombok.extern.slf4j.Slf4j;
import org.reflections.Reflections;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Optional;
import java.util.Set;
@Slf4j
public class DependencyManager {
private final HashMap<Class<?>, Object> configs = new HashMap<>();
private final HashMap<Class<?>, Object> data = new HashMap<>();
public <T> T getDependencyOrThrow(Class<T> clazz) {
T instance = get(clazz);
if (instance == null) {
throw new RuntimeException("Class was not found!");
}
return instance;
}
public <T> Optional<T> getDependency(Class<T> clazz) {
return Optional.ofNullable(get(clazz));
}
@SuppressWarnings("unchecked")
private <T> T get(Class<T> clazz) {
return (T) data.get(clazz);
}
public DependencyManager(Reflections reflections) {
Set<Class<?>> configClasses = reflections.getTypesAnnotatedWith(Config.class);
for (Class<?> configClass : configClasses) {
try {
Constructor<?> constructor = configClass.getDeclaredConstructor();
var instance = constructor.newInstance();
configs.put(configClass, instance);
} catch (NoSuchMethodException | InvocationTargetException | InstantiationException |
IllegalAccessException _) {
}
}
Set<Class<?>> classes = reflections.getTypesAnnotatedWith(Dependency.class);
// Construct all classes
for (Class<?> clazz : classes) {
for (var constructor : clazz.getDeclaredConstructors()) {
var paramTypes = constructor.getParameterTypes();
var params = new Object[paramTypes.length];
boolean suitable = true;
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())
params[i] = this;
else if (type == Reflections.class)
params[i] = reflections;
else {
suitable = false;
break;
}
}
if (!suitable) continue;
constructor.setAccessible(true);
try {
Object instance = constructor.newInstance(params);
Dependency annotation = clazz.getAnnotation(Dependency.class);
if (annotation.value() != Object.class) {
data.put(annotation.value(), instance);
} else {
data.put(clazz, instance);
}
} catch (InstantiationException | InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
break; // Found a matching constructor, go to next class
}
}
StateManager stateManager = (StateManager) data.get(StateManager.class);
for (Object instance: data.values()) {
Class<?> clazz = instance.getClass();
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(InjectDependency.class)) {
field.setAccessible(true);
if (!data.containsKey(field.getType())) continue;
Object dependency = data.get(field.getType());
if (!field.getType().isAssignableFrom(dependency.getClass())) continue;
try {
field.set(instance, dependency);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
} else if (field.isAnnotationPresent(InjectState.class)) {
field.setAccessible(true);
Optional<?> stateOptional = stateManager.get(field.getType());
if (stateOptional.isEmpty()) continue;
try {
field.set(instance, stateOptional.get());
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
} else if (field.isAnnotationPresent(InjectConfig.class)) {
field.setAccessible(true);
Optional<?> config = Optional.ofNullable(configs.get(field.getType()));
if (config.isEmpty()) continue;
try {
field.set(instance, config.get());
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
}
}
}

View File

@@ -0,0 +1,38 @@
// NOTE: [StateManager] cannot use injectors like @InjectDependency or @InjectState
package cz.jzitnik.utils;
import cz.jzitnik.annotations.Dependency;
import cz.jzitnik.annotations.State;
import org.reflections.Reflections;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Optional;
@Dependency
public class StateManager {
private final HashMap<Class<?>, Object> data = new HashMap<>();
public StateManager(Reflections reflections) {
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 _) {
}
}
}
@SuppressWarnings("unchecked")
public <T> Optional<T> get(Class<T> clazz) {
return Optional.ofNullable((T) data.get(clazz));
}
@SuppressWarnings("unchecked")
public <T> T getOrThrow(Class<T> clazz) {
return (T) data.get(clazz);
}
}

View File

@@ -0,0 +1,13 @@
package cz.jzitnik.utils.events;
import cz.jzitnik.utils.DependencyManager;
public abstract class AbstractEventHandler<T> {
protected final DependencyManager dm;
public AbstractEventHandler(DependencyManager dm) {
this.dm = dm;
}
public abstract void handle(T event);
}

View File

@@ -0,0 +1,4 @@
package cz.jzitnik.utils.events;
public interface Event {
}

View File

@@ -0,0 +1,74 @@
package cz.jzitnik.utils.events;
import cz.jzitnik.annotations.Dependency;
import cz.jzitnik.annotations.EventHandler;
import cz.jzitnik.annotations.injectors.InjectState;
import cz.jzitnik.states.RunningState;
import cz.jzitnik.utils.DependencyManager;
import org.reflections.Reflections;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
@Dependency
public class EventManager extends Thread {
@InjectState
private RunningState runningState;
private final HashMap<Class<? extends Event>, AbstractEventHandler<? extends Event>> handlers = new HashMap<>();
private final BlockingQueue<Event> eventQueue = new LinkedBlockingQueue<>();
@SuppressWarnings("unchecked")
private <T extends Event> AbstractEventHandler<T> getHandler(Class<T> type) {
return (AbstractEventHandler<T>) handlers.get(type);
}
public void emitEvent(Event event) {
eventQueue.add(event);
}
public EventManager(Reflections reflections, DependencyManager dependencyManager) {
setDaemon(true);
var classes = reflections.getTypesAnnotatedWith(EventHandler.class);
for (var clazz : classes) {
EventHandler eventHandler = clazz.getAnnotation(EventHandler.class);
try {
var instance = (AbstractEventHandler<? extends Event>) clazz.getDeclaredConstructor(DependencyManager.class).newInstance(dependencyManager);
handlers.put(eventHandler.value(), instance);
} catch (InstantiationException | IllegalAccessException | InvocationTargetException |
NoSuchMethodException e) {
}
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
@Override
public void run() {
while (runningState.isRunning()) {
try {
Event event = eventQueue.take();
handleEvent(event);
} catch (InterruptedException e) {
// Program stops
}
}
}
@SuppressWarnings("unchecked")
private <T extends Event> void handleEvent(Event event) {
T typedEvent = (T) event; // safe because handler is keyed by event.getClass()
AbstractEventHandler<T> handler = getHandler((Class<T>) event.getClass());
Thread thread = new Thread(() -> handler.handle(typedEvent));
thread.setDaemon(true);
thread.start();
}
}