All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.ocraft.s2client.api.controller.S2Controller Maven / Gradle / Ivy

package com.github.ocraft.s2client.api.controller;

/*-
 * #%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.protocol.game.GameStatus;
import com.github.ocraft.s2client.protocol.response.Response;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import io.reactivex.subscribers.DefaultSubscriber;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import static com.github.ocraft.s2client.api.OcraftApiConfig.*;
import static com.github.ocraft.s2client.protocol.Preconditions.isSet;
import static java.lang.String.format;

public class S2Controller extends DefaultSubscriber {

    private static Logger log = LoggerFactory.getLogger(S2Controller.class);

    private static final int CONNECTION_TIMEOUT_IN_MILLIS = cfg().getInt(GAME_NET_TIMEOUT);
    private static final int MAX_CONNECTION_TRY_COUNT = cfg().getInt(GAME_NET_RETRY_COUNT);
    private static final int STOP_PROCESS_TIMEOUT_IN_MILLIS = 4000;

    private Config cfg;

    private Process s2Process;
    private int tryCount;
    private GameStatus lastGameStatus;

    private S2Controller(Config config) {
        this.cfg = config;
    }

    public static class Builder {

        private static int instanceCount = 0;

        private Map builderConfig = new HashMap<>();

        private Builder() {
            Config referenceConfig = cfg();
            prepareWindowPosition(referenceConfig);
            instanceCount++;
        }

        // TODO p.picheta move this outside?
        private void prepareWindowPosition(Config referenceConfig) {
            int w = referenceConfig.getInt(GAME_WINDOW_W);
            int h = referenceConfig.getInt(GAME_WINDOW_H);
            int x = referenceConfig.getInt(GAME_WINDOW_X);
            int y = referenceConfig.getInt(GAME_WINDOW_Y);

            int columnIsEven = instanceCount % 2;
            int rowIsEven = (int) Math.floor(instanceCount / 2.0) % 2;

            builderConfig.put(GAME_WINDOW_X, x + w * columnIsEven);
            builderConfig.put(GAME_WINDOW_Y, y + h * rowIsEven);
        }

        public Builder withExecutablePath(Path executablePath) {
            if (isSet(executablePath)) {
                builderConfig.put(GAME_EXE_PATH, executablePath.toString());
            }
            return this;
        }

        public Builder withListenIp(String gameListenIp) {
            if (isSet(gameListenIp)) builderConfig.put(GAME_NET_IP, gameListenIp);
            return this;
        }

        public Builder withPort(Integer gameListenPort) {
            if (isSet(gameListenPort)) builderConfig.put(GAME_NET_PORT, gameListenPort);
            return this;
        }

        public Builder withWindowSize(Integer w, Integer h) {
            if (isSet(w)) builderConfig.put(GAME_WINDOW_W, w);
            if (isSet(h)) builderConfig.put(GAME_WINDOW_H, h);
            return this;
        }

        public Builder withWindowPosition(Integer x, Integer y) {
            if (isSet(x)) builderConfig.put(GAME_WINDOW_X, x);
            if (isSet(y)) builderConfig.put(GAME_WINDOW_Y, y);
            return this;
        }

        public Builder withDataVersion(String dataVersion) {
            if (isSet(dataVersion)) builderConfig.put(GAME_EXE_DATA_VER, dataVersion);
            return this;
        }

        public Builder withBaseBuild(Integer baseBuild) {
            // TODO p.picheta to test
            if (isSet(baseBuild)) builderConfig.put(GAME_EXE_BUILD, ExecutableParser.BUILD_PREFIX + baseBuild);
            return this;
        }

        public Builder verbose(Boolean value) {
            if (isSet(value)) builderConfig.put(GAME_CLI_VERBOSE, String.valueOf(value));
            return this;
        }

        public Builder needsSupportDir(Boolean value) {
            if (isSet(value)) builderConfig.put(GAME_CLI_NEEDS_SUPPORT_DIR, String.valueOf(value));
            return this;
        }

        public Builder withTmpDir(Path tmpDir) {
            if (isSet(tmpDir)) builderConfig.put(GAME_CLI_TEMP_DIR, tmpDir.toString());
            return this;
        }

        public Builder withDataDir(Path dataDir) {
            if (isSet(dataDir)) builderConfig.put(GAME_CLI_DATA_DIR, dataDir.toString());
            return this;
        }

        public Builder withOsMesaPath(Path osMesaPath) {
            if (isSet(osMesaPath)) builderConfig.put(GAME_CLI_OS_MESA_PATH, osMesaPath.toString());
            return this;
        }

        public Builder withEglPath(Path eglPath) {
            if (isSet(eglPath)) builderConfig.put(GAME_CLI_EGL_PATH, eglPath.toString());
            return this;
        }

        public S2Controller launch() {
            return new S2Controller(getGameConfiguration()).launch();
        }

        Config getGameConfiguration() {

            Config referenceConfig = cfg();

            Config customConfig = builderConfig();
            String customPath = customConfig.hasPath(GAME_EXE_PATH) ? customConfig.getString(GAME_EXE_PATH) : null;
            String customDataVersion = customConfig.hasPath(GAME_EXE_DATA_VER)
                    ? customConfig.getString(GAME_EXE_DATA_VER)
                    : null;
            String customBaseBuild = customConfig.hasPath(GAME_EXE_BUILD)
                    ? customConfig.getString(GAME_EXE_BUILD)
                    : null;

            Map executableConfig = ExecutableParser.loadSettings(
                    customPath, customDataVersion, customBaseBuild);

            Config gameConfig = ConfigFactory.parseMap(executableConfig).withFallback(customConfig);
            gameConfig.checkValid(referenceConfig);

            return gameConfig;
        }

        public Config builderConfig() {
            return ConfigFactory.parseMap(builderConfig).withFallback(cfg());
        }

        static void reset() {
            instanceCount = 0;
        }

    }

    public static Builder starcraft2Game() {
        return new Builder();
    }

    private S2Controller launch() {
        log.info("Launching Starcraft II with configuration: {}.", cfg);
        try {
            Path gameRoot = Paths.get(cfg.getString(GAME_EXE_ROOT));
            String exeFile = gameRoot
                    .resolve(Paths.get(
                            ExecutableParser.VERSIONS_DIR,
                            cfg.getString(GAME_EXE_BUILD),
                            cfg.getString(GAME_EXE_FILE)))
                    .toString();

            List args = new ArrayList<>();
            args.add(exeFile);
            command(args, "-listen", GAME_NET_IP);
            command(args, "-port", GAME_NET_PORT);
            command(args, "-displayMode", GAME_WINDOW_MODE);
            command(args, "-dataVersion", GAME_EXE_DATA_VER);
            command(args, "-windowwidth", GAME_WINDOW_W);
            command(args, "-windowheight", GAME_WINDOW_H);
            command(args, "-windowx", GAME_WINDOW_X);
            command(args, "-windowy", GAME_WINDOW_Y);
            command(args, "-verbose", GAME_CLI_VERBOSE);
            command(args, "-tempDir", GAME_CLI_TEMP_DIR);
            command(args, "-dataDir", GAME_CLI_DATA_DIR);
            command(args, "-osmesapath", GAME_CLI_OS_MESA_PATH);
            command(args, "-eglpath", GAME_CLI_EGL_PATH);

            s2Process = new ProcessBuilder(args)
                    .redirectErrorStream(true)
                    .directory(cfg.hasPath(GAME_CLI_NEEDS_SUPPORT_DIR) && cfg.getBoolean(GAME_CLI_NEEDS_SUPPORT_DIR)
                            ? gameRoot.resolve(getSupportDirPath()).toFile() : null)
                    .start();

            log.info("Launched SC2 ({}), PID: {}", exeFile, s2Process.pid());

            return this;
        } catch (IOException e) {
            log.error("StarCraft 2 launching process error.", e);
            throw new StarCraft2ControllerException(
                    ControllerError.PROCESS_START_FAILED, "StarCraft 2 launching process error.", e);
        }
    }

    private void command(List args, String name, String value) {
        if (cfg.hasPath(value)) {
            args.add(name);
            args.add(cfg.getString(value));
        }
    }

    private Path getSupportDirPath() {
        return Paths.get(cfg.getBoolean(GAME_EXE_IS_64) ? "Support64" : "Support");
    }

    public S2Controller untilReady() {
        reachTheGame(new InetSocketAddress(cfg.getString(GAME_NET_IP), cfg.getInt(GAME_NET_PORT)));
        return this;
    }

    private void reachTheGame(InetSocketAddress endpoint) {
        if (!processIsAlive()) {
            throw new IllegalStateException("game is not running");
        }
        try (Socket socket = new Socket()) {
            socket.connect(endpoint, CONNECTION_TIMEOUT_IN_MILLIS);
            tryCount = 0;
        } catch (SocketTimeoutException e) {
            tryReachAgain(endpoint);
        } catch (IOException e) {
            // Port is probably not open yet -> wait for some time
            try {
                TimeUnit.MILLISECONDS.sleep(CONNECTION_TIMEOUT_IN_MILLIS);
            } catch (InterruptedException ignored) {
            } finally {
                tryReachAgain(endpoint);
            }
        }
    }

    private void tryReachAgain(InetSocketAddress endpoint) {
        if (tryCount > MAX_CONNECTION_TRY_COUNT) {
            log.error("Game process in unreachable.");
            throw new StarCraft2ControllerException(ControllerError.GAME_UNREACHABLE, "Game is unreachable.");
        } else {
            tryCount++;
            reachTheGame(endpoint);
        }
    }

    private boolean processIsAlive() {
        return isSet(s2Process) && s2Process.isAlive();
    }

    public void stop() {
        if (isSet(s2Process)) {
            s2Process.destroy();
        }
    }

    public boolean stopAndWait() {
        return stopAndWait(STOP_PROCESS_TIMEOUT_IN_MILLIS);
    }

    public boolean stopAndWait(long timeoutInMillis) {
        try {
            if (isSet(s2Process)) {
                stop();
                BufferedReader reader = new BufferedReader(new InputStreamReader(s2Process.getInputStream()));
                reader.lines().forEach(log::debug);

                return s2Process.waitFor(timeoutInMillis, TimeUnit.MILLISECONDS);
            }
            return true;
        } catch (InterruptedException e) {
            log.debug("Thread was interrupted.", e);
            Thread.currentThread().interrupt();
            return false;
        }
    }

    public void isInState(GameStatus expectedStatus) {
        if (!expectedStatus.equals(lastGameStatus)) {
            throw new AssertionError(
                    format("Game status: expected [%s] but was [%s].", expectedStatus, lastGameStatus));
        }
    }

    public boolean inState(GameStatus expectedStatus) {
        return expectedStatus.equals(lastGameStatus);
    }

    public Config getConfig() {
        return cfg;
    }

    @Override
    public void onNext(Response response) {
        lastGameStatus = response.getStatus();
    }

    @Override
    public void onComplete() {
        // Nothing to do here, client stops the game if needed.
    }

    @Override
    public void onError(Throwable e) {
        log.error("S2Controller.onError", e);
    }

    public S2Controller relaunchIfNeeded(int baseBuild, String dataVersion) {
        String currentBaseBuild = cfg.getString(GAME_EXE_BUILD);
        String currentDataVersion = cfg.hasPath(GAME_EXE_DATA_VER) ? cfg.getString(GAME_EXE_DATA_VER) : "";
        String baseBuildName = ExecutableParser.BUILD_PREFIX + baseBuild;
        if (!currentBaseBuild.equals(baseBuildName) || !currentDataVersion.equals(dataVersion)) {

            log.warn("Expected base build: {} and data version: {}. " +
                            "Actual base build: {} and data version: {}. " +
                            "Relaunching to expected version...",
                    baseBuildName, dataVersion, currentBaseBuild, currentDataVersion);

            if (!stopAndWait()) {
                throw new IllegalStateException("Failed to stop previous game instance.");
            }

            cfg = ConfigFactory.parseMap(
                    Map.of(GAME_EXE_BUILD, baseBuildName, GAME_EXE_DATA_VER, dataVersion)
            ).withFallback(cfg);

            return launch();
        }
        return this;
    }

    public void restart() {
        stopAndWait();
        launch();
    }

    public Process getS2Process() {
        return s2Process;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy