def playerJoin(config, agentCallBack, lobbyTimeout=c.INITIAL_TIMEOUT, debug=True):
    """cause an agent to join an already hosted game"""
    FLAGS(sys.argv) # ignore pysc2 command-line handling (eww)
    log = protocol.logging.logging
    log.disable(log.CRITICAL) # disable pysc2 logging
    amHosting   = not bool(config.host)
    thisPlayer  = config.whoAmI()
    operPrefix  = "HOST" if amHosting else "JOIN"
    operType    = "%sGAME"%operPrefix
    createReq   = config.requestCreateDetails() if amHosting else None
    joinReq     = config.requestJoinDetails()
    selectedIP  = config.clientInitHost()
    selectPort  = config.clientInitPort()
    controller  = None # the object that manages the application process
    finalResult = rh.playerSurrendered(config) # default to this player losing if somehow a result wasn't acquired normally
    replayData  = "" # complete raw replay data for the match
    if debug: print("[%s] Starcraft2 game process is launching (fullscreen=%s)."%(operType, config.fullscreen))
    with config.launchApp(ip_address=selectedIP, port=selectPort, connect=False):
      try: # WARNING: if port equals the same port of the host on the same machine, this subsequent process closes!
        controller = ClientController()
        controller.connect(url=selectedIP, port=selectPort, timeout=lobbyTimeout) # establish socket connection
        if amHosting:
            if debug:
                  print("[%s] Starcraft2 host application is live. (%s)"%(operType, controller.status)) # status: launched
                  print("[%s] Creating Starcraft Game at %s"%(operType, controller))
            controller.create_game(createReq)
            if debug:
                  print("[%s] Starcraft2 is waiting for %d player(s) to join. (%s)"%(operType, config.numAgents, controller.status)) # status: init_game
                  print("[%s] sending request to join game. (%s)"%(operType, controller.status)) # status: init_game
        else: # joining clients must wait for host to perfrom its join request
            timeToWait = c.DEFAULT_HOST_DELAY
            for i in range(timeToWait): # WARNING: the host must perform its join action with its client before any joining players issue join requests to their clients
                if debug: print("[%s] waiting %d seconds for the host to finish its init sequence."%(operType, timeToWait-i))
                time.sleep(1)
        joinResp = controller.join_game(joinReq) # SC2APIProtocol.RequestJoinGame
        print("[%s] connection to %s:%d was successful. Game is starting! (%s)"%(operType, selectedIP, selectPort, controller.status)) # status: in_game
        thisPlayer.playerID = int(joinResp.player_id) # update playerID; repsponse to join game request is authority
        if debug: print("[%s] joined match as %s."%(operType, thisPlayer)) # all players have actually joined already to advance beyond join_game (init_game)
        config.updateIDs(controller.game_info(), tag=operType, debug=debug) # SC2APIProtocol.ResponseGameInfo object
        if debug: print("[%s] all %d player(s) found; game has started! (%s)"%(operType, config.numGameClients, controller.status)) # status: init_game
        config.save() # "publish" the configuration file for other procs
        try:    agentCallBack(config.name) # send the configuration to the controlling agent
        except Exception as e:
            print("ERROR: agent %s crashed during init: %s (%s)"%(thisPlayer.initCmd, e, type(e)))
            return (rh.playerCrashed(config), "") # no replay information to get
        getGameState = controller.observe # function that observes what's changed since the prior gameloop(s)
        startWaitTime = now()
        while True:  # wait for game to end while players/bots do their thing
            obs = getGameState()
            result = obs.player_result
            if result: # match end condition was supplied by the client
                finalResult = rh.idPlayerResults(config, result)
                break
            try: agentCallBack(obs) # do developer's creative stuff
            except Exception as e:
                print("%s ERROR: agent callback %s of %s crashed during game: %s"%(type(e), agentCallBack, thisPlayer.initCmd, e))
                finalResult = rh.playerCrashed(config)
                break
            newNow = now() # periodicially acquire the game's replay data (in case of abnormal termination)
            if newNow - startWaitTime > c.REPLAY_SAVE_FREQUENCY:
                replayData = controller.save_replay()
                startWaitTime = newNow
        replayData = controller.save_replay() # one final attempt to get the complete replay data
        #controller.leave() # the connection to the server process is (cleanly) severed
      except (protocol.ConnectionError, protocol.ProtocolError, remote_controller.RequestError) as e:
        if "Status.in_game" in str(e): # state was previously in game and then exited that state
            finalResult = rh.playerSurrendered(config) # rage quit is losing
        else:
            finalResult = rh.playerDisconnected(config)
            print("%s Connection to game host has ended, even intentionally by agent. Message:%s%s"%(type(e), os.linesep, e))
      except KeyboardInterrupt:
        if debug: print("caught command to forcibly shutdown Starcraft2 client.")
        finalResult = rh.playerSurrendered(config)
      finally:
        if replayData: # ensure replay data can be transmitted over http
            replayData = base64.encodestring(replayData).decode() # convert raw bytes into str
        if controller: controller.quit() # force the sc2 application to close
    return (finalResult, replayData)
cause an agent to join an already hosted game