def update_scores(self) -> None:
        """ update scoreboard and check for winners """
        # FIXME: tidy this up
        # pylint: disable=too-many-nested-blocks
        have_scoring_team = False
        win_score = self._score_to_win
        for team in [self.teams[0], self._bot_team]:
            assert team is not None
            assert self._scoreboard is not None
            self._scoreboard.set_team_value(team, team.gamedata['score'],
                                            win_score)
            if team.gamedata['score'] >= win_score:
                if not have_scoring_team:
                    self.scoring_team = team
                    if team is self._bot_team:
                        self.continue_or_end_game()
                    else:
                        ba.setmusic(ba.MusicType.VICTORY)

                        # Completion achievements.
                        assert self._bot_team is not None
                        if self._preset in ['rookie', 'rookie_easy']:
                            self._award_achievement('Rookie Football Victory',
                                                    sound=False)
                            if self._bot_team.gamedata['score'] == 0:
                                self._award_achievement(
                                    'Rookie Football Shutout', sound=False)
                        elif self._preset in ['pro', 'pro_easy']:
                            self._award_achievement('Pro Football Victory',
                                                    sound=False)
                            if self._bot_team.gamedata['score'] == 0:
                                self._award_achievement('Pro Football Shutout',
                                                        sound=False)
                        elif self._preset in ['uber', 'uber_easy']:
                            self._award_achievement('Uber Football Victory',
                                                    sound=False)
                            if self._bot_team.gamedata['score'] == 0:
                                self._award_achievement(
                                    'Uber Football Shutout', sound=False)
                            if (not self._player_has_dropped_bomb
                                    and not self._player_has_punched):
                                self._award_achievement('Got the Moves',
                                                        sound=False)
                        self._bots.stop_moving()
                        self.show_zoom_message(ba.Lstr(resource='victoryText'),
                                               scale=1.0,
                                               duration=4.0)
                        self.celebrate(10.0)
                        assert self._starttime_ms is not None
                        self._final_time_ms = int(
                            ba.time(timeformat=ba.TimeFormat.MILLISECONDS) -
                            self._starttime_ms)
                        self._time_text_timer = None
                        assert (self._time_text_input is not None
                                and self._time_text_input.node)
                        self._time_text_input.node.timemax = (
                            self._final_time_ms)

                        # FIXME: Does this still need to be deferred?
                        ba.pushcall(ba.Call(self.do_end, 'victory'))
Exemple #2
0
    def _connect(self, textwidget: ba.Widget,
                 port_textwidget: ba.Widget) -> None:
        addr = cast(str, ba.textwidget(query=textwidget))
        if addr == '':
            ba.screenmessage(
                ba.Lstr(resource='internal.invalidAddressErrorText'),
                color=(1, 0, 0))
            ba.playsound(ba.getsound('error'))
            return
        try:
            port = int(cast(str, ba.textwidget(query=port_textwidget)))
        except ValueError:
            # EWWWW; this exception causes a dependency loop that won't
            # go away until the next cyclical collection, which can
            # keep us alive. Perhaps should rethink our garbage
            # collection strategy, but for now just explicitly running
            # a cycle.
            ba.pushcall(ba.garbage_collect)
            port = -1
        if port > 65535 or port < 0:
            ba.screenmessage(ba.Lstr(resource='internal.invalidPortErrorText'),
                             color=(1, 0, 0))
            ba.playsound(ba.getsound('error'))
            return

        _HostLookupThread(name=addr,
                          port=port,
                          call=ba.WeakCall(self._host_lookup_result)).start()
Exemple #3
0
    def _print(text: str, color: tuple[float, float, float] = None) -> None:
        def _print_in_logic_thread() -> None:
            win = weakwin()
            if win is not None:
                win.print(text, (1.0, 1.0, 1.0) if color is None else color)

        ba.pushcall(_print_in_logic_thread, from_other_thread=True)
