From d9f7f5a2aca98cbabfb16954e0573d94b012ead0 Mon Sep 17 00:00:00 2001 From: jzitnik-dev Date: Thu, 1 Jan 2026 13:36:31 +0100 Subject: [PATCH] feat: Room Task Scheduler for mob logic --- .../java/cz/jzitnik/annotations/PostInit.java | 11 ++++ ...ventThreadPoolConfig.java => Logging.java} | 4 +- .../cz/jzitnik/config/ThreadPoolConfig.java | 11 ++++ .../events/handlers/ExitEventHandler.java | 5 ++ .../events/handlers/FullRoomDrawHandler.java | 5 ++ .../handlers/PlayerMoveEventHandler.java | 8 ++- .../handlers/RoomChangeEventHandler.java | 5 ++ src/main/java/cz/jzitnik/game/GameRoom.java | 6 ++ .../java/cz/jzitnik/game/objects/Mob.java | 11 ++++ .../cz/jzitnik/game/setup/rooms/MainRoom.java | 17 +++++ .../cz/jzitnik/screens/VideoPlayScreen.java | 2 +- .../cz/jzitnik/threads/MicrophoneThread.java | 6 +- .../cz/jzitnik/utils/DependencyManager.java | 21 +++++++ .../cz/jzitnik/utils/events/EventManager.java | 7 +-- .../cz/jzitnik/utils/roomtasks/RoomTask.java | 6 ++ .../utils/roomtasks/RoomTaskScheduler.java | 62 +++++++++++++++++++ 16 files changed, 177 insertions(+), 10 deletions(-) create mode 100644 src/main/java/cz/jzitnik/annotations/PostInit.java rename src/main/java/cz/jzitnik/config/{EventThreadPoolConfig.java => Logging.java} (57%) create mode 100644 src/main/java/cz/jzitnik/config/ThreadPoolConfig.java create mode 100644 src/main/java/cz/jzitnik/game/objects/Mob.java create mode 100644 src/main/java/cz/jzitnik/utils/roomtasks/RoomTask.java create mode 100644 src/main/java/cz/jzitnik/utils/roomtasks/RoomTaskScheduler.java diff --git a/src/main/java/cz/jzitnik/annotations/PostInit.java b/src/main/java/cz/jzitnik/annotations/PostInit.java new file mode 100644 index 0000000..7861229 --- /dev/null +++ b/src/main/java/cz/jzitnik/annotations/PostInit.java @@ -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.METHOD) +public @interface PostInit { +} diff --git a/src/main/java/cz/jzitnik/config/EventThreadPoolConfig.java b/src/main/java/cz/jzitnik/config/Logging.java similarity index 57% rename from src/main/java/cz/jzitnik/config/EventThreadPoolConfig.java rename to src/main/java/cz/jzitnik/config/Logging.java index 6532f6b..2d3eae4 100644 --- a/src/main/java/cz/jzitnik/config/EventThreadPoolConfig.java +++ b/src/main/java/cz/jzitnik/config/Logging.java @@ -5,6 +5,6 @@ import lombok.Getter; @Getter @Config -public class EventThreadPoolConfig { - private final int threadCount = 8; +public class Logging { + private final boolean showPlayerCordsLogs = false; } diff --git a/src/main/java/cz/jzitnik/config/ThreadPoolConfig.java b/src/main/java/cz/jzitnik/config/ThreadPoolConfig.java new file mode 100644 index 0000000..7fb457c --- /dev/null +++ b/src/main/java/cz/jzitnik/config/ThreadPoolConfig.java @@ -0,0 +1,11 @@ +package cz.jzitnik.config; + +import cz.jzitnik.annotations.Config; +import lombok.Getter; + +@Getter +@Config +public class ThreadPoolConfig { + private final int eventThreadCount = 8; + private final int taskThreadCount = 6; +} diff --git a/src/main/java/cz/jzitnik/events/handlers/ExitEventHandler.java b/src/main/java/cz/jzitnik/events/handlers/ExitEventHandler.java index 7fed6a5..2111e34 100644 --- a/src/main/java/cz/jzitnik/events/handlers/ExitEventHandler.java +++ b/src/main/java/cz/jzitnik/events/handlers/ExitEventHandler.java @@ -9,6 +9,7 @@ import cz.jzitnik.utils.DependencyManager; import cz.jzitnik.utils.StateManager; import cz.jzitnik.utils.ThreadManager; import cz.jzitnik.utils.events.AbstractEventHandler; +import cz.jzitnik.utils.roomtasks.RoomTaskScheduler; @EventHandler(ExitEvent.class) public class ExitEventHandler extends AbstractEventHandler { @@ -18,6 +19,9 @@ public class ExitEventHandler extends AbstractEventHandler { @InjectState private RunningState runningState; + @InjectDependency + private RoomTaskScheduler roomTaskScheduler; + public ExitEventHandler(DependencyManager dm) { super(dm); } @@ -25,6 +29,7 @@ public class ExitEventHandler extends AbstractEventHandler { @Override public void handle(ExitEvent event) { threadManager.shutdownAll(); + roomTaskScheduler.finalShutdown(); runningState.setRunning(false); System.exit(0); // Pls don't blame me } diff --git a/src/main/java/cz/jzitnik/events/handlers/FullRoomDrawHandler.java b/src/main/java/cz/jzitnik/events/handlers/FullRoomDrawHandler.java index 6e179dc..5dd5be3 100644 --- a/src/main/java/cz/jzitnik/events/handlers/FullRoomDrawHandler.java +++ b/src/main/java/cz/jzitnik/events/handlers/FullRoomDrawHandler.java @@ -22,6 +22,7 @@ import cz.jzitnik.utils.DependencyManager; import cz.jzitnik.utils.RerenderUtils; import cz.jzitnik.utils.events.AbstractEventHandler; import cz.jzitnik.utils.events.EventManager; +import cz.jzitnik.utils.roomtasks.RoomTaskScheduler; import lombok.extern.slf4j.Slf4j; import java.awt.image.BufferedImage; @@ -52,6 +53,9 @@ public class FullRoomDrawHandler extends AbstractEventHandler { @InjectConfig private Debugging debugging; + @InjectDependency + private RoomTaskScheduler roomTaskScheduler; + public FullRoomDrawHandler(DependencyManager dm) { super(dm); } @@ -86,6 +90,7 @@ public class FullRoomDrawHandler extends AbstractEventHandler { if (renderState.isFirstRender() || event.isFullRerender()) { eventManager.emitEvent(RerenderScreen.full(terminalSize)); renderState.setFirstRender(false); + roomTaskScheduler.setupNewSchedulers(currentRoom); } else { eventManager.emitEvent(new RerenderScreen(partsToRerender.toArray(RerenderScreen.ScreenPart[]::new))); } diff --git a/src/main/java/cz/jzitnik/events/handlers/PlayerMoveEventHandler.java b/src/main/java/cz/jzitnik/events/handlers/PlayerMoveEventHandler.java index a8cea5a..37d20f9 100644 --- a/src/main/java/cz/jzitnik/events/handlers/PlayerMoveEventHandler.java +++ b/src/main/java/cz/jzitnik/events/handlers/PlayerMoveEventHandler.java @@ -7,6 +7,7 @@ import cz.jzitnik.annotations.injectors.InjectConfig; import cz.jzitnik.annotations.injectors.InjectDependency; import cz.jzitnik.annotations.injectors.InjectState; import cz.jzitnik.config.Debugging; +import cz.jzitnik.config.Logging; import cz.jzitnik.config.PlayerConfig; import cz.jzitnik.events.PlayerMoveEvent; import cz.jzitnik.events.RerenderScreen; @@ -58,6 +59,9 @@ public class PlayerMoveEventHandler extends AbstractEventHandler objects = new ArrayList<>(); + private final List mobs = new ArrayList<>(); private final List colliders = new ArrayList<>(); public GameRoom(ResourceManager.Resource texture) { @@ -41,6 +43,10 @@ public class GameRoom { objects.add(gameObject); } + public void addMob(Mob mob) { + mobs.add(mob); + } + public void addCollider(GameRoomPart collider) { colliders.add(collider); } diff --git a/src/main/java/cz/jzitnik/game/objects/Mob.java b/src/main/java/cz/jzitnik/game/objects/Mob.java new file mode 100644 index 0000000..7163583 --- /dev/null +++ b/src/main/java/cz/jzitnik/game/objects/Mob.java @@ -0,0 +1,11 @@ +package cz.jzitnik.game.objects; + +import cz.jzitnik.utils.roomtasks.RoomTask; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public abstract class Mob { + private final RoomTask task; +} diff --git a/src/main/java/cz/jzitnik/game/setup/rooms/MainRoom.java b/src/main/java/cz/jzitnik/game/setup/rooms/MainRoom.java index 33e6f83..b2cf294 100644 --- a/src/main/java/cz/jzitnik/game/setup/rooms/MainRoom.java +++ b/src/main/java/cz/jzitnik/game/setup/rooms/MainRoom.java @@ -6,9 +6,17 @@ import cz.jzitnik.game.ResourceManager; import cz.jzitnik.game.items.GameItem; import cz.jzitnik.game.items.WoodenSword; import cz.jzitnik.game.objects.Chest; +import cz.jzitnik.game.objects.Mob; import cz.jzitnik.game.utils.RoomCords; +import cz.jzitnik.states.SoundState; import cz.jzitnik.utils.DependencyManager; +import cz.jzitnik.utils.StateManager; +import cz.jzitnik.utils.roomtasks.RoomTask; +import lombok.extern.slf4j.Slf4j; +import java.util.concurrent.TimeUnit; + +@Slf4j public class MainRoom extends GameRoom { public MainRoom(DependencyManager dependencyManager, ResourceManager resourceManager) { super(ResourceManager.Resource.ROOM1); @@ -23,5 +31,14 @@ public class MainRoom extends GameRoom { new RoomCords(135, 15) )); addObject(chest); + + SoundState soundState = dependencyManager.getDependencyOrThrow(StateManager.class).getOrThrow(SoundState.class); + Mob testMob = new Mob(new RoomTask( + () -> log.debug("Sound: {}", soundState.getSoundVolume()), + 1, + TimeUnit.SECONDS + )) {}; + + addMob(testMob); } } diff --git a/src/main/java/cz/jzitnik/screens/VideoPlayScreen.java b/src/main/java/cz/jzitnik/screens/VideoPlayScreen.java index fe83b09..a973992 100644 --- a/src/main/java/cz/jzitnik/screens/VideoPlayScreen.java +++ b/src/main/java/cz/jzitnik/screens/VideoPlayScreen.java @@ -137,7 +137,7 @@ public abstract class VideoPlayScreen extends Screen { } finally { if (tempVideo != null && tempVideo.exists()) { if (!tempVideo.delete()) { - System.err.println("Warning: failed to delete temp video file " + tempVideo.getAbsolutePath()); + log.error("Warning: failed to delete temp video file {}", tempVideo.getAbsolutePath()); } } } diff --git a/src/main/java/cz/jzitnik/threads/MicrophoneThread.java b/src/main/java/cz/jzitnik/threads/MicrophoneThread.java index 83a0756..369de70 100644 --- a/src/main/java/cz/jzitnik/threads/MicrophoneThread.java +++ b/src/main/java/cz/jzitnik/threads/MicrophoneThread.java @@ -4,9 +4,11 @@ import cz.jzitnik.annotations.ThreadRegistry; import cz.jzitnik.annotations.injectors.InjectState; import cz.jzitnik.states.SoundState; import cz.jzitnik.utils.ShutdownableThread; +import lombok.extern.slf4j.Slf4j; import javax.sound.sampled.*; +@Slf4j @ThreadRegistry public class MicrophoneThread extends ShutdownableThread { @InjectState @@ -20,7 +22,7 @@ public class MicrophoneThread extends ShutdownableThread { DataLine.Info info = new DataLine.Info(TargetDataLine.class, format); if (!AudioSystem.isLineSupported(info)) { - System.err.println("Line not supported: " + info); + log.error("Line not supported: {}", info); return; } @@ -42,7 +44,7 @@ public class MicrophoneThread extends ShutdownableThread { } } } catch (LineUnavailableException e) { - System.err.println("Microphone line unavailable: " + e.getMessage()); + log.error("Microphone line unavailable: {}", e.getMessage()); } finally { if (line != null) { line.stop(); diff --git a/src/main/java/cz/jzitnik/utils/DependencyManager.java b/src/main/java/cz/jzitnik/utils/DependencyManager.java index 25fd885..eb35f70 100644 --- a/src/main/java/cz/jzitnik/utils/DependencyManager.java +++ b/src/main/java/cz/jzitnik/utils/DependencyManager.java @@ -7,6 +7,7 @@ import com.google.common.collect.ClassToInstanceMap; import com.google.common.collect.MutableClassToInstanceMap; import cz.jzitnik.annotations.Config; import cz.jzitnik.annotations.Dependency; +import cz.jzitnik.annotations.PostInit; import cz.jzitnik.annotations.injectors.InjectConfig; import cz.jzitnik.annotations.injectors.InjectDependency; import cz.jzitnik.annotations.injectors.InjectState; @@ -16,6 +17,7 @@ import org.reflections.Reflections; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.*; @Slf4j @@ -153,5 +155,24 @@ public class DependencyManager { } } } + + for (Method method : instance.getClass().getDeclaredMethods()) { + if (!method.isAnnotationPresent(PostInit.class)) { + continue; + } + + if (method.getParameterCount() != 0) { + throw new IllegalStateException("@PostInit method must have no parameters: " + method); + } + + try { + method.setAccessible(true); + method.invoke(instance); + } catch (Exception e) { + throw new RuntimeException( + "Failed to invoke @PostInit method: " + method, e + ); + } + } } } diff --git a/src/main/java/cz/jzitnik/utils/events/EventManager.java b/src/main/java/cz/jzitnik/utils/events/EventManager.java index e781da0..1cb0eed 100644 --- a/src/main/java/cz/jzitnik/utils/events/EventManager.java +++ b/src/main/java/cz/jzitnik/utils/events/EventManager.java @@ -4,12 +4,11 @@ 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.config.ThreadPoolConfig; 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; @@ -21,7 +20,7 @@ public class EventManager extends Thread { @InjectState private RunningState runningState; @InjectConfig - private EventThreadPoolConfig eventThreadPoolConfig; + private ThreadPoolConfig threadPoolConfig; private ExecutorService eventExecutor; private final HashMap, AbstractEventHandler> handlers = new HashMap<>(); @@ -62,7 +61,7 @@ public class EventManager extends Thread { dependencyManager.inject(instance); } - eventExecutor = Executors.newFixedThreadPool(eventThreadPoolConfig.getThreadCount()); + eventExecutor = Executors.newFixedThreadPool(threadPoolConfig.getEventThreadCount()); while (runningState.isRunning()) { try { Event event = eventQueue.take(); diff --git a/src/main/java/cz/jzitnik/utils/roomtasks/RoomTask.java b/src/main/java/cz/jzitnik/utils/roomtasks/RoomTask.java new file mode 100644 index 0000000..fac4e2d --- /dev/null +++ b/src/main/java/cz/jzitnik/utils/roomtasks/RoomTask.java @@ -0,0 +1,6 @@ +package cz.jzitnik.utils.roomtasks; + +import java.util.concurrent.TimeUnit; + +public record RoomTask(Runnable task, long rate, TimeUnit rateUnit) { +} diff --git a/src/main/java/cz/jzitnik/utils/roomtasks/RoomTaskScheduler.java b/src/main/java/cz/jzitnik/utils/roomtasks/RoomTaskScheduler.java new file mode 100644 index 0000000..99588b4 --- /dev/null +++ b/src/main/java/cz/jzitnik/utils/roomtasks/RoomTaskScheduler.java @@ -0,0 +1,62 @@ +package cz.jzitnik.utils.roomtasks; + +import cz.jzitnik.annotations.Dependency; +import cz.jzitnik.annotations.PostInit; +import cz.jzitnik.annotations.injectors.InjectConfig; +import cz.jzitnik.config.ThreadPoolConfig; +import cz.jzitnik.game.GameRoom; +import cz.jzitnik.game.objects.Mob; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Dependency +public class RoomTaskScheduler { + ScheduledExecutorService scheduler; + @InjectConfig + private ThreadPoolConfig threadPoolConfig; + + private boolean firstRun = true; + + @PostInit + public void initExecutor() { + scheduler = Executors.newScheduledThreadPool(threadPoolConfig.getTaskThreadCount()); + } + + private void shutdownAll() { + scheduler.shutdown(); + + try { + if (!scheduler.awaitTermination(1, TimeUnit.SECONDS)) { + scheduler.shutdownNow(); + if (!scheduler.awaitTermination(1, TimeUnit.SECONDS)) { + log.error("Pool did not terminate"); + } + } + } catch (InterruptedException e) { + scheduler.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + + public void finalShutdown() { + shutdownAll(); + } + + public void setupNewSchedulers(GameRoom currentRoom) { + if (!firstRun) { + shutdownAll(); + initExecutor(); + } + + for (Mob mob : currentRoom.getMobs()) { + RoomTask task = mob.getTask(); + scheduler.scheduleAtFixedRate(task.task(), 0, task.rate(), task.rateUnit()); + } + + firstRun = false; + } +}