refactor: Better API for IO handling

This commit is contained in:
2026-01-03 15:48:49 +01:00
parent 9992c753ba
commit 583caa40ba
17 changed files with 313 additions and 90 deletions

View File

@@ -2,6 +2,7 @@ package cz.jzitnik;
import cz.jzitnik.game.setup.GameSetup;
import cz.jzitnik.utils.DependencyManager;
import cz.jzitnik.utils.GlobalIOHandlerRepository;
import cz.jzitnik.utils.ScheduledTaskManager;
import cz.jzitnik.utils.ThreadManager;
import org.reflections.Reflections;
@@ -15,10 +16,12 @@ public class Game {
GameSetup gameSetup = dependencyManager.getDependencyOrThrow(GameSetup.class);
ThreadManager threadManager = dependencyManager.getDependencyOrThrow(ThreadManager.class);
ScheduledTaskManager scheduledTaskManager = dependencyManager.getDependencyOrThrow(ScheduledTaskManager.class);
GlobalIOHandlerRepository globalIOHandlerRepository = dependencyManager.getDependencyOrThrow(GlobalIOHandlerRepository.class);
gameSetup.setup();
threadManager.startAll();
scheduledTaskManager.startAll();
globalIOHandlerRepository.setup();
cli.run();
}

View File

@@ -0,0 +1,13 @@
package cz.jzitnik.annotations.ui;
import com.googlecode.lanterna.input.KeyType;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(KeyboardPressHandlers.class)
public @interface KeyboardPressHandler {
KeyType keyType() default KeyType.Character;
char character() default '\0';
}

View File

@@ -0,0 +1,12 @@
package cz.jzitnik.annotations.ui;
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.METHOD)
public @interface KeyboardPressHandlers {
KeyboardPressHandler[] value();
}

View File

@@ -0,0 +1,12 @@
package cz.jzitnik.annotations.ui;
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.METHOD)
public @interface MouseHandler {
MouseHandlerType value();
}

View File

@@ -0,0 +1,7 @@
package cz.jzitnik.annotations.ui;
public enum MouseHandlerType {
CLICK,
MOVE,
ELSE,
}

View File

@@ -0,0 +1,11 @@
package cz.jzitnik.annotations.ui;
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.METHOD)
public @interface Render {
}

View File

@@ -0,0 +1,11 @@
package cz.jzitnik.annotations.ui;
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 UI {
}

View File

