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

com.github.ocraft.s2client.bot.gateway.impl.ControlInterfaceImpl Maven / Gradle / Ivy

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

/*-
 * #%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.ResponseParseException;
import com.github.ocraft.s2client.api.controller.S2Controller;
import com.github.ocraft.s2client.bot.ClientError;
import com.github.ocraft.s2client.bot.ClientEvents;
import com.github.ocraft.s2client.bot.S2ReplayObserver;
import com.github.ocraft.s2client.bot.gateway.*;
import com.github.ocraft.s2client.bot.setting.InterfaceSettings;
import com.github.ocraft.s2client.bot.setting.PlayerSettings;
import com.github.ocraft.s2client.bot.setting.ProcessSettings;
import com.github.ocraft.s2client.protocol.data.UnitType;
import com.github.ocraft.s2client.protocol.data.Units;
import com.github.ocraft.s2client.protocol.data.Upgrade;
import com.github.ocraft.s2client.protocol.game.*;
import com.github.ocraft.s2client.protocol.observation.raw.ObservationRaw;
import com.github.ocraft.s2client.protocol.request.Requests;
import com.github.ocraft.s2client.protocol.response.*;
import com.github.ocraft.s2client.protocol.syntax.request.FeatureLayerSyntax;
import com.github.ocraft.s2client.protocol.unit.Alliance;
import com.github.ocraft.s2client.protocol.unit.DisplayType;
import com.github.ocraft.s2client.protocol.unit.Tag;
import com.github.ocraft.s2client.protocol.unit.Unit;
import io.reactivex.Maybe;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import static com.github.ocraft.s2client.protocol.Preconditions.isSet;
import static com.github.ocraft.s2client.protocol.Preconditions.require;
import static java.lang.String.format;

class ControlInterfaceImpl implements ControlInterface {

    private Logger log = LoggerFactory.getLogger(ControlInterfaceImpl.class);

    private final ProtoInterfaceImpl protoInterface;
    private final ObservationInterfaceImpl observationInterface;
    private final AgentControlInterface agentControlInterface;
    private final QueryInterface queryInterface;
    private final DebugInterface debugInterface;
    private final ObserverActionInterface observerActionInterface;

    // Kept track of all units you called onCreatedUnit so you call it exactly once per unit.
    private final Set calledOnCreateUnits = new HashSet<>();
    private final List clientErrors = new ArrayList<>();
    private final List protocolErrors = new ArrayList<>();
    private final ClientEvents clientEvents;

    private S2Controller theGame;
    private AppState appState = AppState.NORMAL;
    private boolean multiplayer;
    private ProcessInfo processInfo;
    private boolean useGeneralizedAbilityId;
    private boolean leaveGameWasRequested;

    ControlInterfaceImpl(ClientEvents clientEvents) {
        require("client events callback", clientEvents);

        this.protoInterface = new ProtoInterfaceImpl();
        this.observationInterface = new ObservationInterfaceImpl(this);

        this.protoInterface.setOnError(this::onError);
        this.agentControlInterface = new AgentControlInterfaceImpl(this);
        this.queryInterface = new QueryInterfaceImpl(this);
        this.debugInterface = new DebugInterfaceImpl(this);
        this.observerActionInterface = new ObserverActionInterfaceImpl(this);
        this.clientEvents = clientEvents;
    }

    ControlInterfaceImpl(ClientEvents clientEvents, ProtoInterfaceImpl protoInterface) {
        require("client events callback", clientEvents);
        require("proto interface", protoInterface);

        this.protoInterface = protoInterface;
        this.observationInterface = new ObservationInterfaceImpl(this);

        this.protoInterface.setOnError(this::onError);
        this.agentControlInterface = new AgentControlInterfaceImpl(this);
        this.queryInterface = new QueryInterfaceImpl(this);
        this.debugInterface = new DebugInterfaceImpl(this);
        this.observerActionInterface = new ObserverActionInterfaceImpl(this);
        this.clientEvents = clientEvents;
    }

    ControlInterfaceImpl(
            ClientEvents clientEvents,
            ProtoInterfaceImpl protoInterface,
            ObservationInterfaceImpl observationInterface) {

        require("client events callback", clientEvents);
        require("proto interface", protoInterface);
        require("observation interface", observationInterface);

        this.protoInterface = protoInterface;
        this.observationInterface = observationInterface;

        this.protoInterface.setOnError(this::onError);
        this.agentControlInterface = new AgentControlInterfaceImpl(this);
        this.queryInterface = new QueryInterfaceImpl(this);
        this.debugInterface = new DebugInterfaceImpl(this);
        this.observerActionInterface = new ObserverActionInterfaceImpl(this);
        this.clientEvents = clientEvents;
    }

    private void onError(ClientError clientError, List protocolErrors) {
        clientErrors.add(clientError);
        this.protocolErrors.addAll(protocolErrors);
    }

    @Override
    public ProtoInterface proto() {
        return protoInterface;
    }

    @Override
    public ObservationInterface observation() {
        return observationInterface;
    }

    @Override
    public boolean connect(ProcessSettings processSettings) {
        require("process settings", processSettings);

        log.info("Waiting for connection...");
        boolean connected;
        if (processSettings.withGameController()) {
            theGame = tryLaunchProcess(processSettings);
            connected = proto().connectToGame(
                    theGame,
                    processSettings.getConnectionTimeoutMS(),
                    processSettings.getRequestTimeoutMS(),
                    processSettings.getTraced());
        } else {
            connected = proto().connectToGame(
                    processSettings.getIp(),
                    processSettings.getPort(),
                    processSettings.getConnectionTimeoutMS(),
                    processSettings.getRequestTimeoutMS(),
                    processSettings.getTraced());
        }

        if (connected) {
            log.info("Connected to {}:{}", proto().getConnectToIp(), proto().getConnectToPort());
            if (!isSet(processSettings.getPortSetup())) {
                processSettings.setPortStart(proto().getConnectToPort());
                processSettings.getPortSetup().fetchPort(); // this port is already used
            }
            if (isSet(theGame) && !isSet(processSettings.getRootPath())) {
                processSettings.setRootPath(Paths.get(theGame.getConfig().getString(OcraftApiConfig.GAME_EXE_ROOT)));
            }
            if (isSet(theGame) && !isSet(processSettings.getActualProcessPath())) {
                processSettings.setActualProcessPath(
                        Paths.get(theGame.getConfig().getString(OcraftApiConfig.GAME_EXE_PATH)));
            }
            updateProcessInfo(processSettings);
            return true;
        } else {
            log.error("Unable to connect to game");
            return false;
        }
    }

    private S2Controller tryLaunchProcess(ProcessSettings processSettings) {
        if (isSet(theGame)) return theGame;
        return S2Controller.starcraft2Game()
                .withExecutablePath(processSettings.getProcessPath())
                .withDataVersion(processSettings.getDataVersion())
                .withBaseBuild(processSettings.getBaseBuild())
                .withListenIp(processSettings.getIp())
                .withPort(isSet(processSettings.getPortSetup()) ? processSettings.getPortSetup().fetchPort() : null)
                .withWindowSize(processSettings.getWindowWidth(), processSettings.getWindowHeight())
                .withWindowPosition(processSettings.getWindowX(), processSettings.getWindowY())
                .withDataDir(processSettings.getDataDir())
                .withTmpDir(processSettings.getTmpDir())
                .withEglPath(processSettings.getEglPath())
                .withOsMesaPath(processSettings.getOsMesaPath())
                .verbose(processSettings.getVerbose())
                .needsSupportDir(processSettings.getNeedsSupportDir())
                .launch().untilReady();
    }

    // test purposes only
    void setS2Controller(S2Controller s2Controller) {
        theGame = s2Controller;
    }

    private void updateProcessInfo(ProcessSettings processSettings) {
        setProcessInfo(ProcessInfo.from(
                isSet(theGame)
                        ? Paths.get(theGame.getConfig().getString(OcraftApiConfig.GAME_EXE_PATH))
                        : processSettings.getProcessPath(),
                isSet(theGame)
                        ? theGame.getS2Process().pid()
                        : null,
                isSet(theGame)
                        ? theGame.getConfig().getInt(OcraftApiConfig.GAME_NET_PORT)
                        : processSettings.getPort()));
    }

    @Override
    public boolean remoteSaveMap(LocalMap localMap) {
        Optional responseSaveMap =
                waitForResponse(proto().sendRequest(Requests.saveMap().to(localMap)))
                        .flatMap(response -> response.as(ResponseSaveMap.class));
        boolean isSuccess = responseSaveMap.isPresent() && !responseSaveMap.get().getError().isPresent();
        if (!isSuccess) {
            responseSaveMap.ifPresent(response -> response.getError().ifPresent(
                    errorCode -> log.error("SaveMap request returned an error code: {}", errorCode)));
        }
        return isSuccess;
    }

    @Override
    public boolean createGame(BattlenetMap battlenetMap, List playerSettings, Boolean realtime) {
        Optional responseCreateGame = waitForResponse(
                proto().sendRequest(
                        Requests.createGame()
                                .onBattlenetMap(battlenetMap)
                                .withPlayerSetup(playerSetupFrom(playerSettings))
                                .realTime(realtime)))
                .flatMap(response -> response.as(ResponseCreateGame.class));
        boolean isSuccess = responseCreateGame.isPresent() && !responseCreateGame.get().getError().isPresent();
        if (!isSuccess) {
            responseCreateGame.ifPresent(createGameErrorHandler());
        }
        return isSuccess;
    }

    private PlayerSetup[] playerSetupFrom(List playerSettings) {
        return playerSettings
                .stream()
                .map(PlayerSettings::getPlayerSetup)
                .collect(Collectors.toList())
                .toArray(new PlayerSetup[playerSettings.size()]);
    }

    private Consumer createGameErrorHandler() {
        return response -> {
            response.getError().ifPresent(
                    errorCode -> log.error("CreateGame request returned an error code: {}", errorCode));
            response.getErrorDetails().ifPresent(
                    errorDetails -> log.error("CreateGame request returned error details: {}", errorDetails));
        };
    }

    @Override
    public boolean createGame(LocalMap localMap, List playerSettings, Boolean realtime) {
        Optional responseCreateGame = waitForResponse(
                proto().sendRequest(
                        Requests.createGame()
                                .onLocalMap(localMap)
                                .withPlayerSetup(playerSetupFrom(playerSettings))
                                .realTime(realtime)))
                .flatMap(response -> response.as(ResponseCreateGame.class));
        boolean isSuccess = responseCreateGame.isPresent() && !responseCreateGame.get().getError().isPresent();
        if (!isSuccess) {
            responseCreateGame.ifPresent(createGameErrorHandler());
        }
        return isSuccess;
    }

    @Override
    public Maybe requestJoinGame(
            PlayerSettings playerSettings, InterfaceSettings interfaceSettings, MultiplayerOptions multiplayerOptions) {
        try {
            observationInternal().clearFlags();

            InterfaceOptions interfaceOptions = interfaceOptionsFrom(interfaceSettings);

            return proto().sendRequest(
                    Requests.joinGame()
                            .as(playerSettings.getRace(), playerSettings.getPlayerName())
                            .use(interfaceOptions)
                            .with(multiplayerOptions));
        } finally {
            setMultiplayer(isSet(multiplayerOptions));
        }
    }

    InterfaceOptions interfaceOptionsFrom(InterfaceSettings interfaceSettings) {
        FeatureLayerSyntax interfaces = InterfaceOptions.interfaces()
                .showCloaked(interfaceSettings.getShowCloaked())
                .showBurrowed(interfaceSettings.getShowBurrowed())
                .raw()
                .rawCropToPlayableArea(interfaceSettings.getRawCropToPlayableArea())
                .rawAffectsSelection(interfaceSettings.getRawAffectsSelection())
                .score();
        interfaceSettings.getFeatureLayerSettings().ifPresent(interfaces::featureLayer);
        interfaceSettings.getRenderSettings().ifPresent(interfaces::render);
        return interfaces.build();
    }

    void setMultiplayer(boolean multiplayer) {
        this.multiplayer = multiplayer;
    }

    @Override
    public boolean isMultiplayer() {
        return multiplayer;
    }

    @Override
    public boolean waitJoinGame(Maybe waitFor) {
        log.info("Waiting for the JoinGame response.");
        Optional responseJoinGame = waitForResponse(waitFor)
                .flatMap(response -> response.as(ResponseJoinGame.class));

        if (responseJoinGame.isPresent()) {
            ResponseJoinGame joinGame = responseJoinGame.get();
            if (joinGame.getError().isPresent()) {
                log.error("Error in joining the game.");

                joinGame.getError().ifPresent(
                        errorCode -> log.error("JoinGame request returned an error code: {}", errorCode));
                joinGame.getErrorDetails().ifPresent(
                        errorDetails -> log.error("JoinGame request returned error details: {}", errorDetails));

                return false;
            }

            observationInternal().setPlayerId(joinGame.getPlayerId());

            log.info("WaitJoinGame finished successfully.");
            return true;
        } else {
            log.error("Response received is not JoinGame response.");
            return false;
        }
    }

    @Override
    public Maybe requestLeaveGame() {
        if (!multiplayer) throw new IllegalStateException("LeaveGame request is only available for multiplayer game.");
        leaveGameWasRequested = true;
        return proto().sendRequest(Requests.leaveGame());
    }

    @Override
    public boolean pollLeaveGame(Maybe waitFor) {
        if (!multiplayer) throw new IllegalStateException("LeaveGame response is only available for multiplayer game.");
        return waitForResponse(waitFor).flatMap(response -> response.as(ResponseLeaveGame.class)).isPresent();
    }

    // TODO p.picheta to test
    @Override
    public boolean pollLeaveGame() {
        if (!multiplayer) return false;

        if (!proto().hasResponsePending(ResponseType.LEAVE_GAME)) {
            // If not in a game, then it is in the end state trying to leave the game.
            errorIf(proto().hasResponsePending(), ClientError.RESPONSE_NOT_CONSUMED, Collections.emptyList());
            return !isInGame() && leaveGameWasRequested;
        }

        return waitForResponse(getResponsePending(ResponseType.LEAVE_GAME))
                .flatMap(response -> response.as(ResponseLeaveGame.class))
                .isPresent();
    }

    @Override
    public Maybe step(int count) {
        checkApplicationState();
        return proto().sendRequest(Requests.nextStep().withCount(count));
    }

    private void checkApplicationState() {
        if (invalidState()) {
            throw new IllegalStateException(format("Invalid application state: %s", getAppState()));
        }
    }

    private boolean invalidState() {
        return !getAppState().equals(AppState.NORMAL);
    }

    @Override
    public boolean waitStep(Maybe waitFor) {
        return waitForResponse(waitFor).flatMap(response -> response.as(ResponseStep.class)).isPresent() &&
                getObservation();
    }

    @Override
    public boolean saveReplay(Path path) throws IOException {

        Optional responseSaveReplay =
                waitForResponse(proto().sendRequest(Requests.saveReplay()))
                        .flatMap(response -> response.as(ResponseSaveReplay.class));

        boolean success = responseSaveReplay.isPresent() && responseSaveReplay.get().getData().length > 0;

        if (success) {
            Files.write(path, responseSaveReplay.get().getData());
        }

        return success;
    }

    @Override
    public boolean ping() {
        return waitForResponse(proto().sendRequest(Requests.ping()))
                .flatMap(response -> response.as(ResponsePing.class))
                .isPresent();
    }

    @Override
    public void quit() {
        proto().quit();
        if (isSet(theGame)) {
            theGame.stop();
            theGame = null;
        }
    }

    @Override
    public Optional waitForResponse(Maybe responseMaybe) {
        checkApplicationState();
        try {
            Optional response = proto().waitForResponse(responseMaybe);
            if (!response.isPresent()) {
                onUnresponsiveServer();
            }

            Optional error = response.flatMap(r -> r.as(ResponseError.class));
            if (error.isPresent()) {
                onError(ClientError.SC2_PROTOCOL_ERROR, error.get().getErrors());
                return response;
            }

            return response;
        } catch (ResponseParseException e) {
            onError(ClientError.INVALID_RESPONSE, Collections.emptyList());
            return Optional.empty();
        } catch (Exception e) {
            log.error("waitForResponse error", e);
            if (e.getCause() instanceof TimeoutException) {
                onUnresponsiveServer();
            }
            return Optional.empty();
        }
    }

    private void onUnresponsiveServer() {
        // The game application did not responded, the previous request was either not sent or the app is non-responsive.

        if (isSet(theGame)) {
            handleProcessError();
        } else {
            protocolTimeout();
        }
    }

    private void handleProcessError() {
        // Step 1: distinguish between a hang and a crash. Lots of time has elapsed, so if there was a crash
        // it should have finished by now.
        if (!theGame.getS2Process().isAlive()) {
            log.error("Game process is not alive.");
            onProcessUnexpectedlyClosed();
            return;
        }
        // Step 2: distinguish between a non-responsive app and a failure to deliver a valid request.
        if (!protoInternal().isConnected()) {
            log.error("Connection unexpectedly closed.");
            // Mark the game app as unresponsive.
            onConnectionUnexpectedlyClosed();
        } else {
            // Wait for a ping response. If this fails, the game is unresponsive.
            if (verifyGameResponsiveness()) return;
        }

        // The game application has hanged. Try and terminate it.
        tryTerminateHangedProcess();
    }

    private void onProcessUnexpectedlyClosed() {
        setAppState(AppState.CRASHED);
        onError(ClientError.SC2_APP_FAILURE, Collections.emptyList());
        log.error("Game application has terminated unexpectedly.");
    }

    private void onConnectionUnexpectedlyClosed() {
        protocolTimeout();
    }

    private void protocolTimeout() {
        setAppState(AppState.TIMEOUT);
        onError(ClientError.SC2_PROTOCOL_TIMEOUT, Collections.emptyList());
    }

    private boolean verifyGameResponsiveness() {
        log.info("Verifying game responsiveness...");
        try {
            proto().waitForResponse(proto().sendRequest(Requests.ping()));
            log.info("Game is responsive.");
            if (proto().lastStatus().equals(GameStatus.UNKNOWN)) {
                log.error("Game has unknown status.");
                onError(ClientError.SC2_UNKNOWN_STATUS, Collections.emptyList());
            } else {
                // The game is responsive, but there was another problem. This isn't the right
                // place to handle another type of problem. Just return the nullptr.
                onError(ClientError.SC2_UNKNOWN_STATUS, Collections.emptyList());
                return true;
            }
        } catch (Exception e) {
            log.error("Game is unresponsive.", e);
            protocolTimeout();
        }
        return false;
    }

    private void tryTerminateHangedProcess() {
        setAppState(AppState.TIMEOUT);
        if (!theGame.stopAndWait() || theGame.getS2Process().isAlive()) {
            log.error("Termination of hanged process failed.");
            // Failed to kill the running process.
            setAppState(AppState.TIMEOUT_ZOMBIE);
        }
        log.error("Game application has been terminated due to unresponsiveness.");
        onError(ClientError.SC2_APP_FAILURE, Collections.emptyList());
    }

    @Override
    public void setProcessInfo(ProcessInfo pi) {
        this.processInfo = pi;
    }

    @Override
    public ProcessInfo getProcessInfo() {
        return this.processInfo;
    }

    void setAppState(AppState appState) {
        require("application state", appState);
        this.appState = appState;
    }

    @Override
    public AppState getAppState() {
        return appState;
    }

    @Override
    public GameStatus getLastStatus() {
        return proto().lastStatus();
    }

    @Override
    public boolean isInGame() {
        return !invalidState() &&
                (getLastStatus().equals(GameStatus.IN_GAME) || getLastStatus().equals(GameStatus.IN_REPLAY));
    }

    @Override
    public boolean isFinishedGame() {
        if (invalidState()) return true;
        if (isInGame()) return false;
        return !hasResponsePending();
    }

    @Override
    public boolean isReadyForCreateGame() {
        if (invalidState()) return false;
        // Make sure the pipes are clear first.
        if (hasResponsePending()) return false;

        return (getLastStatus().equals(GameStatus.LAUNCHED) || getLastStatus().equals(GameStatus.ENDED));
    }

    @Override
    public boolean hasResponsePending(ResponseType responseType) {
        return proto().hasResponsePending(responseType);
    }

    @Override
    public boolean hasResponsePending() {
        return proto().hasResponsePending();
    }

    @Override
    public Maybe getResponsePending(ResponseType responseType) {
        return proto().getResponsePending(responseType);
    }

    @Override
    public boolean pollResponse(ResponseType responseType) {
        return proto().pollResponse(responseType);
    }

    @Override
    public boolean getObservation() {
        checkApplicationState();

        Optional responseObservation = waitForResponse(proto().sendRequest(Requests.observation()))
                .flatMap(response -> response.as(ResponseObservation.class));

        return responseObservation.isPresent() && observationInternal().updateObservation(responseObservation.get());
    }

    @Override
    public boolean issueEvents(List commands) {
        if (!observationInternal().gameLoopChanged()) return false;

        issueUnitDestroyedEvents();
        issueUnitAddedEvents();

        observation().getUnits(Alliance.SELF).forEach(unit -> {
            issueIdleEvent(unit, commands);
            issueBuildingCompletedEvent(unit);
        });

        issueUpgradeEvents();
        issueAlertEvents();

        // Run the users onStep function after events have been issued.
        clientEvents.onStep();

        return true;
    }

    private void issueUnitDestroyedEvents() {
        observationInternal().getRawObservation().getRaw()
                .flatMap(ObservationRaw::getEvent)
                .ifPresent(event -> event.getDeadUnits().forEach(tag ->
                        observationInternal().unitPool().getUnit(tag).ifPresent(unit -> {
                            observationInternal().unitPool().markDead(tag);
                            clientEvents.onUnitDestroyed(unit);
                        })));
    }

    private void issueUnitAddedEvents() {
        observationInternal().unitPool().forEachExistingUnit(unitInPool -> unitInPool.getUnit().ifPresent(unit -> {
            if (!hasPreviousState(unit)) {
                if (unit.getAlliance().equals(Alliance.ENEMY) && unit.getDisplayType().equals(DisplayType.VISIBLE)) {
                    clientEvents.onUnitEnterVision(unitInPool);
                } else if (unit.getAlliance().equals(Alliance.SELF) && !calledOnCreateUnits.contains(unit.getTag())) {
                    calledOnCreateUnits.add(unit.getTag());
                    clientEvents.onUnitCreated(unitInPool);
                }
            }
        }));

    }

    private boolean hasPreviousState(Unit unit) {
        return observation().getGameLoop() > 1 && observationInternal().unitPool().previous().containsKey(unit.getTag());
    }

    private void issueIdleEvent(UnitInPool unitInPool, List commands) {
        unitInPool.getUnit()
                .filter(idleUnit())
                .ifPresent(unit -> {
                    // Lookup unit from previous map.
                    if (!hasPreviousState(unit)) {
                        // If it's not in the previous map it's a new unit with new orders so trigger the OnIdle event.
                        clientEvents.onUnitIdle(unitInPool);
                        return;
                    } else {
                        // Otherwise get that unit from the previous list and verify it's state changed to idle.
                        if (!idleUnit().test(getPreviousState(unit))) {
                            clientEvents.onUnitIdle(unitInPool);
                            return;
                        }
                    }

                    // Iterate the issued commands, if a unit exists in that list but does not currently have orders
                    // the order must have failed. Reissue the OnUnitIdle event in that case.
                    commands.stream()
                            .filter(tag -> tag.equals(unit.getTag()))
                            .findFirst()
                            .ifPresent(tag -> clientEvents.onUnitIdle(unitInPool));
                });
    }

    private Unit getPreviousState(Unit unit) {
        return observationInternal().unitPool().previous().get(unit.getTag());
    }

    private Predicate idleUnit() {
        return unit -> unit.getOrders().isEmpty() && isBuild(unit);
    }

    private boolean isBuild(Unit unit) {
        return unit.getBuildProgress() == 1.0f;
    }

    private void issueBuildingCompletedEvent(UnitInPool unitInPool) {
        // If the units build progress is complete but it previously wasn't call construction complete
        unitInPool.getUnit()
                .filter(this::isBuild)
                .ifPresent(unit -> {
                    if (hasPreviousState(unit)) {
                        if (!isBuild(getPreviousState(unit))) {
                            clientEvents.onBuildingConstructionComplete(unitInPool);
                        }
                    }
                });
    }

    private void issueUpgradeEvents() {
        Set upgradesPrevious = observationInternal().getUpgradesPrevious();
        observation().getUpgrades().forEach(upgrade -> {
            if (!upgradesPrevious.contains(upgrade)) {
                clientEvents.onUpgradeCompleted(upgrade);
            }
        });
    }

    private void issueAlertEvents() {
        observationInternal().getRawObservation().getAlerts().forEach(alert -> {
            switch (alert) {
                case NUCLEAR_LAUNCH_DETECTED:
                    clientEvents.onNuclearLaunchDetected();
                    break;
                case NYDUS_WORM_DETECTED:
                    clientEvents.onNydusDetected();
                    break;
                default:
                    clientEvents.onAlert(alert);
            }
        });
    }

    @Override
    public void onGameStart() {
        observation()
                .getUnits(Alliance.SELF, isMainBase())
                .stream()
                .findFirst()
                .ifPresent(unitInPool -> {
                    if (unitInPool.getUnit().isPresent()) {
                        observationInternal().setStartLocation(unitInPool.getUnit().get().getPosition());
                    }
                });
    }

    private Predicate isMainBase() {
        return unitInPool -> {
            Optional unit = unitInPool.getUnit();
            if (unit.isPresent()) {
                UnitType type = unit.get().getType();
                return Set
                        .of((UnitType) Units.TERRAN_COMMAND_CENTER, Units.PROTOSS_NEXUS, Units.ZERG_HATCHERY)
                        .contains(type);
            } else {
                return false;
            }
        };
    }

    @Override
    public void dumpProtoUsage() {
        StringBuilder protoUsage = new StringBuilder();

        protoUsage.append("\n******************************************************\n");
        protoUsage.append("Protocol use by message type:\n");
        proto().getCountUses().forEach((responseType, count) -> {
            if (count != null) {
                protoUsage.append(responseType).append(": ").append(count).append("\n");
            }
        });

        protoUsage.append("******************************************************\n");

        log.info("Dump proto usage" + protoUsage.toString());
    }

    @Override
    public void error(ClientError clientError, List errors) {
        onError(clientError, errors);
    }

    @Override
    public void errorIf(boolean condition, ClientError clientError, List errors) {
        if (condition) onError(clientError, errors);
    }

    @Override
    public List getClientErrors() {
        return new ArrayList<>(clientErrors);
    }

    @Override
    public List getProtocolErrors() {
        return new ArrayList<>(protocolErrors);
    }

    @Override
    public void clearClientErrors() {
        clientErrors.clear();
    }

    @Override
    public void clearProtocolErrors() {
        protocolErrors.clear();
    }

    @Override
    public void useGeneralizedAbility(boolean useGeneralizedAbilityId) {
        this.useGeneralizedAbilityId = useGeneralizedAbilityId;
    }

    @Override
    public boolean save() {
        return waitForResponse(proto().sendRequest(Requests.quickSave()))
                .flatMap(response -> response.as(ResponseQuickSave.class))
                .isPresent();
    }

    @Override
    public boolean load() {
        return waitForResponse(proto().sendRequest(Requests.quickLoad()))
                .flatMap(response -> response.as(ResponseQuickLoad.class))
                .isPresent();
    }

    @Override
    public AgentControlInterface agentControl() {
        return agentControlInterface;
    }

    @Override
    public QueryInterface query() {
        return queryInterface;
    }

    @Override
    public DebugInterface debug() {
        return debugInterface;
    }

    @Override
    public ObserverActionInterface observerAction() {
        return observerActionInterface;
    }

    @Override
    public ReplayControlInterface replayControl(S2ReplayObserver replayObserver) {
        return new ReplayControlInterfaceImpl(this, replayObserver);
    }

    boolean isUseGeneralizedAbilityId() {
        return useGeneralizedAbilityId;
    }

    // test purposes only
    S2Controller getTheGame() {
        return theGame;
    }

    // test purposes only
    void disconnect() {
        protoInternal().disconnect();
    }

    // test purposes only
    void forceStop() {
        if (isSet(theGame)) {
            theGame.stopAndWait();
            theGame = null;
        }
    }

    ClientEvents clientEvents() {
        return clientEvents;
    }

    ObservationInterfaceImpl observationInternal() {
        return observationInterface;
    }

    private ProtoInterfaceImpl protoInternal() {
        return protoInterface;
    }

    @Override
    public String toString() {
        return "ControlInterfaceImpl{" +
                "clientErrors=" + clientErrors +
                ", protocolErrors=" + protocolErrors +
                ", appState=" + appState +
                ", multiplayer=" + multiplayer +
                '}';
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy