Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 12
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
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
3808f730cae24e486bf4f43c9ac3165db7979f6fe0d4d03b6fd921e8931beb15c9dc36341056d30e7a525b36174156c3824cf39fc3159f73be52a24edddd2b24aa1cb80d37cfd8e045a4be2706c30289b1970dd520f9d00af4c9860e3516893a9791cb5a3fd4520108879f6cf8bb4f80512795bd016e0371dbc8db735923024630ba333f15ab695adebae46cb40e612aa17f4fdcaaa12e021d19736ff2c26ee6a230d67924bbef4500b126604db785e76bed2c193d9fa078758a7f2f0834e5d4c43f5bd5bcaafb9cdb67d5File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
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() { | ||
Collaborator 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 ContributorAuthor 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 | ||
ContributorAuthor 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 { | ||
Collaborator 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: BWClientConfigurationbwc = newBWClientConfiguration()
.withAutoContinue()
.withAsync()
.withMaxFrameDuration(30);/**Javadoc*/publicBWClientConfigurationwithMaxFrameDuration(intvalue) {
if (value < 1) {
thrownewIllegalArgumentException("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;
returnthis;
}This way we don't need a "validate" method as we can check the arguments on creating the configuration ContributorAuthor 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); | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
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