Ejemplo n.º 1
0
    def connect(self):
        """ Connects to the Slippi server (dolphin or wii).

        Returns boolean of success """
        self.slippstream = SlippstreamClient(self.slippi_address,
                                             self.slippi_port)
        return self.slippstream.connect()
Ejemplo n.º 2
0
    def __init__(self,
                 path=None,
                 is_dolphin=True,
                 dolphin_home_path=None,
                 tmp_home_directory=True,
                 slippi_address="127.0.0.1",
                 slippi_port=51441,
                 online_delay=2,
                 blocking_input=False,
                 polling_mode=False,
                 allow_old_version=False,
                 logger=None):
        """Create a Console object

        Args:
            path (str): Path to the directory where your dolphin executable is located.
                If None, will assume the dolphin is remote and won't try to configure it.
            dolphin_home_path (str): Path to dolphin user directory. Optional.
            is_dolphin (bool): Is this console a dophin instance, or SLP file?
            tmp_home_directory (bool): Use a temporary directory for the dolphin User path
                This is useful so instances don't interfere with each other.
            slippi_address (str): IP address of the Dolphin / Wii to connect to.
            slippi_port (int): UDP port that slippi will listen on
            online_delay (int): How many frames of delay to apply in online matches
            blocking_input (bool): Should dolphin block waiting for bot input
                This is only really useful if you're doing ML training.
            polling_mode (bool): Polls input to console rather than blocking for it
                When set, step() will always return immediately, but may be None if no
                gamestate is available yet.
            allow_old_version (bool): Allow SLP versions older than 3.0.0 (rollback era)
                Only enable if you know what you're doing. You probably don't want this.
                Gamestates will be missing key information, come in really late, or possibly not work at all
            logger (logger.Logger): Logger instance to use. None for no logger.
        """
        self.logger = logger
        self.is_dolphin = is_dolphin
        self.path = path
        self.dolphin_home_path = dolphin_home_path
        if tmp_home_directory and self.is_dolphin:
            temp_dir = tempfile.mkdtemp(prefix='libmelee_')
            temp_dir += "/User/"
            _copytree_safe(self._get_dolphin_home_path(), temp_dir)
            self.dolphin_home_path = temp_dir

        self.processingtime = 0
        self._frametimestamp = time.time()
        self.slippi_address = slippi_address
        """(str): IP address of the Dolphin / Wii to connect to."""
        self.slippi_port = slippi_port
        """(int): UDP port of slippi server. Default 51441"""
        self.eventsize = [0] * 0x100
        self.connected = False
        self.nick = ""
        """(str): The nickname the console has given itself."""
        self.version = ""
        """(str): The Slippi version of the console"""
        self.cursor = 0
        self.controllers = []
        self._current_stage = enums.Stage.NO_STAGE
        self._frame = 0
        self._polling_mode = polling_mode
        self.slp_version = "unknown"
        """(str): The SLP version this stream/file currently is."""
        self._allow_old_version = allow_old_version
        self._use_manual_bookends = False
        self._costumes = {0:0, 1:0, 2:0, 3:0}
        self._cpu_level = {0:0, 1:0, 2:0, 3:0}
        self._invuln_start = {1:(0,0), 2:(0,0), 3:(0,0), 4:(0,0)}

        # Keep a running copy of the last gamestate produced
        self._prev_gamestate = GameState()
        # Half-completed gamestate not yet ready to add to the list
        self._temp_gamestate = None
        self._process = None
        if self.is_dolphin:
            self._slippstream = SlippstreamClient(self.slippi_address, self.slippi_port)
            if self.path:
                # Setup some dolphin config options
                dolphin_ini_path = self._get_dolphin_config_path() + "Dolphin.ini"
                if not os.path.isfile(dolphin_ini_path):
                    raise InvalidDolphinPath(self._get_dolphin_config_path())
                config = configparser.ConfigParser()
                config.read(dolphin_ini_path)
                config.set("Core", 'slippienablespectator', "True")
                config.set("Core", 'slippispectatorlocalport', str(self.slippi_port))
                # Set online delay
                config.set("Core", 'slippionlinedelay', str(online_delay))
                # Turn on background input so we don't need to have window focus on dolphin
                config.set("Input", 'backgroundinput', "True")
                config.set("Core", 'BlockingPipes', str(blocking_input))
                with open(dolphin_ini_path, 'w') as dolphinfile:
                    config.write(dolphinfile)
        else:
            self._slippstream = SLPFileStreamer(self.path)

        # Prepare some structures for fixing melee data
        path = os.path.dirname(os.path.realpath(__file__))
        with open(path + "/actiondata.csv") as csvfile:
            #A list of dicts containing the frame data
            actiondata = list(csv.DictReader(csvfile))
            #Dict of sets
            self.zero_indices = defaultdict(set)
            for line in actiondata:
                if line["zeroindex"] == "True":
                    self.zero_indices[int(line["character"])].add(int(line["action"]))

        # Read the character data csv
        self.characterdata = dict()
        with open(path + "/characterdata.csv") as csvfile:
            reader = csv.DictReader(csvfile)
            for line in reader:
                del line["Character"]
                #Convert all fields to numbers
                for key, value in line.items():
                    line[key] = float(value)
                self.characterdata[enums.Character(line["CharacterIndex"])] = line
Ejemplo n.º 3
0
    def __init__(self, is_dolphin, ai_port, opponent_port, opponent_type,
                 dolphin_executable_path=None, slippi_address="", logger=None):
        """Create a Console object

        Args:
            is_dolphin (boolean): Is this console a dolphin? (Or a Wii)
            ai_port (int): 1-4 for the controller port your bot will take
            opponent_port (int): 1-4 for the controller port your opponent will take
            opponent_type (:obj:`enums.ControllerType`): Enum of your opponent's controller type
            dolphin_executable_path (str): Path to the directory where your dolphin executable is
                located. (if applicable) None tells console to use the installed copy of the emulator
            slippi_address (str): IP address of the Dolphin / Wii to connect to.
                Empty string will try to autodiscover a nearby SlippiComm server
        """
        self.logger = logger
        self.ai_port = ai_port
        self.opponent_port = opponent_port
        self.is_dolphin = is_dolphin
        self.dolphin_executable_path = dolphin_executable_path
        self.processingtime = 0
        self._frametimestamp = time.time()
        self.slippi_address = slippi_address
        self.slippi_port = 51441
        self.eventsize = [0] * 0x100
        self.render = True

        # Keep a running copy of the last gamestate produced
        self._prev_gamestate = GameState(ai_port, opponent_port)
        self._process = None
        if self.is_dolphin:
            pipes_path = self._get_dolphin_home_path() + "Pipes/"
            path = os.path.dirname(os.path.realpath(__file__))

            if platform.system() != "Windows":
                #Create the Pipes directory if it doesn't already exist
                if not os.path.exists(pipes_path):
                    os.makedirs(pipes_path)
                    print("WARNING: Had to create a Pipes directory in Dolphin just now. " \
                          "You may need to restart Dolphin and this program in order for this to work. " \
                          "(You should only see this warning once)")

                pipes_path += "slippibot" + str(ai_port)
                if not os.path.exists(pipes_path):
                    os.mkfifo(pipes_path)

            #setup the controllers specified
            self.setup_dolphin_controller(ai_port)
            self.setup_dolphin_controller(opponent_port, opponent_type)
            self._slippstream = SlippstreamClient(self.slippi_address, self.slippi_port)

        # Prepare some structures for fixing melee data
        path = os.path.dirname(os.path.realpath(__file__))
        with open(path + "/actiondata.csv") as csvfile:
            #A list of dicts containing the frame data
            actiondata = list(csv.DictReader(csvfile))
            #Dict of sets
            self.zero_indices = defaultdict(set)
            for line in actiondata:
                if line["zeroindex"] == "True":
                    self.zero_indices[int(line["character"])].add(int(line["action"]))

        # Read the character data csv
        self.characterdata = dict()
        with open(path + "/characterdata.csv") as csvfile:
            reader = csv.DictReader(csvfile)
            for line in reader:
                del line["Character"]
                #Convert all fields to numbers
                for key, value in line.items():
                    line[key] = float(value)
                self.characterdata[enums.Character(line["CharacterIndex"])] = line