Exemple #4
0
    def _set_sub_tab(self, value: SubTabType, playsound: bool = False) -> None:
        assert self._container
        if playsound:
            ba.playsound(ba.getsound('click01'))

        # If switching from join to host, do a fresh state query.
        if self._state.sub_tab is SubTabType.JOIN and value is SubTabType.HOST:
            # Prevent taking any action until we've gotten a fresh state.
            self._waiting_for_initial_state = True

            # This will get a state query sent out immediately.
            self._last_hosting_state_query_time = None
            self._last_action_send_time = None  # So we don't ignore response.
            self._update()

        self._state.sub_tab = value
        active_color = (0.6, 1.0, 0.6)
        inactive_color = (0.5, 0.4, 0.5)
        ba.textwidget(
            edit=self._join_sub_tab_text,
            color=active_color if value is SubTabType.JOIN else inactive_color)
        ba.textwidget(
            edit=self._host_sub_tab_text,
            color=active_color if value is SubTabType.HOST else inactive_color)

        self._refresh_sub_tab()

        # Kick off an update to get any needed messages sent/etc.
        ba.pushcall(self._update)
Exemple #5
0
    def on_player_leave(self, player: ba.Player) -> None:
        ba.TeamGameActivity.on_player_leave(self, player)

        # A player leaving disqualifies the team if 'Entire Team Must Finish'
        # is on (otherwise in teams mode everyone could just leave except the
        # leading player to win).
        if (isinstance(self.session, ba.DualTeamSession)
                and self.settings.get('Entire Team Must Finish')):
            ba.screenmessage(ba.Lstr(
                translate=('statements',
                           '${TEAM} is disqualified because ${PLAYER} left'),
                subs=[('${TEAM}', player.team.name),
                      ('${PLAYER}', player.get_name(full=True))]),
                             color=(1, 1, 0))
            player.team.gamedata['finished'] = True
            player.team.gamedata['time'] = None
            player.team.gamedata['lap'] = 0
            ba.playsound(ba.getsound('boo'))
            for otherplayer in player.team.players:
                otherplayer.gamedata['lap'] = 0
                otherplayer.gamedata['finished'] = True
                try:
                    if otherplayer.actor is not None:
                        otherplayer.actor.handlemessage(ba.DieMessage())
                except Exception:
                    ba.print_exception('Error sending diemessages')

        # Defer so team/player lists will be updated.
        ba.pushcall(self._check_end_game)
Exemple #6
0
    def handlemessage(self, msg: Any) -> Any:
        if isinstance(msg, PlayerSpazDeathMessage):

            # Augment standard behavior.
            super().handlemessage(msg)

            curtime = ba.time()

            # Record the player's moment of death.
            PlayerData.get(msg.spaz.player).death_time = curtime

            # In co-op mode, end the game the instant everyone dies
            # (more accurate looking).
            # In teams/ffa, allow a one-second fudge-factor so we can
            # get more draws if players die basically at the same time.
            if isinstance(self.session, ba.CoopSession):
                # Teams will still show up if we check now.. check in
                # the next cycle.
                ba.pushcall(self._check_end_game)

                # Also record this for a final setting of the clock.
                self._last_player_death_time = curtime
            else:
                ba.timer(1.0, self._check_end_game)

        else:
            # Default handler:
            super().handlemessage(msg)
Exemple #7
0
 def run(self) -> None:
     result: Optional[str]
     try:
         import socket
         result = socket.gethostbyname(self._name)
     except Exception:
         result = None
     ba.pushcall(lambda: self._call(result, self._port),
                 from_other_thread=True)
Exemple #8
0
 def run() -> None:
     if os.path.exists(src) and os.path.exists(dst):
         if not os.path.exists(dst + os.path.sep + os.path.basename(src)):
             try:
                 shutil.copy(src, dst)
             except:
                 pass
             # pass permission errors
             if callback is not None:
                 ba.pushcall(ba.Call(callback, True),
                             from_other_thread=True)
         elif callback is not None:
             ba.pushcall(ba.Call(callback, False), from_other_thread=True)
Exemple #9
0
 def _uninstall_target():
     try:
         bap.uninstall(self.pkginfo.name)
     except Exception as e:
         ba.print_exception()
         ba.pushcall(ba.Call(ba.screenmessage,
                             f'Error: {e}',
                             color=(1, 0, 0)),
                     from_other_thread=True)
     else:
         ba.pushcall(ba.Call(ba.screenmessage, 'Done', color=(0, 1, 0)),
                     from_other_thread=True)
         if self._parent:
             self._parent._push_refresh()
