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

com.github.ocraft.s2client.bot.S2Coordinator Maven / Gradle / Ivy

There is a newer version: 0.4.20
Show newest version
package com.github.ocraft.s2client.bot;

/*-
 * #%L
 * ocraft-s2client-bot
 * %%
 * 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.OcraftApiConfig;
import com.github.ocraft.s2client.api.controller.ExecutableParser;
import com.github.ocraft.s2client.bot.gateway.ActionInterface;
import com.github.ocraft.s2client.bot.gateway.AppState;
import com.github.ocraft.s2client.bot.gateway.ControlInterface;
import com.github.ocraft.s2client.bot.setting.*;
import com.github.ocraft.s2client.bot.syntax.SettingsSyntax;
import com.github.ocraft.s2client.bot.syntax.StartGameSyntax;
import com.github.ocraft.s2client.protocol.game.*;
import com.github.ocraft.s2client.protocol.response.Response;
import com.github.ocraft.s2client.protocol.response.ResponseType;
import com.github.ocraft.s2client.protocol.spatial.SpatialCameraSetup;
import io.reactivex.Maybe;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import picocli.CommandLine;

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Supplier;

import static com.github.ocraft.s2client.protocol.Constants.nothing;
import static com.github.ocraft.s2client.protocol.Errors.required;
import static com.github.ocraft.s2client.protocol.Preconditions.*;
import static com.github.ocraft.s2client.protocol.game.MultiplayerOptions.multiplayerSetupFor;
import static java.util.Arrays.asList;

/**
 * Coordinator of one or more clients. Used to start, step and stop games and replays.
 */
public class S2Coordinator {

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

    private final List agents;
    private final List replayObservers;

    // TODO p.picheta refactor settings to immutables
    private final InterfaceSettings interfaceSettings;
    private final ProcessSettings processSettings;
    private final ReplaySettings replaySettings;
    private final GameSettings gameSettings;

    private final boolean useGeneralizedAbilityId;


    private S2Coordinator(Builder builder) {
        oneOfIsNotEmpty("agents or replay observers", builder.agents, builder.replayObservers);

        agents = builder.agents;
        replayObservers = builder.replayObservers;
        processSettings = builder.processSettings;
        gameSettings = builder.gameSettings;

        agents.forEach(agent -> {
            if (!agent.control().connect(processSettings)) {
                log.error("Failed to attach to starcraft.");
                throw new IllegalStateException("Failed to attach to starcraft.");
            }
        });

        replayObservers.forEach(replayObserver -> {
            if (!replayObserver.control().connect(processSettings)) {
                log.error("Failed to attach to starcraft.");
                throw new IllegalStateException("Failed to attach to starcraft.");
            }
        });

        interfaceSettings = new InterfaceSettings(
                builder.featureLayerSettings,
                builder.renderSettings,
                builder.showCloaked,
                builder.showBurrowed,
                builder.rawAffectsSelection,
                builder.rawCropToPlayableArea);
        replaySettings = builder.replaySettings;
        useGeneralizedAbilityId = builder.useGeneralizedAbilityId;

        if (builder.multiplayerOptions.isPresent()) {
            if (processSettings.isLadderGame()) {
                log.warn("Multiplayer options were provided, which supercedes the isLadderGame.");
            }
            builder.multiplayerOptions.ifPresent(gameSettings::setMultiplayerOptions);
        } else if (processSettings.isLadderGame() && !builder.ladderSettings.getComputerOpponent()) {
            setupPorts(agents.size() + 1, () -> processSettings.getPortSetup().fetchPort(), false);
        } else {
            setupPorts(agents.size(), () -> processSettings.getPortSetup().fetchPort(), true);
        }

        gameSettings.resolveMap(processSettings);
    }

    /**
     * Sets up the sc2 game ports to use
     *
     * @param numberOfAgents Number of agents in the game
     * @param portStart      Starting port number
     * @param checkSingle    Checks if the game is a single player or multiplayer game
     */
    public void setupPorts(int numberOfAgents, Supplier portStart, boolean checkSingle) {
        withPorts(numberOfAgents, portStart, checkSingle);
    }

    /**
     * Sets up the sc2 game ports to use
     *
     * @param numberOfAgents Number of agents in the game
     * @param portStart      Starting port number
     * @param checkSingle    Checks if the game is a single player or multiplayer game
     */
    public S2Coordinator withPorts(int numberOfAgents, Supplier portStart, boolean checkSingle) {
        int bots;
        if (checkSingle) {
            bots = (int) gameSettings.getPlayerSettings().stream()
                    .filter(player -> player.getPlayerSetup().getPlayerType().equals(PlayerType.PARTICIPANT))
                    .count();
        } else {
            bots = numberOfAgents;
        }
        if (bots > 1) {
            gameSettings.setMultiplayerOptions(multiplayerSetupFor(portStart.get(), bots));
            log.info("Setting multiplayer options: {}", gameSettings.getMultiplayerOptions());
        }
        return this;
    }

    public static SettingsSyntax setup() {
        return new Builder();
    }

    // Initialization and setup.
    private static class Builder implements SettingsSyntax, StartGameSyntax {

        private List agents = new ArrayList<>();
        private List replayObservers = new ArrayList<>();

        private ProcessSettings processSettings = new ProcessSettings();
        private ReplaySettings replaySettings = new ReplaySettings();
        private GameSettings gameSettings = new GameSettings();
        private CliSettings cliSettings = new CliSettings();
        private LadderSettings ladderSettings = new LadderSettings();
        private Optional multiplayerOptions = Optional.empty();

        private SpatialCameraSetup featureLayerSettings;
        private SpatialCameraSetup renderSettings;

        private boolean useGeneralizedAbilityId = true; // TODO p.picheta move to settings class?
        private Boolean showCloaked;
        private Boolean showBurrowed;
        private Boolean rawAffectsSelection;
        private Boolean rawCropToPlayableArea;

        private Builder() {
            if (OcraftBotConfig.cfg().hasPath(OcraftBotConfig.BOT_MAP)) {
                gameSettings.resolveMap(OcraftBotConfig.cfg().getString(OcraftBotConfig.BOT_MAP));
            }
        }

        @Override
        public SettingsSyntax loadSettings(String[] args) {
            try {
                CommandLine.populateCommand(cliSettings, args);

                if (cliSettings.isUsageHelpRequested()) {
                    printHelp();
                    System.exit(0);
                }

                Map exeSettings = tryDiscoverDefaultSettings(args);

                setProcessPath(cliSettings.getProcessPath().orElse(
                        Optional.ofNullable(exeSettings.get(OcraftApiConfig.GAME_EXE_PATH))
                                .map(p -> Paths.get(String.valueOf(p)))
                                .orElse(nothing())));
                setStepSize(cliSettings.getStepSize().orElse(nothing()));
                setPortStart(cliSettings.getPort().orElse(nothing()));
                setRealtime(cliSettings.getRealtime().orElse(nothing()));
                gameSettings.resolveMap(cliSettings.getMapName().orElse(nothing()));
                setTimeoutMS(cliSettings.getTimeout().orElse(nothing()));

            } catch (Exception e) {
                printHelp();
                throw e;
            }

            return this;
        }

        private Map tryDiscoverDefaultSettings(String[] args) {
            Map exeSettings = new HashMap<>();
            try {
                exeSettings = ExecutableParser.loadSettings(null, null, null);
            } catch (Exception e) {
                log.debug("Auto-discovering of Starcraft II configuration failed", e);
                if (args.length == 0) {
                    System.err.println("Please run StarCraft II before running this API");
                    printHelp();
                    System.exit(0);
                }
            }
            return exeSettings;
        }

        private void printHelp() {
            CommandLine.usage(cliSettings, System.out);
        }

        @Override
        public SettingsSyntax loadLadderSettings(String[] args) {
            try {
                CommandLine cmd = new CommandLine(ladderSettings);
                cmd.registerConverter(Race.class, Race::forName);
                cmd.registerConverter(Difficulty.class, Difficulty::forName);
                cmd.parse(args);

                if (ladderSettings.isUsageHelpRequested()) {
                    printLadderHelp();
                    System.exit(0);
                }

                setPortStart(ladderSettings.getStartPort());
                processSettings.setConnection(ladderSettings.getLadderServer(), ladderSettings.getGamePort());
                if (ladderSettings.getComputerOpponent()) {
                    setParticipants(createComputer(
                            ladderSettings.getComputerRace().orElseThrow(required("computer race")),
                            ladderSettings.getComputerDifficulty().orElseThrow(required("computer difficulty"))));
                }
                setRealtime(ladderSettings.isRealTime());

            } catch (Exception e) {
                printLadderHelp();
                throw e;
            }

            return this;
        }