Ejemplo n.º 4
0
class Console:
    """The console object that represents your Dolphin / Wii / SLP file

    Attributes:
        slippi_address (str): IP address of the Dolphin / Wii to connect to.
            Empty string will try to autodiscover a nearby SlippiComm server
        slippi_port (int): TCP port of slippi server. Default 51441
    """
    def __init__(self, is_dolphin, ai_port, opponent_port, opponent_type,
                 dolphin_executable_path=None, slippi_address="", logger=None):
        """Create a Console object

        Args:
            is_dolphin (boolean): Is this console a dolphin? (Or a Wii)
            ai_port (int): 1-4 for the controller port your bot will take
            opponent_port (int): 1-4 for the controller port your opponent will take
            opponent_type (:obj:`enums.ControllerType`): Enum of your opponent's controller type
            dolphin_executable_path (str): Path to the directory where your dolphin executable is
                located. (if applicable) None tells console to use the installed copy of the emulator
            slippi_address (str): IP address of the Dolphin / Wii to connect to.
                Empty string will try to autodiscover a nearby SlippiComm server
        """
        self.logger = logger
        self.ai_port = ai_port
        self.opponent_port = opponent_port
        self.is_dolphin = is_dolphin
        self.dolphin_executable_path = dolphin_executable_path
        self.processingtime = 0
        self._frametimestamp = time.time()
        self.slippi_address = slippi_address
        self.slippi_port = 51441
        self.eventsize = [0] * 0x100
        self.render = True

        # Keep a running copy of the last gamestate produced
        self._prev_gamestate = GameState(ai_port, opponent_port)
        self._process = None
        if self.is_dolphin:
            pipes_path = self._get_dolphin_home_path() + "Pipes/"
            path = os.path.dirname(os.path.realpath(__file__))

            if platform.system() != "Windows":
                #Create the Pipes directory if it doesn't already exist
                if not os.path.exists(pipes_path):
                    os.makedirs(pipes_path)
                    print("WARNING: Had to create a Pipes directory in Dolphin just now. " \
                          "You may need to restart Dolphin and this program in order for this to work. " \
                          "(You should only see this warning once)")

                pipes_path += "slippibot" + str(ai_port)
                if not os.path.exists(pipes_path):
                    os.mkfifo(pipes_path)

            #setup the controllers specified
            self.setup_dolphin_controller(ai_port)
            self.setup_dolphin_controller(opponent_port, opponent_type)
            self._slippstream = SlippstreamClient(self.slippi_address, self.slippi_port)

        # Prepare some structures for fixing melee data
        path = os.path.dirname(os.path.realpath(__file__))
        with open(path + "/actiondata.csv") as csvfile:
            #A list of dicts containing the frame data
            actiondata = list(csv.DictReader(csvfile))
            #Dict of sets
            self.zero_indices = defaultdict(set)
            for line in actiondata:
                if line["zeroindex"] == "True":
                    self.zero_indices[int(line["character"])].add(int(line["action"]))

        # Read the character data csv
        self.characterdata = dict()
        with open(path + "/characterdata.csv") as csvfile:
            reader = csv.DictReader(csvfile)
            for line in reader:
                del line["Character"]
                #Convert all fields to numbers
                for key, value in line.items():
                    line[key] = float(value)
                self.characterdata[enums.Character(line["CharacterIndex"])] = line

    def connect(self):
        """ Connects to the Slippi server (dolphin or wii).

        Returns:
            True is successful, False otherwise
        """
        # It can take a short amount of time after starting the emulator
        #   for the actual server to start. So try a few times before giving up.
        for _ in range(4):
            if self._slippstream.connect():
                return True
        return False

    def run(self, iso_path=None, movie_path=None, dolphin_config_path=None):
        """Run dolphin-emu

        Args:
            iso_path (str, optional): Path to Melee ISO for dolphin to read
            dolphin_config_path (str, optional): Alternative config path for dolphin
                if not using the default
        """
        if self.is_dolphin:
            exe_name = "dolphin-emu"
            if platform.system() == "Windows":
                exe_name = "Dolphin.exe"

            exe_path = ""
            if self.dolphin_executable_path:
                exe_path = self.dolphin_executable_path
            command = [exe_path + exe_name]
            if not self.render:
                #Use the "Null" renderer
                command.append("-v")
                command.append("Null")
            if movie_path is not None:
                command.append("-m")
                command.append(movie_path)
            if iso_path is not None:
                command.append("-e")
                command.append(iso_path)
            if dolphin_config_path is not None:
                command.append("-u")
                command.append(dolphin_config_path)
            self._process = subprocess.Popen(command)

    def stop(self):
        """ Stop the console.

        For Dolphin instances, this will kill the dolphin process.
        For Wiis and SLP files, it just shuts down our connection
         """
        self._slippstream.shutdown()
        # If dolphin, kill the process
        if self._process is not None:
            self._process.terminate()

    def setup_dolphin_controller(self, port, controllertype=enums.ControllerType.STANDARD):
        """Setup the necessary files for dolphin to recognize the player at the given
        controller port and type"""
        #Read in dolphin's controller config file
        controller_config_path = self._get_dolphin_config_path() + "GCPadNew.ini"
        config = configparser.SafeConfigParser()
        config.read(controller_config_path)

        #Add a bot standard controller config to the given port
        section = "GCPad" + str(port)
        if not config.has_section(section):
            config.add_section(section)

        if controllertype == enums.ControllerType.STANDARD:
            config.set(section, 'Device', 'Pipe/0/slippibot' + str(port))
            config.set(section, 'Buttons/A', 'Button A')
            config.set(section, 'Buttons/B', 'Button B')
            config.set(section, 'Buttons/X', 'Button X')
            config.set(section, 'Buttons/Y', 'Button Y')
            config.set(section, 'Buttons/Z', 'Button Z')
            config.set(section, 'Buttons/L', 'Button L')
            config.set(section, 'Buttons/R', 'Button R')
            config.set(section, 'Main Stick/Up', 'Axis MAIN Y +')
            config.set(section, 'Main Stick/Down', 'Axis MAIN Y -')
            config.set(section, 'Main Stick/Left', 'Axis MAIN X -')
            config.set(section, 'Main Stick/Right', 'Axis MAIN X +')
            config.set(section, 'Triggers/L', 'Button L')
            config.set(section, 'Triggers/R', 'Button R')
            config.set(section, 'Main Stick/Modifier', 'Shift_L')
            config.set(section, 'Main Stick/Modifier/Range', '50.000000000000000')
            config.set(section, 'D-Pad/Up', 'Button D_UP')
            config.set(section, 'D-Pad/Down', 'Button D_DOWN')
            config.set(section, 'D-Pad/Left', 'Button D_LEFT')
            config.set(section, 'D-Pad/Right', 'Button D_RIGHT')
            config.set(section, 'Buttons/Start', 'Button START')
            config.set(section, 'Buttons/A', 'Button A')
            config.set(section, 'C-Stick/Up', 'Axis C Y +')
            config.set(section, 'C-Stick/Down', 'Axis C Y -')
            config.set(section, 'C-Stick/Left', 'Axis C X -')
            config.set(section, 'C-Stick/Right', 'Axis C X +')
            config.set(section, 'Triggers/L-Analog', 'Axis L -+')
            config.set(section, 'Triggers/R-Analog', 'Axis R -+')
        #This section is unused if it's not a standard input (I think...)
        else:
            config.set(section, 'Device', 'XInput2/0/Virtual core pointer')

        with open(controller_config_path, 'w') as configfile:
            config.write(configfile)

        #Change the bot's controller port to use "standard" input
        dolphin_config_path = self._get_dolphin_config_path() + "Dolphin.ini"
        config = configparser.SafeConfigParser()
        config.read(dolphin_config_path)
        #Indexed at 0. "6" means standard controller, "12" means GCN Adapter
        # The enum is scoped to the proper value, here
        config.set("Core", 'SIDevice'+str(port-1), controllertype.value)
        # Enable networking
        config.set("General", 'EnableSlippiNetworkingOutput', "True")
        #Enable Cheats
        config.set("Core", 'enablecheats', "True")
        #Turn on background input so we don't need to have window focus on dolphin
        config.set("Input", 'backgroundinput', "True")
        with open(dolphin_config_path, 'w') as dolphinfile:
            config.write(dolphinfile)

        # #Enable the specific cheats we need (Netplay community settings)
        # melee_config_path = self._get_dolphin_home_path() + "/GameSettings/GALE01.ini"
        # config = configparser.SafeConfigParser(allow_no_value=True)
        # config.optionxform = str
        # config.read(melee_config_path)
        # if not config.has_section("Gecko_Enabled"):
        #     config.add_section("Gecko_Enabled")
        # config.set("Gecko_Enabled", "$Netplay Community Settings")
        # with open(melee_config_path, 'w') as dolphinfile:
        #     config.write(dolphinfile)

    def step(self):
        """ 'step' to the next state of the game
        Returns a new gamestate object that represents current state of the game"""
        # Keep looping until we get a REPLAY message
        self.processingtime = time.time() - self._frametimestamp
        gamestate = GameState(self.ai_port, self.opponent_port)
        frame_ended = False
        while not frame_ended:
            msg = self._slippstream.read_message()
            if msg:
                if CommType(msg['type']) == CommType.REPLAY:
                    events = msg['payload']['data']
                    frame_ended = self.__handle_slippstream_events(events, gamestate)

                # We can basically just ignore keepalives
                elif CommType(msg['type']) == CommType.KEEPALIVE:
                    pass

                elif CommType(msg['type']) == CommType.HANDSHAKE:
                    handshake = msg['payload']
                    print("Connected to console '{}' (Slippi Nintendont {})".format(
                        handshake['nick'],
                        handshake['nintendontVersion'],
                    ))
                # Handle menu-state event
                elif CommType(msg['type']) == CommType.MENU:
                    events = msg['payload']['data']
                    self.__handle_slippstream_menu_event(events, gamestate)
                    frame_ended = True

        self.__fixframeindexing(gamestate)
        self.__fixiasa(gamestate)
        # Start the processing timer now that we're done reading messages
        self._frametimestamp = time.time()
        return gamestate

    def __handle_slippstream_events(self, event_bytes, gamestate):
        """ Handle a series of events, provided sequentially in a byte array """
        gamestate.menu_state = enums.Menu.IN_GAME
        while len(event_bytes) > 0:
            event_size = self.eventsize[event_bytes[0]]
            if len(event_bytes) < event_size:
                print("WARNING: Something went wrong unpacking events. Data is probably missing")
                print("\tDidn't have enough data for event")
                return False
            if EventType(event_bytes[0]) == EventType.PAYLOADS:
                cursor = 0x2
                payload_size = event_bytes[1]
                num_commands = (payload_size - 1) // 3
                for i in range(0, num_commands):
                    command, command_len = unpack(">bH", event_bytes[cursor:cursor+3])
                    self.eventsize[command] = command_len+1
                    cursor += 3
                event_bytes = event_bytes[payload_size + 1:]

            elif EventType(event_bytes[0]) == EventType.FRAME_START:
                event_bytes = event_bytes[event_size:]

            elif EventType(event_bytes[0]) == EventType.GAME_START:
                event_bytes = event_bytes[event_size:]

            elif EventType(event_bytes[0]) == EventType.GAME_END:
                event_bytes = event_bytes[event_size:]

            elif EventType(event_bytes[0]) == EventType.PRE_FRAME:
                event_bytes = event_bytes[event_size:]

            elif EventType(event_bytes[0]) == EventType.POST_FRAME:
                gamestate.frame = unpack(">i", event_bytes[0x1:0x1+4])[0]
                controller_port = unpack(">B", event_bytes[0x5:0x5+1])[0] + 1

                gamestate.player[controller_port].x = unpack(">f", event_bytes[0xa:0xa+4])[0]
                gamestate.player[controller_port].y = unpack(">f", event_bytes[0xe:0xe+4])[0]

                gamestate.player[controller_port].character = enums.Character(unpack(">B", event_bytes[0x7:0x7+1])[0])
                try:
                    gamestate.player[controller_port].action = enums.Action(unpack(">H", event_bytes[0x8:0x8+2])[0])
                except ValueError:
                    gamestate.player[controller_port].action = enums.Action.UNKNOWN_ANIMATION

                # Melee stores this in a float for no good reason. So we have to convert
                facing_float = unpack(">f", event_bytes[0x12:0x12+4])[0]
                gamestate.player[controller_port].facing = facing_float > 0

                gamestate.player[controller_port].percent = int(unpack(">f", event_bytes[0x16:0x16+4])[0])
                gamestate.player[controller_port].stock = unpack(">B", event_bytes[0x21:0x21+1])[0]
                gamestate.player[controller_port].action_frame = int(unpack(">f", event_bytes[0x22:0x22+4])[0])

                # Extract the bit at mask 0x20
                bitflags2 = unpack(">B", event_bytes[0x27:0x27+1])[0]
                gamestate.player[controller_port].hitlag = bool(bitflags2 & 0x20)

                try:
                    gamestate.player[controller_port].hitstun_frames_left = int(unpack(">f", event_bytes[0x2b:0x2b+4])[0])
                except ValueError:
                    gamestate.player[controller_port].hitstun_frames_left = 0
                gamestate.player[controller_port].on_ground = not bool(unpack(">B", event_bytes[0x2f:0x2f+1])[0])
                gamestate.player[controller_port].jumps_left = unpack(">B", event_bytes[0x32:0x32+1])[0]
                gamestate.player[controller_port].invulnerable = int(unpack(">B", event_bytes[0x34:0x34+1])[0]) != 0

                try:
                    gamestate.player[controller_port].speed_air_x_self = unpack(">f", event_bytes[0x35:0x35+4])[0]
                except error:
                    gamestate.player[controller_port].speed_air_x_self = 0

                try:
                    gamestate.player[controller_port].speed_y_self = unpack(">f", event_bytes[0x39:0x39+4])[0]
                except error:
                    gamestate.player[controller_port].speed_y_self = 0

                try:
                    gamestate.player[controller_port].speed_x_attack = unpack(">f", event_bytes[0x3d:0x3d+4])[0]
                except error:
                    gamestate.player[controller_port].speed_x_attack = 0

                try:
                    gamestate.player[controller_port].speed_y_attack = unpack(">f", event_bytes[0x41:0x41+4])[0]
                except error:
                    gamestate.player[controller_port].speed_y_attack = 0

                try:
                    gamestate.player[controller_port].speed_ground_x_self = unpack(">f", event_bytes[0x45:0x45+4])[0]
                except error:
                    gamestate.player[controller_port].speed_ground_x_self = 0

                # Keep track of a player's invulnerability due to respawn or ledge grab
                gamestate.player[controller_port].invulnerability_left = max(0, self._prev_gamestate.player[controller_port].invulnerability_left - 1)
                if gamestate.player[controller_port].action == Action.ON_HALO_WAIT:
                    gamestate.player[controller_port].invulnerability_left = 120
                # Don't give invulnerability to the first descent
                if gamestate.player[controller_port].action == Action.ON_HALO_DESCENT and gamestate.frame > 150:
                    gamestate.player[controller_port].invulnerability_left = 120
                if gamestate.player[controller_port].action == Action.EDGE_CATCHING and gamestate.player[controller_port].action_frame == 1:
                    gamestate.player[controller_port].invulnerability_left = 36

                # The pre-warning occurs when we first start a dash dance.
                if gamestate.player[controller_port].action == Action.DASHING and \
                        self._prev_gamestate.player[controller_port].action not in [Action.DASHING, Action.TURNING]:
                    gamestate.player[controller_port].moonwalkwarning = True

                # Take off the warning if the player does an action other than dashing
                if gamestate.player[controller_port].action != Action.DASHING:
                    gamestate.player[controller_port].moonwalkwarning = False

                # "off_stage" helper
                if (abs(gamestate.player[controller_port].x) > stages.EDGE_GROUND_POSITION[gamestate.stage] or \
                        gamestate.player[controller_port].y < -6) and not gamestate.player[controller_port].on_ground:
                    gamestate.player[controller_port].off_stage = True
                else:
                    gamestate.player[controller_port].off_stage = False

                event_bytes = event_bytes[event_size:]

            elif EventType(event_bytes[0]) == EventType.GECKO_CODES:
                event_bytes = event_bytes[event_size:]

            elif EventType(event_bytes[0]) == EventType.FRAME_BOOKEND:
                self._prev_gamestate = gamestate

                # Calculate helper distance variable
                #   This is a bit kludgey.... :/
                i = 0
                player_one_x, player_one_y, player_two_x, player_two_y = 0, 0, 0, 0
                for _, player_state in gamestate.player.items():
                    if i == 0:
                        player_one_x, player_one_y = player_state.x, player_state.y
                    if i == 1:
                        player_two_x, player_two_y = player_state.x, player_state.y
                    i += 1
                xdist = player_one_x - player_two_x
                ydist = player_one_y - player_two_y
                gamestate.distance = math.sqrt((xdist**2) + (ydist**2))
                event_bytes = event_bytes[event_size:]
                return True

            elif EventType(event_bytes[0]) == EventType.ITEM_UPDATE:
                projectile = Projectile()
                projectile.x = unpack(">f", event_bytes[0x14:0x14+4])[0]
                projectile.y = unpack(">f", event_bytes[0x18:0x18+4])[0]
                projectile.x_speed = unpack(">f", event_bytes[0x0c:0x0c+4])[0]
                projectile.y_speed = unpack(">f", event_bytes[0x10:0x10+4])[0]
                try:
                    projectile.owner = unpack(">B", event_bytes[0x2A:0x2A+1])[0] + 1
                    if projectile.owner > 4:
                        projectile.owner = -1
                except error:
                    projectile.owner = -1
                try:
                    projectile.subtype = enums.ProjectileSubtype(unpack(">H", event_bytes[0x05:0x05+2])[0])
                except ValueError:
                    projectile.subtype = enums.ProjectileSubtype.UNKNOWN_PROJECTILE
                # Add the projectile to the gamestate list
                gamestate.projectiles.append(projectile)

                event_bytes = event_bytes[event_size:]

            else:
                print("WARNING: Something went wrong unpacking events. " + \
                    "Data is probably missing")
                print("\tGot invalid event type: ", event_bytes[0])
                return False

        return False

    def __handle_slippstream_menu_event(self, event_bytes, gamestate):
        """ Internal handler for slippstream menu events

        Modifies specified gamestate based on the event bytes
         """
        scene = unpack(">H", event_bytes[0x1:0x1+2])[0]
        if scene == 0x02:
            gamestate.menu_state = enums.Menu.CHARACTER_SELECT
        elif scene == 0x0102:
            gamestate.menu_state = enums.Menu.STAGE_SELECT
        elif scene == 0x0202:
            gamestate.menu_state = enums.Menu.IN_GAME
        else:
            gamestate.menu_state = enums.Menu.UNKNOWN_MENU

        # CSS Cursors
        gamestate.player[1].cursor_x = unpack(">f", event_bytes[0x3:0x3+4])[0]
        gamestate.player[1].cursor_y = unpack(">f", event_bytes[0x7:0x7+4])[0]
        gamestate.player[2].cursor_x = unpack(">f", event_bytes[0xB:0xB+4])[0]
        gamestate.player[2].cursor_y = unpack(">f", event_bytes[0xF:0xF+4])[0]
        gamestate.player[3].cursor_x = unpack(">f", event_bytes[0x13:0x13+4])[0]
        gamestate.player[3].cursor_y = unpack(">f", event_bytes[0x17:0x17+4])[0]
        gamestate.player[4].cursor_x = unpack(">f", event_bytes[0x1B:0x1B+4])[0]
        gamestate.player[4].cursor_y = unpack(">f", event_bytes[0x1F:0x1F+4])[0]

        # Ready to fight banner
        gamestate.ready_to_start = unpack(">B", event_bytes[0x23:0x23+1])[0] == 0

        # Stage
        try:
            gamestate.stage = enums.Stage(unpack(">B", event_bytes[0x24:0x24+1])[0])
        except ValueError:
            gamestate.stage = enums.Stage.NO_STAGE

        # controller port statuses at CSS
        try:
            gamestate.player[1].controller_status = enums.ControllerStatus(unpack(">B", event_bytes[0x25:0x25+1])[0])
        except error:
            gamestate.player[1].controller_status = enums.ControllerStatus.CONTROLLER_UNPLUGGED
        try:
            gamestate.player[2].controller_status = enums.ControllerStatus(unpack(">B", event_bytes[0x26:0x26+1])[0])
        except error:
            gamestate.player[2].controller_status = enums.ControllerStatus.CONTROLLER_UNPLUGGED
        try:
            gamestate.player[3].controller_status = enums.ControllerStatus(unpack(">B", event_bytes[0x27:0x27+1])[0])
        except error:
            gamestate.player[3].controller_status = enums.ControllerStatus.CONTROLLER_UNPLUGGED
        try:
            gamestate.player[4].controller_status = enums.ControllerStatus(unpack(">B", event_bytes[0x28:0x28+1])[0])
        except error:
            gamestate.player[4].controller_status = enums.ControllerStatus.CONTROLLER_UNPLUGGED

        # Character selected
        try:
            tmp = unpack(">B", event_bytes[0x29:0x29+1])[0]
            gamestate.player[1].character_selected = enums.to_internal(tmp)
        except error:
            gamestate.player[1].character_selected = enums.Character.UNKNOWN_CHARACTER
        try:
            tmp = unpack(">B", event_bytes[0x2A:0x2A+1])[0]
            gamestate.player[2].character_selected = enums.to_internal(tmp)
        except error:
            gamestate.player[2].character_selected = enums.Character.UNKNOWN_CHARACTER
        try:
            tmp = unpack(">B", event_bytes[0x2B:0x2B+1])[0]
            gamestate.player[3].character_selected = enums.to_internal(tmp)
        except error:
            gamestate.player[3].character_selected = enums.Character.UNKNOWN_CHARACTER
        try:
            tmp = unpack(">B", event_bytes[0x2C:0x2C+1])[0]
            gamestate.player[4].character_selected = enums.to_internal(tmp)
        except error:
            gamestate.player[4].character_selected = enums.Character.UNKNOWN_CHARACTER

        # Coin down
        try:
            gamestate.player[1].coin_down = unpack(">B", event_bytes[0x2D:0x2D+1])[0] == 2
        except error:
            gamestate.player[1].coin_down = False
        try:
            gamestate.player[2].coin_down = unpack(">B", event_bytes[0x2E:0x2E+1])[0] == 2
        except error:
            gamestate.player[2].coin_down = False
        try:
            gamestate.player[3].coin_down = unpack(">B", event_bytes[0x2F:0x2F+1])[0] == 2
        except error:
            gamestate.player[3].coin_down = False
        try:
            gamestate.player[4].coin_down = unpack(">B", event_bytes[0x30:0x30+1])[0] == 2
        except error:
            gamestate.player[4].coin_down = False

        # Stage Select Cursor X, Y
        gamestate.stage_select_cursor_x = unpack(">f", event_bytes[0x31:0x31+4])[0]
        gamestate.stage_select_cursor_y = unpack(">f", event_bytes[0x35:0x35+4])[0]

        # Frame count
        gamestate.frame = unpack(">i", event_bytes[0x39:0x39+4])[0]


    def _get_dolphin_home_path(self):
        """Return the path to dolphin's home directory"""
        if self.dolphin_executable_path:
            return self.dolphin_executable_path + "/User/"

        home_path = str(Path.home())
        legacy_config_path = home_path + "/.dolphin-emu/"

        #Are we using a legacy Linux home path directory?
        if os.path.isdir(legacy_config_path):
            return legacy_config_path

        #Are we on OSX?
        osx_path = home_path + "/Library/Application Support/Dolphin/"
        if os.path.isdir(osx_path):
            return osx_path

        #Are we on a new Linux distro?
        linux_path = home_path + "/.local/share/dolphin-emu/"
        if os.path.isdir(linux_path):
            return linux_path

        print("ERROR: Are you sure Dolphin is installed? Make sure it is, and then run again.")
        return ""

    def _get_dolphin_config_path(self):
        """ Return the path to dolphin's config directory
        (which is not necessarily the same as the home path)"""
        if self.dolphin_executable_path:
            return self.dolphin_executable_path + "/User/Config/"

        home_path = str(Path.home())

        if platform.system() == "Windows":
            return home_path + "\\Dolphin Emulator\\Config\\"

        legacy_config_path = home_path + "/.dolphin-emu/"

        #Are we using a legacy Linux home path directory?
        if os.path.isdir(legacy_config_path):
            return legacy_config_path

        #Are we on a new Linux distro?
        linux_path = home_path + "/.config/dolphin-emu/"
        if os.path.isdir(linux_path):
            return linux_path

        #Are we on OSX?
        osx_path = home_path + "/Library/Application Support/Dolphin/Config/"
        if os.path.isdir(osx_path):
            return osx_path

        print("ERROR: Are you sure Dolphin is installed? Make sure it is, and then run again.")
        return ""

    def get_dolphin_pipes_path(self, port):
        """Get the path of the named pipe input file for the given controller port
        """
        if platform.system() == "Windows":
            return '\\\\.\\pipe\\slippibot' + str(port)
        return self._get_dolphin_home_path() + "/Pipes/slippibot" + str(port)

    def __fixframeindexing(self, gamestate):
        """ Melee's indexing of action frames is wildly inconsistent.
            Here we adjust all of the frames to be indexed at 1 (so math is easier)"""
        for _, player in gamestate.player.items():
            if player.action.value in self.zero_indices[player.character.value]:
                player.action_frame = player.action_frame + 1

    def __fixiasa(self, gamestate):
        """ The IASA flag doesn't set or reset for special attacks.
            So let's just set IASA to False for all non-A attacks.
        """
        for _, player in gamestate.player.items():
            # Luckily for us, all the A-attacks are in a contiguous place in the enums!
            #   So we don't need to call them out one by one
            if player.action.value < Action.NEUTRAL_ATTACK_1.value or player.action.value > Action.DAIR.value:
                player.iasa = False
Ejemplo n.º 5
0
    def __init__(self,
                 path=None,
                 is_dolphin=True,
                 slippi_address="127.0.0.1",
                 logger=None):
        """Create a Console object

        Args:
            path (str): Path to the directory where your dolphin executable is
                located. (if applicable) None tells console to use the installed copy of the emulator
            slippi_address (str): IP address of the Dolphin / Wii to connect to.
                Empty string will try to autodiscover a nearby SlippiComm server
            logger (logger.Logger): Logger instance to use. None for no logger.
        """
        self.logger = logger
        self.is_dolphin = is_dolphin
        self.path = path
        self.processingtime = 0
        self._frametimestamp = time.time()
        self.slippi_address = slippi_address
        """(str): IP address of the Dolphin / Wii to connect to."""
        self.slippi_port = 51441
        """(int): TCP port of slippi server. Default 51441"""
        self.eventsize = [0] * 0x100
        self.render = True
        self.connected = False
        self.nick = ""
        """(str): The nickname the console has given itself."""
        self.version = ""
        """(str): The Slippi version of the console"""
        self.cursor = 0
        self.controllers = []
        self._current_stage = enums.Stage.NO_STAGE
        self._frame = 0
        self.slp_version = "unknown"
        """(str): The SLP version this stream/file currently is."""

        # Keep a running copy of the last gamestate produced
        self._prev_gamestate = GameState()
        self._process = None
        if self.is_dolphin:
            self._slippstream = SlippstreamClient(self.slippi_address,
                                                  self.slippi_port)
        else:
            self._slippstream = SLPFileStreamer(self.path)

        # Prepare some structures for fixing melee data
        path = os.path.dirname(os.path.realpath(__file__))
        with open(path + "/actiondata.csv") as csvfile:
            #A list of dicts containing the frame data
            actiondata = list(csv.DictReader(csvfile))
            #Dict of sets
            self.zero_indices = defaultdict(set)
            for line in actiondata:
                if line["zeroindex"] == "True":
                    self.zero_indices[int(line["character"])].add(
                        int(line["action"]))

        # Read the character data csv
        self.characterdata = dict()
        with open(path + "/characterdata.csv") as csvfile:
            reader = csv.DictReader(csvfile)
            for line in reader:
                del line["Character"]
                #Convert all fields to numbers
                for key, value in line.items():
                    line[key] = float(value)
                self.characterdata[enums.Character(
                    line["CharacterIndex"])] = line