Exemple #10
0
 def _run_addr_fetch(self) -> None:
     try:
         # FIXME: Update this to work with IPv6.
         import socket
         sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
         sock.connect(('8.8.8.8', 80))
         val = sock.getsockname()[0]
         sock.close()
         ba.pushcall(
             ba.Call(
                 _safe_set_text,
                 self._checking_state_text,
                 val,
             ),
             from_other_thread=True,
         )
     except Exception as exc:
         from efro.error import is_udp_network_error
         if is_udp_network_error(exc):
             ba.pushcall(ba.Call(
                 _safe_set_text, self._checking_state_text,
                 ba.Lstr(resource='gatherWindow.'
                         'noConnectionText'), False),
                         from_other_thread=True)
         else:
             ba.pushcall(ba.Call(
                 _safe_set_text, self._checking_state_text,
                 ba.Lstr(resource='gatherWindow.'
                         'addressFetchErrorText'), False),
                         from_other_thread=True)
             ba.pushcall(ba.Call(ba.print_error,
                                 'error in AddrFetchThread: ' + str(exc)),
                         from_other_thread=True)
    def __del__(self) -> None:
        scoreboard = self._scoreboard()

        # Remove our team from the scoreboard if its still around.
        # (but deferred, in case we die in a sim step or something where
        # its illegal to modify nodes)
        if scoreboard is None:
            return

        try:
            ba.pushcall(ba.Call(scoreboard.remove_team, self._team_id))
        except ba.ContextError:
            # This happens if we fire after the activity expires.
            # In that case we don't need to do anything.
            pass
 def run(self) -> None:
     try:
         # FIXME: Update this to work with IPv6 at some point.
         import socket
         sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
         sock.connect(('8.8.8.8', 80))
         val = sock.getsockname()[0]
         sock.close()
         ba.pushcall(ba.Call(self._call, val), from_other_thread=True)
     except Exception as exc:
         # Ignore expected network errors; log others.
         import errno
         if isinstance(exc, OSError) and exc.errno == errno.ENETUNREACH:
             pass
         else:
             ba.print_exception()
Exemple #13
0
 def run(self) -> None:
     try:
         # FIXME: Update this to work with IPv6 at some point.
         import socket
         sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
         sock.connect(('8.8.8.8', 80))
         val = sock.getsockname()[0]
         sock.close()
         ba.pushcall(ba.Call(self._call, val), from_other_thread=True)
     except Exception as exc:
         from efro.error import is_udp_network_error
         # Ignore expected network errors; log others.
         if is_udp_network_error(exc):
             pass
         else:
             ba.print_exception()
Exemple #14
0
    def run(self) -> None:
        ba.app.ping_thread_count += 1
        sock: Optional[socket.socket] = None
        try:
            import socket
            from ba.internal import get_ip_address_type
            socket_type = get_ip_address_type(self._address)
            sock = socket.socket(socket_type, socket.SOCK_DGRAM)
            sock.connect((self._address, self._port))

            accessible = False
            starttime = time.time()

            # Send a few pings and wait a second for
            # a response.
            sock.settimeout(1)
            for _i in range(3):
                sock.send(b'\x0b')
                result: Optional[bytes]
                try:
                    # 11: BA_PACKET_SIMPLE_PING
                    result = sock.recv(10)
                except Exception:
                    result = None
                if result == b'\x0c':
                    # 12: BA_PACKET_SIMPLE_PONG
                    accessible = True
                    break
                time.sleep(1)
            ping = (time.time() - starttime) * 1000.0
            ba.pushcall(ba.Call(self._call, self._address, self._port,
                                ping if accessible else None),
                        from_other_thread=True)
        except Exception as exc:
            from efro.error import is_udp_network_error
            if is_udp_network_error(exc):
                pass
            else:
                ba.print_exception('Error on gather ping', once=True)
        finally:
            try:
                if sock is not None:
                    sock.close()
            except Exception:
                ba.print_exception('Error on gather ping cleanup', once=True)

        ba.app.ping_thread_count -= 1
    def handlemessage(self, msg: Any) -> Any:

        # A player has died.
        if isinstance(msg, playerspaz.PlayerSpazDeathMessage):
            super().handlemessage(msg)  # do standard stuff
            self.respawn_player(msg.spaz.player)  # kick off a respawn

        # A spaz-bot has died.
        elif isinstance(msg, spazbot.SpazBotDeathMessage):
            # Unfortunately the bot-set will always tell us there are living
            # bots if we ask here (the currently-dying bot isn't officially
            # marked dead yet) ..so lets push a call into the event loop to
            # check once this guy has finished dying.
            ba.pushcall(self._check_if_won)
        else:
            # Let the base class handle anything we don't.
            super().handlemessage(msg)
