com.github.ocraft.s2client.api.S2Client Maven / Gradle / Ivy
package com.github.ocraft.s2client.api;
/*-
* #%L
* ocraft-s2client-api
* %%
* Copyright (C) 2017 - 2018 Ocraft Project
* %%
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
* #L%
*/
import com.github.ocraft.s2client.api.controller.S2Controller;
import com.github.ocraft.s2client.api.log.DataFlowTracer;
import com.github.ocraft.s2client.api.syntax.OptionsSyntax;
import com.github.ocraft.s2client.api.syntax.S2ClientSyntax;
import com.github.ocraft.s2client.api.syntax.StartSyntax;
import com.github.ocraft.s2client.api.syntax.WithTracerSyntax;
import com.github.ocraft.s2client.api.vertx.VertxChannelProvider;
import com.github.ocraft.s2client.protocol.BuilderSyntax;
import com.github.ocraft.s2client.protocol.RequestSerializer;
import com.github.ocraft.s2client.protocol.ResponseParser;
import com.github.ocraft.s2client.protocol.request.Request;
import com.github.ocraft.s2client.protocol.response.Response;
import com.github.ocraft.s2client.protocol.response.ResponseError;
import com.github.ocraft.s2client.protocol.response.ResponseType;
import io.reactivex.BackpressureStrategy;
import io.reactivex.Flowable;
import io.reactivex.Maybe;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.MaybeSubject;
import io.reactivex.subscribers.DefaultSubscriber;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Optional;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import static com.github.ocraft.s2client.api.OcraftApiConfig.CLIENT_BUFFER_SIZE_RESPONSE_STREAM;
import static com.github.ocraft.s2client.api.OcraftApiConfig.cfg;
import static com.github.ocraft.s2client.protocol.Preconditions.isSet;
import static com.github.ocraft.s2client.protocol.Preconditions.require;
import static java.lang.String.format;
public class S2Client extends DefaultSubscriber {
private final Logger log = LoggerFactory.getLogger(S2Client.class);
private final ChannelProvider channelProvider = new VertxChannelProvider();
private final Flowable responseStream;
private final String connectToIp;
private final Integer connectToPort;
private final int requestTimeoutInMillis;
private final int connectTimeoutInMillis;
private final AtomicBoolean done = new AtomicBoolean(false);
private final boolean traced;
private final DataFlowTracer tracer;
private final Phaser await = new Phaser(1);
private final S2Controller game;
public static class Builder implements S2ClientSyntax, OptionsSyntax, WithTracerSyntax {
private String connectToIp = cfg().getString(OcraftApiConfig.CLIENT_NET_IP);
private Integer connectToPort = cfg().getInt(OcraftApiConfig.CLIENT_NET_PORT);
private boolean traced = cfg().getBoolean(OcraftApiConfig.CLIENT_TRACED);
private int requestTimeoutInMillis = cfg().getInt(OcraftApiConfig.CLIENT_NET_SYNCH_REQUEST_TIMEOUT);
private int connectTimeoutInMillis = cfg().getInt(OcraftApiConfig.CLIENT_NET_CONNECT_TIMEOUT);
private S2Controller game;
private DataFlowTracer tracer = new DataFlowTracer();
private Runnable onConnectionLost;
@Override
public OptionsSyntax connectTo(String gameListenIp, Integer gameListenPort) {
if (isSet(gameListenIp)) connectToIp = gameListenIp;
if (isSet(gameListenPort)) connectToPort = gameListenPort;
return this;
}
@Override
public OptionsSyntax connectTo(S2Controller theGame) {
require("game", theGame);
this.connectToIp = theGame.getConfig().getString(OcraftApiConfig.GAME_NET_IP);
this.connectToPort = theGame.getConfig().getInt(OcraftApiConfig.GAME_NET_PORT);
this.game = theGame;
return this;
}
@Override
public OptionsSyntax requestTimeout(Integer timeoutInMillis) {
if (isSet(timeoutInMillis)) this.requestTimeoutInMillis = timeoutInMillis;
return this;
}
@Override
public OptionsSyntax connectTimeout(Integer timeoutInMillis) {
if (isSet(timeoutInMillis)) this.connectTimeoutInMillis = timeoutInMillis;
return this;
}
@Override
public OptionsSyntax onConnectionLost(Runnable callback) {
if (isSet(callback)) this.onConnectionLost = callback;
return this;
}
@Override
public WithTracerSyntax traced(Boolean traced) {
if (isSet(traced)) this.traced = traced;
return this;
}
@Override
public StartSyntax withTracer(DataFlowTracer tracer) {
if (isSet(tracer)) this.tracer = tracer;
return this;
}
@Override
public S2Client start() {
require("ip", connectToIp);
require("port", connectToPort);
if (traced) require("data flow tracer", tracer);
return new S2Client(this);
}
}
public static S2ClientSyntax starcraft2Client() {
return new Builder();
}
private S2Client(Builder builder) {
connectToIp = builder.connectToIp;
connectToPort = builder.connectToPort;
requestTimeoutInMillis = builder.requestTimeoutInMillis;
connectTimeoutInMillis = builder.connectTimeoutInMillis;
traced = builder.traced;
tracer = builder.tracer;
game = builder.game;
log.info("Starting: {}", this);
Channel channel = channelProvider.getChannel();
channel.onConnectionLost(builder.onConnectionLost);
responseStream = channel.outputStream().mergeWith(channel.errorStream())
.map(this::prepareResponse)
.toFlowable(BackpressureStrategy.ERROR)
.onBackpressureBuffer(cfg().getInt(OcraftApiConfig.CLIENT_BUFFER_SIZE_RESPONSE_BACKPRESSURE))
.observeOn(Schedulers.computation(), false, cfg().getInt(CLIENT_BUFFER_SIZE_RESPONSE_STREAM))
.publish()
.autoConnect()
.doOnSubscribe(s -> await.register())
.doOnCancel(await::arriveAndDeregister);
responseStream().subscribe(this);
await.arriveAndDeregister();
Optional.ofNullable(game).ifPresent(s2Controller -> {
responseStream().subscribe(s2Controller);
await.arriveAndDeregister();
});
channelProvider.start(connectToIp, connectToPort, connectTimeoutInMillis);
}
private Response prepareResponse(byte[] responseBytes) {
try {
Response response = new ResponseParser().apply(responseBytes);
if (traced) tracer.fire(response);
return response;
} catch (IllegalArgumentException e) {
throw new ResponseParseException(e);
}
}
public Flowable responseStream() {
return responseStream;
}
public void request(T requestData) {
if (!done.get()) {
require("request", requestData);
if (traced) tracer.fire(requestData);
channelProvider.getChannel().input(new RequestSerializer().apply(requestData));
} else {
throw new IllegalStateException("Client is already stopped.");
}
}
public void request(BuilderSyntax requestDataBuilder) {
request(requestDataBuilder.build());
}
public Response requestSync(T requestData) {
Maybe responseMaybe = waitForResponse(requestData.responseType());
request(requestData);
return responseMaybe.blockingGet();
}
public Maybe waitForResponse(ResponseType responseType) {
MaybeSubject maybeSubject = MaybeSubject.create();
responseStream()
.filter(response -> response.is(responseType) || response.is(ResponseType.ERROR))
.firstElement()
.timeout(requestTimeoutInMillis, TimeUnit.MILLISECONDS)
.subscribe(maybeSubject);
return maybeSubject;
}
public Response requestSync(BuilderSyntax requestDataBuilder) {
return requestSync(requestDataBuilder.build());
}
public S requestSync(T requestData, Class responseClass) {
Response response = requestSync(requestData);
response.as(ResponseError.class).ifPresent(responseError -> {
throw ResponseErrorException.from(responseError);
});
return responseClass.cast(response);
}
public S requestSync(
BuilderSyntax requestDataBuilder, Class responseClass) {
return requestSync(requestDataBuilder.build(), responseClass);
}
public boolean isDone() {
return done.get();
}
public boolean stop() {
log.debug("S2Client: STOP");
if (done.compareAndSet(false, true)) {
channelProvider.stop();
return true;
} else {
return false;
}
}
public boolean fullStop() {
boolean status = stop();
if (isSet(game)) {
game.stop();
}
return status;
}
public boolean await() {
await.arriveAndAwaitAdvance();
return fullStop();
}
Channel channel() {
return channelProvider.getChannel();
}
@Override
public void onNext(Response response) {
// Client is passive, nothing to do here.
}
@Override
public void onError(Throwable throwable) {
if (!(throwable instanceof ResponseParseException)) {
stop();
}
}
@Override
public void onComplete() {
stop();
}
public String getConnectToIp() {
return connectToIp;
}
public Integer getConnectToPort() {
return connectToPort;
}
public int getRequestTimeoutInMillis() {
return requestTimeoutInMillis;
}
public int getConnectTimeoutInMillis() {
return connectTimeoutInMillis;
}
public boolean isTraced() {
return traced;
}
public S2Client untilReady() throws TimeoutException {
return untilReady(() -> {
});
}
public S2Client untilReady(Runnable onPull) throws TimeoutException {
try {
Boolean connectionResult = pullConnectionStatus(onPull);
if (isDone()) {
throw new IllegalStateException("Client is already closed.");
}
if (!connectionResult) {
throw new IllegalStateException("Connection failed.");
}
return this;
} catch (TimeoutException e) {
throw new TimeoutException(format("connection timeout exceeded: %d ms", connectTimeoutInMillis));
} catch (InterruptedException e) {
log.debug("Thread was interrupted.", e);
Thread.currentThread().interrupt();
throw new IllegalStateException(e);
} catch (ExecutionException e) {
throw new IllegalStateException(e);
}
}
private Boolean pullConnectionStatus(Runnable onPull)
throws InterruptedException, ExecutionException, TimeoutException {
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
try {
CompletableFuture completionFuture = new CompletableFuture<>();
final ScheduledFuture> checkFuture = executor.scheduleAtFixedRate(() -> {
onPull.run();
if (isDone() || channel().ready()) {
completionFuture.complete(true);
}
}, 0, 1, TimeUnit.SECONDS);
return completionFuture
.whenComplete((result, thrown) -> checkFuture.cancel(true))
.exceptionally(throwable -> completionFuture.complete(false))
.get(connectTimeoutInMillis, TimeUnit.MILLISECONDS);
} finally {
executor.shutdownNow();
}
}
@Override
public String toString() {
return "S2Client{" +
"connectToIp='" + connectToIp + '\'' +
", connectToPort=" + connectToPort +
", requestTimeoutInMillis=" + requestTimeoutInMillis +
", connectTimeoutInMillis=" + connectTimeoutInMillis +
", done=" + done +
", traced=" + traced +
'}';
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy