Multi-threaded and non-blocking Starcraft 2 API client with fluent and reactive interface.
-
Full access to StarCraft 2 API that was made available by Blizzard → https://github.com/Blizzard/s2client-proto.
-
Fluent and reactive interface.
-
All objects are thread safe - you do not have to synchronize access to them because your request and responses are immutable.
-
Prepared to working in highly concurrent and multi-threaded environments.
-
Asynchronous and non-blocking architecture without putting threads to sleep.
-
You can have as many subscribers as you want, they all work on predefined thread pool.
-
You can enable monitoring of your communication in JSON format that is easy to process and analyze in many different tools.
<dependency>
<groupId>com.github.ocraft</groupId>
<artifactId>ocraft-s2client-api</artifactId>
<version>0.4.20</version>
</dependency>
S2Controller game = starcraft2Game().launch(); // (1)
S2Client client = starcraft2Client().connectTo(game).traced(true).start(); // (2)
client.request(createGame()
.onBattlenetMap(BattlenetMap.of("Lava Flow"))
.withPlayerSetup(participant(), computer(PROTOSS, Difficulty.MEDIUM))); // (3)
client.responseStream()
.takeWhile(Responses.isNot(ResponseLeaveGame.class))
.subscribe(response -> { // (4)
response.as(ResponseCreateGame.class).ifPresent(r -> client.request(joinGame().as(TERRAN)));
response.as(ResponseJoinGame.class).ifPresent(r -> {
client.request(actions().of(
action().raw(unitCommand().forUnits(Tag.of(COMMAND_CENTER)).useAbility(TRAIN_SCV)),
action().raw(cameraMove().to(Point.of(10, 10))),
action().featureLayer(click().on(PointI.of(15, 10)).withMode(TOGGLE)),
action().ui(selectArmy().add())
)); // (5)
client.request(leaveGame());
});
});
client.await(); // (6)
-
Launch StarCraft 2 with automatic configuration discovery.
-
Connect new client to the game and enable tracing of all data flow in json format.
-
Create new game using fluent api.
-
Subscribe to stream of responses.
-
Choose your action in the game.
-
Wait for all threads to finish their job.
After successful connection to the game you have to do two main things:
-
Send some requests.
-
Observe and react to game responses.
First one you achieve by invoking com.github.ocraft.s2client.api.S2Client#request(T) method. Second one is done by observing response stream that is based on rxjava2: com.github.ocraft.s2client.api.S2Client#responseStream. All requests inherit from com.github.ocraft.s2client.protocol.request.Request and are located in com.github.ocraft.s2client.protocol.request package. Interface com.github.ocraft.s2client.protocol.request.Requests provides entry point for building all of possible requests. Building of some requests require more work than the others: actions(), debug(), query(). For them there are another entry points that should allow to easy browsing of possible options. There are:
-
for actions(): com.github.ocraft.s2client.protocol.action.Actions,
-
for debug(): com.github.ocraft.s2client.protocol.debug.Commands,
-
for query(): com.github.ocraft.s2client.protocol.query.Queries.
Responses inherits from com.github.ocraft.s2client.protocol.response.Response class and are located in com.github.ocraft.s2client.protocol.response package. There are two different way to identify what type of response is:
-
checking instance type using instanceof operator,
-
checking type of response by invoking com.github.ocraft.s2client.protocol.response.Response#getType.
There are also several enum classes with different type of data:
-
com.github.ocraft.s2client.protocol.data.Units,
-
com.github.ocraft.s2client.protocol.data.Effects,
-
com.github.ocraft.s2client.protocol.data.Buffs,
-
com.github.ocraft.s2client.protocol.data.Upgrades,
-
com.github.ocraft.s2client.protocol.data.Abilities.
actions() |
Executes an action for a participant. |
observerActions() |
Executes an action for an observer. |
availableMaps() |
Returns directory of maps that can be played on. |
createGame() |
Send to host to initialize game. |
data() |
Data about different gameplay elements. May be different for different games. |
debug() |
Display debug information and execute debug actions. |
gameInfo() |
Static data about the current game and map. |
joinGame() |
Send to host and all clients for game to begin. |
leaveGame() |
Multiplayer only. Disconnects from a multiplayer game, equivalent to surrender. |
observation() |
Snapshot of the current game state. |
ping() |
Network ping for testing connection. |
query() |
Additional methods for inspecting game state. |
quickLoad() |
Loads from an in-memory bookmark. |
quickSave() |
Saves game to an in-memory bookmark. |
quitGame() |
Terminates the application. |
replayInfo() |
Returns metadata about a replay file. Does not load the replay. |
restartGame() |
Single player only. Reinitializes the game with the same player setup. |
saveMap() |
Saves binary map data to the local temp directory. |
saveReplay() |
Generates a replay. |
startReplay() |
Start playing a replay. |
nextStep() |
Advances the game simulation. |
message() |
Chat messages as a player typing into the chat channel. |
Actions.Raw |
unitCommand(), cameraMove(), toggleAutocast() |
Actions.Spatial |
unitCommand(), cameraMove(), click(), select() |
Actions.Ui |
controlGroup(), selectArmy(), selectWarpGates(), selectLarva(), selectIdleWorker(), multiPanel(), cargoPanelUnload(), removeFromQueue(), toggleAutocast() |
Observer |
playerPerspective(), cameraMove(), cameraFollowPlayer(), cameraFollowUnits() |
Commands |
draw(), createUnit(), killUnit(), testProcess(), setScore(), endGame(), setUnitValue() |
Commands.Draw |
text(), line(), box(), sphere() |
Queries |
path(), placeBuilding(), availableAbilities() |
Configuration is provided using typesafe config. That means that you can use default options or override them by system properties, your own config files or using programming api.
All logs are provided using slf4j binding. If you want to get full data flow monitoring in JSON format you must do two things:
-
Enable tracing either by using library api (starcraft2Client().connectTo(game).traced(true)), or by configuration file/system property (ocraft.client.traced=true).
-
Append your logger at trace level for class com.github.ocraft.s2client.api.log.DataFlowTracer. For example in log4j:
<Logger name="com.github.ocraft.s2client.api.log.DataFlowTracer" level="trace" additivity="false">
<AppenderRef ref="Tracer"/>
</Logger>
You will get full request/response in JSON format, that can be loaded to many different tools, like grafana or kibana for further analysis.
{"ResponseObservation":{"type":"OBSERVATION","status":"IN_REPLAY","nanoTime":2273482598073,"actions":[{"featureLayer":{"unitSelectionPoint":{"selectionInScreenCoord":{"x":33,"y":34}...
S2Controller game = starcraft2Game().launch();
S2Client client = starcraft2Client().connectTo(game).traced(true).start();
client.request(replayInfo().of(REPLAY_PATH).download());
client.responseStream()
.takeWhile(Responses.isNot(ResponseType.START_REPLAY))
.subscribe(response -> response.as(ResponseReplayInfo.class).ifPresent(r -> {
r.getReplayInfo()
.ifPresent(info -> game.relaunchIfNeeded(info.getBaseBuild(), info.getDataVersion()));
client.request(startReplay()
.from(REPLAY_PATH).use(defaultInterfaces()).toObserve(PLAYER_ID).disableFog());
}));
client.responseStream()
.takeWhile(response -> !game.inState(GameStatus.ENDED))
.subscribe(response -> {
response.as(ResponseStartReplay.class).ifPresent(r -> client.request(observation()));
response.as(ResponseObservation.class).ifPresent(r -> {
client.request(nextStep().withCount(GAME_LOOP_COUNT));
client.request(observation());
});
});
client.await();
PortSetup portSetup = PortSetup.init(5000);
S2Controller game01 = starcraft2Game().withPort(portSetup.fetchPort()).launch();
S2Client client01 = starcraft2Client().connectTo(game01).traced(true).start();
S2Controller game02 = starcraft2Game().withPort(portSetup.fetchPort()).launch();
S2Client client02 = starcraft2Client().connectTo(game02).traced(true).start();
client01.request(createGame()
.onBattlenetMap(BattlenetMap.of("Lava Flow"))
.withPlayerSetup(participant(), participant()).realTime());
MultiplayerOptions multiplayerOptions = multiplayerSetupFor(portSetup.lastPort(), PLAYER_COUNT);
client01.request(joinGame().as(PROTOSS).use(interfaces().raw()).with(multiplayerOptions));
client02.request(joinGame().as(ZERG).use(interfaces().raw()).with(multiplayerOptions));
client01.responseStream()
.takeWhile(Responses.isNot(ResponseType.QUIT_GAME))
.subscribe(response -> {
response.as(ResponseJoinGame.class).ifPresent(r -> client01.request(leaveGame()));
response.as(ResponseLeaveGame.class).ifPresent(r -> client01.request(quitGame()));
});
client02.responseStream()
.takeWhile(Responses.isNot(ResponseType.QUIT_GAME))
.subscribe(response -> {
response.as(ResponseJoinGame.class).ifPresent(r -> client02.request(leaveGame()));
response.as(ResponseLeaveGame.class).ifPresent(r -> client02.request(quitGame()));
});
client01.await();
client02.await();