Exemple #16
0
    def _restore_state(self) -> None:
        try:
            for tab in self._tabs.values():
                tab.restore_state()

            sel: Optional[ba.Widget]
            winstate = ba.app.ui.window_states.get(self.__class__.__name__, {})
            sel_name = winstate.get('sel_name', None)
            assert isinstance(sel_name, (str, type(None)))
            current_tab = self.TabID.ABOUT
            gather_tab_val = ba.app.config.get('Gather Tab')
            try:
                stored_tab = self.TabID(gather_tab_val)
                if stored_tab in self._tab_row.tabs:
                    current_tab = stored_tab
            except ValueError:
                # EWWWW; this exception causes a dependency loop that won't
                # go away until the next cyclical collection, which can
                # keep us alive. Perhaps should rethink our garbage
                # collection strategy, but for now just explicitly running
                # a cycle.
                ba.pushcall(ba.garbage_collect)
            self._set_tab(current_tab)
            if sel_name == 'Back':
                sel = self._back_button
            elif sel_name == 'TabContainer':
                sel = self._tab_container
            elif isinstance(sel_name, str) and sel_name.startswith('Tab:'):
                try:
                    sel_tab_id = self.TabID(sel_name.split(':')[-1])
                except ValueError:
                    sel_tab_id = self.TabID.ABOUT
                    # EWWWW; this exception causes a dependency loop that won't
                    # go away until the next cyclical collection, which can
                    # keep us alive. Perhaps should rethink our garbage
                    # collection strategy, but for now just explicitly running
                    # a cycle.
                    ba.pushcall(ba.garbage_collect)
                sel = self._tab_row.tabs[sel_tab_id].button
            else:
                sel = self._tab_row.tabs[current_tab].button
            ba.containerwidget(edit=self._root_widget, selected_child=sel)
        except Exception:
            ba.print_exception('Error restoring gather-win state.')
Exemple #17
0
    def handlemessage(self, msg: Any) -> Any:

        # A player has died.
        if isinstance(msg, ba.PlayerDiedMessage):
            super().handlemessage(msg)  # Augment standard behavior.
            self.respawn_player(msg.getplayer(Player))

        # A spaz-bot has died.
        elif isinstance(msg, SpazBotDiedMessage):
            # Unfortunately the bot-set will always tell us there are living
            # bots if we ask here (the currently-dying bot isn't officially
            # marked dead yet) ..so lets push a call into the event loop to
            # check once this guy has finished dying.
            ba.pushcall(self._check_if_won)

        # Let the base class handle anything we don't.
        else:
            return super().handlemessage(msg)
        return None
Exemple #18
0
        def __del__(self) -> None:

            # ew.  our destructor here may get called as part of an internal
            # widget tear-down.
            # running further widget calls here can quietly break stuff, so we
            # need to push a deferred call to kill these as necessary instead.
            # (should bulletproof internal widget code to give a clean error
            # in this case)
            def kill_widgets(widgets: Sequence[Optional[ba.Widget]]) -> None:
                for widget in widgets:
                    if widget:
                        widget.delete()

            ba.pushcall(
                ba.Call(kill_widgets, [
                    self._body_image, self._eyes_image,
                    self._body_image_target, self._eyes_image_target,
                    self._name_text
                ]))
        def run(self) -> None:
            try:
                starttime = time.time()
                files = os.listdir(self._path)
                duration = time.time() - starttime
                min_time = 0.1

                # Make sure this takes at least 1/10 second so the user
                # has time to see the selection highlight.
                if duration < min_time:
                    time.sleep(min_time - duration)
                ba.pushcall(ba.Call(self._callback, files, None),
                            from_other_thread=True)
            except Exception as exc:
                # Ignore permission-denied.
                if 'Errno 13' not in str(exc):
                    ba.print_exception()
                nofiles: List[str] = []
                ba.pushcall(ba.Call(self._callback, nofiles, str(exc)),
                            from_other_thread=True)