Ejemplo n.º 6
0
class Console:
    def __init__(self,
                 is_dolphin,
                 ai_port,
                 opponent_port,
                 opponent_type,
                 config_path="",
                 home_path="",
                 logger=None):
        self.logger = logger
        self.ai_port = ai_port
        self.opponent_port = opponent_port
        self.is_dolphin = is_dolphin
        self.config_path = config_path
        self.home_path = home_path

        self.processingtime = 0
        self._frametimestamp = time.time()
        self.slippi_address = ""
        self.slippi_port = 51441
        self.eventsize = [0] * 0x100

        # Keep a running copy of the last gamestate produced
        #   game info is only produced as diffs, not whole snapshots
        #   so if nothing changes, we need to know what the last value was
        self.render = True
        self._prev_gamestate = GameState(ai_port, opponent_port)
        self.process = None
        if self.is_dolphin:
            config_path = self.get_dolphin_home_path()
            pipes_path = config_path + "Pipes/"
            path = os.path.dirname(os.path.realpath(__file__))

            #Create the Pipes directory if it doesn't already exist
            if not os.path.exists(pipes_path):
                os.makedirs(pipes_path)
                print("WARNING: Had to create a Pipes directory in Dolphin just now. " \
                    "You may need to restart Dolphin and this program in order for this to work. " \
                    "(You should only see this warning once)")

            pipes_path += "Bot" + str(ai_port)
            if not os.path.exists(pipes_path):
                os.mkfifo(pipes_path)

            #setup the controllers specified
            self.setup_dolphin_controller(ai_port)
            self.setup_dolphin_controller(opponent_port, opponent_type)

        # Prepare some structures for fixing melee data
        path = os.path.dirname(os.path.realpath(__file__))
        with open(path + "/actiondata.csv") as csvfile:
            #A list of dicts containing the frame data
            actiondata = list(csv.DictReader(csvfile))
            #Dict of sets
            self.zero_indices = defaultdict(set)
            for line in actiondata:
                if line["zeroindex"] == "True":
                    self.zero_indices[int(line["character"])].add(
                        int(line["action"]))

        # Read the character data csv
        self.characterdata = dict()
        with open(path + "/characterdata.csv") as csvfile:
            reader = csv.DictReader(csvfile)
            for line in reader:
                del line["Character"]
                #Convert all fields to numbers
                for key, value in line.items():
                    line[key] = float(value)
                self.characterdata[enums.Character(
                    line["CharacterIndex"])] = line

    def connect(self):
        """ Connects to the Slippi server (dolphin or wii).

        Returns boolean of success """
        self.slippstream = SlippstreamClient(self.slippi_address,
                                             self.slippi_port)
        return self.slippstream.connect()

    def run(self,
            iso_path=None,
            movie_path=None,
            dolphin_executable_path=None,
            dolphin_config_path=None):
        """Run dolphin-emu"""
        if self.is_dolphin:
            if dolphin_executable_path is not None:
                command = [dolphin_executable_path]
            else:
                command = ["dolphin-emu"]
            if not self.render:
                #Use the "Null" renderer
                command.append("-v")
                command.append("Null")
            if movie_path is not None:
                command.append("-m")
                command.append(movie_path)
            if iso_path is not None:
                command.append("-e")
                command.append(iso_path)
            if dolphin_config_path is not None:
                command.append("-u")
                command.append(dolphin_config_path)
            self.process = subprocess.Popen(command)
            # TODO proper error tracking here
            return True

    def stop(self):
        self.slippstream.shutdown()
        # If dolphin, kill the process
        if self.process != None:
            self.process.terminate()
        pass

    """Setup the necessary files for dolphin to recognize the player at the given
    controller port and type"""

    def setup_dolphin_controller(self,
                                 port,
                                 controllertype=enums.ControllerType.STANDARD):
        #Read in dolphin's controller config file
        controller_config_path = self.get_dolphin_config_path(
        ) + "GCPadNew.ini"
        config = configparser.SafeConfigParser()
        config.read(controller_config_path)

        #Add a bot standard controller config to the given port
        section = "GCPad" + str(port)
        if not config.has_section(section):
            config.add_section(section)

        if controllertype == enums.ControllerType.STANDARD:
            config.set(section, 'Device', 'Pipe/0/Bot' + str(port))
            config.set(section, 'Buttons/A', 'Button A')
            config.set(section, 'Buttons/B', 'Button B')
            config.set(section, 'Buttons/X', 'Button X')
            config.set(section, 'Buttons/Y', 'Button Y')
            config.set(section, 'Buttons/Z', 'Button Z')
            config.set(section, 'Buttons/L', 'Button L')
            config.set(section, 'Buttons/R', 'Button R')
            config.set(section, 'Main Stick/Up', 'Axis MAIN Y +')
            config.set(section, 'Main Stick/Down', 'Axis MAIN Y -')
            config.set(section, 'Main Stick/Left', 'Axis MAIN X -')
            config.set(section, 'Main Stick/Right', 'Axis MAIN X +')
            config.set(section, 'Triggers/L', 'Button L')
            config.set(section, 'Triggers/R', 'Button R')
            config.set(section, 'Main Stick/Modifier', 'Shift_L')
            config.set(section, 'Main Stick/Modifier/Range',
                       '50.000000000000000')
            config.set(section, 'D-Pad/Up', 'T')
            config.set(section, 'D-Pad/Down', 'G')
            config.set(section, 'D-Pad/Left', 'F')
            config.set(section, 'D-Pad/Right', 'H')
            config.set(section, 'Buttons/Start', 'Button START')
            config.set(section, 'Buttons/A', 'Button A')
            config.set(section, 'C-Stick/Up', 'Axis C Y +')
            config.set(section, 'C-Stick/Down', 'Axis C Y -')
            config.set(section, 'C-Stick/Left', 'Axis C X -')
            config.set(section, 'C-Stick/Right', 'Axis C X +')
            config.set(section, 'Triggers/L-Analog', 'Axis L -+')
            config.set(section, 'Triggers/R-Analog', 'Axis R -+')
        #This section is unused if it's not a standard input (I think...)
        else:
            config.set(section, 'Device', 'XInput2/0/Virtual core pointer')

        with open(controller_config_path, 'w') as configfile:
            config.write(configfile)

        #Change the bot's controller port to use "standard" input
        dolphin_config_path = self.get_dolphin_config_path() + "Dolphin.ini"
        config = configparser.SafeConfigParser()
        config.read(dolphin_config_path)
        #Indexed at 0. "6" means standard controller, "12" means GCN Adapter
        # The enum is scoped to the proper value, here
        config.set("Core", 'SIDevice' + str(port - 1), controllertype.value)
        #Enable Cheats
        config.set("Core", 'enablecheats', "True")
        #Turn on background input so we don't need to have window focus on dolphin
        config.set("Input", 'backgroundinput', "True")
        with open(dolphin_config_path, 'w') as dolphinfile:
            config.write(dolphinfile)

        # #Enable the specific cheats we need (Netplay community settings)
        # melee_config_path = self.get_dolphin_home_path() + "/GameSettings/GALE01.ini"
        # config = configparser.SafeConfigParser(allow_no_value=True)
        # config.optionxform = str
        # config.read(melee_config_path)
        # if not config.has_section("Gecko_Enabled"):
        #     config.add_section("Gecko_Enabled")
        # config.set("Gecko_Enabled", "$Netplay Community Settings")
        # with open(melee_config_path, 'w') as dolphinfile:
        #     config.write(dolphinfile)

    def step(self):
        # Keep looping until we get a REPLAY message
        self.processingtime = time.time() - self._frametimestamp
        gamestate = GameState(self.ai_port, self.opponent_port)
        frame_ended = False
        while not frame_ended:
            msg = self.slippstream.read_message()
            if msg:
                if (CommType(msg['type']) == CommType.REPLAY):
                    events = msg['payload']['data']
                    frame_ended = self.__handle_slippstream_events(
                        events, gamestate)
                    # Start the processing timer now that we're done reading messages
                    self._frametimestamp = time.time()
                    continue

                # We can basically just ignore keepalives
                elif (CommType(msg['type']) == CommType.KEEPALIVE):
                    print("Keepalive")
                    continue

                elif (CommType(msg['type']) == CommType.HANDSHAKE):
                    p = msg['payload']
                    print("Connected to console '{}' (Slippi Nintendont {})".
                          format(
                              p['nick'],
                              p['nintendontVersion'],
                          ))
                    continue

        self.__fixframeindexing(gamestate)
        self.__fixiasa(gamestate)
        return gamestate

    def __handle_slippstream_events(self, event_bytes, gamestate):
        """ Handle a series of events, provided sequentially in a byte array """
        lastmessage = EventType.GAME_START
        while len(event_bytes) > 0:
            lastmessage = EventType(event_bytes[0])
            event_size = self.eventsize[event_bytes[0]]
            if len(event_bytes) < event_size:
                print(
                    "WARNING: Something went wrong unpacking events. Data is probably missing"
                )
                print("\tDidn't have enough data for event")
                return False
            if (EventType(event_bytes[0]) == EventType.PAYLOADS):
                cursor = 0x2
                payload_size = event_bytes[1]
                num_commands = (payload_size - 1) // 3
                for i in range(0, num_commands):
                    command, command_len = unpack(
                        ">bH", event_bytes[cursor:cursor + 3])
                    self.eventsize[command] = command_len + 1
                    cursor += 3
                event_bytes = event_bytes[payload_size + 1:]
                continue

            elif (EventType(event_bytes[0]) == EventType.FRAME_START):
                self.frame_num = unpack(">i", event_bytes[1:5])[0]
                event_bytes = event_bytes[event_size:]
                continue

            elif (EventType(event_bytes[0]) == EventType.GAME_START):
                event_bytes = event_bytes[event_size:]
                continue

            elif (EventType(event_bytes[0]) == EventType.GAME_END):
                event_bytes = event_bytes[event_size:]
                continue

            elif (EventType(event_bytes[0]) == EventType.PRE_FRAME):
                event_bytes = event_bytes[event_size:]
                continue

            elif (EventType(event_bytes[0]) == EventType.POST_FRAME):
                gamestate.frame = unpack(">i", event_bytes[0x1:0x1 + 4])[0]
                controller_port = unpack(">B", event_bytes[0x5:0x5 + 1])[0] + 1

                gamestate.player[controller_port].x = unpack(
                    ">f", event_bytes[0xa:0xa + 4])[0]
                gamestate.player[controller_port].y = unpack(
                    ">f", event_bytes[0xe:0xe + 4])[0]

                gamestate.player[controller_port].character = enums.Character(
                    unpack(">B", event_bytes[0x7:0x7 + 1])[0])
                try:
                    gamestate.player[controller_port].action = enums.Action(
                        unpack(">H", event_bytes[0x8:0x8 + 2])[0])
                except ValueError:
                    gamestate.player[
                        controller_port].action = enums.Action.UNKNOWN_ANIMATION

                # Melee stores this in a float for no good reason. So we have to convert
                facing_float = unpack(">f", event_bytes[0x12:0x12 + 4])[0]
                gamestate.player[controller_port].facing = facing_float > 0

                gamestate.player[controller_port].percent = int(
                    unpack(">f", event_bytes[0x16:0x16 + 4])[0])
                gamestate.player[controller_port].stock = unpack(
                    ">B", event_bytes[0x21:0x21 + 1])[0]
                gamestate.player[controller_port].action_frame = int(
                    unpack(">f", event_bytes[0x22:0x22 + 4])[0])

                # Extract the bit at mask 0x20
                bitflags2 = unpack(">B", event_bytes[0x27:0x27 + 1])[0]
                gamestate.player[controller_port].hitlag = bool(bitflags2
                                                                & 0x20)

                gamestate.player[controller_port].hitstun_frames_left = int(
                    unpack(">f", event_bytes[0x2b:0x2b + 4])[0])
                gamestate.player[controller_port].on_ground = not bool(
                    unpack(">B", event_bytes[0x2f:0x2f + 1])[0])
                gamestate.player[controller_port].jumps_left = unpack(
                    ">B", event_bytes[0x32:0x32 + 1])[0]

                event_bytes = event_bytes[event_size:]
                continue

            elif (EventType(event_bytes[0]) == EventType.GECKO_CODES):
                event_bytes = event_bytes[event_size:]
                continue

            elif (EventType(event_bytes[0]) == EventType.FRAME_BOOKEND):
                event_bytes = event_bytes[event_size:]
                return True

            elif (EventType(event_bytes[0]) == EventType.ITEM_UPDATE):
                # TODO projectiles
                projectile = Projectile()
                projectile.x = unpack(">f", event_bytes[0x14:0x14 + 4])[0]
                projectile.y = unpack(">f", event_bytes[0x18:0x18 + 4])[0]
                projectile.x_speed = unpack(">f",
                                            event_bytes[0x0c:0x0c + 4])[0]
                projectile.y_speed = unpack(">f",
                                            event_bytes[0x10:0x10 + 4])[0]
                try:
                    projectile.subtype = enums.ProjectileSubtype(
                        unpack(">H", event_bytes[0x05:0x05 + 2])[0])
                except ValueError:
                    projectile.subtype = enums.UNKNOWN_PROJECTILE
                # Add the projectile to the gamestate list
                gamestate.projectiles.append(projectile)

                event_bytes = event_bytes[event_size:]
                continue

            else:
                print("WARNING: Something went wrong unpacking events. " + \
                    "Data is probably missing")
                print("\tGot invalid event type: ", event_bytes[0])
                return False

        return False

    def get_dolphin_home_path(self):
        """Return the path to dolphin's home directory"""
        # If a home path is set manually, use that
        if self.home_path:
            return self.home_path

        home_path = pwd.getpwuid(os.getuid()).pw_dir
        legacy_config_path = home_path + "/.dolphin-emu/"

        #Are we using a legacy Linux home path directory?
        if os.path.isdir(legacy_config_path):
            return legacy_config_path

        #Are we on OSX?
        osx_path = home_path + "/Library/Application Support/Dolphin/"
        if os.path.isdir(osx_path):
            return osx_path

        #Are we on a new Linux distro?
        linux_path = home_path + "/.local/share/dolphin-emu/"
        if os.path.isdir(linux_path):
            return linux_path

        print("ERROR: Are you sure Dolphin is installed? Make sure it is,\
                and then run again.")
        sys.exit(1)
        return ""

    def get_dolphin_config_path(self):
        """ Return the path to dolphin's config directory
        (which is not necessarily the same as the home path)"""
        # If a config path is set manually, use that
        if self.config_path:
            return self.config_path

        home_path = pwd.getpwuid(os.getuid()).pw_dir
        legacy_config_path = home_path + "/.dolphin-emu/"

        #Are we using a legacy Linux home path directory?
        if os.path.isdir(legacy_config_path):
            return legacy_config_path

        #Are we on a new Linux distro?
        linux_path = home_path + "/.config/dolphin-emu/"
        if os.path.isdir(linux_path):
            return linux_path

        #Are we on OSX?
        osx_path = home_path + "/Library/Application Support/Dolphin/Config/"
        if os.path.isdir(osx_path):
            return osx_path

        print("ERROR: Are you sure Dolphin is installed? Make sure it is,\
                and then run again.")
        sys.exit(1)
        return ""

    def get_dolphin_pipes_path(self, port):
        """Get the path of the named pipe input file for the given controller port"""
        return self.get_dolphin_home_path() + "/Pipes/Bot" + str(port)

    # Melee's indexing of action frames is wildly inconsistent.
    #   Here we adjust all of the frames to be indexed at 1 (so math is easier)
    def __fixframeindexing(self, gamestate):
        for _, player in gamestate.player.items():
            if player.action.value in self.zero_indices[
                    player.character.value]:
                player.action_frame = player.action_frame + 1

    # The IASA flag doesn't set or reset for special attacks.
    #   So let's just set IASA to False for all non-A attacks.
    def __fixiasa(self, gamestate):
        for index, player in gamestate.player.items():
            # Luckily for us, all the A-attacks are in a contiguous place in the enums!
            #   So we don't need to call them out one by one
            if player.action.value < Action.NEUTRAL_ATTACK_1.value or player.action.value > Action.DAIR.value:
                player.iasa = False
