package app.playmancala.loop; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import org.jboss.logging.Logger; // The one thread that owns all live game state: games, matchmaking, previews and the WebSocket registry. Like the // Node backend's event loop, it lets that state go without locks. Never block it on I/O. public final class GameLoop implements Scheduler, AutoCloseable { private static final Logger LOG = Logger.getLogger(GameLoop.class); private final ScheduledExecutorService executor; private volatile Thread thread; public GameLoop() { executor = Executors.newSingleThreadScheduledExecutor(runnable -> { Thread loopThread = new Thread(runnable, "game-loop"); loopThread.setDaemon(true); thread = loopThread; return loopThread; }); } @Override public long now() { return System.currentTimeMillis(); } @Override public void execute(Runnable task) { executor.execute(guarded(task)); } @Override public Cancellable schedule(Runnable task, long delayMs) { Future future = executor.schedule(guarded(task), delayMs, TimeUnit.MILLISECONDS); return () -> future.cancel(false); } @Override public Cancellable scheduleAtFixedRate(Runnable task, long periodMs) { Future future = executor.scheduleAtFixedRate(guarded(task), periodMs, periodMs, TimeUnit.MILLISECONDS); return () -> future.cancel(false); } // Runs the task on the loop and waits for its result; for threads that need to read live state. public T call(Callable task) { if (Thread.currentThread() == thread) { try { return task.call(); } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new IllegalStateException(e); } } try { return executor.submit(task).get(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new IllegalStateException("Interrupted while waiting for the game loop", e); } catch (ExecutionException e) { if (e.getCause() instanceof RuntimeException runtime) throw runtime; throw new IllegalStateException(e.getCause()); } } @Override public void close() { executor.shutdownNow(); } // An exception must not stop a repeating task or reach the executor, which would swallow it. private static Runnable guarded(Runnable task) { return () -> { try { task.run(); } catch (Throwable e) { LOG.error("(GameLoop) Task failed", e); } }; } }