Exemple #20
0
    def _capture_button(self,
                        pos: Tuple[float, float],
                        color: Tuple[float, float, float],
                        texture: ba.Texture,
                        button: str,
                        scale: float = 1.0) -> None:
        base_size = 79
        btn = ba.buttonwidget(parent=self._root_widget,
                              autoselect=True,
                              position=(pos[0] - base_size * 0.5 * scale,
                                        pos[1] - base_size * 0.5 * scale),
                              size=(base_size * scale, base_size * scale),
                              texture=texture,
                              label='',
                              color=color)

        # Do this deferred so it shows up on top of other buttons. (ew.)
        def doit() -> None:
            if not self._root_widget:
                return
            uiscale = 0.66 * scale * 2.0
            maxwidth = 76.0 * scale
            txt = ba.textwidget(parent=self._root_widget,
                                position=(pos[0] + 0.0 * scale,
                                          pos[1] - (57.0 - 18.0) * scale),
                                color=(1, 1, 1, 0.3),
                                size=(0, 0),
                                h_align='center',
                                v_align='top',
                                scale=uiscale,
                                maxwidth=maxwidth,
                                text=self._input.get_button_name(
                                    self._settings[button]))
            ba.buttonwidget(edit=btn,
                            autoselect=True,
                            on_activate_call=ba.Call(AwaitKeyboardInputWindow,
                                                     button, txt,
                                                     self._settings))

        ba.pushcall(doit)
Exemple #21
0
def _test_v1_transaction() -> None:
    """Dummy fail test case."""
    if _ba.get_v1_account_state() != 'signed_in':
        raise RuntimeError('Not signed in.')

    starttime = time.monotonic()

    # Gets set to True on success or string on error.
    results: list[Any] = [False]

    def _cb(cbresults: Any) -> None:
        # Simply set results here; our other thread acts on them.
        if not isinstance(cbresults, dict) or 'party_code' not in cbresults:
            results[0] = 'Unexpected transaction response'
            return
        results[0] = True  # Success!

    def _do_it() -> None:
        # Fire off a transaction with a callback.
        _ba.add_transaction(
            {
                'type': 'PRIVATE_PARTY_QUERY',
                'expire_time': time.time() + 20,
            },
            callback=_cb,
        )
        _ba.run_transactions()

    ba.pushcall(_do_it, from_other_thread=True)

    while results[0] is False:
        time.sleep(0.01)
        if time.monotonic() - starttime > 10.0:
            raise RuntimeError('timed out')

    # If we got left a string, its an error.
    if isinstance(results[0], str):
        raise RuntimeError(results[0])
Exemple #22
0
 def _sync_target(self):
     try:
         bap.repo.sync()
     except Exception as e:
         ba.pushcall(ba.Call(ba.screenmessage,
                             f"Error: {e}",
                             color=(1, 0, 0)),
                     from_other_thread=True)
     else:
         ba.pushcall(ba.Call(ba.screenmessage, "Done", color=(0, 1, 0)),
                     from_other_thread=True)
         ba.pushcall(self._refresh, from_other_thread=True)
Exemple #23
0
    def _run_addr_fetch(self) -> None:
        try:
            # FIXME: Update this to work with IPv6.
            import socket
            sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            sock.connect(('8.8.8.8', 80))
            val = sock.getsockname()[0]
            sock.close()
            ba.pushcall(
                ba.Call(
                    _safe_set_text,
                    self._checking_state_text,
                    val,
                ),
                from_other_thread=True,
            )
        except Exception as exc:
            err_str = str(exc)

            # FIXME: Should look at exception types here,
            #  not strings.
            if 'Network is unreachable' in err_str:
                ba.pushcall(ba.Call(
                    _safe_set_text, self._checking_state_text,
                    ba.Lstr(resource='gatherWindow.'
                            'noConnectionText'), False),
                            from_other_thread=True)
            else:
                ba.pushcall(ba.Call(
                    _safe_set_text, self._checking_state_text,
                    ba.Lstr(resource='gatherWindow.'
                            'addressFetchErrorText'), False),
                            from_other_thread=True)
                ba.pushcall(ba.Call(ba.print_error,
                                    'error in AddrFetchThread: ' + str(exc)),
                            from_other_thread=True)
