-
Notifications
You must be signed in to change notification settings - Fork 10
Asynchronous mode #60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3808f73
0cae24e
486bf4f
43c9ac3
165db79
79f6fe0
d4d03b6
fd921e8
931beb1
5c9dc36
341056d
30e7a52
5b36174
156c382
4cf39fc
3159f73
be52a24
edddd2b
24aa1cb
80d37cf
d8e045a
4be2706
c30289b
1970dd5
20f9d00
af4c986
0e35168
93a9791
cb5a3fd
4520108
879f6cf
8bb4f80
512795b
d016e03
71dbc8d
b735923
024630b
a333f15
ab695ad
ebae46c
b40e612
aa17f4f
dcaaa12
e021d19
736ff2c
26ee6a2
30d6792
4bbef45
00b1266
04db785
e76bed2
c193d9f
a078758
a7f2f08
34e5d4c
43f5bd5
bcaafb9
cdb67d5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Iis this tested in the synchro test? impl looks good to me, but just making sure There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, there are a few tests which verify its values. |
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This overload is replaced by one which takes a BWClientConfiguration to indicate thhe autoContinue setting. |
||
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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
package bwapi; | ||
|
||
/** | ||
* Configuration for constructing a BWClient | ||
*/ | ||
public class BWClientConfiguration { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd prefer the variables to be private and modifiable using a example API in my mind: BWClientConfiguration bwc = new BWClientConfiguration()
.withAutoContinue()
.withAsync()
.withMaxFrameDuration(30); /**Javadoc*/
public BWClientConfiguration withMaxFrameDuration(int value) {
if (value < 1) {
throw new IllegalArgumentException("maxFrameDuration needs to be a non-negative number (it's how long JBWAPI waits for a bot response before returning control to BWAPI).");
}
this.maxFrameDuration = value;
return this;
} This way we don't need a "validate" method as we can check the arguments on creating the configuration There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will do, although I think the validate method is still necessary since it considers interactions between properties and it would be annoying to require them to be set in a specific order, and it doesn't prevent modification of the properties. |
||
|
||
/** | ||
* 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); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
debugConnection moved to BWClientConfiguration