        private void printLadderHelp() {
            CommandLine.usage(ladderSettings, System.out);
        }


        @Override
        public SettingsSyntax setMultithreaded(Boolean value) {
            if (isSet(value)) processSettings.setMultithreaded(value);
            return this;
        }

        @Override
        public SettingsSyntax setRealtime(Boolean value) {
            if (isSet(value)) processSettings.setRealtime(value);
            return this;
        }

        @Override
        public SettingsSyntax setStepSize(Integer stepSize) {
            if (isSet(stepSize)) processSettings.setStepSize(stepSize);
            return this;
        }

        @Override
        public SettingsSyntax setProcessPath(Path path) {
            if (isSet(path)) processSettings.setProcessPath(path);
            return this;
        }

        @Override
        public SettingsSyntax setDataVersion(String version) {
            if (isSet(version)) processSettings.setDataVersion(version);
            return this;
        }

        @Override
        public SettingsSyntax setTimeoutMS(Integer timeoutInMillis) {
            if (isSet(timeoutInMillis)) {
                processSettings.setRequestTimeoutMS(timeoutInMillis).setConnectionTimeoutMS(timeoutInMillis);
            }
            return this;
        }

        @Override
        public SettingsSyntax setPortStart(Integer portStart) {
            if (isSet(portStart)) processSettings.setPortStart(portStart);
            return this;
        }

        @Override
        public SettingsSyntax setFeatureLayers(SpatialCameraSetup settings) {
            if (isSet(settings)) this.featureLayerSettings = settings;
            return this;
        }

        @Override
        public SettingsSyntax setRender(SpatialCameraSetup settings) {
            if (isSet(settings)) this.renderSettings = settings;
            return this;
        }

        @Override
        public SettingsSyntax setWindowSize(Integer width, Integer height) {
            if (isSet(width) && isSet(height)) processSettings.setWindowSize(width, height);
            return this;
        }

        @Override
        public SettingsSyntax setWindowLocation(Integer x, Integer y) {
            if (isSet(x) && isSet(y)) processSettings.setWindowLocation(x, y);
            return this;
        }

        @Override
        public SettingsSyntax setUseGeneralizedAbilityId(Boolean value) {
            if (isSet(value)) this.useGeneralizedAbilityId = value;
            return this;
        }

        @Override
        public SettingsSyntax setVerbose(Boolean value) {
            if (isSet(value)) processSettings.setVerbose(value);
            return this;
        }

        @Override
        public SettingsSyntax setNeedsSupportDir(Boolean value) {
            if (isSet(value)) processSettings.setNeedsSupportDir(value);
            return this;
        }

        @Override
        public SettingsSyntax setTraced(Boolean value) {
            if (isSet(value)) processSettings.setTraced(value);
            return this;
        }

        @Override
        public SettingsSyntax setTmpDir(Path tmpDirPath) {
            if (isSet(tmpDirPath)) processSettings.setTmpDir(tmpDirPath);
            return this;
        }

        @Override
        public SettingsSyntax setDataDir(Path dataDirPath) {
            if (isSet(dataDirPath)) processSettings.setDataDir(dataDirPath);
            return this;
        }

        @Override
        public SettingsSyntax setOsMesaPath(Path osMesaPath) {
            if (isSet(osMesaPath)) processSettings.setOsMesaPath(osMesaPath);
            return this;
        }

        @Override
        public SettingsSyntax setEglPath(Path eglPath) {
            if (isSet(eglPath)) processSettings.setEglPath(eglPath);
            return this;
        }

        @Override
        public SettingsSyntax setShowCloaked(Boolean showCloaked) {
            if (isSet(showCloaked)) this.showCloaked = showCloaked;
            return this;
        }

        @Override
        public SettingsSyntax setShowBurrowed(Boolean showBurrowed) {
            if (isSet(showBurrowed)) this.showBurrowed = showBurrowed;
            return this;
        }

        @Override
        public SettingsSyntax setRawAffectsSelection(Boolean rawAffectsSelection) {
            if (isSet(rawAffectsSelection)) this.rawAffectsSelection = rawAffectsSelection;
            return this;
        }

        @Override
        public SettingsSyntax setRawCropToPlayableArea(Boolean rawCropToPlayableArea) {
            if (isSet(rawCropToPlayableArea)) this.rawCropToPlayableArea = rawCropToPlayableArea;
            return this;
        }