Exemple #24
0
 def _install_target():
     from bap.consts import CACHE_DIR
     import os
     try:
         for p in bap.repo.download(self.pkginfo.name, progress=True):
             ba.pushcall(ba.Call(ba.screenmessage,
                                 f'Downloading ({p}%)...'),
                         from_other_thread=True)
         ba.pushcall(ba.Call(ba.screenmessage,
                             'Installing...',
                             color=(1, 1, 1)),
                     from_other_thread=True)
         bap.install(os.path.join(CACHE_DIR,
                                  self.pkginfo.name + '.bap'),
                     upgrade=upgrade)
     except Exception as e:
         ba.print_exception()
         ba.pushcall(ba.Call(ba.screenmessage,
                             f'Error: {e}',
                             color=(1, 0, 0)),
                     from_other_thread=True)
     else:
         ba.pushcall(ba.Call(ba.screenmessage, 'Done', color=(0, 1, 0)),
                     from_other_thread=True)
Exemple #25
0
 def end_game(self) -> None:
     ba.pushcall(ba.Call(self.do_end, 'defeat'))
     ba.setmusic(None)
     ba.playsound(self._player_death_sound)
 def end_game(self) -> None:
     # Tell our bots to celebrate just to rub it in.
     self._bots.final_celebrate()
     ba.setmusic(None)
     ba.pushcall(ba.WeakCall(self.do_end, 'defeat'))
 def _call() -> None:
     result = call()
     if callback:
         ba.pushcall(ba.Call(callback, result), from_other_thread=True)
