-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #60 from dgant/async
Asynchronous mode
- Loading branch information
Showing
31 changed files
with
4,128 additions
and
2,390 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
Oops, something went wrong.