        @Override
        public StartGameSyntax setParticipants(PlayerSettings... participants) {
            if (isEmpty(participants)) return this;
            List playerSettings = new ArrayList<>();
            asList(participants).forEach(participant -> {
                if (participant.getAgent() != null) agents.add(participant.getAgent());
                playerSettings.add(participant);
            });
            gameSettings.setPlayerSettings(playerSettings);
            return this;
        }

        @Override
        public SettingsSyntax setReplayRecovery(Boolean value) {
            if (isSet(value)) replaySettings.setReplayRecovery(value);
            return this;
        }

        @Override
        public SettingsSyntax addReplayObserver(S2ReplayObserver replayObserver) {
            if (isSet(replayObserver)) this.replayObservers.add(replayObserver);
            return this;
        }

        @Override
        public SettingsSyntax setReplayPath(Path path) throws IOException {
            if (isSet(path)) replaySettings.setReplayPath(path);
            return this;
        }

        @Override
        public SettingsSyntax loadReplayList(Path path) throws IOException {
            if (isSet(path)) replaySettings.loadReplayList(path);
            return this;
        }

        @Override
        public SettingsSyntax setMultiplayerOptions(MultiplayerOptions multiplayerOptions) {
            if (isSet(multiplayerOptions)) this.multiplayerOptions = Optional.of(multiplayerOptions);
            return this;
        }

        @Override
        public S2Coordinator launchStarcraft() {
            try {
                processSettings.setWithGameController(true);
                return new S2Coordinator(this);
            } catch (Exception e) {
                printHelp();
                throw e;
            }
        }

        @Override
        public S2Coordinator launchStarcraft(Integer port) {
            try {
                processSettings.setWithGameController(true);
                processSettings.setPortStart(port);
                return new S2Coordinator(this);
            } catch (Exception e) {
                printHelp();
                throw e;
            }
        }

        @Override
        public S2Coordinator connect(String ip, Integer port) {
            try {
                processSettings.setConnection(ip, port);
                processSettings.setWithGameController(false);
                return new S2Coordinator(this);
            } catch (Exception e) {
                printHelp();
                throw e;
            }
        }

        @Override
        public S2Coordinator connectToLadder() {
            try {
                processSettings.setWithGameController(false);
                processSettings.setLadderGame(true);
                return new S2Coordinator(this);
            } catch (Exception e) {
                printLadderHelp();
                throw e;
            }
        }
    }

    // Start-up

    /**
     * Starts a game on a certain map. There are multiple ways to specify a map:
     * Absolute path: Any .SC2Map file.
     * Relative path: Any .SC2Map file relative to either the library or installation maps folder.
     * Map name: Any BattleNet published map that has been locally cached.
     *
     * @see #startGame(BattlenetMap)
     * @see #startGame(LocalMap)
     */
    public S2Coordinator startGame() {
        createGame();
        return joinGame();
    }

    /**
     * Starts a game on a certain map. There are multiple ways to specify a map:
     * Absolute path: Any .SC2Map file.
     * Relative path: Any .SC2Map file relative to either the library or installation maps folder.
     * Map name: Any BattleNet published map that has been locally cached.
     *
     * @param localMap Path to the map to run.
     * @see #startGame(BattlenetMap)
     * @see #startGame()
     */
    public S2Coordinator startGame(LocalMap localMap) {
        require("local map", localMap);
        gameSettings.setLocalMap(localMap);
        gameSettings.resolveMap(processSettings);
        return startGame();
    }

    /**
     * Starts a game on a certain map. There are multiple ways to specify a map:
     * Absolute path: Any .SC2Map file.
     * Relative path: Any .SC2Map file relative to either the library or installation maps folder.
     * Map name: Any BattleNet published map that has been locally cached.
     *
     * @param battlenetMap Name of the battlenet map to run.
     * @see #startGame(LocalMap)
     * @see #startGame()
     */
    public S2Coordinator startGame(BattlenetMap battlenetMap) {
        require("battlenet map", battlenetMap);
        gameSettings.setBattlenetMap(battlenetMap);
        return startGame();
    }

