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

39
.gitignore vendored Normal file
View File

@@ -0,0 +1,39 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
.kotlin
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

3
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

7
.idea/encodings.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>

14
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_22" default="true" project-jdk-name="22" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

143
pom.xml Normal file
View File

@@ -0,0 +1,143 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cz.jzitnik</groupId>
<artifactId>game</artifactId>
<version>1.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.38</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>cz.jzitnik.Main</mainClass>
</transformer>
</transformers>
<shadedArtifactAttached>false</shadedArtifactAttached>
<createDependencyReducedPom>false</createDependencyReducedPom>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<goals>
<goal>java</goal>
</goals>
</execution>
</executions>
<configuration>
<mainClass>cz.jzitnik.Main</mainClass>
<classpathScope>compile</classpathScope>
</configuration>
</plugin>
</plugins>
</build>
<properties>
<maven.compiler.source>22</maven.compiler.source>
<maven.compiler.target>22</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.38</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.2</version>
</dependency>
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<version>0.10.2</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.18.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.17</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.18</version>
</dependency>
<dependency>
<groupId>com.github.trilarion</groupId>
<artifactId>java-vorbis-support</artifactId>
<version>1.2.1</version>
</dependency>
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>5.6.2</version>
</dependency>
<dependency>
<groupId>com.googlecode.lanterna</groupId>
<artifactId>lanterna</artifactId>
<version>3.1.3</version>
</dependency>
</dependencies>
</project>

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();
}
}