Ejemplo n.º 7
0
    def __init__(self,
                 path=None,
                 slippi_address="127.0.0.1",
                 slippi_port=51441,
                 volume=70,
                 menu=False):
        """Create a Console object
        Args:
            path (str): Path to your dolphin executable.
                If None, will assume the dolphin is remote and won't try to configure it.
            slippi_address (str): IP address of the Dolphin / Wii to connect to.
            slippi_port (int): UDP port that slippi will listen on
            volume (int) the volume music should play at, between 0 and 100
            menu (boolean) True if you want menu music to play, false otherwise. 
        """
        self.path = path
        self.slippi_address = slippi_address
        """(str): IP address of the Dolphin / Wii to connect to."""
        self.slippi_port = slippi_port
        """(int): UDP port of slippi server. Default 51441"""
        self.eventsize = [0] * 0x100
        self.cursor = 0
        self._frame = 0
        self._process = None
        self._stocks = [-1] * 4
        self._current_loop = None

        try:
            mixer.init()
        except pg_error:
            print(
                "Failed to initialize the mixer! Check that your audio devices are working properly"
            )
        else:
            mixer.music.set_volume(volume / 100.0)

        self.menu = menu

        self.fileNames = [None] * 33
        configPath = os.path.normpath(
            os.path.join(os.path.dirname(__file__), 'music/musicConfig.txt'))
        try:
            configFile = open(configPath)
        except FileNotFoundError:
            input("No music config file found at " + configPath +
                  "! Press enter to exit...")
            sys.exit(1)
        else:
            for line in configFile:
                split = line.split(":")
                if (len(split) < 2):
                    continue
                for i in range(len(split)):
                    split[i] = split[i].strip()
                try:
                    stageID = int(split[0])
                except ValueError:
                    if (split[0].lower() == "menu"):
                        stageID = MENU
                    else:
                        continue
                if (stageID > 32):
                    continue  #skip invalid
                if (self.fileNames[stageID] is None):
                    self.fileNames[stageID] = []
                self.fileNames[stageID].append(split[1:])
        #self._slippstream = SlippstreamClient(self.slippi_address, self.slippi_port)
        if self.path:
            # Setup some dolphin config options
            dolphin_config_path = self._get_dolphin_config_path()
            config = configparser.SafeConfigParser()
            config.read(dolphin_config_path)
            try:
                self.slippi_port = int(
                    config["Core"]["SlippiSpectatorLocalPort"])
                if (
                        config["Core"]["SlippiEnableSpectator"] != "True"
                ):  #this needs to be true in order for this to connect to slippi
                    user_input = input(
                        "SlippiEnableSpectator is set to false in your config file! SlippiMusic requires this to be true. Set it to true now? y/n"
                    )
                    if (str(user_input).strip().lower()[0] == 'y'):
                        config.set("Core", 'SlippiEnableSpectator', "True")
                        try:
                            with open(dolphin_config_path, 'w') as dolphinfile:
                                config.write(dolphinfile)
                        except PermissionError:
                            print(
                                "Access denied to your Dolphin.ini file at " +
                                dolphin_config_path +
                                "! Means I can't automatically configure Dolphin. Run as administrator or make open that config file and make sure that \"SlippiEnableSpectator\" is set to True."
                            )
            except (configparser.NoSectionError, KeyError):
                print(
                    "Invalid Dolphin.ini file! Usually means your Dolphin path is wrong."
                )
        self._slippstream = SlippstreamClient(self.slippi_address,
                                              self.slippi_port)