    /**
     * Creates a game but does not join the bots to the game
     *
     * @see #createGame(BattlenetMap)
     * @see #createGame(LocalMap)
     */
    public S2Coordinator createGame() {
//        // Create the game with the first client.
        S2Agent firstClient = agents.get(0);

        Optional battlenetMap = gameSettings.getBattlenetMap();
        if (battlenetMap.isPresent()) {
            firstClient.control().createGame(
                    battlenetMap.get(),
                    gameSettings.getPlayerSettings(),
                    processSettings.getRealtime());
        } else {
            Optional localMap = gameSettings.getLocalMap();
            if (localMap.isPresent()) {
                firstClient.control().createGame(
                        localMap.get(),
                        gameSettings.getPlayerSettings(),
                        processSettings.getRealtime());
            } else {
                throw new IllegalArgumentException("map is required");
            }
        }
        return this;
    }


    /**
     * Creates a game but does not join the bots to the game
     *
     * @param localMap Path to the map to run.
     * @see #createGame(BattlenetMap)
     * @see #createGame()
     */
    public S2Coordinator createGame(LocalMap localMap) {
        require("local map", localMap);
        gameSettings.setLocalMap(localMap);
        return createGame();
    }

    /**
     * Creates a game but does not join the bots to the game
     *
     * @param battlenetMap Name of the battlenet map to run.
     * @see #createGame(LocalMap)
     * @see #createGame()
     */
    public S2Coordinator createGame(BattlenetMap battlenetMap) {
        require("battlenet map", battlenetMap);
        gameSettings.setBattlenetMap(battlenetMap);
        return createGame();
    }

    /**
     * Joins agents to the game
     */
    public S2Coordinator joinGame() {
        Map> waitForJoin = new HashMap<>();
        agents.forEach(agent -> waitForJoin.put(agent, agent.control().requestJoinGame(
                gameSettings.playerSettingsFor(agent).orElseThrow(required("player settings")),
                interfaceSettings,
                gameSettings.getMultiplayerOptions())));

        agents.forEach(agent -> agent.control().waitJoinGame(waitForJoin.get(agent)));

        // TODO p.picheta to test
        // Check if any errors occurred during game start.
        boolean errorOccurred = false;
        for (S2Agent agent : agents) {
            if (!agent.control().getClientErrors().isEmpty()) {
                agent.onError(agent.control().getClientErrors(), agent.control().getProtocolErrors());
                errorOccurred = true;
            }
        }

        if (errorOccurred) {
            throw new IllegalStateException("Game failed to start");
        }

        agents.forEach(agent -> agent.control().useGeneralizedAbility(useGeneralizedAbilityId));
        agents.forEach(agent -> agent.control().getObservation());
        agents.forEach(ClientEvents::onGameFullStart);
        agents.forEach(agent -> {
            agent.control().onGameStart();
            agent.onGameStart();
        });
        agents.forEach(agent -> agent.control().issueEvents(agent.actions().commands()));

        return this;
    }

    // Run.

