Skip to content

Commit d21d42e

Browse files
authored
Merge pull request #60 from dgant/async
Asynchronous mode
2 parents d2f252b + cdb67d5 commit d21d42e

31 files changed

+4128
-2390
lines changed

pom.xml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,10 @@
6161
<artifactId>maven-surefire-plugin</artifactId>
6262
<version>2.22.2</version>
6363
<configuration>
64-
<parallel>all</parallel> <!-- Run tests in parallel-->
65-
<useUnlimitedThreads>true</useUnlimitedThreads>
64+
<!-- <parallel>all</parallel> &lt;!&ndash; Run tests in parallel&ndash;&gt;-->
65+
<!-- <useUnlimitedThreads>true</useUnlimitedThreads>-->
66+
<rerunFailingTestsCount>10</rerunFailingTestsCount>
67+
<argLine>-Xms1g -Xmx1g</argLine>
6668
</configuration>
6769
</plugin>
6870
<plugin>

src/main/java/bwapi/BWClient.java

Lines changed: 98 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,142 @@
11
package bwapi;
22

3+
import com.sun.jna.platform.win32.Kernel32;
4+
35
import java.util.Objects;
46

57
/**
68
* Client class to connect to the game with.
79
*/
810
public class BWClient {
11+
private BWClientConfiguration configuration = new BWClientConfiguration();
912
private final BWEventListener eventListener;
10-
private final boolean debugConnection;
11-
private EventHandler handler;
13+
private BotWrapper botWrapper;
14+
private Client client;
15+
private PerformanceMetrics performanceMetrics;
1216

1317
public BWClient(final BWEventListener eventListener) {
14-
this(eventListener, false);
15-
}
16-
17-
/**
18-
* @param debugConnection set to `true` for more explicit error messages (might spam the terminal).
19-
* `false` by default
20-
*/
21-
public BWClient(final BWEventListener eventListener, final boolean debugConnection) {
2218
Objects.requireNonNull(eventListener);
23-
this.debugConnection = debugConnection;
2419
this.eventListener = eventListener;
2520
}
2621

2722
/**
2823
* Get the {@link Game} instance of the currently running game.
24+
* When running in asynchronous mode, this is the game from the bot's perspective, eg. potentially a previous frame.
2925
*/
3026
public Game getGame() {
31-
return handler == null ? null : handler.getGame();
27+
return botWrapper == null ? null : botWrapper.getGame();
28+
}
29+
30+
/**
31+
* @return JBWAPI performance metrics.
32+
*/
33+
public PerformanceMetrics getPerformanceMetrics() {
34+
return performanceMetrics;
35+
}
36+
37+
/**
38+
* @return The current configuration
39+
*/
40+
public BWClientConfiguration getConfiguration() {
41+
return configuration;
42+
}
43+
44+
/**
45+
* @return Whether the current frame should be subject to timing.
46+
*/
47+
boolean doTime() {
48+
return ! configuration.getUnlimitedFrameZero() || (client.isConnected() && client.liveClientData().gameData().getFrameCount() > 0);
49+
}
50+
51+
/**
52+
* @return The number of frames between the one exposed to the bot and the most recent received by JBWAPI.
53+
* This tracks the size of the frame buffer except when the game is paused (which results in multiple frames arriving with the same count).
54+
*/
55+
public int framesBehind() {
56+
return botWrapper == null ? 0 : Math.max(0, client.liveClientData().gameData().getFrameCount() - getGame().getFrameCount());
3257
}
3358

59+
/**
60+
* For internal test use.
61+
*/
62+
Client getClient() {
63+
return client;
64+
}
65+
66+
/**
67+
* Start the game with default settings.
68+
*/
3469
public void startGame() {
35-
startGame(false);
70+
BWClientConfiguration configuration = new BWClientConfiguration();
71+
startGame(configuration);
3672
}
3773

3874
/**
3975
* Start the game.
4076
*
4177
* @param autoContinue automatically continue playing the next game(s). false by default
4278
*/
79+
@Deprecated
4380
public void startGame(boolean autoContinue) {
44-
Client client = new Client(debugConnection);
81+
BWClientConfiguration configuration = new BWClientConfiguration();
82+
configuration.withAutoContinue(autoContinue);
83+
startGame(configuration);
84+
}
85+
86+
/**
87+
* Start the game.
88+
*
89+
* @param gameConfiguration Settings for playing games with this client.
90+
*/
91+
public void startGame(BWClientConfiguration gameConfiguration) {
92+
gameConfiguration.validateAndLock();
93+
this.configuration = gameConfiguration;
94+
this.performanceMetrics = new PerformanceMetrics(configuration);
95+
botWrapper = new BotWrapper(configuration, eventListener);
96+
97+
// Use reduced priority to encourage Windows to give priority to StarCraft.exe/BWAPI.
98+
// If BWAPI doesn't get priority, it may not detect completion of a frame on our end in timely fashion.
99+
Thread.currentThread().setName("JBWAPI Client");
100+
if (configuration.getAsync()) {
101+
Thread.currentThread().setPriority(4);
102+
}
103+
104+
if (client == null) {
105+
client = new Client(this);
106+
}
45107
client.reconnect();
46-
handler = new EventHandler(eventListener, client);
47108

48109
do {
49-
while (!getGame().isInGame()) {
110+
ClientData.GameData liveGameData = client.liveClientData().gameData();
111+
while (!liveGameData.isInGame()) {
50112
if (!client.isConnected()) {
51113
return;
52114
}
53-
client.update(handler);
115+
client.sendFrameReceiveFrame();
116+
if (liveGameData.isInGame()) {
117+
performanceMetrics = new PerformanceMetrics(configuration);
118+
botWrapper.startNewGame(client.mapFile(), performanceMetrics);
119+
}
54120
}
55-
while (getGame().isInGame()) {
56-
client.update(handler);
121+
while (liveGameData.isInGame()) {
122+
botWrapper.onFrame();
123+
performanceMetrics.getFlushSideEffects().time(() -> getGame().sideEffects.flushTo(liveGameData));
124+
performanceMetrics.getFrameDurationReceiveToSend().stopTiming();
125+
126+
client.sendFrameReceiveFrame();
57127
if (!client.isConnected()) {
58128
System.out.println("Reconnecting...");
59129
client.reconnect();
60130
}
61131
}
62-
} while (autoContinue); // lgtm [java/constant-loop-condition]
132+
botWrapper.endGame();
133+
} while (configuration.getAutoContinue());
134+
}
135+
136+
/**
137+
* Provides a Client. Intended for test consumers only.
138+
*/
139+
void setClient(Client client) {
140+
this.client = client;
63141
}
64142
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package bwapi;
2+
3+
/**
4+
* Configuration for constructing a BWClient
5+
*/
6+
public class BWClientConfiguration {
7+
8+
/**
9+
* Set to `true` for more explicit error messages (which might spam the terminal).
10+
*/
11+
public BWClientConfiguration withDebugConnection(boolean value) {
12+
throwIfLocked();
13+
debugConnection = value;
14+
return this;
15+
}
16+
public boolean getDebugConnection() {
17+
return debugConnection;
18+
}
19+
private boolean debugConnection;
20+
21+
/**
22+
* When true, restarts the client loop when a game ends, allowing the client to play multiple games without restarting.
23+
*/
24+
public BWClientConfiguration withAutoContinue(boolean value) {
25+
throwIfLocked();
26+
autoContinue = value;
27+
return this;
28+
}
29+
public boolean getAutoContinue() {
30+
return autoContinue;
31+
}
32+
private boolean autoContinue = false;
33+
34+
/**
35+
* 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,
36+
* as the bot has no prior access to BWAPI or game information.
37+
*
38+
* This flag indicates that taking arbitrarily long on frame zero is acceptable.
39+
* Performance metrics omit the frame as an outlier.
40+
* Asynchronous operation will block until the bot's event handlers are complete.
41+
*/
42+
public BWClientConfiguration withUnlimitedFrameZero(boolean value) {
43+
throwIfLocked();
44+
unlimitedFrameZero = value;
45+
return this;
46+
}
47+
public boolean getUnlimitedFrameZero() {
48+
return unlimitedFrameZero;
49+
}
50+
private boolean unlimitedFrameZero = true;
51+
52+
/**
53+
* The maximum amount of time the bot is supposed to spend on a single frame.
54+
* 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.
55+
* In synchronous mode, JBWAPI is not empowered to prevent the bot to exceed this amount, but will record overruns in performance metrics.
56+
* Real-time human play typically uses the "fastest" game speed, which has 42.86ms (42,860ns) between frames.
57+
*/
58+
public BWClientConfiguration withMaxFrameDurationMs(int value) {
59+
throwIfLocked();
60+
maxFrameDurationMs = value;
61+
return this;
62+
}
63+
public int getMaxFrameDurationMs() {
64+
return maxFrameDurationMs;
65+
}
66+
private int maxFrameDurationMs = 40;
67+
68+
/**
69+
* Runs the bot in asynchronous mode. Asynchronous mode helps attempt to ensure that the bot adheres to real-time performance constraints.
70+
*
71+
* Humans playing StarCraft (and some tournaments) expect bots to return commands within a certain period of time; ~42ms for humans ("fastesT" game speed),
72+
* and some tournaments enforce frame-wise time limits (at time of writing, 55ms for COG and AIIDE; 85ms for SSCAIT).
73+
*
74+
* 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
75+
* returns control to StarCraft, allowing the game to proceed while the bot continues to step in the background. This increases the likelihood of meeting
76+
* 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
77+
* issuing commands later than intended, and a marginally larger memory footprint.
78+
*
79+
* Asynchronous mode is not compatible with latency compensation. Enabling asynchronous mode automatically disables latency compensation.
80+
*/
81+
public BWClientConfiguration withAsync(boolean value) {
82+
throwIfLocked();
83+
async = value;
84+
return this;
85+
}
86+
public boolean getAsync() {
87+
return async;
88+
}
89+
private boolean async = false;
90+
91+
/**
92+
* The maximum number of frames to buffer while waiting on a bot.
93+
* Each frame buffered adds about 33 megabytes to JBWAPI's memory footprint.
94+
*/
95+
public BWClientConfiguration withAsyncFrameBufferCapacity(int size) {
96+
throwIfLocked();
97+
asyncFrameBufferCapacity = size;
98+
return this;
99+
}
100+
public int getAsyncFrameBufferCapacity() {
101+
return asyncFrameBufferCapacity;
102+
}
103+
private int asyncFrameBufferCapacity = 10;
104+
105+
/**
106+
* Enables thread-unsafe async mode.
107+
* In this mode, the bot is allowed to read directly from shared memory until shared memory has been copied into the frame buffer,
108+
* at wihch point the bot switches to using the frame buffer.
109+
* This should enhance performance by allowing the bot to act while the frame is copied, but poses unidentified risk due to
110+
* the non-thread-safe switc from shared memory reads to frame buffer reads.
111+
*/
112+
public BWClientConfiguration withAsyncUnsafe(boolean value) {
113+
throwIfLocked();
114+
asyncUnsafe = value;
115+
return this;
116+
}
117+
public boolean getAsyncUnsafe() {
118+
return asyncUnsafe;
119+
}
120+
private boolean asyncUnsafe = false;
121+
122+
/**
123+
* Toggles verbose logging, particularly of synchronization steps.
124+
*/
125+
public BWClientConfiguration withLogVerbosely(boolean value) {
126+
throwIfLocked();
127+
logVerbosely = value;
128+
return this;
129+
}
130+
public boolean getLogVerbosely() {
131+
return logVerbosely;
132+
}
133+
private boolean logVerbosely = false;
134+
135+
/**
136+
* Checks that the configuration is in a valid state. Throws an IllegalArgumentException if it isn't.
137+
*/
138+
void validateAndLock() {
139+
if (asyncUnsafe && ! async) {
140+
throw new IllegalArgumentException("asyncUnsafe mode needs async mode.");
141+
}
142+
if (async && maxFrameDurationMs < 0) {
143+
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).");
144+
}
145+
if (async && asyncFrameBufferCapacity < 1) {
146+
throw new IllegalArgumentException("asyncFrameBufferCapacity needs to be a positive number (There needs to be at least one frame buffer).");
147+
}
148+
locked = true;
149+
}
150+
private boolean locked = false;
151+
152+
void throwIfLocked() {
153+
if (locked) {
154+
throw new RuntimeException("Configuration can not be modified after the game has started");
155+
}
156+
}
157+
158+
void log(String value) {
159+
if (logVerbosely) {
160+
System.out.println(value);
161+
}
162+
}
163+
}

0 commit comments

Comments
 (0)