diff --git a/src/main/java/bwapi/BWClient.java b/src/main/java/bwapi/BWClient.java index 3573318..297263c 100644 --- a/src/main/java/bwapi/BWClient.java +++ b/src/main/java/bwapi/BWClient.java @@ -88,14 +88,9 @@ public void startGame(boolean autoContinue) { public void startGame(BWClientConfiguration gameConfiguration) { this.configuration = gameConfiguration; this.performanceMetrics = new PerformanceMetrics(configuration); - botWrapper = new BotWrapper(configuration, eventListener); - - // Use reduced priority to encourage Windows to give priority to StarCraft.exe/BWAPI. - // If BWAPI doesn't get priority, it may not detect completion of a frame on our end in timely fashion. - Thread.currentThread().setName("JBWAPI Client"); - if (configuration.getAsync()) { - Thread.currentThread().setPriority(4); - } + botWrapper = configuration.getAsync() + ? new BotWrapperAsync(configuration, eventListener) + : new BotWrapper(configuration, eventListener); if (client == null) { client = new Client(this); diff --git a/src/main/java/bwapi/BotWrapper.java b/src/main/java/bwapi/BotWrapper.java index 3f439f2..086a20d 100644 --- a/src/main/java/bwapi/BotWrapper.java +++ b/src/main/java/bwapi/BotWrapper.java @@ -1,45 +1,29 @@ package bwapi; -import java.util.concurrent.locks.ReentrantLock; - /** * Manages invocation of bot event handlers */ class BotWrapper { - private final ClientData liveClientData = new ClientData(); - private final BWClientConfiguration configuration; - private final BWEventListener eventListener; - private final FrameBuffer frameBuffer; - private WrappedBuffer liveData; - private Game botGame; - private Thread botThread; - private boolean gameOver; - private PerformanceMetrics performanceMetrics; - private Throwable lastBotThrow; - private final ReentrantLock lastBotThrowLock = new ReentrantLock(); - private final ReentrantLock unsafeReadReadyLock = new ReentrantLock(); - private boolean unsafeReadReady = false; + protected final BWClientConfiguration configuration; + protected final BWEventListener eventListener; + + protected Game botGame; + protected boolean gameOver; + protected PerformanceMetrics performanceMetrics; BotWrapper(BWClientConfiguration configuration, BWEventListener eventListener) { this.configuration = configuration; this.eventListener = eventListener; - frameBuffer = configuration.getAsync() ? new FrameBuffer(configuration) : null; } /** * Resets the BotWrapper for a new botGame. */ void startNewGame(WrappedBuffer liveData, PerformanceMetrics performanceMetrics) { - if (configuration.getAsync()) { - frameBuffer.initialize(liveData, performanceMetrics); - } this.performanceMetrics = performanceMetrics; botGame = new Game(); botGame.setConfiguration(configuration); botGame.botClientData().setBuffer(liveData); - liveClientData.setBuffer(liveData); - this.liveData = liveData; - botThread = null; gameOver = false; } @@ -51,193 +35,18 @@ Game getGame() { return botGame; } - private boolean isUnsafeReadReady() { - unsafeReadReadyLock.lock(); - try { return unsafeReadReady; } - finally { unsafeReadReadyLock.unlock(); } - } - - private void setUnsafeReadReady(boolean value) { - unsafeReadReadyLock.lock(); - try { unsafeReadReady = value; } - finally { unsafeReadReadyLock.unlock(); } - frameBuffer.lockSize.lock(); - try { - frameBuffer.conditionSize.signalAll(); - } finally { - frameBuffer.lockSize.unlock(); - } - } - /** * Handles the arrival of a new frame from BWAPI */ void onFrame() { - if (configuration.getAsync()) { - configuration.log("Main: onFrame asynchronous start"); - asyncOnFrame(); - configuration.log("Main: onFrame asynchronous end"); - } else { - configuration.log("Main: onFrame synchronous start"); - handleEvents(); - configuration.log("Main: onFrame synchronous end"); - } + configuration.log("Main: onFrame synchronous start"); + handleEvents(); + configuration.log("Main: onFrame synchronous end"); } - 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(); - } + void endGame() { } - // 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(); - - 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); - } - - 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(); - } - } - - /** - * Allows an asynchronous bot time to finish operation - */ - void endGame() { - if (botThread != null) { - try { - botThread.join(); - } catch (InterruptedException ignored) {} - } - } - - Throwable getLastBotThrow() { - lastBotThrowLock.lock(); - Throwable output = lastBotThrow; - lastBotThrowLock.unlock(); - return output; - } - - private Thread createBotThread() { - return new Thread(() -> { - try { - configuration.log("Bot: Thread started"); - while (!gameOver) { - - boolean doUnsafeRead = false; - configuration.log("Bot: Ready for another frame"); - performanceMetrics.getBotIdle().startTiming(); - frameBuffer.lockSize.lock(); - try { - doUnsafeRead = isUnsafeReadReady(); - while ( ! doUnsafeRead && frameBuffer.empty()) { - configuration.log("Bot: Waiting for a frame"); - frameBuffer.conditionSize.awaitUninterruptibly(); - doUnsafeRead = isUnsafeReadReady(); - } - } finally { - frameBuffer.lockSize.unlock(); - } - performanceMetrics.getBotIdle().stopTiming(); - - if (doUnsafeRead) { - configuration.log("Bot: Reading live frame"); - setUnsafeReadReady(false); - } else { - configuration.log("Bot: Peeking next frame from buffer"); - botGame.botClientData().setBuffer(frameBuffer.peek()); - } - - configuration.log("Bot: Handling events on frame #" + botGame.getFrameCount()); - handleEvents(); - - configuration.log("Bot: Events handled. Dequeuing frame #" + botGame.getFrameCount()); - frameBuffer.dequeue(); - } - } catch (Throwable throwable) { - // Record the throw, - // Then allow the thread to terminate silently. - // The main thread will look for the stored throw. - lastBotThrowLock.lock(); - lastBotThrow = throwable; - lastBotThrowLock.unlock(); - - // Awaken any threads waiting on bot progress - while (!frameBuffer.empty()) { - frameBuffer.dequeue(); - } - } - }); - } - - private void handleEvents() { + protected void handleEvents() { ClientData.GameData botGameData = botGame.botClientData().gameData(); // Populate gameOver before invoking event handlers (in case the bot throws) @@ -245,10 +54,6 @@ private void handleEvents() { gameOver = gameOver || botGameData.getEvents(i).getType() == EventType.MatchEnd; } - if (configuration.getAsync()) { - performanceMetrics.getFramesBehind().record(Math.max(1, frameBuffer.framesBuffered()) - 1); - } - performanceMetrics.getBotResponse().timeIf( ! gameOver && (botGameData.getFrameCount() > 0 || ! configuration.getUnlimitedFrameZero()), () -> { diff --git a/src/main/java/bwapi/BotWrapperAsync.java b/src/main/java/bwapi/BotWrapperAsync.java new file mode 100644 index 0000000..17e361d --- /dev/null +++ b/src/main/java/bwapi/BotWrapperAsync.java @@ -0,0 +1,230 @@ +package bwapi; + +import java.util.concurrent.locks.ReentrantLock; + +/** + * Manages invocation of bot event handlers + */ +class BotWrapperAsync extends BotWrapper { + private final ClientData liveClientData = new ClientData(); + private final FrameBuffer frameBuffer; + private WrappedBuffer liveData; + private Thread botThread; + private Throwable lastBotThrow; + private final ReentrantLock lastBotThrowLock = new ReentrantLock(); + private final ReentrantLock unsafeReadReadyLock = new ReentrantLock(); + private boolean unsafeReadReady = false; + + BotWrapperAsync(BWClientConfiguration configuration, BWEventListener eventListener) { + super(configuration, eventListener); + frameBuffer = new FrameBuffer(configuration); + + // Use reduced priority to encourage Windows to give priority to StarCraft.exe/BWAPI. + // If BWAPI doesn't get priority, it may not detect completion of a frame on our end in timely fashion. + Thread.currentThread().setName("JBWAPI Client"); + Thread.currentThread().setPriority(Thread.NORM_PRIORITY - 1); + } + + @Override + void startNewGame(WrappedBuffer liveData, PerformanceMetrics performanceMetrics) { + frameBuffer.initialize(liveData, performanceMetrics); + + super.startNewGame(liveData, performanceMetrics); + + liveClientData.setBuffer(liveData); + this.liveData = liveData; + + 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(Thread.NORM_PRIORITY - 2); + botThread.start(); + } + + + private boolean isUnsafeReadReady() { + unsafeReadReadyLock.lock(); + try { + return unsafeReadReady; + } + finally { + unsafeReadReadyLock.unlock(); + } + } + + private void setUnsafeReadReady(boolean value) { + unsafeReadReadyLock.lock(); + try { + unsafeReadReady = value; + } + finally { + unsafeReadReadyLock.unlock(); + } + frameBuffer.lockSize.lock(); + try { + frameBuffer.conditionSize.signalAll(); + } finally { + frameBuffer.lockSize.unlock(); + } + } + + @Override + void onFrame() { + configuration.log("Main: onFrame asynchronous start"); + asyncOnFrame(); + configuration.log("Main: onFrame asynchronous end"); + } + + void asyncOnFrame() { + long startNanos = System.nanoTime(); + long endNanos = startNanos + (long) configuration.getMaxFrameDurationMs() * 1000000; + + // 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(); + + 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); + } + + 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(); + } + } + + /** + * Allows an asynchronous bot time to finish operation + */ + @Override + void endGame() { + if (botThread != null) { + try { + botThread.join(); + } catch (InterruptedException ignored) {} + } + } + + Throwable getLastBotThrow() { + lastBotThrowLock.lock(); + Throwable output = lastBotThrow; + lastBotThrowLock.unlock(); + return output; + } + + private Thread createBotThread() { + return new Thread(() -> { + try { + configuration.log("Bot: Thread started"); + while (!gameOver) { + + boolean doUnsafeRead = false; + configuration.log("Bot: Ready for another frame"); + performanceMetrics.getBotIdle().startTiming(); + frameBuffer.lockSize.lock(); + try { + doUnsafeRead = isUnsafeReadReady(); + while ( ! doUnsafeRead && frameBuffer.empty()) { + configuration.log("Bot: Waiting for a frame"); + frameBuffer.conditionSize.awaitUninterruptibly(); + doUnsafeRead = isUnsafeReadReady(); + } + } finally { + frameBuffer.lockSize.unlock(); + } + performanceMetrics.getBotIdle().stopTiming(); + + if (doUnsafeRead) { + configuration.log("Bot: Reading live frame"); + setUnsafeReadReady(false); + } else { + configuration.log("Bot: Peeking next frame from buffer"); + botGame.botClientData().setBuffer(frameBuffer.peek()); + } + + configuration.log("Bot: Handling events on frame #" + botGame.getFrameCount()); + handleEvents(); + + configuration.log("Bot: Events handled. Dequeuing frame #" + botGame.getFrameCount()); + frameBuffer.dequeue(); + } + } catch (Throwable throwable) { + // Record the throw, + // Then allow the thread to terminate silently. + // The main thread will look for the stored throw. + lastBotThrowLock.lock(); + lastBotThrow = throwable; + lastBotThrowLock.unlock(); + + // Awaken any threads waiting on bot progress + while (!frameBuffer.empty()) { + frameBuffer.dequeue(); + } + } + }); + } + + @Override + protected void handleEvents() { + performanceMetrics.getFramesBehind().record(Math.max(1, frameBuffer.framesBuffered()) - 1); + super.handleEvents(); + } +}