Exemple #28
0
    def _update_server_list(self) -> None:
        cur_time = ba.time(ba.TimeType.REAL)
        if self._first_server_list_rebuild_time is None:
            self._first_server_list_rebuild_time = cur_time

        # We get called quite often (for each ping response, etc) so we want
        # to limit our rebuilds to keep the UI responsive.
        # Let's update faster for the first few seconds,
        # then ease off to keep the list from jumping around.
        since_first = cur_time - self._first_server_list_rebuild_time
        wait_time = (1.0 if since_first < 2.0 else
                     2.5 if since_first < 10.0 else 5.0)
        if (not self._server_list_dirty
                and self._last_server_list_update_time is not None
                and cur_time - self._last_server_list_update_time < wait_time):
            return

        # If we somehow got here without the required UI being in place...
        columnwidget = self._join_list_column
        if not columnwidget:
            return

        self._last_server_list_update_time = cur_time
        self._server_list_dirty = False

        with ba.Context('ui'):

            # Now kill and recreate all widgets.
            for widget in columnwidget.get_children():
                widget.delete()

            ordered_parties = self._get_ordered_parties()

            # If we've got a filter, filter them.
            if self._filter_value:
                # Let's do case-insensitive searching.
                filterval = self._filter_value.lower()
                ordered_parties = [
                    p for p in ordered_parties if filterval in p.name.lower()
                ]

            sub_scroll_width = 830
            lineheight = 42
            sub_scroll_height = lineheight * len(ordered_parties) + 50
            ba.containerwidget(edit=columnwidget,
                               size=(sub_scroll_width, sub_scroll_height))

            # Ew; this rebuilding generates deferred selection callbacks
            # so we need to generated deferred ignore notices for ourself.
            def refresh_on() -> None:
                self._refreshing_list = True

            ba.pushcall(refresh_on)

            # Janky - allow escaping if there's nothing in us.
            ba.containerwidget(edit=self._host_scrollwidget,
                               claims_up_down=(len(ordered_parties) > 0))

            self._build_server_entry_lines(lineheight, ordered_parties,
                                           sub_scroll_height, sub_scroll_width)

            # So our selection callbacks can start firing..
            def refresh_off() -> None:
                self._refreshing_list = False

            ba.pushcall(refresh_off)
    def run(self) -> None:
        # pylint: disable=too-many-branches
        # pylint: disable=too-many-statements
        ba.app.ping_thread_count += 1
        sock: Optional[socket.socket] = None
        try:
            import socket
            from ba.internal import get_ip_address_type
            socket_type = get_ip_address_type(self._address)
            sock = socket.socket(socket_type, socket.SOCK_DGRAM)
            sock.connect((self._address, self._port))

            accessible = False
            starttime = time.time()

            # Send a few pings and wait a second for
            # a response.
            sock.settimeout(1)
            for _i in range(3):
                sock.send(b'\x0b')
                result: Optional[bytes]
                try:
                    # 11: BA_PACKET_SIMPLE_PING
                    result = sock.recv(10)
                except Exception:
                    result = None
                if result == b'\x0c':
                    # 12: BA_PACKET_SIMPLE_PONG
                    accessible = True
                    break
                time.sleep(1)
            ping = (time.time() - starttime) * 1000.0
            ba.pushcall(ba.Call(self._call, self._address, self._port,
                                ping if accessible else None),
                        from_other_thread=True)
        except ConnectionRefusedError:
            # Fine, server; sorry we pinged you. Hmph.
            pass
        except OSError as exc:
            import errno

            # Ignore harmless errors.
            if exc.errno in {
                    errno.EHOSTUNREACH, errno.ENETUNREACH, errno.EINVAL,
                    errno.EPERM, errno.EACCES
            }:
                pass
            elif exc.errno == 10022:
                # Windows 'invalid argument' error.
                pass
            elif exc.errno == 10051:
                # Windows 'a socket operation was attempted
                # to an unreachable network' error.
                pass
            elif exc.errno == errno.EADDRNOTAVAIL:
                if self._port == 0:
                    # This has happened. Ignore.
                    pass
                elif ba.do_once():
                    print(f'Got EADDRNOTAVAIL on gather ping'
                          f' for addr {self._address}'
                          f' port {self._port}.')
            else:
                ba.print_exception(
                    f'Error on gather ping '
                    f'(errno={exc.errno})', once=True)
        except Exception:
            ba.print_exception('Error on gather ping', once=True)
        finally:
            try:
                if sock is not None:
                    sock.close()
            except Exception:
                ba.print_exception('Error on gather ping cleanup', once=True)

        ba.app.ping_thread_count -= 1
    def _update_party_rows(self) -> None:
        columnwidget = self._join_list_column
        if not columnwidget:
            return

        assert self._join_text
        assert self._filter_text

        # Janky - allow escaping when there's nothing in our list.
        assert self._host_scrollwidget
        ba.containerwidget(edit=self._host_scrollwidget,
                           claims_up_down=(len(self._parties_displayed) > 0))

        # Clip if we have more UI rows than parties to show.
        clipcount = len(self._ui_rows) - len(self._parties_displayed)
        if clipcount > 0:
            clipcount = max(clipcount, 50)
            self._ui_rows = self._ui_rows[:-clipcount]

        # If we have no parties to show, we're done.
        if not self._parties_displayed:
            return

        sub_scroll_width = 830
        lineheight = 42
        sub_scroll_height = lineheight * len(self._parties_displayed) + 50
        ba.containerwidget(edit=columnwidget,
                           size=(sub_scroll_width, sub_scroll_height))

        # Any time our height changes, reset the refresh back to the top
        # so we don't see ugly empty spaces appearing during initial list
        # filling.
        if sub_scroll_height != self._last_sub_scroll_height:
            self._refresh_ui_row = 0
            self._last_sub_scroll_height = sub_scroll_height

            # Also note that we need to redisplay everything since its pos
            # will have changed.. :(
            for party in self._parties.values():
                party.clean_display_index = None

        # Ew; this rebuilding generates deferred selection callbacks
        # so we need to push deferred notices so we know to ignore them.
        def refresh_on() -> None:
            self._refreshing_list = True

        ba.pushcall(refresh_on)

        # Ok, now here's the deal: we want to avoid creating/updating this
        # entire list at one time because it will lead to hitches. So we
        # refresh individual rows quickly in a loop.
        rowcount = min(12, len(self._parties_displayed))

        party_vals_displayed = list(self._parties_displayed.values())
        while rowcount > 0:
            refresh_row = self._refresh_ui_row % len(self._parties_displayed)
            if refresh_row >= len(self._ui_rows):
                self._ui_rows.append(UIRow())
                refresh_row = len(self._ui_rows) - 1

            # For the first few seconds after getting our first server-list,
            # refresh only the top section of the list; this allows the lowest
            # ping servers to show up more quickly.
            if self._first_valid_server_list_time is not None:
                if time.time() - self._first_valid_server_list_time < 4.0:
                    if refresh_row > 40:
                        refresh_row = 0

            self._ui_rows[refresh_row].update(
                refresh_row,
                party_vals_displayed[refresh_row],
                sub_scroll_width=sub_scroll_width,
                sub_scroll_height=sub_scroll_height,
                lineheight=lineheight,
                columnwidget=columnwidget,
                join_text=self._join_text,
                existing_selection=self._selection,
                filter_text=self._filter_text,
                tab=self)
            self._refresh_ui_row = refresh_row + 1
            rowcount -= 1

        # So our selection callbacks can start firing..
        def refresh_off() -> None:
            self._refreshing_list = False

        ba.pushcall(refresh_off)