From ba8afc392b459a1cf48f4b5a13678c0ef02e478a Mon Sep 17 00:00:00 2001 From: JasperGeurtz Date: Thu, 13 Oct 2022 17:32:24 +0200 Subject: [PATCH] Make BWClientConfiguration use the builder pattern --- src/main/java/bwapi/BWClient.java | 14 +- .../java/bwapi/BWClientConfiguration.java | 232 +++++++++--------- src/main/java/bwapi/BotWrapper.java | 159 ++++++------ src/main/java/bwapi/FrameBuffer.java | 6 +- src/main/java/bwapi/Game.java | 2 +- src/test/java/bwapi/GameTest.java | 21 +- src/test/java/bwapi/PointTest.java | 4 +- .../bwapi/SynchronizationEnvironment.java | 12 +- src/test/java/bwapi/SynchronizationTest.java | 135 +++++----- 9 files changed, 299 insertions(+), 286 deletions(-) diff --git a/src/main/java/bwapi/BWClient.java b/src/main/java/bwapi/BWClient.java index 653cf297..35733183 100644 --- a/src/main/java/bwapi/BWClient.java +++ b/src/main/java/bwapi/BWClient.java @@ -6,7 +6,7 @@ * Client class to connect to the game with. */ public class BWClient { - private BWClientConfiguration configuration = new BWClientConfiguration(); + private BWClientConfiguration configuration; private final BWEventListener eventListener; private BotWrapper botWrapper; private Client client; @@ -19,7 +19,7 @@ public BWClient(final BWEventListener eventListener) { /** * Get the {@link Game} instance of the currently running game. - * When running in asynchronous mode, this is the game from the bot's perspective, eg. potentially a previous frame. + * When running in asynchronous mode, this is the game from the bot's perspective, e.g. potentially a previous frame. */ public Game getGame() { return botWrapper == null ? null : botWrapper.getGame(); @@ -65,8 +65,7 @@ Client getClient() { * Start the game with default settings. */ public void startGame() { - BWClientConfiguration configuration = new BWClientConfiguration(); - startGame(configuration); + startGame(BWClientConfiguration.DEFAULT); } /** @@ -76,9 +75,9 @@ public void startGame() { */ @Deprecated public void startGame(boolean autoContinue) { - BWClientConfiguration configuration = new BWClientConfiguration(); - configuration.withAutoContinue(autoContinue); - startGame(configuration); + startGame(new BWClientConfiguration.Builder() + .withAutoContinue(autoContinue) + .build()); } /** @@ -87,7 +86,6 @@ public void startGame(boolean autoContinue) { * @param gameConfiguration Settings for playing games with this client. */ public void startGame(BWClientConfiguration gameConfiguration) { - gameConfiguration.validateAndLock(); this.configuration = gameConfiguration; this.performanceMetrics = new PerformanceMetrics(configuration); botWrapper = new BotWrapper(configuration, eventListener); diff --git a/src/main/java/bwapi/BWClientConfiguration.java b/src/main/java/bwapi/BWClientConfiguration.java index c440fa86..c0f36087 100644 --- a/src/main/java/bwapi/BWClientConfiguration.java +++ b/src/main/java/bwapi/BWClientConfiguration.java @@ -3,157 +3,157 @@ /** * Configuration for constructing a BWClient */ -public class BWClientConfiguration { +public final class BWClientConfiguration { + public final static BWClientConfiguration DEFAULT = new BWClientConfiguration(); + + private boolean debugConnection = false; + private boolean autoContinue = false; + private boolean unlimitedFrameZero = true; + private int maxFrameDurationMs = 40; + private boolean async = false; + private int asyncFrameBufferCapacity = 10; + private boolean asyncUnsafe = false; + private boolean logVerbosely = false; /** - * Set to `true` for more explicit error messages (which might spam the terminal). + * Use the Builder to build a valid BWClientConfiguration object. */ - public BWClientConfiguration withDebugConnection(boolean value) { - throwIfLocked(); - debugConnection = value; - return this; + private BWClientConfiguration() {} + public static class Builder { + final BWClientConfiguration bwClientConfiguration = new BWClientConfiguration(); + + /** + * Set to `true` for more explicit error messages (which might spam the terminal). + */ + public Builder withDebugConnection(boolean value) { + bwClientConfiguration.debugConnection = value; + return this; + } + + /** + * When true, restarts the client loop when a game ends, allowing the client to play multiple games without restarting. + */ + public Builder withAutoContinue(boolean value) { + bwClientConfiguration.autoContinue = value; + return this; + } + + /** + * Most bot tournaments allow bots to take an indefinite amount of time on frame #0 (the first frame of the game) to analyze the map and load data, + * as the bot has no prior access to BWAPI or game information. + * + * This flag indicates that taking arbitrarily long on frame zero is acceptable. + * Performance metrics omit the frame as an outlier. + * Asynchronous operation will block until the bot's event handlers are complete. + */ + public Builder withUnlimitedFrameZero(boolean value) { + bwClientConfiguration.unlimitedFrameZero = value; + return this; + } + + /** + * The maximum amount of time the bot is supposed to spend on a single frame. + * In asynchronous mode, JBWAPI will attempt to let the bot use up to this much time to process all frames before returning control to BWAPI. + * In synchronous mode, JBWAPI is not empowered to prevent the bot to exceed this amount, but will record overruns in performance metrics. + * Real-time human play typically uses the "fastest" game speed, which has 42.86ms (42,860ns) between frames. + */ + public Builder withMaxFrameDurationMs(int value) { + if (value < 0) { + throw new IllegalArgumentException("maxFrameDurationMs needs to be a non-negative number (it's how long JBWAPI waits for a bot response before returning control to BWAPI)."); + } + bwClientConfiguration.maxFrameDurationMs = value; + return this; + } + + /** + * Runs the bot in asynchronous mode. Asynchronous mode helps attempt to ensure that the bot adheres to real-time performance constraints. + * + * Humans playing StarCraft (and some tournaments) expect bots to return commands within a certain period of time; ~42ms for humans ("fastest" game speed), + * and some tournaments enforce frame-wise time limits (at time of writing, 55ms for COG and AIIDE; 85ms for SSCAIT). + * + * Asynchronous mode invokes bot event handlers in a separate thread, and if all event handlers haven't returned by a specified period of time, + * returns control to StarCraft, allowing the game to proceed while the bot continues to step in the background. This increases the likelihood of meeting + * real-time performance requirements, while not fully guaranteeing it (subject to the whims of the JVM thread scheduler), at a cost of the bot possibly + * issuing commands later than intended, and a marginally larger memory footprint. + * + * Asynchronous mode is not compatible with latency compensation. Enabling asynchronous mode automatically disables latency compensation. + */ + public Builder withAsync(boolean value) { + bwClientConfiguration.async = value; + return this; + } + + /** + * The maximum number of frames to buffer while waiting on a bot. + * Each frame buffered adds about 33 megabytes to JBWAPI's memory footprint. + */ + public Builder withAsyncFrameBufferCapacity(int size) { + if (size < 1) { + throw new IllegalArgumentException("asyncFrameBufferCapacity needs to be a positive number (There needs to be at least one frame buffer)."); + } + bwClientConfiguration.asyncFrameBufferCapacity = size; + return this; + } + + /** + * Enables thread-unsafe async mode. + * In this mode, the bot is allowed to read directly from shared memory until shared memory has been copied into the frame buffer, + * at which point the bot switches to using the frame buffer. + * This should enhance performance by allowing the bot to act while the frame is copied, but poses unidentified risk due to + * the non-thread-safe switch from shared memory reads to frame buffer reads. + */ + public Builder withAsyncUnsafe(boolean value) { + bwClientConfiguration.asyncUnsafe = value; + return this; + } + + /** + * Toggles verbose logging, particularly of synchronization steps. + */ + public Builder withLogVerbosely(boolean value) { + bwClientConfiguration.logVerbosely = value; + return this; + } + + public BWClientConfiguration build() { + if (bwClientConfiguration.asyncUnsafe && ! bwClientConfiguration.async) { + throw new IllegalArgumentException("asyncUnsafe mode needs async mode."); + } + return bwClientConfiguration; + } } + public boolean getDebugConnection() { return debugConnection; } - private boolean debugConnection; - /** - * When true, restarts the client loop when a game ends, allowing the client to play multiple games without restarting. - */ - public BWClientConfiguration withAutoContinue(boolean value) { - throwIfLocked(); - autoContinue = value; - return this; - } public boolean getAutoContinue() { return autoContinue; } - private boolean autoContinue = false; - /** - * Most bot tournaments allow bots to take an indefinite amount of time on frame #0 (the first frame of the game) to analyze the map and load data, - * as the bot has no prior access to BWAPI or game information. - * - * This flag indicates that taking arbitrarily long on frame zero is acceptable. - * Performance metrics omit the frame as an outlier. - * Asynchronous operation will block until the bot's event handlers are complete. - */ - public BWClientConfiguration withUnlimitedFrameZero(boolean value) { - throwIfLocked(); - unlimitedFrameZero = value; - return this; - } public boolean getUnlimitedFrameZero() { return unlimitedFrameZero; } - private boolean unlimitedFrameZero = true; - /** - * The maximum amount of time the bot is supposed to spend on a single frame. - * In asynchronous mode, JBWAPI will attempt to let the bot use up to this much time to process all frames before returning control to BWAPI. - * In synchronous mode, JBWAPI is not empowered to prevent the bot to exceed this amount, but will record overruns in performance metrics. - * Real-time human play typically uses the "fastest" game speed, which has 42.86ms (42,860ns) between frames. - */ - public BWClientConfiguration withMaxFrameDurationMs(int value) { - throwIfLocked(); - maxFrameDurationMs = value; - return this; - } public int getMaxFrameDurationMs() { return maxFrameDurationMs; } - private int maxFrameDurationMs = 40; - /** - * Runs the bot in asynchronous mode. Asynchronous mode helps attempt to ensure that the bot adheres to real-time performance constraints. - * - * Humans playing StarCraft (and some tournaments) expect bots to return commands within a certain period of time; ~42ms for humans ("fastesT" game speed), - * and some tournaments enforce frame-wise time limits (at time of writing, 55ms for COG and AIIDE; 85ms for SSCAIT). - * - * Asynchronous mode invokes bot event handlers in a separate thread, and if all event handlers haven't returned by a specified period of time, sends an - * returns control to StarCraft, allowing the game to proceed while the bot continues to step in the background. This increases the likelihood of meeting - * real-time performance requirements, while not fully guaranteeing it (subject to the whims of the JVM thread scheduler), at a cost of the bot possibly - * issuing commands later than intended, and a marginally larger memory footprint. - * - * Asynchronous mode is not compatible with latency compensation. Enabling asynchronous mode automatically disables latency compensation. - */ - public BWClientConfiguration withAsync(boolean value) { - throwIfLocked(); - async = value; - return this; - } public boolean getAsync() { return async; } - private boolean async = false; - /** - * The maximum number of frames to buffer while waiting on a bot. - * Each frame buffered adds about 33 megabytes to JBWAPI's memory footprint. - */ - public BWClientConfiguration withAsyncFrameBufferCapacity(int size) { - throwIfLocked(); - asyncFrameBufferCapacity = size; - return this; - } public int getAsyncFrameBufferCapacity() { return asyncFrameBufferCapacity; } - private int asyncFrameBufferCapacity = 10; - /** - * Enables thread-unsafe async mode. - * In this mode, the bot is allowed to read directly from shared memory until shared memory has been copied into the frame buffer, - * at wihch point the bot switches to using the frame buffer. - * This should enhance performance by allowing the bot to act while the frame is copied, but poses unidentified risk due to - * the non-thread-safe switc from shared memory reads to frame buffer reads. - */ - public BWClientConfiguration withAsyncUnsafe(boolean value) { - throwIfLocked(); - asyncUnsafe = value; - return this; - } public boolean getAsyncUnsafe() { return asyncUnsafe; } - private boolean asyncUnsafe = false; - /** - * Toggles verbose logging, particularly of synchronization steps. - */ - public BWClientConfiguration withLogVerbosely(boolean value) { - throwIfLocked(); - logVerbosely = value; - return this; - } public boolean getLogVerbosely() { return logVerbosely; } - private boolean logVerbosely = false; - - /** - * Checks that the configuration is in a valid state. Throws an IllegalArgumentException if it isn't. - */ - void validateAndLock() { - if (asyncUnsafe && ! async) { - throw new IllegalArgumentException("asyncUnsafe mode needs async mode."); - } - if (async && maxFrameDurationMs < 0) { - throw new IllegalArgumentException("maxFrameDurationMs needs to be a non-negative number (it's how long JBWAPI waits for a bot response before returning control to BWAPI)."); - } - if (async && asyncFrameBufferCapacity < 1) { - throw new IllegalArgumentException("asyncFrameBufferCapacity needs to be a positive number (There needs to be at least one frame buffer)."); - } - locked = true; - } - private boolean locked = false; - - void throwIfLocked() { - if (locked) { - throw new RuntimeException("Configuration can not be modified after the game has started"); - } - } void log(String value) { if (logVerbosely) { diff --git a/src/main/java/bwapi/BotWrapper.java b/src/main/java/bwapi/BotWrapper.java index d94c58e2..3f439f27 100644 --- a/src/main/java/bwapi/BotWrapper.java +++ b/src/main/java/bwapi/BotWrapper.java @@ -1,6 +1,5 @@ package bwapi; -import java.nio.ByteBuffer; import java.util.concurrent.locks.ReentrantLock; /** @@ -17,8 +16,8 @@ class BotWrapper { private boolean gameOver; private PerformanceMetrics performanceMetrics; private Throwable lastBotThrow; - private ReentrantLock lastBotThrowLock = new ReentrantLock(); - private ReentrantLock unsafeReadReadyLock = new ReentrantLock(); + private final ReentrantLock lastBotThrowLock = new ReentrantLock(); + private final ReentrantLock unsafeReadReadyLock = new ReentrantLock(); private boolean unsafeReadReady = false; BotWrapper(BWClientConfiguration configuration, BWEventListener eventListener) { @@ -76,91 +75,95 @@ private void setUnsafeReadReady(boolean value) { void onFrame() { if (configuration.getAsync()) { configuration.log("Main: onFrame asynchronous start"); - long startNanos = System.nanoTime(); - long endNanos = startNanos + (long) configuration.getMaxFrameDurationMs() * 1000000; - if (botThread == null) { - configuration.log("Main: Starting bot thread"); - botThread = createBotThread(); - botThread.setName("JBWAPI Bot"); - // Reduced priority helps ensure that StarCraft.exe/BWAPI pick up on our frame completion in timely fashion - botThread.setPriority(3); - botThread.start(); - } + asyncOnFrame(); + configuration.log("Main: onFrame asynchronous end"); + } else { + configuration.log("Main: onFrame synchronous start"); + handleEvents(); + configuration.log("Main: onFrame synchronous end"); + } + } - // Unsafe mode: - // If the frame buffer is empty (meaning the bot must be idle) - // allow the bot to read directly from shared memory while we copy it over - if (configuration.getAsyncUnsafe()) { - frameBuffer.lockSize.lock(); - try { - if (frameBuffer.empty()) { - configuration.log("Main: Putting bot on live data"); - botGame.botClientData().setBuffer(liveData); - setUnsafeReadReady(true); - } else { - setUnsafeReadReady(false); - } - } finally { - frameBuffer.lockSize.unlock(); + void asyncOnFrame() { + long startNanos = System.nanoTime(); + long endNanos = startNanos + (long) configuration.getMaxFrameDurationMs() * 1000000; + if (botThread == null) { + configuration.log("Main: Starting bot thread"); + botThread = createBotThread(); + botThread.setName("JBWAPI Bot"); + // Reduced priority helps ensure that StarCraft.exe/BWAPI pick up on our frame completion in timely fashion + botThread.setPriority(3); + botThread.start(); + } + + // Unsafe mode: + // If the frame buffer is empty (meaning the bot must be idle) + // allow the bot to read directly from shared memory while we copy it over + if (configuration.getAsyncUnsafe()) { + frameBuffer.lockSize.lock(); + try { + if (frameBuffer.empty()) { + configuration.log("Main: Putting bot on live data"); + botGame.botClientData().setBuffer(liveData); + setUnsafeReadReady(true); + } else { + setUnsafeReadReady(false); } + } finally { + frameBuffer.lockSize.unlock(); } + } - // Add a frame to buffer - // If buffer is full, will wait until it has capacity. - // Then wait for the buffer to empty or to run out of time in the frame. - int frame = liveClientData.gameData().getFrameCount(); - configuration.log("Main: Enqueuing frame #" + frame); - frameBuffer.enqueueFrame(); + // Add a frame to buffer + // If buffer is full, will wait until it has capacity. + // Then wait for the buffer to empty or to run out of time in the frame. + int frame = liveClientData.gameData().getFrameCount(); + configuration.log("Main: Enqueuing frame #" + frame); + frameBuffer.enqueueFrame(); - configuration.log("Main: Enqueued frame #" + frame); - if (frame > 0) { - performanceMetrics.getClientIdle().startTiming(); - } - frameBuffer.lockSize.lock(); - try { - while (!frameBuffer.empty()) { - // Unsafe mode: Move the bot off of live data onto the frame buffer - // This is the unsafe step! - // We don't synchronize on calls which access the buffer - // (to avoid tens of thousands of synchronized calls per frame) - // so there's no guarantee of safety here. - if (configuration.getAsyncUnsafe() && frameBuffer.size() == 1) { - configuration.log("Main: Weaning bot off live data"); - botGame.botClientData().setBuffer(frameBuffer.peek()); - } + configuration.log("Main: Enqueued frame #" + frame); + if (frame > 0) { + performanceMetrics.getClientIdle().startTiming(); + } + frameBuffer.lockSize.lock(); + try { + while (!frameBuffer.empty()) { + // Unsafe mode: Move the bot off of live data onto the frame buffer + // This is the unsafe step! + // We don't synchronize on calls which access the buffer + // (to avoid tens of thousands of synchronized calls per frame) + // so there's no guarantee of safety here. + if (configuration.getAsyncUnsafe() && frameBuffer.size() == 1) { + configuration.log("Main: Weaning bot off live data"); + botGame.botClientData().setBuffer(frameBuffer.peek()); + } - // Make bot exceptions fall through to the main thread. - Throwable lastThrow = getLastBotThrow(); - if (lastThrow != null) { - configuration.log("Main: Rethrowing bot throwable"); - throw new RuntimeException(lastThrow); - } + // Make bot exceptions fall through to the main thread. + Throwable lastThrow = getLastBotThrow(); + if (lastThrow != null) { + configuration.log("Main: Rethrowing bot throwable"); + throw new RuntimeException(lastThrow); + } - if (configuration.getUnlimitedFrameZero() && frame == 0) { - configuration.log("Main: Waiting indefinitely on frame #" + frame); - frameBuffer.conditionSize.await(); - } else { - long remainingNanos = endNanos - System.nanoTime(); - if (remainingNanos <= 0) { - configuration.log("Main: Out of time in frame #" + frame); - break; - } - configuration.log("Main: Waiting " + remainingNanos / 1000000 + "ms for bot on frame #" + frame); - frameBuffer.conditionSize.awaitNanos(remainingNanos); - long excessNanos = Math.max(0, (System.nanoTime() - endNanos) / 1000000); - performanceMetrics.getExcessSleep().record(excessNanos); + if (configuration.getUnlimitedFrameZero() && frame == 0) { + configuration.log("Main: Waiting indefinitely on frame #" + frame); + frameBuffer.conditionSize.await(); + } else { + long remainingNanos = endNanos - System.nanoTime(); + if (remainingNanos <= 0) { + configuration.log("Main: Out of time in frame #" + frame); + break; } + configuration.log("Main: Waiting " + remainingNanos / 1000000 + "ms for bot on frame #" + frame); + frameBuffer.conditionSize.awaitNanos(remainingNanos); + long excessNanos = Math.max(0, (System.nanoTime() - endNanos) / 1000000); + performanceMetrics.getExcessSleep().record(excessNanos); } - } catch(InterruptedException ignored) { - } finally { - frameBuffer.lockSize.unlock(); - performanceMetrics.getClientIdle().stopTiming(); - configuration.log("Main: onFrame asynchronous end"); } - } else { - configuration.log("Main: onFrame synchronous start"); - handleEvents(); - configuration.log("Main: onFrame synchronous end"); + } catch(InterruptedException ignored) { + } finally { + frameBuffer.lockSize.unlock(); + performanceMetrics.getClientIdle().stopTiming(); } } diff --git a/src/main/java/bwapi/FrameBuffer.java b/src/main/java/bwapi/FrameBuffer.java index cebe9809..d245f70b 100644 --- a/src/main/java/bwapi/FrameBuffer.java +++ b/src/main/java/bwapi/FrameBuffer.java @@ -16,11 +16,11 @@ class FrameBuffer { private WrappedBuffer liveData; private PerformanceMetrics performanceMetrics; - private BWClientConfiguration configuration; - private int capacity; + private final BWClientConfiguration configuration; + private final int capacity; private int stepGame = 0; private int stepBot = 0; - private ArrayList dataBuffer = new ArrayList<>(); + private final ArrayList dataBuffer = new ArrayList<>(); private final Lock lockWrite = new ReentrantLock(); final Lock lockSize = new ReentrantLock(); diff --git a/src/main/java/bwapi/Game.java b/src/main/java/bwapi/Game.java index 8cfed9fc..a9d8a92c 100644 --- a/src/main/java/bwapi/Game.java +++ b/src/main/java/bwapi/Game.java @@ -96,7 +96,7 @@ public final class Game { // USER DEFINED private Text.Size textSize = Text.Size.Default; - private BWClientConfiguration configuration = new BWClientConfiguration(); + private BWClientConfiguration configuration = BWClientConfiguration.DEFAULT; private boolean latcom = true; final ConnectedUnitCache loadedUnitsCache = new ConnectedUnitCache(this, Unit::getTransport); diff --git a/src/test/java/bwapi/GameTest.java b/src/test/java/bwapi/GameTest.java index 557b6257..2548a123 100644 --- a/src/test/java/bwapi/GameTest.java +++ b/src/test/java/bwapi/GameTest.java @@ -1,15 +1,5 @@ package bwapi; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.assertNull; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.Collections; -import java.util.List; - import org.junit.Before; import org.junit.Test; import org.junit.experimental.theories.DataPoints; @@ -19,18 +9,19 @@ import org.junit.runner.RunWith; import java.io.IOException; -import java.util.ArrayList; +import java.util.Collections; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @RunWith(Theories.class) public class GameTest { - private Game sut = new Game(); + private final Game sut = new Game(); private Unit dummy; @DataPoints("overlapping") @@ -104,10 +95,10 @@ public void ifReplaySelfAndEnemyShouldBeNull() throws IOException { game.botClientData().setBuffer(buffer); game.init(); - assertThat(game.isReplay()); + assertTrue(game.isReplay()); assertNull(game.self()); assertNull(game.enemy()); - assertThat(game.enemies().isEmpty()); - assertThat(game.allies().isEmpty()); + assertTrue(game.enemies().isEmpty()); + assertTrue(game.allies().isEmpty()); } } \ No newline at end of file diff --git a/src/test/java/bwapi/PointTest.java b/src/test/java/bwapi/PointTest.java index 946e5046..a31fff11 100644 --- a/src/test/java/bwapi/PointTest.java +++ b/src/test/java/bwapi/PointTest.java @@ -5,8 +5,6 @@ import java.util.Random; import static org.junit.Assert.*; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; public class PointTest { @@ -24,7 +22,7 @@ public void pointEqualsTest() { } @Test - public void pointDistanceAccesibleTest() { + public void pointDistanceAccessibleTest() { TilePosition tp = TilePosition.Origin; assertEquals(0, tp.getApproxDistance(tp)); diff --git a/src/test/java/bwapi/SynchronizationEnvironment.java b/src/test/java/bwapi/SynchronizationEnvironment.java index 049a8287..fc4ecc11 100644 --- a/src/test/java/bwapi/SynchronizationEnvironment.java +++ b/src/test/java/bwapi/SynchronizationEnvironment.java @@ -14,17 +14,17 @@ * Mocks BWAPI and a bot listener, for synchronization tests. */ class SynchronizationEnvironment { - BWClientConfiguration configuration; - BWClient bwClient; - private Client client; + final BWClientConfiguration configuration; + final BWClient bwClient; + private final Client client; private int onEndFrame; private long bwapiDelayMs; - private Map onFrames; + private final Map onFrames; - SynchronizationEnvironment() { + SynchronizationEnvironment(final BWClientConfiguration bwClientConfiguration) { BWEventListener listener = mock(BWEventListener.class); - configuration = new BWClientConfiguration(); + configuration = bwClientConfiguration; client = mock(Client.class); bwClient = new BWClient(listener); bwClient.setClient(client); diff --git a/src/test/java/bwapi/SynchronizationTest.java b/src/test/java/bwapi/SynchronizationTest.java index 1bf111fa..43cd14fc 100644 --- a/src/test/java/bwapi/SynchronizationTest.java +++ b/src/test/java/bwapi/SynchronizationTest.java @@ -32,8 +32,10 @@ private void assertWithin(String message, double expected, double actual, double @Test public void sync_IfException_ThrowException() { - SynchronizationEnvironment environment = new SynchronizationEnvironment(); - environment.configuration.withAsync(false); + BWClientConfiguration config = new BWClientConfiguration.Builder() + .withAsync(false) + .build(); + SynchronizationEnvironment environment = new SynchronizationEnvironment(config); environment.onFrame(0, () -> { throw new RuntimeException("Simulated bot exception"); }); assertThrows(RuntimeException.class, environment::runGame); } @@ -41,21 +43,24 @@ public void sync_IfException_ThrowException() { @Test public void async_IfException_ThrowException() { // An exception in the bot thread must be re-thrown by the main thread. - SynchronizationEnvironment environment = new SynchronizationEnvironment(); - environment.configuration - .withAsync(true) - .withAsyncFrameBufferCapacity(3); + BWClientConfiguration config = new BWClientConfiguration.Builder() + .withAsync(true) + .withAsyncFrameBufferCapacity(3) + .build(); + SynchronizationEnvironment environment = new SynchronizationEnvironment(config); + environment.onFrame(0, () -> { throw new RuntimeException("Simulated bot exception"); }); assertThrows(RuntimeException.class, environment::runGame); } @Test public void sync_IfDelay_ThenNoBuffer() { - SynchronizationEnvironment environment = new SynchronizationEnvironment(); - environment.configuration - .withAsync(false) - .withMaxFrameDurationMs(1) - .withAsyncFrameBufferCapacity(3); + BWClientConfiguration config = new BWClientConfiguration.Builder() + .withAsync(false) + .withMaxFrameDurationMs(1) + .withAsyncFrameBufferCapacity(3) + .build(); + SynchronizationEnvironment environment = new SynchronizationEnvironment(config); IntStream.range(0, 5).forEach(frame -> { environment.onFrame(frame, () -> { @@ -71,11 +76,12 @@ public void sync_IfDelay_ThenNoBuffer() { @Test public void async_IfBotDelay_ThenClientBuffers() { - SynchronizationEnvironment environment = new SynchronizationEnvironment(); - environment.configuration - .withAsync(true) - .withMaxFrameDurationMs(100) - .withAsyncFrameBufferCapacity(4); + BWClientConfiguration config = new BWClientConfiguration.Builder() + .withAsync(true) + .withMaxFrameDurationMs(100) + .withAsyncFrameBufferCapacity(4) + .build(); + SynchronizationEnvironment environment = new SynchronizationEnvironment(config); environment.onFrame(1, () -> { sleepUnchecked(500); @@ -95,11 +101,12 @@ public void async_IfBotDelay_ThenClientBuffers() { @Test public void async_IfBotDelay_ThenClientStalls() { - SynchronizationEnvironment environment = new SynchronizationEnvironment(); - environment.configuration - .withAsync(true) - .withMaxFrameDurationMs(200) - .withAsyncFrameBufferCapacity(5); + BWClientConfiguration config = new BWClientConfiguration.Builder() + .withAsync(true) + .withMaxFrameDurationMs(200) + .withAsyncFrameBufferCapacity(5) + .build(); + SynchronizationEnvironment environment = new SynchronizationEnvironment(config); environment.onFrame(1, () -> { sleepUnchecked(500); @@ -121,12 +128,13 @@ public void async_IfBotDelay_ThenClientStalls() { @Test public void async_IfFrameZeroWaitsEnabled_ThenAllowInfiniteTime() { - SynchronizationEnvironment environment = new SynchronizationEnvironment(); - environment.configuration - .withAsync(true) - .withUnlimitedFrameZero(true) - .withMaxFrameDurationMs(5) - .withAsyncFrameBufferCapacity(2); + BWClientConfiguration config = new BWClientConfiguration.Builder() + .withAsync(true) + .withUnlimitedFrameZero(true) + .withMaxFrameDurationMs(5) + .withAsyncFrameBufferCapacity(2) + .build(); + SynchronizationEnvironment environment = new SynchronizationEnvironment(config); environment.onFrame(0, () -> { sleepUnchecked(50); @@ -140,12 +148,14 @@ public void async_IfFrameZeroWaitsEnabled_ThenAllowInfiniteTime() { @Test public void async_IfFrameZeroWaitsDisabled_ThenClientBuffers() { - SynchronizationEnvironment environment = new SynchronizationEnvironment(); - environment.configuration + BWClientConfiguration config = new BWClientConfiguration.Builder() .withAsync(true) .withUnlimitedFrameZero(false) .withMaxFrameDurationMs(5) - .withAsyncFrameBufferCapacity(2); + .withAsyncFrameBufferCapacity(2) + .build(); + SynchronizationEnvironment environment = new SynchronizationEnvironment(config); + environment.onFrame(0, () -> { sleepUnchecked(50); @@ -160,8 +170,10 @@ public void async_IfFrameZeroWaitsDisabled_ThenClientBuffers() { @Test public void async_MeasurePerformance_CopyingToBuffer() { // Somewhat lazy test; just verify that we're getting sane values - SynchronizationEnvironment environment = new SynchronizationEnvironment(); - environment.configuration.withAsync(true); + BWClientConfiguration config = new BWClientConfiguration.Builder() + .withAsync(true) + .build(); + SynchronizationEnvironment environment = new SynchronizationEnvironment(config); environment.runGame(20); final double minObserved = 0.25; final double maxObserved = 15; @@ -175,12 +187,13 @@ public void async_MeasurePerformance_CopyingToBuffer() { @Test public void async_MeasurePerformance_FrameBufferSizeAndFramesBehind() { - SynchronizationEnvironment environment = new SynchronizationEnvironment(); - environment.configuration - .withAsync(true) - .withUnlimitedFrameZero(true) - .withAsyncFrameBufferCapacity(3) - .withMaxFrameDurationMs(20); + BWClientConfiguration config = new BWClientConfiguration.Builder() + .withAsync(true) + .withUnlimitedFrameZero(true) + .withAsyncFrameBufferCapacity(3) + .withMaxFrameDurationMs(20) + .build(); + SynchronizationEnvironment environment = new SynchronizationEnvironment(config); environment.onFrame(5, () -> { assertWithin("5: Frame buffer average", 0, environment.metrics().getFrameBufferSize().getRunningTotal().getMean(), 0.1); @@ -215,11 +228,12 @@ public void async_MeasurePerformance_FrameBufferSizeAndFramesBehind() { @Test public void MeasurePerformance_BotResponse() { - SynchronizationEnvironment environment = new SynchronizationEnvironment(); - // Frame zero appears to take an extra 60ms, so let's disable timing for it // (and also verify that we omit frame zero from performance metrics) - environment.configuration.withUnlimitedFrameZero(true); + BWClientConfiguration config = new BWClientConfiguration.Builder() + .withUnlimitedFrameZero(true) + .build(); + SynchronizationEnvironment environment = new SynchronizationEnvironment(config); environment.onFrame(1, () -> { sleepUnchecked(100); @@ -251,11 +265,13 @@ public void MeasurePerformance_BotResponse() { public void MeasurePerformance_BotIdle() { final long bwapiDelayMs = 10; final int frames = 10; - SynchronizationEnvironment environment = new SynchronizationEnvironment(); - environment.configuration - .withAsync(true) - .withAsyncFrameBufferCapacity(3) - .withUnlimitedFrameZero(true); + BWClientConfiguration config = new BWClientConfiguration.Builder() + .withAsync(true) + .withAsyncFrameBufferCapacity(3) + .withUnlimitedFrameZero(true) + .build(); + SynchronizationEnvironment environment = new SynchronizationEnvironment(config); + environment.setBwapiDelayMs(bwapiDelayMs); environment.runGame(frames); double expected = environment.metrics().getCopyingToBuffer().getRunningTotal().getMean() + bwapiDelayMs; @@ -264,12 +280,14 @@ public void MeasurePerformance_BotIdle() { @Test public void async_MeasurePerformance_IntentionallyBlocking() { - SynchronizationEnvironment environment = new SynchronizationEnvironment(); - environment.configuration - .withAsync(true) - .withUnlimitedFrameZero(true) - .withAsyncFrameBufferCapacity(2) - .withMaxFrameDurationMs(20); + BWClientConfiguration config = new BWClientConfiguration.Builder() + .withAsync(true) + .withUnlimitedFrameZero(true) + .withAsyncFrameBufferCapacity(2) + .withMaxFrameDurationMs(20) + .build(); + SynchronizationEnvironment environment = new SynchronizationEnvironment(config); + final int frameDelayMs = 100; environment.onFrame(1, () -> { sleepUnchecked(100); @@ -287,13 +305,18 @@ public void async_MeasurePerformance_IntentionallyBlocking() { @Test public void async_DisablesLatencyCompensation() { - SynchronizationEnvironment environmentSync = new SynchronizationEnvironment(); - environmentSync.configuration.withAsync(false); + BWClientConfiguration config = new BWClientConfiguration.Builder() + .withAsync(false) + .build(); + SynchronizationEnvironment environmentSync = new SynchronizationEnvironment(config); environmentSync.onFrame(1, () -> { assertTrue(environmentSync.bwClient.getGame().isLatComEnabled()); }); environmentSync.runGame(2); - SynchronizationEnvironment environmentAsync = new SynchronizationEnvironment(); - environmentAsync.configuration.withAsync(true).withAsyncFrameBufferCapacity(2); + BWClientConfiguration configAsync = new BWClientConfiguration.Builder() + .withAsync(true) + .withAsyncFrameBufferCapacity(2) + .build(); + SynchronizationEnvironment environmentAsync = new SynchronizationEnvironment(configAsync); environmentAsync.onFrame(1, () -> { assertFalse(environmentAsync.bwClient.getGame().isLatComEnabled()); }); environmentAsync.onFrame(2, () -> { assertThrows(IllegalStateException.class, () -> environmentAsync.bwClient.getGame().setLatCom(true)); }); environmentAsync.runGame(3);