Ejemplo n.º 8
0
class Console:
    """The console object that represents your Dolphin / Wii / SLP file
    """
    def __init__(self,
                 path=None,
                 slippi_address="127.0.0.1",
                 slippi_port=51441,
                 volume=70,
                 menu=False):
        """Create a Console object
        Args:
            path (str): Path to your dolphin executable.
                If None, will assume the dolphin is remote and won't try to configure it.
            slippi_address (str): IP address of the Dolphin / Wii to connect to.
            slippi_port (int): UDP port that slippi will listen on
            volume (int) the volume music should play at, between 0 and 100
            menu (boolean) True if you want menu music to play, false otherwise. 
        """
        self.path = path
        self.slippi_address = slippi_address
        """(str): IP address of the Dolphin / Wii to connect to."""
        self.slippi_port = slippi_port
        """(int): UDP port of slippi server. Default 51441"""
        self.eventsize = [0] * 0x100
        self.cursor = 0
        self._frame = 0
        self._process = None
        self._stocks = [-1] * 4
        self._current_loop = None

        try:
            mixer.init()
        except pg_error:
            print(
                "Failed to initialize the mixer! Check that your audio devices are working properly"
            )
        else:
            mixer.music.set_volume(volume / 100.0)

        self.menu = menu

        self.fileNames = [None] * 33
        configPath = os.path.normpath(
            os.path.join(os.path.dirname(__file__), 'music/musicConfig.txt'))
        try:
            configFile = open(configPath)
        except FileNotFoundError:
            input("No music config file found at " + configPath +
                  "! Press enter to exit...")
            sys.exit(1)
        else:
            for line in configFile:
                split = line.split(":")
                if (len(split) < 2):
                    continue
                for i in range(len(split)):
                    split[i] = split[i].strip()
                try:
                    stageID = int(split[0])
                except ValueError:
                    if (split[0].lower() == "menu"):
                        stageID = MENU
                    else:
                        continue
                if (stageID > 32):
                    continue  #skip invalid
                if (self.fileNames[stageID] is None):
                    self.fileNames[stageID] = []
                self.fileNames[stageID].append(split[1:])
        #self._slippstream = SlippstreamClient(self.slippi_address, self.slippi_port)
        if self.path:
            # Setup some dolphin config options
            dolphin_config_path = self._get_dolphin_config_path()
            config = configparser.SafeConfigParser()
            config.read(dolphin_config_path)
            try:
                self.slippi_port = int(
                    config["Core"]["SlippiSpectatorLocalPort"])
                if (
                        config["Core"]["SlippiEnableSpectator"] != "True"
                ):  #this needs to be true in order for this to connect to slippi
                    user_input = input(
                        "SlippiEnableSpectator is set to false in your config file! SlippiMusic requires this to be true. Set it to true now? y/n"
                    )
                    if (str(user_input).strip().lower()[0] == 'y'):
                        config.set("Core", 'SlippiEnableSpectator', "True")
                        try:
                            with open(dolphin_config_path, 'w') as dolphinfile:
                                config.write(dolphinfile)
                        except PermissionError:
                            print(
                                "Access denied to your Dolphin.ini file at " +
                                dolphin_config_path +
                                "! Means I can't automatically configure Dolphin. Run as administrator or make open that config file and make sure that \"SlippiEnableSpectator\" is set to True."
                            )
            except (configparser.NoSectionError, KeyError):
                print(
                    "Invalid Dolphin.ini file! Usually means your Dolphin path is wrong."
                )
        self._slippstream = SlippstreamClient(self.slippi_address,
                                              self.slippi_port)
        #else:
        #self._slippstream = SLPFileStreamer(self.path)

    def connect(self):
        """ Connects to the Slippi server (dolphin or wii).
        Returns:
            True if successful, False otherwise
        """
        to_return = self._slippstream.connect()
        if (to_return):
            if (self.menu):
                stageFiles = self.fileNames[MENU]
                self.playMusic(stageFiles[random.randrange(len(stageFiles))])
        return to_return

    def run(self,
            iso_path=None,
            dolphin_config_path=None,
            environment_vars=None):
        """Run the Dolphin emulator.
        This starts the Dolphin process, so don't run this if you're connecting to an
        already running Dolphin instance.
        Args:
            iso_path (str, optional): Path to Melee ISO for dolphin to read
            dolphin_config_path (str, optional): Alternative config path for dolphin
                if not using the default
            environment_vars (dict, optional): Dict (string->string) of environment variables to set
        """
        if self.path:
            command = [os.path.normpath(self.path)]
            if platform.system() == "Darwin":  #mac
                command.insert(
                    0,
                    "open")  #can't run directly on mac, gotta call open on it
                command.append("-W")  #tells thread to wait until program exits
            if iso_path is not None:
                if platform.system() == "Darwin":
                    command.append("--args")
                command.append("-e")
                command.append(iso_path)
            if dolphin_config_path is not None:
                command.append("-u")
                command.append(dolphin_config_path)
            env = os.environ.copy()
            if environment_vars is not None:
                for var, value in environment_vars.items():
                    env[var] = value
            #print(command)
            try:
                self._process = subprocess.Popen(command, env=env)
            except PermissionError:
                print(
                    "Access denied to your Dolphin executable! Can't open Dolphin automatically"
                )
            except FileNotFoundError:
                print(
                    "Path to your Dolphin executable is incorrect! Can't open Dolphin automatically"
                )

    def stop(self):
        """ Stop the console.
        For Dolphin instances, this will kill the dolphin process.
        For Wiis and SLP files, it just shuts down our connection
         """
        if self.path:
            self._slippstream.shutdown()
            # If dolphin, kill the process
            if self._process is not None:
                self._process.terminate()

    def step(self):
        """ 'step' to the next state of the game and flushes all controllers
        Returns:
            GameState object that represents new current state of the game"""
        frame_ended = False
        while not frame_ended:
            if (self._current_loop is not None):
                mixer.music.queue(self._current_loop)
            if (self._process is not None
                    and self._process.poll() is not None):
                print("Dolphin closed! Exiting...")
                sys.exit(0)
            message = self._slippstream.dispatch()
            if (message and message["type"] == "connect_reply"):
                self.cursor = message["cursor"]
            if message and message["type"] == "game_event" and len(
                    message["payload"]) > 0:
                frame_ended = self.__handle_slippstream_events(
                    base64.b64decode(message["payload"]))
            else:
                continue
        return None
        """gamestate = self._temp_gamestate
        self._temp_gamestate = None
        self.__fixframeindexing(gamestate)
        self.__fixiasa(gamestate)
        # Start the processing timer now that we're done reading messages
        self._frametimestamp = time.time()
        return gamestate"""

    def __handle_slippstream_events(self, event_bytes):
        """ Handle a series of events, provided sequentially in a byte array """
        #gamestate.menu_state = enums.Menu.IN_GAME
        while len(event_bytes) > 0:
            event_size = self.eventsize[event_bytes[0]]
            if len(event_bytes) < event_size:
                print(
                    "WARNING: Something went wrong unpacking events. Data is probably missing"
                )
                print("\tDidn't have enough data for event")
                return False
            if EventType(event_bytes[0]) == EventType.PAYLOADS:
                cursor = 0x2
                payload_size = event_bytes[1]
                num_commands = (payload_size - 1) // 3
                for i in range(0, num_commands):
                    command = int(event_bytes[cursor])
                    command_len = (int(event_bytes[cursor + 0x1]) << 8) + int(
                        event_bytes[cursor + 0x2])
                    self.eventsize[command] = command_len + 1
                    cursor += 3
                event_bytes = event_bytes[payload_size + 1:]

            elif EventType(event_bytes[0]) == EventType.FRAME_START:
                event_bytes = event_bytes[event_size:]

            elif EventType(event_bytes[0]) == EventType.GAME_START:
                #self.__game_start(gamestate, event_bytes)
                print("Game start")
                self._stocks = [-1] * 4
                stage = (int(event_bytes[0x13]) << 8) + int(event_bytes[0x14])
                print(stage)
                if (self.fileNames[stage] != None):
                    stageFiles = self.fileNames[stage]
                    self.playMusic(stageFiles[random.randrange(
                        len(stageFiles))])
                else:
                    self.stop_music()
                event_bytes = event_bytes[event_size:]

            elif EventType(event_bytes[0]) == EventType.GAME_END:
                event_bytes = event_bytes[event_size:]
                print("Game end")
                self.stop_music()
                if (self.menu and self.fileNames[MENU] != None):
                    if (self._stocks.count(0) >= int(
                        (4 - self._stocks.count(-1)) /
                            2)):  #Check if game ended in not LRAS
                        time.sleep(
                            2
                        )  #magic, time that the "GAME" message is on screen in melee
                    stageFiles = self.fileNames[MENU]
                    self.playMusic(stageFiles[random.randrange(
                        len(stageFiles))])
                return False

            elif (EventType(event_bytes[0]) in [
                    EventType.PRE_FRAME, EventType.GECKO_CODES,
                    EventType.ITEM_UPDATE
            ]):
                event_bytes = event_bytes[event_size:]

            elif EventType(event_bytes[0]) == EventType.POST_FRAME:
                self._stocks[event_bytes[0x5]] = event_bytes[0x21]
                event_bytes = event_bytes[event_size:]

            elif EventType(event_bytes[0]) == EventType.FRAME_BOOKEND:
                #self.__frame_bookend(gamestate, event_bytes)
                event_bytes = event_bytes[event_size:]
                # If this is an old frame, then don't return it.
                #if gamestate.frame <= self._frame:
                #return False
                #self._frame = gamestate.frame
                return True

            else:
                print("WARNING: Something went wrong unpacking events. " + \
                    "Data is probably missing")
                print("\tGot invalid event type: ", event_bytes[0])
                return False
        return False

    def playMusic(self, fileName):
        try:
            #print(os.path.dirname(__file__))
            dirname = os.path.dirname(__file__)
            musicPath = os.path.normpath(
                os.path.join(dirname, 'music/' + fileName[0]))
            print(musicPath)
            mixer.music.stop()
            mixer.music.load(musicPath)
            if (len(fileName) == 1):
                self._current_loop = None
                mixer.music.play(-1)
            else:
                self._current_loop = os.path.normpath(
                    os.path.join(dirname, 'music/' + fileName[1]))
                print(self._current_loop)
                mixer.music.queue(self._current_loop)
                mixer.music.play()
        except pg_error:
            print(
                "Couldn't play the media. Usually means the filename is wrong."
            )

    def stop_music(self):
        mixer.music.stop()

    def _get_dolphin_config_path(self):
        """ Return the path to dolphin's config directory
        (which is not necessarily the same as the home path)"""
        if self.path:
            dirname = os.path.dirname(self.path)
            normpath = os.path.normpath(
                os.path.join(dirname, "User/Config/Dolphin.ini"))
            if platform.system() == "Linux":
                # First check if the config path is here in the same directory
                if os.path.isdir(normpath):
                    return normpath
                # Otherwise, this must be an appimage install. Use the .config
                return os.path.join(str(Path.home()),
                                    ".config/SlippiOnline/Config/Dolphin.ini")
            elif platform.system() == "Darwin":  #mac
                return os.path.join(
                    self.path, "Contents/Resources/User/Config/Dolphin.ini")
            return normpath