@@ -21,8 +21,9 @@ import cz.jzitnik.states.TerminalState;
import cz.jzitnik.ui.Inventory;
import cz.jzitnik.ui.Stats;
import cz.jzitnik.utils.DependencyManager;
import cz.jzitnik.utils.GlobalIOHandlerRepository;
import cz.jzitnik.utils.RerenderUtils;
import cz.jzitnik.utils.UIClickHandlerRepository;
import cz.jzitnik.utils.UIRoomClickHandlerRepository;
import cz.jzitnik.utils.events.AbstractEventHandler;
import cz.jzitnik.utils.events.EventManager;
import cz.jzitnik.utils.roomtasks.RoomTaskScheduler;
@@ -56,11 +57,7 @@ public class FullRoomDrawHandler extends AbstractEventHandler<FullRoomDraw> {
@InjectDependency
private RoomTaskScheduler roomTaskScheduler;
@InjectDependency
private UIClickHandlerRepository uiClickHandlerRepository;
@InjectDependency
private Inventory inventory;
@InjectDependency
private Stats stats;
private GlobalIOHandlerRepository globalIOHandlerRepository;
public FullRoomDrawHandler(DependencyManager dm) {
super(dm);
@@ -88,9 +85,7 @@ public class FullRoomDrawHandler extends AbstractEventHandler<FullRoomDraw> {
RerenderUtils.rerenderPart(0, width - 1, 0, height - 1, startX, startY, currentRoom, room, player, playerTexture, screenBuffer, resourceManager, debugging);
if (event.isFullRerender()) {
inventory.renderInventoryRerender();
stats.rerender();
uiClickHandlerRepository.registerGlobalHandler(inventory);
globalIOHandlerRepository.renderAll();
}
partsToRerender.add(new RerenderScreen.ScreenPart(

View File

@@ -1,12 +1,12 @@
package cz.jzitnik.events.handlers;
import com.googlecode.lanterna.input.KeyStroke;
import cz.jzitnik.annotations.EventHandler;
import cz.jzitnik.annotations.injectors.InjectDependency;
import cz.jzitnik.annotations.injectors.InjectState;
import cz.jzitnik.events.*;
import cz.jzitnik.game.GameState;
import cz.jzitnik.utils.DependencyManager;
import cz.jzitnik.utils.GlobalIOHandlerRepository;
import cz.jzitnik.utils.events.AbstractEventHandler;
import cz.jzitnik.utils.events.EventManager;
@@ -22,6 +22,9 @@ public class KeyboardPressEventHandler extends AbstractEventHandler<KeyboardPres
@InjectState
private GameState gameState;
@InjectDependency
private GlobalIOHandlerRepository globalIOHandlerRepository;
@Override
public void handle(KeyboardPressEvent event) {
if (gameState.getScreen() != null) {
@@ -29,22 +32,6 @@ public class KeyboardPressEventHandler extends AbstractEventHandler<KeyboardPres
return;
}
KeyStroke keyStroke = event.getKeyStroke();
switch (keyStroke.getKeyType()) {
case Escape:
eventManager.emitEvent(new ExitEvent());
break;
case F5:
eventManager.emitEvent(new FullRedrawEvent());
break;
case Character:
switch (keyStroke.getCharacter()) {
case 'w','a','s','d' -> eventManager.emitEvent(new PlayerMoveEvent(keyStroke));
}
break;
default:
break;
}
globalIOHandlerRepository.keyboard(event);
}
}

View File

@@ -11,7 +11,7 @@ import cz.jzitnik.game.GameState;
import cz.jzitnik.game.utils.Selectable;
import cz.jzitnik.states.RenderState;
import cz.jzitnik.utils.DependencyManager;
import cz.jzitnik.utils.UIClickHandlerRepository;
import cz.jzitnik.utils.UIRoomClickHandlerRepository;
import cz.jzitnik.utils.events.AbstractEventHandler;
import cz.jzitnik.utils.events.EventManager;
@@ -28,7 +28,7 @@ public class MouseActionEventHandler extends AbstractEventHandler<MouseAction> {
private EventManager eventManager;
@InjectDependency
private UIClickHandlerRepository uiClickHandlerRepository;
private UIRoomClickHandlerRepository uiRoomClickHandlerRepository;
@InjectState
private GameState gameState;
@@ -52,14 +52,14 @@ public class MouseActionEventHandler extends AbstractEventHandler<MouseAction> {
switch (event.getActionType()) {
case MOVE -> {
boolean registered = uiClickHandlerRepository.handleMove(event);
boolean registered = uiRoomClickHandlerRepository.handleMove(event);
if (!registered) {
eventManager.emitEvent(new MouseMoveEvent(event));
}
}
case CLICK_RELEASE -> {
boolean clicked = uiClickHandlerRepository.handleClick(event);
boolean clicked = uiRoomClickHandlerRepository.handleClick(event);
if (clicked || gameState.getPlayer().isSwinging()) {
return;
@@ -76,7 +76,7 @@ public class MouseActionEventHandler extends AbstractEventHandler<MouseAction> {
object.ifPresent(selectable -> selectable.interact(dm));
}
default -> uiClickHandlerRepository.handleElse(event);
default -> uiRoomClickHandlerRepository.handleElse(event);
}
}
}

View File

@@ -26,7 +26,7 @@ import cz.jzitnik.ui.pixels.Pixel;
import cz.jzitnik.utils.DependencyManager;
import cz.jzitnik.utils.RerenderUtils;
import cz.jzitnik.utils.StateManager;
import cz.jzitnik.utils.UIClickHandlerRepository;
import cz.jzitnik.utils.UIRoomClickHandlerRepository;
import cz.jzitnik.utils.events.EventManager;
import lombok.extern.slf4j.Slf4j;
@@ -56,7 +56,7 @@ public final class Chest extends GameObject implements UIClickHandler {
@InjectDependency
private EventManager eventManager;
@InjectDependency
private UIClickHandlerRepository uiClickHandlerRepository;
private UIRoomClickHandlerRepository uiRoomClickHandlerRepository;
@InjectState
private GameState gameState;
@@ -174,7 +174,7 @@ public final class Chest extends GameObject implements UIClickHandler {
RerenderScreen.ScreenPart sp = new RerenderScreen.ScreenPart(guiStart, guiEnd);
if (!items.isEmpty()) {
listenerHashCode = uiClickHandlerRepository.registerCurrentRoomHandler(sp, this);
listenerHashCode = uiRoomClickHandlerRepository.registerCurrentRoomHandler(sp, this);
}
eventManager.emitEvent(new RerenderScreen(sp));
@@ -275,7 +275,7 @@ public final class Chest extends GameObject implements UIClickHandler {
items.remove(item);
if (items.isEmpty()) {
uiClickHandlerRepository.removeHandlerForCurrentRoom(listenerHashCode);
uiRoomClickHandlerRepository.removeHandlerForCurrentRoom(listenerHashCode);
}
render(true);

View File

@@ -1,15 +0,0 @@
package cz.jzitnik.game.objects;
import cz.jzitnik.events.MouseAction;
public interface GlobalUIClickHandler {
boolean handleClick(MouseAction mouseAction);
default boolean handleMove(MouseAction ignoredMouseAction) {
return false;
}
default boolean handleElse(MouseAction ignoredMouseAction) {
return false;
}
}

View File

@@ -0,0 +1,40 @@
package cz.jzitnik.ui;
import com.googlecode.lanterna.input.KeyType;
import cz.jzitnik.annotations.Dependency;
import cz.jzitnik.annotations.injectors.InjectDependency;
import cz.jzitnik.annotations.ui.KeyboardPressHandler;
import cz.jzitnik.annotations.ui.UI;
import cz.jzitnik.events.ExitEvent;
import cz.jzitnik.events.FullRedrawEvent;
import cz.jzitnik.events.KeyboardPressEvent;
import cz.jzitnik.events.PlayerMoveEvent;
import cz.jzitnik.utils.events.EventManager;
@UI
@Dependency
public class GlobalShortcuts {
@InjectDependency
private EventManager eventManager;
@KeyboardPressHandler(keyType = KeyType.F5)
public boolean refresh(KeyboardPressEvent ignored) {
eventManager.emitEvent(new FullRedrawEvent());
return true;
}
@KeyboardPressHandler(keyType = KeyType.Escape)
public boolean exit(KeyboardPressEvent ignored) {
eventManager.emitEvent(new ExitEvent());
return true;
}
@KeyboardPressHandler(character = 'w')
@KeyboardPressHandler(character = 'a')
@KeyboardPressHandler(character = 's')
@KeyboardPressHandler(character = 'd')
public boolean move(KeyboardPressEvent event) {
eventManager.emitEvent(new PlayerMoveEvent(event.getKeyStroke()));
return true;
}
}

View File

@@ -6,13 +6,16 @@ import com.googlecode.lanterna.TextColor;
import cz.jzitnik.annotations.Dependency;
import cz.jzitnik.annotations.injectors.InjectDependency;
import cz.jzitnik.annotations.injectors.InjectState;
import cz.jzitnik.annotations.ui.MouseHandler;
import cz.jzitnik.annotations.ui.MouseHandlerType;
import cz.jzitnik.annotations.ui.Render;
import cz.jzitnik.annotations.ui.UI;
import cz.jzitnik.events.InventoryRerender;
import cz.jzitnik.events.MouseAction;
import cz.jzitnik.game.GameState;
import cz.jzitnik.game.ResourceManager;
import cz.jzitnik.game.items.GameItem;
import cz.jzitnik.game.items.types.InteractableItem;
import cz.jzitnik.game.objects.GlobalUIClickHandler;
import cz.jzitnik.states.ScreenBuffer;
import cz.jzitnik.states.TerminalState;
import cz.jzitnik.ui.pixels.ColoredPixel;
@@ -33,8 +36,9 @@ import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;
@Slf4j
@UI
@Dependency
public class Inventory implements GlobalUIClickHandler {
public class Inventory {
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
public static final int ITEMS_X = 3;
public static final int ITEMS_Y = 5;
@@ -104,7 +108,7 @@ public class Inventory implements GlobalUIClickHandler {
offsetX = (maxX / 2) - (INVENTORY_WIDTH / 2);
}
@Override
@MouseHandler(MouseHandlerType.CLICK)
public boolean handleClick(MouseAction mouseAction) {
inventoryState.onNextDragTakeItemOnIndex = -1;
@@ -194,7 +198,7 @@ public class Inventory implements GlobalUIClickHandler {
return true;
}
@Override
@MouseHandler(MouseHandlerType.MOVE)
public boolean handleMove(MouseAction mouseAction) {
TerminalPosition terminalPosition = calculateActualCords(mouseAction.getPosition());
@@ -216,7 +220,7 @@ public class Inventory implements GlobalUIClickHandler {
return true;
}
@Override
@MouseHandler(MouseHandlerType.ELSE)
public boolean handleElse(MouseAction mouseAction) {
TerminalPosition terminalPosition = calculateActualCords(mouseAction.getPosition());
@@ -254,6 +258,7 @@ public class Inventory implements GlobalUIClickHandler {
return new TerminalPosition(position.getColumn() - offsetX, position.getRow() * 2 - offsetY);
}
@Render
public void renderInventoryRerender() {
calculateOffset();
var inventory = gameState.getPlayer().getInventory();

View File

@@ -3,6 +3,8 @@ package cz.jzitnik.ui;
import com.googlecode.lanterna.TextColor;
import cz.jzitnik.annotations.Dependency;
import cz.jzitnik.annotations.injectors.InjectState;
import cz.jzitnik.annotations.ui.Render;
import cz.jzitnik.annotations.ui.UI;
import cz.jzitnik.game.GameState;
import cz.jzitnik.game.Player;
import cz.jzitnik.states.ScreenBuffer;
@@ -12,6 +14,7 @@ import cz.jzitnik.ui.pixels.Pixel;
import lombok.Getter;
@Getter
@UI
@Dependency
public class Stats {
public static final int BARS_COUNT = 2;
@@ -33,6 +36,7 @@ public class Stats {
@InjectState
private ScreenBuffer screenBuffer;
@Render
public void rerender() {
var buffer = screenBuffer.getRenderedBuffer();

View File

@@ -0,0 +1,165 @@
package cz.jzitnik.utils;
import com.googlecode.lanterna.input.KeyType;
import cz.jzitnik.annotations.Dependency;
import cz.jzitnik.annotations.injectors.InjectDependency;
import cz.jzitnik.annotations.ui.KeyboardPressHandler;
import cz.jzitnik.annotations.ui.MouseHandler;
import cz.jzitnik.annotations.ui.Render;
import cz.jzitnik.annotations.ui.UI;
import cz.jzitnik.events.KeyboardPressEvent;
import cz.jzitnik.events.MouseAction;
import lombok.extern.slf4j.Slf4j;
import org.reflections.Reflections;
import java.lang.reflect.Method;
import java.util.*;
import java.util.function.Predicate;
@Slf4j
@Dependency
public class GlobalIOHandlerRepository {
private final Reflections reflections;
private final Set<Predicate<MouseAction>> mouseClickHandlers = new HashSet<>();
private final Set<Predicate<MouseAction>> mouseMoveHandlers = new HashSet<>();
private final Set<Predicate<MouseAction>> mouseElseHandlers = new HashSet<>();
private final Map<KeyType, Predicate<KeyboardPressEvent>> keyPressHandlers = new HashMap<>();
private final Map<Character, Predicate<KeyboardPressEvent>> characterPressHandlers = new HashMap<>();
private final Set<Runnable> renderers = new HashSet<>();
@InjectDependency
private DependencyManager dependencyManager;
public GlobalIOHandlerRepository(Reflections reflections) {
this.reflections = reflections;
}
public boolean click(MouseAction mouseEvent) {
for (Predicate<MouseAction> handler : mouseClickHandlers) {
if (handler.test(mouseEvent)) {
return true;
}
}
return false;
}
public boolean move(MouseAction mouseEvent) {
for (Predicate<MouseAction> handler : mouseMoveHandlers) {
if (handler.test(mouseEvent)) {
return true;
}
}
return false;
}
public boolean mElse(MouseAction mouseEvent) {
for (Predicate<MouseAction> handler : mouseElseHandlers) {
if (handler.test(mouseEvent)) {
return true;
}
}
return false;
}
public boolean keyboard(KeyboardPressEvent keyboardPressEvent) {
KeyType keyType = keyboardPressEvent.getKeyStroke().getKeyType();
if (keyType == KeyType.Character) {
char character = keyboardPressEvent.getKeyStroke().getCharacter();
if (characterPressHandlers.containsKey(character)) {
return characterPressHandlers.get(character).test(keyboardPressEvent);
}
} else {
if (keyPressHandlers.containsKey(keyType)) {
return keyPressHandlers.get(keyType).test(keyboardPressEvent);
}
}
return false;
}
public void renderAll() {
renderers.forEach(Runnable::run);
}
public void setup() {
Set<Class<?>> classes = reflections.getTypesAnnotatedWith(UI.class);
for (Class<?> clazz : classes) {
Optional<?> instanceOptional = dependencyManager.getDependency(clazz);
if (instanceOptional.isEmpty()) {
continue;
}
Object instance = instanceOptional.get();
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
if (method.getParameterCount() == 0 && method.getReturnType() == void.class && method.isAnnotationPresent(Render.class)) {
Runnable runnable = () -> {
try {
method.invoke(instance);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
};
renderers.add(runnable);
continue;
}
if (method.getParameterCount() != 1) {
continue;
}
if (method.getParameters()[0].getType().equals(MouseAction.class) && method.getReturnType().equals(boolean.class) && method.isAnnotationPresent(MouseHandler.class)) {
MouseHandler annotation = method.getAnnotation(MouseHandler.class);
method.setAccessible(true);
Predicate<MouseAction> predicate = event -> {
try {
return (boolean) method.invoke(instance, event);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
};
assert annotation != null;
(switch (annotation.value()) {
case CLICK -> mouseClickHandlers;
case MOVE -> mouseMoveHandlers;
case ELSE -> mouseElseHandlers;
}).add(predicate);
continue;
}
if (method.getParameters().length == 1 &&
method.getParameters()[0].getType().equals(KeyboardPressEvent.class)) {
KeyboardPressHandler[] annotations =
method.getAnnotationsByType(KeyboardPressHandler.class);
if (annotations.length == 0) {
return;
}
method.setAccessible(true);
Predicate<KeyboardPressEvent> predicate = event -> {
try {
return (boolean) method.invoke(instance, event);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
};
for (KeyboardPressHandler annotation : annotations) {
if (annotation.keyType() == KeyType.Character) {
characterPressHandlers.put(annotation.character(), predicate);
} else {
keyPressHandlers.put(annotation.keyType(), predicate);
}
}
}
}
}
}
}

View File

@@ -2,33 +2,24 @@ package cz.jzitnik.utils;
import com.googlecode.lanterna.TerminalPosition;
import cz.jzitnik.annotations.Dependency;
import cz.jzitnik.annotations.injectors.InjectDependency;
import cz.jzitnik.annotations.injectors.InjectState;
import cz.jzitnik.events.MouseAction;
import cz.jzitnik.events.RerenderScreen;
import cz.jzitnik.game.GameRoom;
import cz.jzitnik.game.GameState;
import cz.jzitnik.game.objects.GlobalUIClickHandler;
import cz.jzitnik.game.objects.UIClickHandler;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@Dependency
public class UIClickHandlerRepository {
public class UIRoomClickHandlerRepository {
private final Map<GameRoom, Map<RerenderScreen.ScreenPart, UIClickHandler>> roomSpecificHandlers = new ConcurrentHashMap<>();
private final Set<GlobalUIClickHandler> globalHandlers = ConcurrentHashMap.newKeySet();
@InjectState
private GameState gameState;
public int registerGlobalHandler(GlobalUIClickHandler handler) {
globalHandlers.add(handler);
return handler.hashCode();
}
public void removeGlobalHandler(int hashCode) {
globalHandlers.removeIf(handler -> handler.hashCode() == hashCode);
}
@InjectDependency
private GlobalIOHandlerRepository globalIOHandlerRepository;
public int registerCurrentRoomHandler(RerenderScreen.ScreenPart screenPart, UIClickHandler uiClickHandler) {
GameRoom currentRoom = gameState.getCurrentRoom();
@@ -62,13 +53,7 @@ public class UIClickHandlerRepository {
}
}
for (var handler : globalHandlers) {
if (handler.handleClick(mouseAction)) {
return true;
}
}
return false;
return globalIOHandlerRepository.click(mouseAction);
}
public boolean handleElse(MouseAction mouseAction) {
@@ -88,13 +73,7 @@ public class UIClickHandlerRepository {
}
}
for (var handler : globalHandlers) {
if (handler.handleElse(mouseAction)) {
return true;
}
}
return false;
return globalIOHandlerRepository.mElse(mouseAction);
}
public boolean handleMove(MouseAction mouseAction) {
@@ -113,13 +92,7 @@ public class UIClickHandlerRepository {
}
}
for (var handler : globalHandlers) {
if (handler.handleMove(mouseAction)) {
return true;
}
}
return false;
return globalIOHandlerRepository.move(mouseAction);
}
public void removeHandlerForCurrentRoom(int screenPartHashCode) {