diff --git a/forge-core/src/main/java/forge/util/ThreadUtil.java b/forge-core/src/main/java/forge/util/ThreadUtil.java index fd49e51506e..4ec6fb0f0a9 100644 --- a/forge-core/src/main/java/forge/util/ThreadUtil.java +++ b/forge-core/src/main/java/forge/util/ThreadUtil.java @@ -20,8 +20,10 @@ public class ThreadUtil { } } - private final static ExecutorService gameThreadPool = Executors.newCachedThreadPool(new WorkerThreadFactory("Game")); - private static ExecutorService getGameThreadPool() { return gameThreadPool; } + // Use a single game thread, rather than a pool of them, because when something + // needs to execute on the game thread, it's because it's not thread safe. + private final static ExecutorService gameThread = Executors.newSingleThreadExecutor(new WorkerThreadFactory("Game")); + private static ExecutorService getGameThreadPool() { return gameThread; } private final static ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(2, new WorkerThreadFactory("Delayed")); private static ScheduledExecutorService getScheduledPool() { return scheduledPool; } diff --git a/forge-game/src/main/java/forge/game/GameAction.java b/forge-game/src/main/java/forge/game/GameAction.java index b035731f1f2..83bf3505205 100644 --- a/forge-game/src/main/java/forge/game/GameAction.java +++ b/forge-game/src/main/java/forge/game/GameAction.java @@ -17,6 +17,7 @@ */ package forge.game; +import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Iterables; @@ -76,6 +77,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.concurrent.Callable; /** * Methods for common actions performed during a game. @@ -1744,8 +1746,17 @@ public class GameAction { // state effects are checked only when someone gets priority } - // Invokes given runnable in Game thread pool - used to start game and perform actions from UI (when game-0 waits for input) - public void invoke(final Runnable proc) { + private Function invokeFunction; + public synchronized void setInvokeFunction(Function invokeFunction) { + this.invokeFunction = invokeFunction; + } + + // Invokes given runnable on the Game thread - used to start game and perform actions from UI when waiting for input. + public synchronized void invoke(final Runnable proc) { + if (invokeFunction != null) { + invokeFunction.apply(proc); + return; + } if (ThreadUtil.isGameThread()) { proc.run(); } diff --git a/forge-gui/src/main/java/forge/match/input/InputSyncronizedBase.java b/forge-gui/src/main/java/forge/match/input/InputSyncronizedBase.java index 9ca2f6ce712..d916514c1b4 100644 --- a/forge-gui/src/main/java/forge/match/input/InputSyncronizedBase.java +++ b/forge-gui/src/main/java/forge/match/input/InputSyncronizedBase.java @@ -1,25 +1,34 @@ package forge.match.input; -import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingDeque; +import com.google.common.base.Function; import forge.FThreads; import forge.error.BugReporter; import forge.player.PlayerControllerHuman; +import forge.util.ThreadUtil; public abstract class InputSyncronizedBase extends InputBase implements InputSynchronized { private static final long serialVersionUID = 8756177361251703052L; - private final CountDownLatch cdlDone; + private static final Runnable terminationMarker = new Runnable() { public void run() { } }; + + // The gameTaskQueue indicates tasks to run while blocked on the game. To stop, add terminationMarker (as null is + // not allowed). + private LinkedBlockingDeque gameTaskQueue = new LinkedBlockingDeque(); public InputSyncronizedBase(final PlayerControllerHuman controller) { super(controller); - cdlDone = new CountDownLatch(1); } @Override public void awaitLatchRelease() { FThreads.assertExecutedByEdt(false); - try{ - cdlDone.await(); + try { + Runnable r = gameTaskQueue.take(); + while (r != terminationMarker) { + r.run(); + r = gameTaskQueue.take(); + } } catch (final InterruptedException e) { BugReporter.reportException(e); } @@ -27,12 +36,34 @@ public abstract class InputSyncronizedBase extends InputBase implements InputSyn @Override public final void relaseLatchWhenGameIsOver() { - cdlDone.countDown(); + gameTaskQueue.add(terminationMarker); } public void showAndWait() { + final boolean isGameThread = ThreadUtil.isGameThread(); + + if (isGameThread) { + // If we're on the game thread, redirect the "run on game thread" function to instead go through us. + getController().getGame().getAction().setInvokeFunction(new Function() { + public Void apply(Runnable r) { + gameTaskQueue.add(r); + return null; + } + }); + } + getController().getInputQueue().setInput(this); awaitLatchRelease(); + + if (isGameThread) { + // Reset the invoke function to null. + getController().getGame().getAction().setInvokeFunction(null); + // There's a race that a new task may be queued up before we've reset the invoke function. + // To handle this, schedule any remaining tasks normally. + for (Runnable r : gameTaskQueue) { + getController().getGame().getAction().invoke(r); + } + } } protected final void stop() { @@ -50,7 +81,7 @@ public abstract class InputSyncronizedBase extends InputBase implements InputSyn if (getController().getInputQueue().getInput() != null) { getController().getInputQueue().removeInput(InputSyncronizedBase.this); } - cdlDone.countDown(); + gameTaskQueue.add(terminationMarker); } protected void onStop() { }