Skip to content

Commit

Permalink
Merge pull request #60 from dgant/async
Browse files Browse the repository at this point in the history
Asynchronous mode
  • Loading branch information
JasperGeurtz authored Dec 28, 2020
2 parents d2f252b + cdb67d5 commit d21d42e
Show file tree
Hide file tree
Showing 31 changed files with 4,128 additions and 2,390 deletions.
6 changes: 4 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,10 @@
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<parallel>all</parallel> <!-- Run tests in parallel-->
<useUnlimitedThreads>true</useUnlimitedThreads>
<!-- <parallel>all</parallel> &lt;!&ndash; Run tests in parallel&ndash;&gt;-->
<!-- <useUnlimitedThreads>true</useUnlimitedThreads>-->
<rerunFailingTestsCount>10</rerunFailingTestsCount>
<argLine>-Xms1g -Xmx1g</argLine>
</configuration>
</plugin>
<plugin>
Expand Down
118 changes: 98 additions & 20 deletions src/main/java/bwapi/BWClient.java
Original file line number Diff line number Diff line change
@@ -1,64 +1,142 @@
package bwapi;

import com.sun.jna.platform.win32.Kernel32;

import java.util.Objects;

/**
* Client class to connect to the game with.
*/
public class BWClient {
private BWClientConfiguration configuration = new BWClientConfiguration();
private final BWEventListener eventListener;
private final boolean debugConnection;
private EventHandler handler;
private BotWrapper botWrapper;
private Client client;
private PerformanceMetrics performanceMetrics;

public BWClient(final BWEventListener eventListener) {
this(eventListener, false);
}

/**
* @param debugConnection set to `true` for more explicit error messages (might spam the terminal).
* `false` by default
*/
public BWClient(final BWEventListener eventListener, final boolean debugConnection) {
Objects.requireNonNull(eventListener);
this.debugConnection = debugConnection;
this.eventListener = 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.
*/
public Game getGame() {
return handler == null ? null : handler.getGame();
return botWrapper == null ? null : botWrapper.getGame();
}

/**
* @return JBWAPI performance metrics.
*/
public PerformanceMetrics getPerformanceMetrics() {
return performanceMetrics;
}

/**
* @return The current configuration
*/
public BWClientConfiguration getConfiguration() {
return configuration;
}

/**
* @return Whether the current frame should be subject to timing.
*/
boolean doTime() {
return ! configuration.getUnlimitedFrameZero() || (client.isConnected() && client.liveClientData().gameData().getFrameCount() > 0);
}

/**
* @return The number of frames between the one exposed to the bot and the most recent received by JBWAPI.
* This tracks the size of the frame buffer except when the game is paused (which results in multiple frames arriving with the same count).
*/
public int framesBehind() {
return botWrapper == null ? 0 : Math.max(0, client.liveClientData().gameData().getFrameCount() - getGame().getFrameCount());
}

/**
* For internal test use.
*/
Client getClient() {
return client;
}

/**
* Start the game with default settings.
*/
public void startGame() {
startGame(false);
BWClientConfiguration configuration = new BWClientConfiguration();
startGame(configuration);
}

/**
* Start the game.
*
* @param autoContinue automatically continue playing the next game(s). false by default
*/
@Deprecated
public void startGame(boolean autoContinue) {
Client client = new Client(debugConnection);
BWClientConfiguration configuration = new BWClientConfiguration();
configuration.withAutoContinue(autoContinue);
startGame(configuration);
}

/**
* Start the game.
*
* @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);

// 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);
}

if (client == null) {
client = new Client(this);
}
client.reconnect();
handler = new EventHandler(eventListener, client);

do {
while (!getGame().isInGame()) {
ClientData.GameData liveGameData = client.liveClientData().gameData();
while (!liveGameData.isInGame()) {
if (!client.isConnected()) {
return;
}
client.update(handler);
client.sendFrameReceiveFrame();
if (liveGameData.isInGame()) {
performanceMetrics = new PerformanceMetrics(configuration);
botWrapper.startNewGame(client.mapFile(), performanceMetrics);
}
}
while (getGame().isInGame()) {
client.update(handler);
while (liveGameData.isInGame()) {
botWrapper.onFrame();
performanceMetrics.getFlushSideEffects().time(() -> getGame().sideEffects.flushTo(liveGameData));
performanceMetrics.getFrameDurationReceiveToSend().stopTiming();

client.sendFrameReceiveFrame();
if (!client.isConnected()) {
System.out.println("Reconnecting...");
client.reconnect();
}
}
} while (autoContinue); // lgtm [java/constant-loop-condition]
botWrapper.endGame();
} while (configuration.getAutoContinue());
}

/**
* Provides a Client. Intended for test consumers only.
*/
void setClient(Client client) {
this.client = client;
}
}
163 changes: 163 additions & 0 deletions src/main/java/bwapi/BWClientConfiguration.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package bwapi;

/**
* Configuration for constructing a BWClient
*/
public class BWClientConfiguration {

/**
* Set to `true` for more explicit error messages (which might spam the terminal).
*/
public BWClientConfiguration withDebugConnection(boolean value) {
throwIfLocked();
debugConnection = value;
return this;
}
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) {
System.out.println(value);
}
}
}
Loading

0 comments on commit d21d42e

Please sign in to comment.