    /**
     * Helper function used to actually run a bot. This function will behave differently in real-time compared to
     * non real-time. In real-time there is no step sent over the wire but instead will request and read observations
     * as the game runs.
     * 

* For non-real time Update will perform the following: *

    *
  1. Step the simulation forward by a certain amount of game steps, this essentially moves the game loops * forward.
  2. *
  3. Wait for the step to complete, the step is completed when a response is received and read from * the StarCraft II binary. When the step is completed an Observation has been received. It is parsed and various * client events are dispatched.
  4. *
  5. Call the user's onStep function.
  6. *
*

* Real time applications will perform the following: *

    *
  1. The Observation is directly requested. The process will block while waiting for it.
  2. *
  3. The Observation is parsed and client events are dispatched.
  4. *
  5. Unit actions batched from the ActionInterface are dispatched.
  6. *
* * @return False if the game has ended, true otherwise. */ public boolean update() { if (!agents.isEmpty()) { if (processSettings.getRealtime()) { stepAgentsRealtime(); } else { stepAgents(); } } if (!replayObservers.isEmpty()) { if (processSettings.getRealtime()) { stepReplayObserversRealtime(); } else { stepReplayObservers(); } if (anyObserverAvailable()) { startReplay(); } } // Check for errors in all agents/replay observers at the end of an update. boolean errorOccurred = false; for (S2Agent agent : agents) { ControlInterface control = agent.control(); if (!control.getClientErrors().isEmpty()) { agent.onError(control.getClientErrors(), control.getProtocolErrors()); errorOccurred = true; } } boolean relaunched = false; for (S2ReplayObserver replayObserver : replayObservers) { ControlInterface control = replayObserver.control(); if (!control.getClientErrors().isEmpty()) { replayObserver.onError(control.getClientErrors(), control.getProtocolErrors()); errorOccurred = true; if (replaySettings.isReplayRecovery()) { // An error did occur but if we successfully recovered ignore it. The client will still gets // its event boolean connected = relaunch(replayObserver); if (connected) { errorOccurred = false; relaunched = true; } } } } // End the coordinator update on the idea that an error in the API should mean it's time to stop. if (errorOccurred) { return false; } return !allGamesEnded() || relaunched; } private void stepAgentsRealtime() { if (processSettings.getMultithreaded()) { runParallel(stepAgentRealtime()); } else { agents.forEach(stepAgentRealtime()); } } private Consumer stepAgentRealtime() { return agent -> { ControlInterface control = agent.control(); if (control.getAppState() != AppState.NORMAL) return; if (control.pollLeaveGame()) return; if (control.isFinishedGame()) return; ActionInterface actions = agent.actions(); // This agent shouldn't call step since it's real time. control.getObservation(); control.issueEvents(actions.commands()); actions.sendActions(); if (!control.isInGame()) { agent.onGameEnd(); if (control.isMultiplayer()) control.requestLeaveGame(); // Only for multiplayer. } }; } private void stepAgents() { if (agents.size() == 1) { stepAgent().accept(agents.get(0)); } else { runParallel(stepAgent()); } if (!processSettings.getMultithreaded()) { agents.stream() .filter(agent -> agent.control().getAppState() == AppState.NORMAL) // It is possible to have a pending leave game request here. .filter(agent -> !agent.control().pollLeaveGame()) .forEach(this::callOnStep); } } private Consumer stepAgent() { return agent -> { ControlInterface control = agent.control(); if (control.getAppState() != AppState.NORMAL) return; if (control.pollLeaveGame()) return; if (control.isFinishedGame()) return; control.waitStep(control.step(processSettings.getStepSize())); if (processSettings.getMultithreaded()) { callOnStep(agent); } }; } private void callOnStep(S2Agent agent) { ControlInterface control = agent.control(); if (!control.isInGame()) { agent.onGameEnd(); if (control.isMultiplayer()) control.requestLeaveGame(); return; } ActionInterface actions = agent.actions(); control.issueEvents(actions.commands()); actions.sendActions(); agent.actionsFeatureLayer().sendActions(); } private void runParallel(Consumer step) { agents.parallelStream().forEach(step); } private boolean anyObserverAvailable() { return replayObservers.stream().anyMatch(replayObserver -> !replayObserver.control().isInGame()); } private boolean startReplay() { // If no replays given in the settings don't try. if (replaySettings.getReplayFiles().isEmpty()) return false; if (replayObservers.isEmpty()) return false; // Run a replay with each available replay observer. replayObservers.forEach(replayObserver -> { // If the replay observer is idle or out of game use it for a new replay. if (!replayObserver.control().isReadyForCreateGame()) return; replayObserver.replayControl().useGeneralizedAbility(useGeneralizedAbilityId); List replays = replaySettings.getReplayFiles(); while (!replays.isEmpty()) { Path replay = replays.get(replays.size() - 1); if (shouldIgnore(replayObserver, replay)) { replays.remove(replays.size() - 1); continue; } if (shouldRelaunch(replayObserver)) break; boolean launched = !replayObserver.replayControl() .loadReplay(replay, interfaceSettings, 0, processSettings.getRealtime()) .isEmpty() .blockingGet(); replays.remove(replays.size() - 1); if (launched) break; } }); return true; } private boolean shouldIgnore(S2ReplayObserver replayObserver, Path replay) { if (replay.toString().isEmpty()) return true; // Gather replay information with the available observer. replayObserver.replayControl().gatherReplayInfo(replay, true); // If the replay isn't being pruned based on replay info start it. return replayObserver.ignoreReplay(replayObserver.replayControl().getReplayInfo(), 1); } private boolean shouldRelaunch(S2ReplayObserver replayObserver) { ReplayInfo replayInfo = replayObserver.replayControl().getReplayInfo(); boolean versionMatch = replayInfo.getBaseBuild() == replayObserver.control().proto().getBaseBuild() && replayInfo.getDataVersion().equals(replayObserver.control().proto().getDataVersion()); if (versionMatch) return false; // Version failed to download. Just continue with trying to load in current version. // It will likely fail, and then just skip past this replay. if (!ExecutableParser.findBaseExe( processSettings.getRootPath(), replayInfo.getBaseBuild(), processSettings.getActualProcessPath().getFileName().toString())) { return false; } log.warn("Replay is from a different version. Relaunching client into the correct version..."); processSettings.setDataVersion(replayInfo.getDataVersion()); processSettings.setBaseBuild(replayInfo.getBaseBuild()); processSettings.setRootPath(null); processSettings.setActualProcessPath(null); replayObserver.control().error(ClientError.WRONG_GAME_VERSION, Collections.emptyList()); return true; } private void stepReplayObservers() { // Run all replay observers. if (replayObservers.size() == 1) { runReplay().accept(replayObservers.get(0)); } else { // Run all steps in parallel. replayObservers.parallelStream().forEach(runReplay()); } // Do everyone's OnStep, if not multi threaded, in single threaded mode. if (!processSettings.getMultithreaded()) { replayObservers.stream() .filter(replayObserver -> replayObserver.control().getAppState().equals(AppState.NORMAL)) .forEach(replayObserver -> { replayObserver.control().issueEvents(Collections.emptyList()); replayObserver.observerAction().sendActions(); }); } } private Consumer runReplay() { return replayObserver -> { if (!replayObserver.control().getAppState().equals(AppState.NORMAL)) return; // If the replay is loading wait for it to finish loading before performing a step. if (replayObserver.control().hasResponsePending(ResponseType.START_REPLAY)) { // Don't consume a response if there isn't one in the queue. if (replayObservers.size() > 1 && !replayObserver.control().pollResponse(ResponseType.START_REPLAY)) { return; } replayObserver.replayControl() .waitForReplay(replayObserver.control().getResponsePending(ResponseType.START_REPLAY)); } if (replayObserver.control().isInGame()) { replayObserver.control().waitStep(replayObserver.control().step(processSettings.getStepSize())); // If multithreaded run everyone's OnStep in parallel. if (processSettings.getMultithreaded()) { replayObserver.control().issueEvents(Collections.emptyList()); replayObserver.observerAction().sendActions(); } if (!replayObserver.control().isInGame()) { replayObserver.onGameEnd(); } } }; } private void stepReplayObserversRealtime() { // Run all replay observers. if (replayObservers.size() == 1) { runReplayRealtime().accept(replayObservers.get(0)); } else { // Run all steps in parallel. replayObservers.parallelStream().forEach(runReplayRealtime()); } // Do everyone's OnStep, if not multi threaded, in single threaded mode. if (!processSettings.getMultithreaded()) { replayObservers.stream() .filter(replayObserver -> replayObserver.control().getAppState().equals(AppState.NORMAL)) .forEach(replayObserver -> replayObserver.control().issueEvents(Collections.emptyList())); } } private Consumer runReplayRealtime() { return replayObserver -> { if (!replayObserver.control().getAppState().equals(AppState.NORMAL)) return; // If the replay is loading wait for it to finish loading before performing a step. if (replayObserver.control().hasResponsePending(ResponseType.START_REPLAY)) { // Don't consume a response if there isn't one in the queue. if (replayObservers.size() > 1 && !replayObserver.control().pollResponse(ResponseType.START_REPLAY)) { return; } replayObserver.replayControl() .waitForReplay(replayObserver.control().getResponsePending(ResponseType.START_REPLAY)); } if (replayObserver.control().isInGame()) { replayObserver.control().getObservation(); // If multithreaded run everyone's OnStep in parallel. if (processSettings.getMultithreaded()) { replayObserver.control().issueEvents(Collections.emptyList()); } if (!replayObserver.control().isInGame()) { replayObserver.onGameEnd(); } } }; } private boolean relaunch(S2ReplayObserver replayObserver) { // TODO p.picheta to test replayObserver.reset(); return replayObserver.control() .connect(processSettings.setPortStart(processSettings.getPortSetup().fetchPort())); } // TODO p.picheta WaitForAllResponses /** * Requests for the currently running game to end. */ public void leaveGame() { // TODO p.picheta to test agents.forEach(agent -> agent.control().requestLeaveGame()); } // Status. /** * Returns true if all running games have ended. */ public boolean allGamesEnded() { // TODO p.picheta to test return gamesEndedForAgents() && gamesEndedForReplayObservers(); } private boolean gamesEndedForAgents() { return agents.stream() .noneMatch(agent -> agent.control().isInGame() || agent.control().hasResponsePending()); } private boolean gamesEndedForReplayObservers() { return replayObservers.stream() .noneMatch(replayObserver -> replayObserver.control().isInGame() || replayObserver.control().hasResponsePending()); } // Replay specific. /** * Saves replays to a file. * * @param path The file path. */ public S2Coordinator saveReplayList(Path path) throws IOException { // TODO p.picheta to test Files.write( path, (Iterable) replaySettings.getReplayFiles().stream() .map(p -> p.toString() + System.lineSeparator())::iterator, Charset.forName("UTF-8"), StandardOpenOption.CREATE, StandardOpenOption.APPEND); return this; } /** * Determines if there are unprocessed replays. * * @return Is true if there are replays left. */ public boolean hasReplays() { return !replaySettings.getReplayFiles().isEmpty(); } // Misc. /** * Saves a binary blob as a map to a remote location. * * @param data The map data. * @param path The file path to save the data to. */ public boolean remoteSaveMap(byte[] data, Path path) { return agents.stream().allMatch(agent -> agent.control().remoteSaveMap(LocalMap.of(path, data))) && replayObservers.stream() .allMatch(replayObserver -> replayObserver.control().remoteSaveMap(LocalMap.of(path, data))); } /** * Gets the game executable path. * * @return The game executable path. */ public Path getExePath() { return processSettings.getActualProcessPath(); } public void quit() { agents.forEach(agent -> agent.control().quit()); replayObservers.forEach(replayObserver -> replayObserver.control().quit()); } public static PlayerSettings createParticipant(Race race) { return PlayerSettings.participant(race); } public static PlayerSettings createParticipant(Race race, S2Agent bot) { return PlayerSettings.participant(race, bot); } public static PlayerSettings createComputer(Race race, Difficulty difficulty) { return PlayerSettings.computer(race, difficulty); } public static PlayerSettings createParticipant(Race race, S2Agent bot, String playerName) { return PlayerSettings.participant(race, bot, playerName); } public static PlayerSettings createComputer(Race race, Difficulty difficulty, String playerName) { return PlayerSettings.computer(race, difficulty, playerName); } public static PlayerSettings createComputer(Race race, Difficulty difficulty, AiBuild aiBuild) { return PlayerSettings.computer(race, difficulty, aiBuild); } public static PlayerSettings createComputer(Race race, Difficulty difficulty, String playerName, AiBuild aiBuild) { return PlayerSettings.computer(race, difficulty, playerName, aiBuild); } List getAgents() { return new ArrayList<>(agents); } List getReplayObservers() { return new ArrayList<>(replayObservers); } InterfaceSettings getInterfaceSettings() { return interfaceSettings; } ProcessSettings getProcessSettings() { return processSettings; } ReplaySettings getReplaySettings() { return replaySettings; } GameSettings getGameSettings() { return gameSettings; } boolean isUseGeneralizedAbilityId() { return useGeneralizedAbilityId; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; S2Coordinator that = (S2Coordinator) o; if (useGeneralizedAbilityId != that.useGeneralizedAbilityId) return false; if (!Objects.equals(agents, that.agents)) return false; if (!Objects.equals(replayObservers, that.replayObservers)) return false; if (!Objects.equals(interfaceSettings, that.interfaceSettings)) return false; if (!Objects.equals(processSettings, that.processSettings)) return false; if (!Objects.equals(replaySettings, that.replaySettings)) return false; return Objects.equals(gameSettings, that.gameSettings); } @Override public int hashCode() { int result = agents != null ? agents.hashCode() : 0; result = 31 * result + (replayObservers != null ? replayObservers.hashCode() : 0); result = 31 * result + (interfaceSettings != null ? interfaceSettings.hashCode() : 0); result = 31 * result + (processSettings != null ? processSettings.hashCode() : 0); result = 31 * result + (replaySettings != null ? replaySettings.hashCode() : 0); result = 31 * result + (gameSettings != null ? gameSettings.hashCode() : 0); result = 31 * result + (useGeneralizedAbilityId ? 1 : 0); return result; } @Override public String toString() { return "S2Coordinator{" + "agents=" + agents + ", replayObservers=" + replayObservers + ", interfaceSettings=" + interfaceSettings + ", processSettings=" + processSettings + ", replaySettings=" + replaySettings + ", gameSettings=" + gameSettings + ", useGeneralizedAbilityId=" + useGeneralizedAbilityId + '}'; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy