From b248d743f0e28a68590533b0d9f3abdc31f1a4b5 Mon Sep 17 00:00:00 2001 From: Eric Froemling Date: Wed, 21 Oct 2020 15:36:25 -0700 Subject: [PATCH] Split Gather UI code out into more manageable chunks --- .idea/dictionaries/ericf.xml | 7 + CHANGELOG.md | 3 +- assets/.asset_manifest_public.json | 16 +- assets/Makefile | 16 +- assets/src/ba_data/python/ba/_general.py | 2 +- assets/src/ba_data/python/bastd/ui/gather.py | 1968 ----------------- .../python/bastd/ui/gather/__init__.py | 271 +++ .../python/bastd/ui/gather/abouttab.py | 110 + .../ba_data/python/bastd/ui/gather/bases.py | 52 + .../python/bastd/ui/gather/googleplaytab.py | 86 + .../python/bastd/ui/gather/manualtab.py | 426 ++++ .../python/bastd/ui/gather/nearbytab.py | 136 ++ .../python/bastd/ui/gather/publictab.py | 1040 +++++++++ .../.idea/dictionaries/ericf.xml | 7 + docs/ba_module.md | 2 +- tools/efro/call.py | 8 +- 16 files changed, 2172 insertions(+), 1978 deletions(-) delete mode 100644 assets/src/ba_data/python/bastd/ui/gather.py create mode 100644 assets/src/ba_data/python/bastd/ui/gather/__init__.py create mode 100644 assets/src/ba_data/python/bastd/ui/gather/abouttab.py create mode 100644 assets/src/ba_data/python/bastd/ui/gather/bases.py create mode 100644 assets/src/ba_data/python/bastd/ui/gather/googleplaytab.py create mode 100644 assets/src/ba_data/python/bastd/ui/gather/manualtab.py create mode 100644 assets/src/ba_data/python/bastd/ui/gather/nearbytab.py create mode 100644 assets/src/ba_data/python/bastd/ui/gather/publictab.py diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml index c67b7241..98ab2eb4 100644 --- a/.idea/dictionaries/ericf.xml +++ b/.idea/dictionaries/ericf.xml @@ -17,6 +17,7 @@ abeb abishort abot + abouttab abtn accesstime accountname @@ -877,6 +878,7 @@ gnode goles goodlist + googleplaytab googlevr goosey gotresponse @@ -1209,6 +1211,7 @@ malformatted mallimportedby mandir + manualtab mapdata mapdef mapdefs @@ -1340,6 +1343,7 @@ ncpu ndbm ndkpath + nearbytab neededsettings ness netlink @@ -1607,6 +1611,7 @@ pthreads ptrans ptype + publictab pubsync pucknode pulllist @@ -2010,6 +2015,8 @@ sysctl syslogmodule tabdefs + tabtype + tabtypes tabval tagargs tagversion diff --git a/CHANGELOG.md b/CHANGELOG.md index 6434ca12..8fdf2c8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -### 1.5.27 (20218) +### 1.5.27 (20224) - Language functionality has been consolidated into a LanguageSubsystem object at ba.app.lang - ba.get_valid_languages() is now an attr: ba.app.lang.available_languages - Achievement functionality has been consolidated into an AchievementSubsystem object at ba.app.ach @@ -7,6 +7,7 @@ - Ditto with MetadataSubsystem and ba.app.meta - Ditto with AdsSubsystem and ba.app.ads - Revamped tab-button functionality into a cleaner type-safe class (bastd.ui.tabs.TabRow) +- Split Gather-Window tabs out into individual classes for future improvements (bastd.ui.gather.*) ### 1.5.26 (20217) - Simplified licensing header on python scripts. diff --git a/assets/.asset_manifest_public.json b/assets/.asset_manifest_public.json index 601eaa84..3243d27e 100644 --- a/assets/.asset_manifest_public.json +++ b/assets/.asset_manifest_public.json @@ -303,7 +303,6 @@ "ba_data/python/bastd/ui/__pycache__/debug.cpython-38.opt-1.pyc", "ba_data/python/bastd/ui/__pycache__/feedback.cpython-38.opt-1.pyc", "ba_data/python/bastd/ui/__pycache__/fileselector.cpython-38.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/gather.cpython-38.opt-1.pyc", "ba_data/python/bastd/ui/__pycache__/getcurrency.cpython-38.opt-1.pyc", "ba_data/python/bastd/ui/__pycache__/getremote.cpython-38.opt-1.pyc", "ba_data/python/bastd/ui/__pycache__/helpui.cpython-38.opt-1.pyc", @@ -362,7 +361,20 @@ "ba_data/python/bastd/ui/debug.py", "ba_data/python/bastd/ui/feedback.py", "ba_data/python/bastd/ui/fileselector.py", - "ba_data/python/bastd/ui/gather.py", + "ba_data/python/bastd/ui/gather/__init__.py", + "ba_data/python/bastd/ui/gather/__pycache__/__init__.cpython-38.opt-1.pyc", + "ba_data/python/bastd/ui/gather/__pycache__/abouttab.cpython-38.opt-1.pyc", + "ba_data/python/bastd/ui/gather/__pycache__/bases.cpython-38.opt-1.pyc", + "ba_data/python/bastd/ui/gather/__pycache__/googleplaytab.cpython-38.opt-1.pyc", + "ba_data/python/bastd/ui/gather/__pycache__/manualtab.cpython-38.opt-1.pyc", + "ba_data/python/bastd/ui/gather/__pycache__/nearbytab.cpython-38.opt-1.pyc", + "ba_data/python/bastd/ui/gather/__pycache__/publictab.cpython-38.opt-1.pyc", + "ba_data/python/bastd/ui/gather/abouttab.py", + "ba_data/python/bastd/ui/gather/bases.py", + "ba_data/python/bastd/ui/gather/googleplaytab.py", + "ba_data/python/bastd/ui/gather/manualtab.py", + "ba_data/python/bastd/ui/gather/nearbytab.py", + "ba_data/python/bastd/ui/gather/publictab.py", "ba_data/python/bastd/ui/getcurrency.py", "ba_data/python/bastd/ui/getremote.py", "ba_data/python/bastd/ui/helpui.py", diff --git a/assets/Makefile b/assets/Makefile index b38181a3..7b8f4292 100644 --- a/assets/Makefile +++ b/assets/Makefile @@ -294,7 +294,13 @@ SCRIPT_TARGETS_PY_PUBLIC = \ build/ba_data/python/bastd/ui/debug.py \ build/ba_data/python/bastd/ui/feedback.py \ build/ba_data/python/bastd/ui/fileselector.py \ - build/ba_data/python/bastd/ui/gather.py \ + build/ba_data/python/bastd/ui/gather/__init__.py \ + build/ba_data/python/bastd/ui/gather/abouttab.py \ + build/ba_data/python/bastd/ui/gather/bases.py \ + build/ba_data/python/bastd/ui/gather/googleplaytab.py \ + build/ba_data/python/bastd/ui/gather/manualtab.py \ + build/ba_data/python/bastd/ui/gather/nearbytab.py \ + build/ba_data/python/bastd/ui/gather/publictab.py \ build/ba_data/python/bastd/ui/getcurrency.py \ build/ba_data/python/bastd/ui/getremote.py \ build/ba_data/python/bastd/ui/helpui.py \ @@ -533,7 +539,13 @@ SCRIPT_TARGETS_PYC_PUBLIC = \ build/ba_data/python/bastd/ui/__pycache__/debug.cpython-38.opt-1.pyc \ build/ba_data/python/bastd/ui/__pycache__/feedback.cpython-38.opt-1.pyc \ build/ba_data/python/bastd/ui/__pycache__/fileselector.cpython-38.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/gather.cpython-38.opt-1.pyc \ + build/ba_data/python/bastd/ui/gather/__pycache__/__init__.cpython-38.opt-1.pyc \ + build/ba_data/python/bastd/ui/gather/__pycache__/abouttab.cpython-38.opt-1.pyc \ + build/ba_data/python/bastd/ui/gather/__pycache__/bases.cpython-38.opt-1.pyc \ + build/ba_data/python/bastd/ui/gather/__pycache__/googleplaytab.cpython-38.opt-1.pyc \ + build/ba_data/python/bastd/ui/gather/__pycache__/manualtab.cpython-38.opt-1.pyc \ + build/ba_data/python/bastd/ui/gather/__pycache__/nearbytab.cpython-38.opt-1.pyc \ + build/ba_data/python/bastd/ui/gather/__pycache__/publictab.cpython-38.opt-1.pyc \ build/ba_data/python/bastd/ui/__pycache__/getcurrency.cpython-38.opt-1.pyc \ build/ba_data/python/bastd/ui/__pycache__/getremote.cpython-38.opt-1.pyc \ build/ba_data/python/bastd/ui/__pycache__/helpui.cpython-38.opt-1.pyc \ diff --git a/assets/src/ba_data/python/ba/_general.py b/assets/src/ba_data/python/ba/_general.py index 45bdf886..c88ffa19 100644 --- a/assets/src/ba_data/python/ba/_general.py +++ b/assets/src/ba_data/python/ba/_general.py @@ -11,9 +11,9 @@ import inspect from typing import TYPE_CHECKING, TypeVar, Protocol from efro.terminal import Clr +import _ba from ba._error import print_error, print_exception from ba._enums import TimeType -import _ba if TYPE_CHECKING: from typing import Any, Type, Optional diff --git a/assets/src/ba_data/python/bastd/ui/gather.py b/assets/src/ba_data/python/bastd/ui/gather.py deleted file mode 100644 index 400f479f..00000000 --- a/assets/src/ba_data/python/bastd/ui/gather.py +++ /dev/null @@ -1,1968 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""Provides UI for inviting/joining friends.""" -# pylint: disable=too-many-lines - -from __future__ import annotations - -import threading -import time -from enum import Enum -from typing import TYPE_CHECKING, cast - -import _ba -import ba - -if TYPE_CHECKING: - from typing import Any, Optional, Tuple, Dict, List, Union, Callable - - -class GatherWindow(ba.Window): - """Window for joining/inviting friends.""" - - class TabID(Enum): - """Our available tab types.""" - ABOUT = 'about' - INTERNET = 'internet' - GOOGLE_PLAY = 'google_play' - LOCAL_NETWORK = 'local_network' - BLUETOOTH = 'bluetooth' - WIFI_DIRECT = 'wifi_direct' - MANUAL = 'manual' - - def __del__(self) -> None: - _ba.set_party_icon_always_visible(False) - - def __init__(self, - transition: Optional[str] = 'in_right', - origin_widget: ba.Widget = None): - # pylint: disable=too-many-statements - # pylint: disable=too-many-locals - from bastd.ui.tabs import TabRow - ba.set_analytics_screen('Gather Window') - scale_origin: Optional[Tuple[float, float]] - if origin_widget is not None: - self._transition_out = 'out_scale' - scale_origin = origin_widget.get_screen_space_center() - transition = 'in_scale' - else: - self._transition_out = 'out_right' - scale_origin = None - ba.app.ui.set_main_menu_location('Gather') - _ba.set_party_icon_always_visible(True) - self._public_parties: Dict[str, Dict[str, Any]] = {} - uiscale = ba.app.ui.uiscale - self._width = 1240 if uiscale is ba.UIScale.SMALL else 1040 - x_offs = 100 if uiscale is ba.UIScale.SMALL else 0 - self._height = (582 if uiscale is ba.UIScale.SMALL else - 680 if uiscale is ba.UIScale.MEDIUM else 800) - self._current_tab: Optional[GatherWindow.TabID] = None - extra_top = 20 if uiscale is ba.UIScale.SMALL else 0 - self._r = 'gatherWindow' - self._tab_data: Any = None - self._internet_local_address: Optional[str] = None - self._internet_host_text: Optional[ba.Widget] = None - self._internet_join_text: Optional[ba.Widget] = None - self._doing_access_check: Optional[bool] = None - self._access_check_count: Optional[int] = None - self._public_party_list_selection: Optional[Tuple[str, str]] = None - self._internet_tab: Optional[str] = None - self._internet_join_last_refresh_time = -99999.0 - self._last_public_party_list_rebuild_time: Optional[float] = None - self._first_public_party_list_rebuild_time: Optional[float] = None - self._internet_join_party_name_label: Optional[ba.Widget] = None - self._internet_join_party_language_label: Optional[ba.Widget] = None - self._internet_join_party_size_label: Optional[ba.Widget] = None - self._internet_join_party_ping_label: Optional[ba.Widget] = None - self._internet_host_scrollwidget: Optional[ba.Widget] = None - self._internet_host_columnwidget: Optional[ba.Widget] = None - self._internet_join_status_text: Optional[ba.Widget] = None - self._internet_host_name_label_text: Optional[ba.Widget] = None - self._internet_host_name_text: Optional[ba.Widget] = None - self._internet_host_max_party_size_label: Optional[ba.Widget] = None - self._internet_host_max_party_size_value: Optional[ba.Widget] = None - self._internet_host_max_party_size_minus_button: ( - Optional[ba.Widget]) = None - self._internet_host_max_party_size_plus_button: ( - Optional[ba.Widget]) = None - self._internet_host_toggle_button: Optional[ba.Widget] = None - self._internet_host_status_text: Optional[ba.Widget] = None - self._internet_host_dedicated_server_info_text: ( - Optional[ba.Widget]) = None - self._internet_lock_icon: Optional[ba.Widget] = None - self._next_public_party_entry_index = 0 - self._refreshing_public_party_list: Optional[bool] = None - self._last_public_party_connect_attempt_time: Optional[float] = None - self._t_addr: Optional[ba.Widget] = None - self._t_accessible: Optional[ba.Widget] = None - self._t_accessible_extra: Optional[ba.Widget] = None - - super().__init__(root_widget=ba.containerwidget( - size=(self._width, self._height + extra_top), - transition=transition, - toolbar_visibility='menu_minimal', - scale_origin_stack_offset=scale_origin, - scale=(1.3 if uiscale is ba.UIScale.SMALL else - 0.97 if uiscale is ba.UIScale.MEDIUM else 0.8), - stack_offset=(0, -11) if uiscale is ba.UIScale.SMALL else ( - 0, 0) if uiscale is ba.UIScale.MEDIUM else (0, 0))) - - if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: - ba.containerwidget(edit=self._root_widget, - on_cancel_call=self._back) - self._back_button = None - else: - self._back_button = btn = ba.buttonwidget( - parent=self._root_widget, - position=(70 + x_offs, self._height - 74), - size=(140, 60), - scale=1.1, - autoselect=True, - label=ba.Lstr(resource='backText'), - button_type='back', - on_activate_call=self._back) - ba.containerwidget(edit=self._root_widget, cancel_button=btn) - ba.buttonwidget(edit=btn, - button_type='backSmall', - position=(70 + x_offs, self._height - 78), - size=(60, 60), - label=ba.charstr(ba.SpecialChar.BACK)) - - ba.textwidget(parent=self._root_widget, - position=(self._width * 0.5, self._height - 42), - size=(0, 0), - color=ba.app.ui.title_color, - scale=1.5, - h_align='center', - v_align='center', - text=ba.Lstr(resource=self._r + '.titleText'), - maxwidth=550) - - platform = ba.app.platform - subplatform = ba.app.subplatform - - tabs_def: List[Tuple[GatherWindow.TabID, ba.Lstr]] = [ - (self.TabID.ABOUT, ba.Lstr(resource=self._r + '.aboutText')) - ] - if _ba.get_account_misc_read_val('enablePublicParties', True): - tabs_def.append((self.TabID.INTERNET, - ba.Lstr(resource=self._r + '.internetText'))) - if platform == 'android' and subplatform == 'google': - tabs_def.append((self.TabID.GOOGLE_PLAY, - ba.Lstr(resource=self._r + '.googlePlayText'))) - tabs_def.append((self.TabID.LOCAL_NETWORK, - ba.Lstr(resource=self._r + '.localNetworkText'))) - - tabs_def.append( - (self.TabID.MANUAL, ba.Lstr(resource=self._r + '.manualText'))) - - scroll_buffer_h = 130 + 2 * x_offs - tab_buffer_h = 250 + 2 * x_offs - - self._tab_row = TabRow(self._root_widget, - tabs_def, - pos=(tab_buffer_h * 0.5, self._height - 130), - size=(self._width - tab_buffer_h, 50), - on_select_call=self._set_tab) - - if ba.app.ui.use_toolbars: - ba.widget(edit=self._tab_row.tabs[tabs_def[-1][0]].button, - right_widget=_ba.get_special_widget('party_button')) - if uiscale is ba.UIScale.SMALL: - ba.widget(edit=self._tab_row.tabs[tabs_def[0][0]].button, - left_widget=_ba.get_special_widget('back_button')) - - self._scroll_width = self._width - scroll_buffer_h - self._scroll_height = self._height - 180.0 - - # Not actually using a scroll widget anymore; just an image. - scroll_left = (self._width - self._scroll_width) * 0.5 - scroll_bottom = self._height - self._scroll_height - 79 - 48 - buffer_h = 10 - buffer_v = 4 - ba.imagewidget(parent=self._root_widget, - position=(scroll_left - buffer_h, - scroll_bottom - buffer_v), - size=(self._scroll_width + 2 * buffer_h, - self._scroll_height + 2 * buffer_v), - texture=ba.gettexture('scrollWidget'), - model_transparent=ba.getmodel('softEdgeOutside')) - self._tab_container: Optional[ba.Widget] = None - self._restore_state() - - def get_r(self) -> str: - """(internal)""" - return self._r - - def _on_google_play_show_invites_press(self) -> None: - from bastd.ui import account - if (_ba.get_account_state() != 'signed_in' - or _ba.get_account_type() != 'Google Play'): - account.show_sign_in_prompt('Google Play') - else: - _ba.show_invites_ui() - - def _on_google_play_invite_press(self) -> None: - from bastd.ui import confirm - from bastd.ui import account - if (_ba.get_account_state() != 'signed_in' - or _ba.get_account_type() != 'Google Play'): - account.show_sign_in_prompt('Google Play') - else: - # If there's google play people connected to us, inform the user - # that they will get disconnected. Otherwise just go ahead. - google_player_count = (_ba.get_google_play_party_client_count()) - if google_player_count > 0: - confirm.ConfirmWindow( - ba.Lstr(resource=self._r + '.googlePlayReInviteText', - subs=[('${COUNT}', str(google_player_count))]), - lambda: ba.timer( - 0.2, _ba.invite_players, timetype=ba.TimeType.REAL), - width=500, - height=150, - ok_text=ba.Lstr(resource=self._r + - '.googlePlayInviteText')) - else: - ba.timer(0.1, _ba.invite_players, timetype=ba.TimeType.REAL) - - def _invite_to_try_press(self) -> None: - from bastd.ui import account - from bastd.ui import appinvite - if _ba.get_account_state() != 'signed_in': - account.show_sign_in_prompt() - return - appinvite.handle_app_invites_press() - - def _set_tab(self, tab_id: TabID) -> None: - # pylint: disable=too-many-statements - # pylint: disable=too-many-locals - if self._current_tab is tab_id: - return - self._current_tab = tab_id - - # We wanna preserve our current tab between runs. - cfg = ba.app.config - cfg['Gather Tab'] = tab_id.value - cfg.commit() - - # Update tab colors based on which is selected. - self._tab_row.update_appearance(tab_id) - - # (Re)create scroll widget. - if self._tab_container: - self._tab_container.delete() - scroll_left = (self._width - self._scroll_width) * 0.5 - scroll_bottom = self._height - self._scroll_height - 79 - 48 - - # A place where tabs can store data to get cleared when switching to - # a different tab. - self._tab_data = {} - - # So we can still select root level widgets with direction buttons. - def _simple_message(tab2: GatherWindow.TabID, - message: ba.Lstr, - string_height: float, - include_invite: bool = False) -> None: - msc_scale = 1.1 - c_width_2 = self._scroll_width - c_height_2 = min(self._scroll_height, - string_height * msc_scale + 100) - try_tickets = _ba.get_account_misc_read_val( - 'friendTryTickets', None) - if try_tickets is None: - include_invite = False - self._tab_container = cnt2 = ba.containerwidget( - parent=self._root_widget, - position=(scroll_left, scroll_bottom + - (self._scroll_height - c_height_2) * 0.5), - size=(c_width_2, c_height_2), - background=False, - selectable=include_invite) - ba.widget(edit=cnt2, up_widget=self._tab_row.tabs[tab2].button) - - ba.textwidget( - parent=cnt2, - position=(c_width_2 * 0.5, - c_height_2 * (0.58 if include_invite else 0.5)), - color=(0.6, 1.0, 0.6), - scale=msc_scale, - size=(0, 0), - maxwidth=c_width_2 * 0.9, - max_height=c_height_2 * (0.7 if include_invite else 0.9), - h_align='center', - v_align='center', - text=message) - if include_invite: - ba.textwidget(parent=cnt2, - position=(c_width_2 * 0.57, 35), - color=(0, 1, 0), - scale=0.6, - size=(0, 0), - maxwidth=c_width_2 * 0.5, - h_align='right', - v_align='center', - flatness=1.0, - text=ba.Lstr( - resource=self._r + '.inviteAFriendText', - subs=[('${COUNT}', str(try_tickets))])) - ba.buttonwidget( - parent=cnt2, - position=(c_width_2 * 0.59, 10), - size=(230, 50), - color=(0.54, 0.42, 0.56), - textcolor=(0, 1, 0), - label=ba.Lstr(resource='gatherWindow.inviteFriendsText', - fallback_resource=( - 'gatherWindow.getFriendInviteCodeText')), - autoselect=True, - on_activate_call=ba.WeakCall(self._invite_to_try_press), - up_widget=self._tab_row.tabs[tab2].button) - - if tab_id is self.TabID.ABOUT: - msg = ba.Lstr(resource=self._r + '.aboutDescriptionText', - subs=[('${PARTY}', - ba.charstr(ba.SpecialChar.PARTY_ICON)), - ('${BUTTON}', - ba.charstr(ba.SpecialChar.TOP_BUTTON))]) - - # Let's not talk about sharing in vr-mode; its tricky to fit more - # than one head in a VR-headset ;-) - if not ba.app.vr_mode: - msg = ba.Lstr( - value='${A}\n\n${B}', - subs=[ - ('${A}', msg), - ('${B}', - ba.Lstr(resource=self._r + - '.aboutDescriptionLocalMultiplayerExtraText')) - ]) - - _simple_message(tab_id, msg, 400, include_invite=True) - - elif tab_id is self.TabID.GOOGLE_PLAY: - c_width = self._scroll_width - c_height = 380.0 - self._tab_container = cnt = ba.containerwidget( - parent=self._root_widget, - position=(scroll_left, scroll_bottom + - (self._scroll_height - c_height) * 0.5), - size=(c_width, c_height), - background=False, - selection_loops_to_parent=True) - v = c_height - 30.0 - ba.textwidget( - parent=cnt, - position=(c_width * 0.5, v - 140.0), - color=(0.6, 1.0, 0.6), - scale=1.3, - size=(0.0, 0.0), - maxwidth=c_width * 0.9, - h_align='center', - v_align='center', - text=ba.Lstr(resource='googleMultiplayerDiscontinuedText')) - - elif tab_id is self.TabID.INTERNET: - c_width = self._scroll_width - c_height = self._scroll_height - 20 - self._tab_container = cnt = ba.containerwidget( - parent=self._root_widget, - position=(scroll_left, scroll_bottom + - (self._scroll_height - c_height) * 0.5), - size=(c_width, c_height), - background=False, - selection_loops_to_parent=True) - v = c_height - 30 - self._internet_join_text = txt = ba.textwidget( - parent=cnt, - position=(c_width * 0.5 - 245, v - 13), - color=(0.6, 1.0, 0.6), - scale=1.3, - size=(200, 30), - maxwidth=250, - h_align='left', - v_align='center', - click_activate=True, - selectable=True, - autoselect=True, - on_activate_call=lambda: self._set_internet_tab( - 'join', playsound=True), - text=ba.Lstr(resource=self._r + - '.joinPublicPartyDescriptionText')) - ba.widget(edit=txt, up_widget=self._tab_row.tabs[tab_id].button) - self._internet_host_text = txt = ba.textwidget( - parent=cnt, - position=(c_width * 0.5 + 45, v - 13), - color=(0.6, 1.0, 0.6), - scale=1.3, - size=(200, 30), - maxwidth=250, - h_align='left', - v_align='center', - click_activate=True, - selectable=True, - autoselect=True, - on_activate_call=lambda: self._set_internet_tab( - 'host', playsound=True), - text=ba.Lstr(resource=self._r + - '.hostPublicPartyDescriptionText')) - ba.widget(edit=txt, - left_widget=self._internet_join_text, - up_widget=self._tab_row.tabs[tab_id].button) - ba.widget(edit=self._internet_join_text, right_widget=txt) - - # Attempt to fetch our local address so we have it for - # error messages. - self._internet_local_address = None - - class AddrFetchThread(threading.Thread): - """Thread for fetching an address in the bg.""" - - def __init__(self, call: Callable[[Any], Any]): - super().__init__() - self._call = call - - 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() - - AddrFetchThread(ba.WeakCall( - self._internet_fetch_local_addr_cb)).start() - - assert self._internet_tab is not None - self._set_internet_tab(self._internet_tab) - self._tab_data = { - 'update_timer': - ba.Timer(0.2, - ba.WeakCall(self._update_internet_tab), - repeat=True, - timetype=ba.TimeType.REAL) - } - - # Also update it immediately so we don't have to wait for the - # initial query. - self._update_internet_tab() - - elif tab_id is self.TabID.LOCAL_NETWORK: - c_width = self._scroll_width - c_height = self._scroll_height - 20 - sub_scroll_height = c_height - 85 - sub_scroll_width = 650 - - class NetScanner: - """Class for scanning for games on the lan.""" - - def __init__(self, scrollwidget: ba.Widget, - tab_button: ba.Widget, width: float): - self._scrollwidget = scrollwidget - self._tab_button = tab_button - self._columnwidget = ba.columnwidget( - parent=self._scrollwidget, - border=2, - margin=0, - left_border=10) - ba.widget(edit=self._columnwidget, up_widget=tab_button) - self._width = width - self._last_selected_host: Optional[Dict[str, Any]] = None - - self._update_timer = ba.Timer(1.0, - ba.WeakCall(self.update), - timetype=ba.TimeType.REAL, - repeat=True) - # Go ahead and run a few *almost* immediately so we don't - # have to wait a second. - self.update() - ba.timer(0.25, - ba.WeakCall(self.update), - timetype=ba.TimeType.REAL) - - def __del__(self) -> None: - _ba.end_host_scanning() - - def _on_select(self, host: Dict[str, Any]) -> None: - self._last_selected_host = host - - def _on_activate(self, host: Dict[str, Any]) -> None: - _ba.connect_to_party(host['address']) - - def update(self) -> None: - """(internal)""" - t_scale = 1.6 - for child in self._columnwidget.get_children(): - child.delete() - - # Grab this now this since adding widgets will change it. - last_selected_host = self._last_selected_host - hosts = _ba.host_scan_cycle() - for i, host in enumerate(hosts): - txt3 = ba.textwidget( - parent=self._columnwidget, - size=(self._width / t_scale, 30), - selectable=True, - color=(1, 1, 1), - on_select_call=ba.Call(self._on_select, host), - on_activate_call=ba.Call(self._on_activate, host), - click_activate=True, - text=host['display_string'], - h_align='left', - v_align='center', - corner_scale=t_scale, - maxwidth=(self._width / t_scale) * 0.93) - if host == last_selected_host: - ba.containerwidget(edit=self._columnwidget, - selected_child=txt3, - visible_child=txt3) - if i == 0: - ba.widget(edit=txt3, up_widget=self._tab_button) - - self._tab_container = cnt = ba.containerwidget( - parent=self._root_widget, - position=(scroll_left, scroll_bottom + - (self._scroll_height - c_height) * 0.5), - size=(c_width, c_height), - background=False, - selection_loops_to_parent=True) - v = c_height - 30 - ba.textwidget(parent=cnt, - position=(c_width * 0.5, v - 3), - color=(0.6, 1.0, 0.6), - scale=1.3, - size=(0, 0), - maxwidth=c_width * 0.9, - h_align='center', - v_align='center', - text=ba.Lstr(resource=self._r + - '.localNetworkDescriptionText')) - v -= 15 - v -= sub_scroll_height + 23 - scrollw = ba.scrollwidget( - parent=cnt, - position=((self._scroll_width - sub_scroll_width) * 0.5, v), - size=(sub_scroll_width, sub_scroll_height)) - - self._tab_data = NetScanner(scrollw, - self._tab_row.tabs[tab_id].button, - width=sub_scroll_width) - - ba.widget(edit=scrollw, - autoselect=True, - up_widget=self._tab_row.tabs[tab_id].button) - - elif tab_id is self.TabID.BLUETOOTH: - c_width = self._scroll_width - c_height = 380 - sub_scroll_width = 650 - - self._tab_container = cnt = ba.containerwidget( - parent=self._root_widget, - position=(scroll_left, scroll_bottom + - (self._scroll_height - c_height) * 0.5), - size=(c_width, c_height), - background=False, - selection_loops_to_parent=True) - v = c_height - 30 - ba.textwidget(parent=cnt, - position=(c_width * 0.5, v), - color=(0.6, 1.0, 0.6), - scale=1.3, - size=(0, 0), - maxwidth=c_width * 0.9, - h_align='center', - v_align='center', - text=ba.Lstr(resource=self._r + - '.bluetoothDescriptionText')) - v -= 35 - ba.textwidget(parent=cnt, - position=(c_width * 0.5, v), - color=(0.6, 1.0, 0.6), - scale=0.7, - size=(0, 0), - maxwidth=c_width * 0.9, - h_align='center', - v_align='center', - text=ba.Lstr(resource=self._r + - '.bluetoothAndroidSupportText')) - - v -= 55 - btn = ba.buttonwidget( - parent=cnt, - position=(c_width * 0.5 - sub_scroll_width * 0.5 + 10, v - 75), - size=(300, 70), - autoselect=True, - label=ba.Lstr(resource=self._r + '.bluetoothHostText')) - ba.widget(edit=btn, up_widget=self._tab_row.tabs[tab_id].button) - btn = ba.buttonwidget( - parent=cnt, - position=(c_width * 0.5 - sub_scroll_width * 0.5 + 330, - v - 75), - size=(300, 70), - autoselect=True, - on_activate_call=ba.Call(ba.screenmessage, - 'FIXME: Not wired up yet.'), - label=ba.Lstr(resource=self._r + '.bluetoothJoinText')) - ba.widget(edit=btn, up_widget=self._tab_row.tabs[tab_id].button) - ba.widget(edit=self._tab_row.tabs[tab_id].button, down_widget=btn) - - elif tab_id is self.TabID.WIFI_DIRECT: - c_width = self._scroll_width - c_height = self._scroll_height - 20 - self._tab_container = cnt = ba.containerwidget( - parent=self._root_widget, - position=(scroll_left, scroll_bottom + - (self._scroll_height - c_height) * 0.5), - size=(c_width, c_height), - background=False, - selection_loops_to_parent=True) - v = c_height - 80 - - ba.textwidget(parent=cnt, - position=(c_width * 0.5, v), - color=(0.6, 1.0, 0.6), - scale=1.0, - size=(0, 0), - maxwidth=c_width * 0.95, - max_height=140, - h_align='center', - v_align='center', - text=ba.Lstr(resource=self._r + - '.wifiDirectDescriptionTopText')) - v -= 140 - btn = ba.buttonwidget( - parent=cnt, - position=(c_width * 0.5 - 175, v), - size=(350, 65), - label=ba.Lstr(resource=self._r + - '.wifiDirectOpenWiFiSettingsText'), - autoselect=True, - on_activate_call=_ba.android_show_wifi_settings) - v -= 82 - - ba.widget(edit=btn, up_widget=self._tab_row.tabs[tab_id].button) - - ba.textwidget(parent=cnt, - position=(c_width * 0.5, v), - color=(0.6, 1.0, 0.6), - scale=0.9, - size=(0, 0), - maxwidth=c_width * 0.95, - max_height=150, - h_align='center', - v_align='center', - text=ba.Lstr(resource=self._r + - '.wifiDirectDescriptionBottomText', - subs=[('${APP_NAME}', - ba.Lstr(resource='titleText'))])) - - elif tab_id is self.TabID.MANUAL: - c_width = self._scroll_width - c_height = 380 - last_addr = ba.app.config.get('Last Manual Party Connect Address', - '') - - self._tab_container = cnt = ba.containerwidget( - parent=self._root_widget, - position=(scroll_left, scroll_bottom + - (self._scroll_height - c_height) * 0.5), - size=(c_width, c_height), - background=False, - selection_loops_to_parent=True) - v = c_height - 30 - ba.textwidget(parent=cnt, - position=(c_width * 0.5, v), - color=(0.6, 1.0, 0.6), - scale=1.3, - size=(0, 0), - maxwidth=c_width * 0.9, - h_align='center', - v_align='center', - text=ba.Lstr(resource=self._r + - '.manualDescriptionText')) - v -= 30 - v -= 70 - ba.textwidget(parent=cnt, - position=(c_width * 0.5 - 260 - 50, v), - color=(0.6, 1.0, 0.6), - scale=1.0, - size=(0, 0), - maxwidth=130, - h_align='right', - v_align='center', - text=ba.Lstr(resource=self._r + - '.manualAddressText')) - txt = ba.textwidget(parent=cnt, - editable=True, - description=ba.Lstr(resource=self._r + - '.manualAddressText'), - position=(c_width * 0.5 - 240 - 50, v - 30), - text=last_addr, - autoselect=True, - v_align='center', - scale=1.0, - size=(420, 60)) - ba.textwidget(parent=cnt, - position=(c_width * 0.5 - 260 + 490, v), - color=(0.6, 1.0, 0.6), - scale=1.0, - size=(0, 0), - maxwidth=80, - h_align='right', - v_align='center', - text=ba.Lstr(resource=self._r + '.portText')) - txt2 = ba.textwidget(parent=cnt, - editable=True, - description=ba.Lstr(resource=self._r + - '.portText'), - text='43210', - autoselect=True, - max_chars=5, - position=(c_width * 0.5 - 240 + 490, v - 30), - v_align='center', - scale=1.0, - size=(170, 60)) - - v -= 110 - - def _connect(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: - 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 - - class HostAddrFetchThread(threading.Thread): - """Thread to fetch an addr.""" - - def __init__(self, name: str, - call: Callable[[Optional[str]], Any]): - super().__init__() - self._name = name - self._call = call - - def run(self) -> None: - result: Optional[str] - try: - import socket - result = socket.gethostbyname(self._name) - except Exception: - result = None - ba.pushcall(ba.Call(self._call, result), - from_other_thread=True) - - def do_it_2(addr2: Optional[str]) -> None: - if addr2 is None: - ba.screenmessage(ba.Lstr( - resource='internal.unableToResolveHostText'), - color=(1, 0, 0)) - ba.playsound(ba.getsound('error')) - else: - # Store for later. - cfg2 = ba.app.config - cfg2['Last Manual Party Connect Address'] = addr2 - cfg2.commit() - _ba.connect_to_party(addr2, port=port) - - HostAddrFetchThread(addr, do_it_2).start() - - btn = ba.buttonwidget( - parent=cnt, - size=(300, 70), - label=ba.Lstr(resource=self._r + '.manualConnectText'), - position=(c_width * 0.5 - 150, v), - autoselect=True, - on_activate_call=ba.Call(_connect, txt, txt2)) - ba.widget(edit=txt, up_widget=self._tab_row.tabs[tab_id].button) - ba.textwidget(edit=txt, on_return_press_call=btn.activate) - ba.textwidget(edit=txt2, on_return_press_call=btn.activate) - v -= 45 - - tscl = 0.85 - tspc = 25 - - def _safe_set_text(txt3: ba.Widget, - val: Union[str, ba.Lstr], - success: bool = True) -> None: - if txt3: - ba.textwidget(edit=txt3, - text=val, - color=(0, 1, 0) if success else (1, 1, 0)) - - # This currently doesn't work from china since we go through a - # reverse proxy there. - # UPDATE: it should work now; our proxy server forwards along - # original IPs. - do_internet_check = True - - def do_it(v2: float, cnt2: Optional[ba.Widget]) -> None: - if not cnt2: - return - - ba.playsound(ba.getsound('swish')) - ba.textwidget(parent=cnt2, - position=(c_width * 0.5 - 10, v2), - color=(0.6, 1.0, 0.6), - scale=tscl, - size=(0, 0), - maxwidth=c_width * 0.45, - flatness=1.0, - h_align='right', - v_align='center', - text=ba.Lstr(resource=self._r + - '.manualYourLocalAddressText')) - txt3 = ba.textwidget(parent=cnt2, - position=(c_width * 0.5, v2), - color=(0.5, 0.5, 0.5), - scale=tscl, - size=(0, 0), - maxwidth=c_width * 0.45, - flatness=1.0, - h_align='left', - v_align='center', - text=ba.Lstr(resource=self._r + - '.checkingText')) - - class AddrFetchThread2(threading.Thread): - """Thread for fetching an addr.""" - - def __init__(self, window: GatherWindow, - textwidget: ba.Widget): - super().__init__() - self._window = window - self._textwidget = textwidget - - def run(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() - # val = ([(s.connect(('8.8.8.8', 80)), - # s.getsockname()[0], s.close()) - # for s in [ - # socket.socket( - # socket.AF_INET, socket. - # SOCK_DGRAM) - # ]][0][1]) - ba.pushcall(ba.Call(_safe_set_text, - self._textwidget, 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._textwidget, - ba.Lstr(resource=self._window.get_r() + - '.noConnectionText'), False), - from_other_thread=True) - else: - ba.pushcall(ba.Call( - _safe_set_text, self._textwidget, - ba.Lstr(resource=self._window.get_r() + - '.addressFetchErrorText'), False), - from_other_thread=True) - ba.pushcall(ba.Call( - ba.print_error, - 'error in AddrFetchThread: ' + str(exc)), - from_other_thread=True) - - AddrFetchThread2(self, txt3).start() - - v2 -= tspc - ba.textwidget( - parent=cnt2, - position=(c_width * 0.5 - 10, v2), - color=(0.6, 1.0, 0.6), - scale=tscl, - size=(0, 0), - maxwidth=c_width * 0.45, - flatness=1.0, - h_align='right', - v_align='center', - text=ba.Lstr(resource=self._r + - '.manualYourAddressFromInternetText')) - - t_addr = ba.textwidget(parent=cnt2, - position=(c_width * 0.5, v2), - color=(0.5, 0.5, 0.5), - scale=tscl, - size=(0, 0), - maxwidth=c_width * 0.45, - h_align='left', - v_align='center', - flatness=1.0, - text=ba.Lstr(resource=self._r + - '.checkingText')) - v2 -= tspc - ba.textwidget(parent=cnt2, - position=(c_width * 0.5 - 10, v2), - color=(0.6, 1.0, 0.6), - scale=tscl, - size=(0, 0), - maxwidth=c_width * 0.45, - flatness=1.0, - h_align='right', - v_align='center', - text=ba.Lstr(resource=self._r + - '.manualJoinableFromInternetText')) - - t_accessible = ba.textwidget(parent=cnt2, - position=(c_width * 0.5, v2), - color=(0.5, 0.5, 0.5), - scale=tscl, - size=(0, 0), - maxwidth=c_width * 0.45, - flatness=1.0, - h_align='left', - v_align='center', - text=ba.Lstr(resource=self._r + - '.checkingText')) - v2 -= 28 - t_accessible_extra = ba.textwidget(parent=cnt2, - position=(c_width * 0.5, - v2), - color=(1, 0.5, 0.2), - scale=0.7, - size=(0, 0), - maxwidth=c_width * 0.9, - flatness=1.0, - h_align='center', - v_align='center', - text='') - - self._doing_access_check = False - self._access_check_count = 0 # Cap our refreshes eventually. - self._tab_data['access_check_timer'] = ba.Timer( - 10.0, - ba.WeakCall(self._access_check_update, t_addr, - t_accessible, t_accessible_extra), - repeat=True, - timetype=ba.TimeType.REAL) - - # Kick initial off. - self._access_check_update(t_addr, t_accessible, - t_accessible_extra) - if check_button: - check_button.delete() - - if do_internet_check: - check_button = ba.textwidget( - parent=cnt, - size=(250, 60), - text=ba.Lstr(resource=self._r + '.showMyAddressText'), - v_align='center', - h_align='center', - click_activate=True, - position=(c_width * 0.5 - 125, v - 30), - autoselect=True, - color=(0.5, 0.9, 0.5), - scale=0.8, - selectable=True, - on_activate_call=ba.Call(do_it, v, cnt)) - - def _internet_fetch_local_addr_cb(self, val: str) -> None: - self._internet_local_address = str(val) - - def _set_internet_tab(self, value: str, playsound: bool = False) -> None: - # pylint: disable=too-many-locals - # pylint: disable=too-many-statements - if playsound: - ba.playsound(ba.getsound('click01')) - - # If we're switching in from elsewhere, reset our selection. - # (prevents selecting something way down the list if we switched away - # and came back) - if self._internet_tab != value: - self._public_party_list_selection = None - - self._internet_tab = value - active_color = (0.6, 1.0, 0.6) - inactive_color = (0.5, 0.4, 0.5) - ba.textwidget( - edit=self._internet_join_text, - color=active_color if value == 'join' else inactive_color) - ba.textwidget( - edit=self._internet_host_text, - color=active_color if value == 'host' else inactive_color) - - # Clear anything in existence. - for widget in [ - self._internet_host_scrollwidget, - self._internet_host_name_text, - self._internet_host_toggle_button, - self._internet_host_name_label_text, - self._internet_host_status_text, - self._internet_join_party_size_label, - self._internet_join_party_name_label, - self._internet_join_party_language_label, - self._internet_join_party_ping_label, - self._internet_host_max_party_size_label, - self._internet_host_max_party_size_value, - self._internet_host_max_party_size_minus_button, - self._internet_host_max_party_size_plus_button, - self._internet_join_status_text, - self._internet_host_dedicated_server_info_text - ]: - if widget is not None: - widget.delete() - - c_width = self._scroll_width - c_height = self._scroll_height - 20 - sub_scroll_height = c_height - 90 - sub_scroll_width = 830 - v = c_height - 35 - v -= 25 - is_public_enabled = _ba.get_public_party_enabled() - if value == 'join': - # Reset this so we do an immediate refresh query. - self._internet_join_last_refresh_time = -99999.0 - - # Reset our list of public parties. - self._public_parties = {} - self._last_public_party_list_rebuild_time = 0 - self._first_public_party_list_rebuild_time = None - self._internet_join_party_name_label = ba.textwidget( - text=ba.Lstr(resource='nameText'), - parent=self._tab_container, - size=(0, 0), - position=(90, v - 8), - maxwidth=60, - scale=0.6, - color=(0.5, 0.5, 0.5), - flatness=1.0, - shadow=0.0, - h_align='center', - v_align='center') - if bool(False): - self._internet_join_party_language_label = ba.textwidget( - text=ba.Lstr( - resource='settingsWindowAdvanced.languageText'), - parent=self._tab_container, - size=(0, 0), - position=(662, v - 8), - maxwidth=100, - scale=0.6, - color=(0.5, 0.5, 0.5), - flatness=1.0, - shadow=0.0, - h_align='center', - v_align='center') - self._internet_join_party_size_label = ba.textwidget( - text=ba.Lstr(resource=self._r + '.partySizeText'), - parent=self._tab_container, - size=(0, 0), - position=(755, v - 8), - maxwidth=60, - scale=0.6, - color=(0.5, 0.5, 0.5), - flatness=1.0, - shadow=0.0, - h_align='center', - v_align='center') - self._internet_join_party_ping_label = ba.textwidget( - text=ba.Lstr(resource=self._r + '.pingText'), - parent=self._tab_container, - size=(0, 0), - position=(825, v - 8), - maxwidth=60, - scale=0.6, - color=(0.5, 0.5, 0.5), - flatness=1.0, - shadow=0.0, - h_align='center', - v_align='center') - v -= sub_scroll_height + 23 - - self._internet_host_scrollwidget = scrollw = ba.scrollwidget( - parent=self._tab_container, - simple_culling_v=10, - position=((self._scroll_width - sub_scroll_width) * 0.5, v), - size=(sub_scroll_width, sub_scroll_height)) - ba.widget(edit=scrollw, autoselect=True) - colw = self._internet_host_columnwidget = ba.containerwidget( - parent=scrollw, background=False, size=(400, 400)) - ba.containerwidget(edit=scrollw, claims_left_right=True) - ba.containerwidget(edit=colw, claims_left_right=True) - - self._internet_join_status_text = ba.textwidget( - parent=self._tab_container, - text=ba.Lstr(value='${A}...', - subs=[('${A}', - ba.Lstr(resource='store.loadingText'))]), - size=(0, 0), - scale=0.9, - flatness=1.0, - shadow=0.0, - h_align='center', - v_align='top', - maxwidth=c_width, - color=(0.6, 0.6, 0.6), - position=(c_width * 0.5, c_height * 0.5)) - - if value == 'host': - v -= 30 - party_name_text = ba.Lstr( - resource='gatherWindow.partyNameText', - fallback_resource='editGameListWindow.nameText') - self._internet_host_name_label_text = ba.textwidget( - parent=self._tab_container, - size=(0, 0), - h_align='right', - v_align='center', - maxwidth=200, - scale=0.8, - color=ba.app.ui.infotextcolor, - position=(210, v - 9), - text=party_name_text) - self._internet_host_name_text = ba.textwidget( - parent=self._tab_container, - editable=True, - size=(535, 40), - position=(230, v - 30), - text=ba.app.config.get('Public Party Name', ''), - maxwidth=494, - shadow=0.3, - flatness=1.0, - description=party_name_text, - autoselect=True, - v_align='center', - corner_scale=1.0) - - v -= 60 - self._internet_host_max_party_size_label = ba.textwidget( - parent=self._tab_container, - size=(0, 0), - h_align='right', - v_align='center', - maxwidth=200, - scale=0.8, - color=ba.app.ui.infotextcolor, - position=(210, v - 9), - text=ba.Lstr(resource='maxPartySizeText', - fallback_resource='maxConnectionsText')) - self._internet_host_max_party_size_value = ba.textwidget( - parent=self._tab_container, - size=(0, 0), - h_align='center', - v_align='center', - scale=1.2, - color=(1, 1, 1), - position=(240, v - 9), - text=str(_ba.get_public_party_max_size())) - btn1 = self._internet_host_max_party_size_minus_button = ( - ba.buttonwidget( - parent=self._tab_container, - size=(40, 40), - on_activate_call=ba.WeakCall( - self._on_max_public_party_size_minus_press), - position=(280, v - 26), - label='-', - autoselect=True)) - btn2 = self._internet_host_max_party_size_plus_button = ( - ba.buttonwidget(parent=self._tab_container, - size=(40, 40), - on_activate_call=ba.WeakCall( - self._on_max_public_party_size_plus_press), - position=(350, v - 26), - label='+', - autoselect=True)) - v -= 50 - v -= 70 - if is_public_enabled: - label = ba.Lstr( - resource='gatherWindow.makePartyPrivateText', - fallback_resource='gatherWindow.stopAdvertisingText') - else: - label = ba.Lstr( - resource='gatherWindow.makePartyPublicText', - fallback_resource='gatherWindow.startAdvertisingText') - self._internet_host_toggle_button = ba.buttonwidget( - parent=self._tab_container, - label=label, - size=(400, 80), - on_activate_call=self._on_stop_internet_advertising_press - if is_public_enabled else - self._on_start_internet_advertizing_press, - position=(c_width * 0.5 - 200, v), - autoselect=True, - up_widget=btn2) - ba.widget(edit=self._internet_host_name_text, down_widget=btn2) - ba.widget(edit=btn2, up_widget=self._internet_host_name_text) - ba.widget(edit=btn1, up_widget=self._internet_host_name_text) - ba.widget(edit=self._internet_join_text, - down_widget=self._internet_host_name_text) - v -= 10 - self._internet_host_status_text = ba.textwidget( - parent=self._tab_container, - text=ba.Lstr(resource=self._r + '.partyStatusNotPublicText'), - size=(0, 0), - scale=0.7, - flatness=1.0, - shadow=0.0, - h_align='center', - v_align='top', - maxwidth=c_width, - color=(0.6, 0.6, 0.6), - position=(c_width * 0.5, v)) - v -= 90 - self._internet_host_dedicated_server_info_text = ba.textwidget( - parent=self._tab_container, - text=ba.Lstr(resource=self._r + '.dedicatedServerInfoText'), - size=(0, 0), - scale=0.7, - flatness=1.0, - shadow=0.0, - h_align='center', - v_align='center', - maxwidth=c_width * 0.9, - color=ba.app.ui.infotextcolor, - position=(c_width * 0.5, v)) - - # If public sharing is already on, - # launch a status-check immediately. - if _ba.get_public_party_enabled(): - self._do_internet_status_check() - - # Now add a lock icon overlay for if we don't have pro. - icon = self._internet_lock_icon - if icon and self._internet_lock_icon: - self._internet_lock_icon.delete() # Kill any existing. - self._internet_lock_icon = ba.imagewidget( - parent=self._tab_container, - position=(c_width * 0.5 - 60, c_height * 0.5 - 50), - size=(120, 120), - opacity=0.0 if not self._is_internet_locked() else 0.5, - texture=ba.gettexture('lock')) - - def _is_internet_locked(self) -> bool: - if _ba.get_account_misc_read_val('ilck', False): - return not ba.app.accounts.have_pro() - return False - - def _on_max_public_party_size_minus_press(self) -> None: - val = _ba.get_public_party_max_size() - val -= 1 - if val < 1: - val = 1 - _ba.set_public_party_max_size(val) - ba.textwidget(edit=self._internet_host_max_party_size_value, - text=str(val)) - - def _on_max_public_party_size_plus_press(self) -> None: - val = _ba.get_public_party_max_size() - val += 1 - _ba.set_public_party_max_size(val) - ba.textwidget(edit=self._internet_host_max_party_size_value, - text=str(val)) - - def _on_public_party_query_result( - self, result: Optional[Dict[str, Any]]) -> None: - with ba.Context('ui'): - # Any time we get any result at all, kill our loading status. - status_text = self._internet_join_status_text - if status_text: - # Don't show results if not signed in - # (probably didn't get any anyway). - if _ba.get_account_state() != 'signed_in': - ba.textwidget(edit=status_text, - text=ba.Lstr(resource='notSignedInText')) - else: - if result is None: - ba.textwidget(edit=status_text, - text=ba.Lstr(resource='errorText')) - else: - ba.textwidget(edit=status_text, text='') - - if result is not None: - parties_in = result['l'] - else: - parties_in = [] - - for partyval in list(self._public_parties.values()): - partyval['claimed'] = False - - for party_in in parties_in: - # Party is indexed by (ADDR)_(PORT) - party_key = party_in['a'] + '_' + str(party_in['p']) - party = self._public_parties.get(party_key) - if party is None: - # If this party is new to us, init it. - index = self._next_public_party_entry_index - self._next_public_party_entry_index = index + 1 - party = self._public_parties[party_key] = { - 'address': - party_in.get('a'), - 'next_ping_time': - ba.time(ba.TimeType.REAL) + 0.001 * party_in['pd'], - 'ping': - None, - 'index': - index, - } - - # Now, new or not, update its values. - party['queue'] = party_in.get('q') - party['port'] = party_in.get('p') - party['name'] = party_in['n'] - party['size'] = party_in['s'] - party['language'] = party_in['l'] - party['size_max'] = party_in['sm'] - party['claimed'] = True - # (server provides this in milliseconds; we use seconds) - party['ping_interval'] = 0.001 * party_in['pi'] - party['stats_addr'] = party_in['sa'] - - # Prune unclaimed party entries. - self._public_parties = { - key: val - for key, val in list(self._public_parties.items()) - if val['claimed'] - } - - self._rebuild_public_party_list() - - def _rebuild_public_party_list(self) -> None: - # pylint: disable=too-many-branches - # pylint: disable=too-many-locals - # pylint: disable=too-many-statements - cur_time = ba.time(ba.TimeType.REAL) - if self._first_public_party_list_rebuild_time is None: - self._first_public_party_list_rebuild_time = cur_time - - # Update faster for the first few seconds; - # then ease off to keep the list from jumping around. - since_first = cur_time - self._first_public_party_list_rebuild_time - wait_time = (1.0 if since_first < 2.0 else - 2.5 if since_first < 10.0 else 5.0) - assert self._last_public_party_list_rebuild_time is not None - if cur_time - self._last_public_party_list_rebuild_time < wait_time: - return - self._last_public_party_list_rebuild_time = cur_time - - # First off, check for the existence of our column widget; - # if we don't have this, we're done. - columnwidget = self._internet_host_columnwidget - if not columnwidget: - return - - with ba.Context('ui'): - - # Now kill and recreate all widgets. - for widget in columnwidget.get_children(): - widget.delete() - - # Sort - show queue-enabled ones first and sort by lowest ping. - ordered_parties = sorted( - list(self._public_parties.values()), - key=lambda p: ( - p['queue'] is None, # Show non-queued last. - p['ping'] if p['ping'] is not None else 999999, - p['index'])) - existing_selection = self._public_party_list_selection - first = True - - 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_public_party_list = True - - ba.pushcall(refresh_on) - - # Janky - allow escaping if there's nothing in us. - ba.containerwidget(edit=self._internet_host_scrollwidget, - claims_up_down=(len(ordered_parties) > 0)) - - for i, party in enumerate(ordered_parties): - hpos = 20 - vpos = sub_scroll_height - lineheight * i - 50 - party['name_widget'] = ba.textwidget( - text=ba.Lstr(value=party['name']), - parent=columnwidget, - size=(sub_scroll_width * 0.63, 20), - position=(0 + hpos, 4 + vpos), - selectable=True, - on_select_call=ba.WeakCall( - self._set_public_party_selection, - (party['address'], 'name')), - on_activate_call=ba.WeakCall( - self._on_public_party_activate, party), - click_activate=True, - maxwidth=sub_scroll_width * 0.45, - corner_scale=1.4, - autoselect=True, - color=(1, 1, 1, 0.3 if party['ping'] is None else 1.0), - h_align='left', - v_align='center') - ba.widget(edit=party['name_widget'], - left_widget=self._internet_join_text, - show_buffer_top=64.0, - show_buffer_bottom=64.0) - if existing_selection == (party['address'], 'name'): - ba.containerwidget(edit=columnwidget, - selected_child=party['name_widget']) - if bool(False): - party['language_widget'] = ba.textwidget( - text=ba.Lstr(translate=('languages', - party['language'])), - parent=columnwidget, - size=(0, 0), - position=(sub_scroll_width * 0.73 + hpos, 20 + vpos), - maxwidth=sub_scroll_width * 0.13, - scale=0.7, - color=(0.8, 0.8, 0.8), - h_align='center', - v_align='center') - if party['stats_addr'] != '': - url = party['stats_addr'].replace( - '${ACCOUNT}', - _ba.get_account_misc_read_val_2( - 'resolvedAccountID', 'UNKNOWN')) - party['stats_button'] = ba.buttonwidget( - color=(0.3, 0.6, 0.94), - textcolor=(1.0, 1.0, 1.0), - label=ba.Lstr(resource='statsText'), - parent=columnwidget, - autoselect=True, - on_activate_call=ba.Call(ba.open_url, url), - on_select_call=ba.WeakCall( - self._set_public_party_selection, - (party['address'], 'stats_button')), - size=(120, 40), - position=(sub_scroll_width * 0.66 + hpos, 1 + vpos), - scale=0.9) - if existing_selection == (party['address'], - 'stats_button'): - ba.containerwidget( - edit=columnwidget, - selected_child=party['stats_button']) - else: - if 'stats_button' in party: - del party['stats_button'] - - if first: - if 'stats_button' in party: - ba.widget(edit=party['stats_button'], - up_widget=self._internet_join_text) - if 'name_widget' in party: - ba.widget(edit=party['name_widget'], - up_widget=self._internet_join_text) - first = False - - party['size_widget'] = ba.textwidget( - text=str(party['size']) + '/' + str(party['size_max']), - parent=columnwidget, - size=(0, 0), - position=(sub_scroll_width * 0.86 + hpos, 20 + vpos), - scale=0.7, - color=(0.8, 0.8, 0.8), - h_align='right', - v_align='center') - party['ping_widget'] = ba.textwidget( - parent=columnwidget, - size=(0, 0), - position=(sub_scroll_width * 0.94 + hpos, 20 + vpos), - scale=0.7, - h_align='right', - v_align='center') - if party['ping'] is None: - ba.textwidget(edit=party['ping_widget'], - text='-', - color=(0.5, 0.5, 0.5)) - else: - ping_good = _ba.get_account_misc_read_val('pingGood', 100) - ping_med = _ba.get_account_misc_read_val('pingMed', 500) - ba.textwidget(edit=party['ping_widget'], - text=str(party['ping']), - color=(0, 1, - 0) if party['ping'] <= ping_good else - (1, 1, 0) if party['ping'] <= ping_med else - (1, 0, 0)) - - # So our selection callbacks can start firing.. - def refresh_on2() -> None: - self._refreshing_public_party_list = False - - ba.pushcall(refresh_on2) - - def _on_public_party_activate(self, party: Dict[str, Any]) -> None: - from bastd.ui import purchase - from bastd.ui import account - if party['queue'] is not None: - from bastd.ui import partyqueue - ba.playsound(ba.getsound('swish')) - partyqueue.PartyQueueWindow(party['queue'], party['address'], - party['port']) - else: - address = party['address'] - port = party['port'] - if self._is_internet_locked(): - if _ba.get_account_state() != 'signed_in': - account.show_sign_in_prompt() - else: - purchase.PurchaseWindow(items=['pro']) - return - # rate limit this a bit - now = time.time() - last_connect_time = self._last_public_party_connect_attempt_time - if last_connect_time is None or now - last_connect_time > 2.0: - _ba.connect_to_party(address, port=port) - self._last_public_party_connect_attempt_time = now - - def _set_public_party_selection(self, sel: Tuple[str, str]) -> None: - if self._refreshing_public_party_list: - return - self._public_party_list_selection = sel - - def _update_internet_tab(self) -> None: - # pylint: disable=too-many-statements - - # Special case: if a party-queue window is up, don't do any of this - # (keeps things smoother). - if ba.app.ui.have_party_queue_window: - return - - # If we've got a party-name text widget, keep its value plugged - # into our public host name. - text = self._internet_host_name_text - if text: - name = cast(str, - ba.textwidget(query=self._internet_host_name_text)) - _ba.set_public_party_name(name) - - # Show/hide the lock icon depending on if we've got pro. - icon = self._internet_lock_icon - if icon: - if self._is_internet_locked(): - ba.imagewidget(edit=icon, opacity=0.5) - else: - ba.imagewidget(edit=icon, opacity=0.0) - - if self._internet_tab == 'join': - now = ba.time(ba.TimeType.REAL) - if (now - self._internet_join_last_refresh_time > 0.001 * - _ba.get_account_misc_read_val('pubPartyRefreshMS', 10000)): - self._internet_join_last_refresh_time = now - app = ba.app - _ba.add_transaction( - { - 'type': 'PUBLIC_PARTY_QUERY', - 'proto': app.protocol_version, - 'lang': app.lang.language - }, - callback=ba.WeakCall(self._on_public_party_query_result)) - _ba.run_transactions() - - # Go through our existing public party entries firing off pings - # for any that have timed out. - for party in list(self._public_parties.values()): - if (party['next_ping_time'] <= now - and ba.app.ping_thread_count < 15): - - # Make sure to fully catch up and not to multi-ping if - # we're way behind somehow. - while party['next_ping_time'] <= now: - # Crank the interval up for high-latency parties to - # save us some work. - mult = 1 - if party['ping'] is not None: - mult = (10 if party['ping'] > 300 else - 5 if party['ping'] > 150 else 2) - party[ - 'next_ping_time'] += party['ping_interval'] * mult - - class PingThread(threading.Thread): - """Thread for sending out pings.""" - - def __init__(self, address: str, port: int, - call: Callable[[str, int, Optional[int]], - Optional[int]]): - super().__init__() - self._address = address - self._port = port - self._call = call - - def run(self) -> None: - # pylint: disable=too-many-branches - ba.app.ping_thread_count += 1 - 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) - sock.close() - ping = int((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) - ba.app.ping_thread_count -= 1 - - PingThread(party['address'], party['port'], - ba.WeakCall(self._ping_callback)).start() - - def _ping_callback(self, address: str, port: Optional[int], - result: Optional[int]) -> None: - # Look for a widget corresponding to this target. - # If we find one, update our list. - party = self._public_parties.get(address + '_' + str(port)) - if party is not None: - # We now smooth ping a bit to reduce jumping around in the list - # (only where pings are relatively good). - current_ping = party.get('ping') - if (current_ping is not None and result is not None - and result < 150): - smoothing = 0.7 - party['ping'] = int(smoothing * current_ping + - (1.0 - smoothing) * result) - else: - party['ping'] = result - - # This can happen if we switch away and then back to the - # client tab while pings are in flight. - if 'ping_widget' not in party: - pass - elif party['ping_widget']: - self._rebuild_public_party_list() - - def _do_internet_status_check(self) -> None: - from ba.internal import master_server_get - ba.textwidget(edit=self._internet_host_status_text, - color=(1, 1, 0), - text=ba.Lstr(resource=self._r + - '.partyStatusCheckingText')) - master_server_get('bsAccessCheck', {'b': ba.app.build_number}, - callback=ba.WeakCall( - self._on_public_party_accessible_response)) - - def _on_start_internet_advertizing_press(self) -> None: - from bastd.ui import account - from bastd.ui import purchase - if _ba.get_account_state() != 'signed_in': - account.show_sign_in_prompt() - return - - # Requires sign-in and pro. - if self._is_internet_locked(): - if _ba.get_account_state() != 'signed_in': - account.show_sign_in_prompt() - else: - purchase.PurchaseWindow(items=['pro']) - return - - name = cast(str, ba.textwidget(query=self._internet_host_name_text)) - if name == '': - ba.screenmessage(ba.Lstr(resource='internal.invalidNameErrorText'), - color=(1, 0, 0)) - ba.playsound(ba.getsound('error')) - return - _ba.set_public_party_name(name) - cfg = ba.app.config - cfg['Public Party Name'] = name - cfg.commit() - ba.playsound(ba.getsound('shieldUp')) - _ba.set_public_party_enabled(True) - - # In GUI builds we want to authenticate clients only when hosting - # public parties. - _ba.set_authenticate_clients(True) - - self._do_internet_status_check() - ba.buttonwidget( - edit=self._internet_host_toggle_button, - label=ba.Lstr( - resource='gatherWindow.makePartyPrivateText', - fallback_resource='gatherWindow.stopAdvertisingText'), - on_activate_call=self._on_stop_internet_advertising_press) - - def _on_public_party_accessible_response( - self, data: Optional[Dict[str, Any]]) -> None: - # If we've got status text widgets, update them. - text = self._internet_host_status_text - if text: - if data is None: - ba.textwidget( - edit=text, - text=ba.Lstr(resource=self._r + - '.partyStatusNoConnectionText'), - color=(1, 0, 0), - ) - else: - if not data.get('accessible', False): - ex_line: Union[str, ba.Lstr] - if self._internet_local_address is not None: - ex_line = ba.Lstr( - value='\n${A} ${B}', - subs=[('${A}', - ba.Lstr(resource=self._r + - '.manualYourLocalAddressText')), - ('${B}', self._internet_local_address)]) - else: - ex_line = '' - ba.textwidget( - edit=text, - text=ba.Lstr( - value='${A}\n${B}${C}', - subs=[('${A}', - ba.Lstr(resource=self._r + - '.partyStatusNotJoinableText')), - ('${B}', - ba.Lstr(resource=self._r + - '.manualRouterForwardingText', - subs=[('${PORT}', - str(_ba.get_game_port()))])), - ('${C}', ex_line)]), - color=(1, 0, 0)) - else: - ba.textwidget(edit=text, - text=ba.Lstr(resource=self._r + - '.partyStatusJoinableText'), - color=(0, 1, 0)) - - def _on_stop_internet_advertising_press(self) -> None: - _ba.set_public_party_enabled(False) - - # In GUI builds we want to authenticate clients only when hosting - # public parties. - _ba.set_authenticate_clients(False) - - ba.playsound(ba.getsound('shieldDown')) - text = self._internet_host_status_text - if text: - ba.textwidget( - edit=text, - text=ba.Lstr(resource=self._r + '.partyStatusNotPublicText'), - color=(0.6, 0.6, 0.6), - ) - - ba.buttonwidget( - edit=self._internet_host_toggle_button, - label=ba.Lstr( - resource='gatherWindow.makePartyPublicText', - fallback_resource='gatherWindow.startAdvertisingText'), - on_activate_call=self._on_start_internet_advertizing_press) - - def _access_check_update(self, t_addr: ba.Widget, t_accessible: ba.Widget, - t_accessible_extra: ba.Widget) -> None: - from ba.internal import master_server_get - - # If we don't have an outstanding query, start one.. - assert self._doing_access_check is not None - assert self._access_check_count is not None - if not self._doing_access_check and self._access_check_count < 100: - self._doing_access_check = True - self._access_check_count += 1 - self._t_addr = t_addr - self._t_accessible = t_accessible - self._t_accessible_extra = t_accessible_extra - master_server_get('bsAccessCheck', {'b': ba.app.build_number}, - callback=ba.WeakCall( - self._on_accessible_response)) - - def _on_accessible_response(self, data: Optional[Dict[str, Any]]) -> None: - t_addr = self._t_addr - t_accessible = self._t_accessible - t_accessible_extra = self._t_accessible_extra - self._doing_access_check = False - color_bad = (1, 1, 0) - color_good = (0, 1, 0) - if data is None or 'address' not in data or 'accessible' not in data: - if t_addr: - ba.textwidget(edit=t_addr, - text=ba.Lstr(resource=self._r + - '.noConnectionText'), - color=color_bad) - if t_accessible: - ba.textwidget(edit=t_accessible, - text=ba.Lstr(resource=self._r + - '.noConnectionText'), - color=color_bad) - if t_accessible_extra: - ba.textwidget(edit=t_accessible_extra, - text='', - color=color_bad) - return - if t_addr: - ba.textwidget(edit=t_addr, text=data['address'], color=color_good) - if t_accessible: - if data['accessible']: - ba.textwidget(edit=t_accessible, - text=ba.Lstr(resource=self._r + - '.manualJoinableYesText'), - color=color_good) - if t_accessible_extra: - ba.textwidget(edit=t_accessible_extra, - text='', - color=color_good) - else: - ba.textwidget( - edit=t_accessible, - text=ba.Lstr(resource=self._r + - '.manualJoinableNoWithAsteriskText'), - color=color_bad) - if t_accessible_extra: - ba.textwidget(edit=t_accessible_extra, - text=ba.Lstr(resource=self._r + - '.manualRouterForwardingText', - subs=[('${PORT}', - str(_ba.get_game_port())) - ]), - color=color_bad) - - def _save_state(self) -> None: - try: - sel = self._root_widget.get_selected_child() - selected_tab_ids = [ - tab_id for tab_id, tab in self._tab_row.tabs.items() - if sel == tab.button - ] - if sel == self._back_button: - sel_name = 'Back' - elif selected_tab_ids: - assert len(selected_tab_ids) == 1 - sel_name = f'Tab:{selected_tab_ids[0].value}' - elif sel == self._tab_container: - sel_name = 'TabContainer' - else: - raise ValueError(f'unrecognized selection: \'{sel}\'') - ba.app.ui.window_states[self.__class__.__name__] = { - 'sel_name': sel_name, - 'internet_tab': self._internet_tab - } - except Exception: - ba.print_exception(f'Error saving state for {self}.') - - def _restore_state(self) -> None: - try: - 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))) - self._internet_tab = winstate.get('internet_tab', 'join') - # current_tab = ba.app.config.get('Gather Tab', None) - current_tab = self.TabID.ABOUT - try: - stored_tab = self.TabID(ba.app.config.get('Gather Tab')) - if stored_tab in self._tab_row.tabs: - current_tab = stored_tab - except ValueError: - pass - 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 - 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(f'Error restoring state for {self}.') - - def _back(self) -> None: - from bastd.ui.mainmenu import MainMenuWindow - self._save_state() - ba.containerwidget(edit=self._root_widget, - transition=self._transition_out) - ba.app.ui.set_main_menu_window( - MainMenuWindow(transition='in_left').get_root_widget()) diff --git a/assets/src/ba_data/python/bastd/ui/gather/__init__.py b/assets/src/ba_data/python/bastd/ui/gather/__init__.py new file mode 100644 index 00000000..46e0b0ca --- /dev/null +++ b/assets/src/ba_data/python/bastd/ui/gather/__init__.py @@ -0,0 +1,271 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides UI for inviting/joining friends.""" + +from __future__ import annotations + +from enum import Enum +from typing import TYPE_CHECKING + +import _ba +import ba +from bastd.ui.gather.abouttab import AboutGatherTab +from bastd.ui.gather.manualtab import ManualGatherTab +from bastd.ui.gather.googleplaytab import GooglePlayGatherTab +from bastd.ui.gather.publictab import PublicGatherTab +from bastd.ui.gather.nearbytab import NearbyGatherTab +from bastd.ui.tabs import TabRow + +if TYPE_CHECKING: + from typing import (Any, Optional, Tuple, Dict, List, Union, Callable, + Type) + from bastd.ui.gather.bases import GatherTab + + +class GatherWindow(ba.Window): + """Window for joining/inviting friends.""" + + class TabID(Enum): + """Our available tab types.""" + ABOUT = 'about' + INTERNET = 'internet' + GOOGLE_PLAY = 'google_play' + LOCAL_NETWORK = 'local_network' + MANUAL = 'manual' + + def __init__(self, + transition: Optional[str] = 'in_right', + origin_widget: ba.Widget = None): + # pylint: disable=too-many-statements + # pylint: disable=too-many-locals + ba.set_analytics_screen('Gather Window') + scale_origin: Optional[Tuple[float, float]] + if origin_widget is not None: + self._transition_out = 'out_scale' + scale_origin = origin_widget.get_screen_space_center() + transition = 'in_scale' + else: + self._transition_out = 'out_right' + scale_origin = None + ba.app.ui.set_main_menu_location('Gather') + _ba.set_party_icon_always_visible(True) + uiscale = ba.app.ui.uiscale + self._width = 1240 if uiscale is ba.UIScale.SMALL else 1040 + x_offs = 100 if uiscale is ba.UIScale.SMALL else 0 + self._height = (582 if uiscale is ba.UIScale.SMALL else + 680 if uiscale is ba.UIScale.MEDIUM else 800) + self._current_tab: Optional[GatherWindow.TabID] = None + extra_top = 20 if uiscale is ba.UIScale.SMALL else 0 + self._r = 'gatherWindow' + + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height + extra_top), + transition=transition, + toolbar_visibility='menu_minimal', + scale_origin_stack_offset=scale_origin, + scale=(1.3 if uiscale is ba.UIScale.SMALL else + 0.97 if uiscale is ba.UIScale.MEDIUM else 0.8), + stack_offset=(0, -11) if uiscale is ba.UIScale.SMALL else ( + 0, 0) if uiscale is ba.UIScale.MEDIUM else (0, 0))) + + if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: + ba.containerwidget(edit=self._root_widget, + on_cancel_call=self._back) + self._back_button = None + else: + self._back_button = btn = ba.buttonwidget( + parent=self._root_widget, + position=(70 + x_offs, self._height - 74), + size=(140, 60), + scale=1.1, + autoselect=True, + label=ba.Lstr(resource='backText'), + button_type='back', + on_activate_call=self._back) + ba.containerwidget(edit=self._root_widget, cancel_button=btn) + ba.buttonwidget(edit=btn, + button_type='backSmall', + position=(70 + x_offs, self._height - 78), + size=(60, 60), + label=ba.charstr(ba.SpecialChar.BACK)) + + ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, self._height - 42), + size=(0, 0), + color=ba.app.ui.title_color, + scale=1.5, + h_align='center', + v_align='center', + text=ba.Lstr(resource=self._r + '.titleText'), + maxwidth=550) + + platform = ba.app.platform + subplatform = ba.app.subplatform + + scroll_buffer_h = 130 + 2 * x_offs + tab_buffer_h = 250 + 2 * x_offs + + # Build up the set of tabs we want. + tabdefs: List[Tuple[GatherWindow.TabID, ba.Lstr]] = [ + (self.TabID.ABOUT, ba.Lstr(resource=self._r + '.aboutText')) + ] + if _ba.get_account_misc_read_val('enablePublicParties', True): + tabdefs.append((self.TabID.INTERNET, + ba.Lstr(resource=self._r + '.internetText'))) + if platform == 'android' and subplatform == 'google': + tabdefs.append((self.TabID.GOOGLE_PLAY, + ba.Lstr(resource=self._r + '.googlePlayText'))) + tabdefs.append((self.TabID.LOCAL_NETWORK, + ba.Lstr(resource=self._r + '.localNetworkText'))) + tabdefs.append( + (self.TabID.MANUAL, ba.Lstr(resource=self._r + '.manualText'))) + + self._tab_row = TabRow(self._root_widget, + tabdefs, + pos=(tab_buffer_h * 0.5, self._height - 130), + size=(self._width - tab_buffer_h, 50), + on_select_call=self._set_tab) + + # Now instantiate handlers for these tabs. + tabtypes: Dict[GatherWindow.TabID, Type[GatherTab]] = { + self.TabID.ABOUT: AboutGatherTab, + self.TabID.MANUAL: ManualGatherTab, + self.TabID.GOOGLE_PLAY: GooglePlayGatherTab, + self.TabID.INTERNET: PublicGatherTab, + self.TabID.LOCAL_NETWORK: NearbyGatherTab + } + self._tabs: Dict[GatherWindow.TabID, GatherTab] = {} + for tab_id in self._tab_row.tabs: + tabtype = tabtypes.get(tab_id) + if tabtype is not None: + self._tabs[tab_id] = tabtype(self) + + if ba.app.ui.use_toolbars: + ba.widget(edit=self._tab_row.tabs[tabdefs[-1][0]].button, + right_widget=_ba.get_special_widget('party_button')) + if uiscale is ba.UIScale.SMALL: + ba.widget(edit=self._tab_row.tabs[tabdefs[0][0]].button, + left_widget=_ba.get_special_widget('back_button')) + + self._scroll_width = self._width - scroll_buffer_h + self._scroll_height = self._height - 180.0 + + self._scroll_left = (self._width - self._scroll_width) * 0.5 + self._scroll_bottom = self._height - self._scroll_height - 79 - 48 + buffer_h = 10 + buffer_v = 4 + + # Not actually using a scroll widget anymore; just an image. + ba.imagewidget(parent=self._root_widget, + position=(self._scroll_left - buffer_h, + self._scroll_bottom - buffer_v), + size=(self._scroll_width + 2 * buffer_h, + self._scroll_height + 2 * buffer_v), + texture=ba.gettexture('scrollWidget'), + model_transparent=ba.getmodel('softEdgeOutside')) + self._tab_container: Optional[ba.Widget] = None + self._restore_state() + + def __del__(self) -> None: + _ba.set_party_icon_always_visible(False) + + def _set_tab(self, tab_id: TabID) -> None: + if self._current_tab is tab_id: + return + prev_tab_id = self._current_tab + self._current_tab = tab_id + + # We wanna preserve our current tab between runs. + cfg = ba.app.config + cfg['Gather Tab'] = tab_id.value + cfg.commit() + + # Update tab colors based on which is selected. + self._tab_row.update_appearance(tab_id) + + if prev_tab_id is not None: + prev_tab = self._tabs.get(prev_tab_id) + if prev_tab is not None: + prev_tab.on_deactivate() + + # Clear up prev container if it hasn't been done. + if self._tab_container: + self._tab_container.delete() + + tab = self._tabs.get(tab_id) + if tab is not None: + self._tab_container = tab.on_activate( + self._root_widget, + self._tab_row.tabs[tab_id].button, + self._scroll_width, + self._scroll_height, + self._scroll_left, + self._scroll_bottom, + ) + return + + def _save_state(self) -> None: + try: + for tab in self._tabs.values(): + tab.save_state() + + sel = self._root_widget.get_selected_child() + selected_tab_ids = [ + tab_id for tab_id, tab in self._tab_row.tabs.items() + if sel == tab.button + ] + if sel == self._back_button: + sel_name = 'Back' + elif selected_tab_ids: + assert len(selected_tab_ids) == 1 + sel_name = f'Tab:{selected_tab_ids[0].value}' + elif sel == self._tab_container: + sel_name = 'TabContainer' + else: + raise ValueError(f'unrecognized selection: \'{sel}\'') + ba.app.ui.window_states[self.__class__.__name__] = { + 'sel_name': sel_name, + } + except Exception: + ba.print_exception(f'Error saving state for {self}.') + + 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 + try: + stored_tab = self.TabID(ba.app.config.get('Gather Tab')) + if stored_tab in self._tab_row.tabs: + current_tab = stored_tab + except ValueError: + pass + 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 + 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(f'Error restoring state for {self}.') + + def _back(self) -> None: + from bastd.ui.mainmenu import MainMenuWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, + transition=self._transition_out) + ba.app.ui.set_main_menu_window( + MainMenuWindow(transition='in_left').get_root_widget()) diff --git a/assets/src/ba_data/python/bastd/ui/gather/abouttab.py b/assets/src/ba_data/python/bastd/ui/gather/abouttab.py new file mode 100644 index 00000000..7fd1975b --- /dev/null +++ b/assets/src/ba_data/python/bastd/ui/gather/abouttab.py @@ -0,0 +1,110 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines the about tab in the gather UI.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +import _ba +from bastd.ui.gather.bases import GatherTab + +if TYPE_CHECKING: + from typing import Optional + from bastd.ui.gather import GatherWindow + + +class AboutGatherTab(GatherTab): + """The about tab in the gather UI""" + + def __init__(self, window: GatherWindow) -> None: + super().__init__(window) + self._container: Optional[ba.Widget] = None + + def on_activate( + self, + parent_widget: ba.Widget, + tab_button: ba.Widget, + region_width: float, + region_height: float, + region_left: float, + region_bottom: float, + ) -> ba.Widget: + message = ba.Lstr(resource='gatherWindow.aboutDescriptionText', + subs=[('${PARTY}', + ba.charstr(ba.SpecialChar.PARTY_ICON)), + ('${BUTTON}', + ba.charstr(ba.SpecialChar.TOP_BUTTON))]) + + # Let's not talk about sharing in vr-mode; its tricky to fit more + # than one head in a VR-headset ;-) + if not ba.app.vr_mode: + message = ba.Lstr( + value='${A}\n\n${B}', + subs=[('${A}', message), + ('${B}', + ba.Lstr(resource='gatherWindow.' + 'aboutDescriptionLocalMultiplayerExtraText'))]) + string_height = 400 + include_invite = True + msc_scale = 1.1 + c_height_2 = min(region_height, string_height * msc_scale + 100) + try_tickets = _ba.get_account_misc_read_val('friendTryTickets', None) + if try_tickets is None: + include_invite = False + self._container = ba.containerwidget( + parent=parent_widget, + position=(region_left, + region_bottom + (region_height - c_height_2) * 0.5), + size=(region_width, c_height_2), + background=False, + selectable=include_invite) + ba.widget(edit=self._container, up_widget=tab_button) + + ba.textwidget(parent=self._container, + position=(region_width * 0.5, c_height_2 * + (0.58 if include_invite else 0.5)), + color=(0.6, 1.0, 0.6), + scale=msc_scale, + size=(0, 0), + maxwidth=region_width * 0.9, + max_height=c_height_2 * (0.7 if include_invite else 0.9), + h_align='center', + v_align='center', + text=message) + + if include_invite: + ba.textwidget(parent=self._container, + position=(region_width * 0.57, 35), + color=(0, 1, 0), + scale=0.6, + size=(0, 0), + maxwidth=region_width * 0.5, + h_align='right', + v_align='center', + flatness=1.0, + text=ba.Lstr( + resource='gatherWindow.inviteAFriendText', + subs=[('${COUNT}', str(try_tickets))])) + ba.buttonwidget( + parent=self._container, + position=(region_width * 0.59, 10), + size=(230, 50), + color=(0.54, 0.42, 0.56), + textcolor=(0, 1, 0), + label=ba.Lstr(resource='gatherWindow.inviteFriendsText', + fallback_resource=( + 'gatherWindow.getFriendInviteCodeText')), + autoselect=True, + on_activate_call=ba.WeakCall(self._invite_to_try_press), + up_widget=tab_button) + return self._container + + def _invite_to_try_press(self) -> None: + from bastd.ui.account import show_sign_in_prompt + from bastd.ui.appinvite import handle_app_invites_press + if _ba.get_account_state() != 'signed_in': + show_sign_in_prompt() + return + handle_app_invites_press() diff --git a/assets/src/ba_data/python/bastd/ui/gather/bases.py b/assets/src/ba_data/python/bastd/ui/gather/bases.py new file mode 100644 index 00000000..4510e11b --- /dev/null +++ b/assets/src/ba_data/python/bastd/ui/gather/bases.py @@ -0,0 +1,52 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides UI for inviting/joining friends.""" + +from __future__ import annotations + +import weakref +from typing import TYPE_CHECKING + +import ba + +if TYPE_CHECKING: + from bastd.ui.gather import GatherWindow + + +class GatherTab: + """Defines a tab for use in the gather UI.""" + + def __init__(self, window: GatherWindow) -> None: + self._window = weakref.ref(window) + + @property + def window(self) -> GatherWindow: + """The GatherWindow that this tab belongs to.""" + window = self._window() + if window is None: + raise ba.NotFoundError("GatherTab's window no longer exists.") + return window + + def on_activate( + self, + parent_widget: ba.Widget, + tab_button: ba.Widget, + region_width: float, + region_height: float, + region_left: float, + region_bottom: float, + ) -> ba.Widget: + """Called when the tab becomes the active one. + + The tab should create and return a container widget covering the + specified region. + """ + + def on_deactivate(self) -> None: + """Called when the tab will no longer be the active one.""" + + def save_state(self) -> None: + """Called when the parent window is saving state.""" + + def restore_state(self) -> None: + """Called when the parent window is restoring state.""" diff --git a/assets/src/ba_data/python/bastd/ui/gather/googleplaytab.py b/assets/src/ba_data/python/bastd/ui/gather/googleplaytab.py new file mode 100644 index 00000000..de714058 --- /dev/null +++ b/assets/src/ba_data/python/bastd/ui/gather/googleplaytab.py @@ -0,0 +1,86 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines the Google Play tab in the gather UI.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +import ba +from bastd.ui.gather.bases import GatherTab + +if TYPE_CHECKING: + from typing import Optional + from bastd.ui.gather import GatherWindow + + +class GooglePlayGatherTab(GatherTab): + """The public tab in the gather UI""" + + def __init__(self, window: GatherWindow) -> None: + super().__init__(window) + self._container: Optional[ba.Widget] = None + + def on_activate( + self, + parent_widget: ba.Widget, + tab_button: ba.Widget, + region_width: float, + region_height: float, + region_left: float, + region_bottom: float, + ) -> ba.Widget: + c_width = region_width + c_height = 380.0 + self._container = ba.containerwidget( + parent=parent_widget, + position=(region_left, + region_bottom + (region_height - c_height) * 0.5), + size=(c_width, c_height), + background=False, + selection_loops_to_parent=True) + v = c_height - 30.0 + ba.textwidget( + parent=self._container, + position=(c_width * 0.5, v - 140.0), + color=(0.6, 1.0, 0.6), + scale=1.3, + size=(0.0, 0.0), + maxwidth=c_width * 0.9, + h_align='center', + v_align='center', + text=ba.Lstr(resource='googleMultiplayerDiscontinuedText')) + return self._container + + def _on_google_play_show_invites_press(self) -> None: + from bastd.ui import account + if (_ba.get_account_state() != 'signed_in' + or _ba.get_account_type() != 'Google Play'): + account.show_sign_in_prompt('Google Play') + else: + _ba.show_invites_ui() + + def _on_google_play_invite_press(self) -> None: + from bastd.ui.confirm import ConfirmWindow + from bastd.ui.account import show_sign_in_prompt + if (_ba.get_account_state() != 'signed_in' + or _ba.get_account_type() != 'Google Play'): + show_sign_in_prompt('Google Play') + else: + # If there's google play people connected to us, inform the user + # that they will get disconnected. Otherwise just go ahead. + google_player_count = (_ba.get_google_play_party_client_count()) + if google_player_count > 0: + ConfirmWindow( + ba.Lstr(resource='gatherWindow.' + 'googlePlayReInviteText', + subs=[('${COUNT}', str(google_player_count))]), + lambda: ba.timer( + 0.2, _ba.invite_players, timetype=ba.TimeType.REAL), + width=500, + height=150, + ok_text=ba.Lstr(resource='gatherWindow.' + 'googlePlayInviteText')) + else: + ba.timer(0.1, _ba.invite_players, timetype=ba.TimeType.REAL) diff --git a/assets/src/ba_data/python/bastd/ui/gather/manualtab.py b/assets/src/ba_data/python/bastd/ui/gather/manualtab.py new file mode 100644 index 00000000..64c0c2e3 --- /dev/null +++ b/assets/src/ba_data/python/bastd/ui/gather/manualtab.py @@ -0,0 +1,426 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines the manual tab in the gather UI.""" + +from __future__ import annotations + +import threading +from typing import TYPE_CHECKING, cast + +import _ba +import ba +from bastd.ui.gather.bases import GatherTab + +if TYPE_CHECKING: + from typing import Callable, Optional, Any, Union, Dict + from bastd.ui.gather import GatherWindow + + +def _safe_set_text(txt: Optional[ba.Widget], + val: Union[str, ba.Lstr], + success: bool = True) -> None: + if txt: + ba.textwidget(edit=txt, + text=val, + color=(0, 1, 0) if success else (1, 1, 0)) + + +class _HostLookupThread(threading.Thread): + """Thread to fetch an addr.""" + + def __init__(self, name: str, port: int, + call: Callable[[Optional[str], int], Any]): + super().__init__() + self._name = name + self._port = port + self._call = call + + 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) + + +class ManualGatherTab(GatherTab): + """The manual tab in the gather UI""" + + def __init__(self, window: GatherWindow) -> None: + super().__init__(window) + self._check_button: Optional[ba.Widget] = None + self._doing_access_check: Optional[bool] = None + self._access_check_count: Optional[int] = None + self._t_addr: Optional[ba.Widget] = None + self._t_accessible: Optional[ba.Widget] = None + self._t_accessible_extra: Optional[ba.Widget] = None + self._access_check_timer: Optional[ba.Timer] = None + self._checking_state_text: Optional[ba.Widget] = None + self._container: Optional[ba.Widget] = None + + def on_activate( + self, + parent_widget: ba.Widget, + tab_button: ba.Widget, + region_width: float, + region_height: float, + region_left: float, + region_bottom: float, + ) -> ba.Widget: + + c_width = region_width + c_height = 380 + last_addr = ba.app.config.get('Last Manual Party Connect Address', '') + + self._container = ba.containerwidget( + parent=parent_widget, + position=(region_left, + region_bottom + (region_height - c_height) * 0.5), + size=(c_width, c_height), + background=False, + selection_loops_to_parent=True) + v = c_height - 30 + ba.textwidget(parent=self._container, + position=(c_width * 0.5, v), + color=(0.6, 1.0, 0.6), + scale=1.3, + size=(0, 0), + maxwidth=c_width * 0.9, + h_align='center', + v_align='center', + text=ba.Lstr(resource='gatherWindow.' + 'manualDescriptionText')) + v -= 30 + v -= 70 + ba.textwidget(parent=self._container, + position=(c_width * 0.5 - 260 - 50, v), + color=(0.6, 1.0, 0.6), + scale=1.0, + size=(0, 0), + maxwidth=130, + h_align='right', + v_align='center', + text=ba.Lstr(resource='gatherWindow.' + 'manualAddressText')) + txt = ba.textwidget(parent=self._container, + editable=True, + description=ba.Lstr(resource='gatherWindow.' + 'manualAddressText'), + position=(c_width * 0.5 - 240 - 50, v - 30), + text=last_addr, + autoselect=True, + v_align='center', + scale=1.0, + size=(420, 60)) + ba.textwidget(parent=self._container, + position=(c_width * 0.5 - 260 + 490, v), + color=(0.6, 1.0, 0.6), + scale=1.0, + size=(0, 0), + maxwidth=80, + h_align='right', + v_align='center', + text=ba.Lstr(resource='gatherWindow.' + 'portText')) + txt2 = ba.textwidget(parent=self._container, + editable=True, + description=ba.Lstr(resource='gatherWindow.' + 'portText'), + text='43210', + autoselect=True, + max_chars=5, + position=(c_width * 0.5 - 240 + 490, v - 30), + v_align='center', + scale=1.0, + size=(170, 60)) + + v -= 110 + + btn = ba.buttonwidget(parent=self._container, + size=(300, 70), + label=ba.Lstr(resource='gatherWindow.' + 'manualConnectText'), + position=(c_width * 0.5 - 150, v), + autoselect=True, + on_activate_call=ba.Call(self._connect, txt, + txt2)) + ba.widget(edit=txt, up_widget=tab_button) + ba.textwidget(edit=txt, on_return_press_call=btn.activate) + ba.textwidget(edit=txt2, on_return_press_call=btn.activate) + v -= 45 + + self._check_button = ba.textwidget( + parent=self._container, + size=(250, 60), + text=ba.Lstr(resource='gatherWindow.' + 'showMyAddressText'), + v_align='center', + h_align='center', + click_activate=True, + position=(c_width * 0.5 - 125, v - 30), + autoselect=True, + color=(0.5, 0.9, 0.5), + scale=0.8, + selectable=True, + on_activate_call=ba.Call(self._on_show_my_address_button_press, v, + self._container, c_width)) + return self._container + + def on_deactivate(self) -> None: + self._access_check_timer = None + + 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: + 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() + + def _host_lookup_result(self, resolved_address: Optional[str], + port: int) -> None: + if resolved_address is None: + ba.screenmessage( + ba.Lstr(resource='internal.unableToResolveHostText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + else: + # Store for later. + config = ba.app.config + config['Last Manual Party Connect Address'] = resolved_address + config.commit() + _ba.connect_to_party(resolved_address, port=port) + + 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) + + def _on_show_my_address_button_press(self, v2: float, + container: Optional[ba.Widget], + c_width: float) -> None: + if not container: + return + + tscl = 0.85 + tspc = 25 + + ba.playsound(ba.getsound('swish')) + ba.textwidget(parent=container, + position=(c_width * 0.5 - 10, v2), + color=(0.6, 1.0, 0.6), + scale=tscl, + size=(0, 0), + maxwidth=c_width * 0.45, + flatness=1.0, + h_align='right', + v_align='center', + text=ba.Lstr(resource='gatherWindow.' + 'manualYourLocalAddressText')) + self._checking_state_text = ba.textwidget( + parent=container, + position=(c_width * 0.5, v2), + color=(0.5, 0.5, 0.5), + scale=tscl, + size=(0, 0), + maxwidth=c_width * 0.45, + flatness=1.0, + h_align='left', + v_align='center', + text=ba.Lstr(resource='gatherWindow.' + 'checkingText')) + + threading.Thread(target=self._run_addr_fetch).start() + + v2 -= tspc + ba.textwidget(parent=container, + position=(c_width * 0.5 - 10, v2), + color=(0.6, 1.0, 0.6), + scale=tscl, + size=(0, 0), + maxwidth=c_width * 0.45, + flatness=1.0, + h_align='right', + v_align='center', + text=ba.Lstr(resource='gatherWindow.' + 'manualYourAddressFromInternetText')) + + t_addr = ba.textwidget(parent=container, + position=(c_width * 0.5, v2), + color=(0.5, 0.5, 0.5), + scale=tscl, + size=(0, 0), + maxwidth=c_width * 0.45, + h_align='left', + v_align='center', + flatness=1.0, + text=ba.Lstr(resource='gatherWindow.' + 'checkingText')) + v2 -= tspc + ba.textwidget(parent=container, + position=(c_width * 0.5 - 10, v2), + color=(0.6, 1.0, 0.6), + scale=tscl, + size=(0, 0), + maxwidth=c_width * 0.45, + flatness=1.0, + h_align='right', + v_align='center', + text=ba.Lstr(resource='gatherWindow.' + 'manualJoinableFromInternetText')) + + t_accessible = ba.textwidget(parent=container, + position=(c_width * 0.5, v2), + color=(0.5, 0.5, 0.5), + scale=tscl, + size=(0, 0), + maxwidth=c_width * 0.45, + flatness=1.0, + h_align='left', + v_align='center', + text=ba.Lstr(resource='gatherWindow.' + 'checkingText')) + v2 -= 28 + t_accessible_extra = ba.textwidget(parent=container, + position=(c_width * 0.5, v2), + color=(1, 0.5, 0.2), + scale=0.7, + size=(0, 0), + maxwidth=c_width * 0.9, + flatness=1.0, + h_align='center', + v_align='center', + text='') + + self._doing_access_check = False + self._access_check_count = 0 # Cap our refreshes eventually. + self._access_check_timer = ba.Timer( + 10.0, + ba.WeakCall(self._access_check_update, t_addr, t_accessible, + t_accessible_extra), + repeat=True, + timetype=ba.TimeType.REAL) + + # Kick initial off. + self._access_check_update(t_addr, t_accessible, t_accessible_extra) + if self._check_button: + self._check_button.delete() + + def _access_check_update(self, t_addr: ba.Widget, t_accessible: ba.Widget, + t_accessible_extra: ba.Widget) -> None: + from ba.internal import master_server_get + + # If we don't have an outstanding query, start one.. + assert self._doing_access_check is not None + assert self._access_check_count is not None + if not self._doing_access_check and self._access_check_count < 100: + self._doing_access_check = True + self._access_check_count += 1 + self._t_addr = t_addr + self._t_accessible = t_accessible + self._t_accessible_extra = t_accessible_extra + master_server_get('bsAccessCheck', {'b': ba.app.build_number}, + callback=ba.WeakCall( + self._on_accessible_response)) + + def _on_accessible_response(self, data: Optional[Dict[str, Any]]) -> None: + t_addr = self._t_addr + t_accessible = self._t_accessible + t_accessible_extra = self._t_accessible_extra + self._doing_access_check = False + color_bad = (1, 1, 0) + color_good = (0, 1, 0) + if data is None or 'address' not in data or 'accessible' not in data: + if t_addr: + ba.textwidget(edit=t_addr, + text=ba.Lstr(resource='gatherWindow.' + 'noConnectionText'), + color=color_bad) + if t_accessible: + ba.textwidget(edit=t_accessible, + text=ba.Lstr(resource='gatherWindow.' + 'noConnectionText'), + color=color_bad) + if t_accessible_extra: + ba.textwidget(edit=t_accessible_extra, + text='', + color=color_bad) + return + if t_addr: + ba.textwidget(edit=t_addr, text=data['address'], color=color_good) + if t_accessible: + if data['accessible']: + ba.textwidget(edit=t_accessible, + text=ba.Lstr(resource='gatherWindow.' + 'manualJoinableYesText'), + color=color_good) + if t_accessible_extra: + ba.textwidget(edit=t_accessible_extra, + text='', + color=color_good) + else: + ba.textwidget( + edit=t_accessible, + text=ba.Lstr(resource='gatherWindow.' + 'manualJoinableNoWithAsteriskText'), + color=color_bad, + ) + if t_accessible_extra: + ba.textwidget( + edit=t_accessible_extra, + text=ba.Lstr(resource='gatherWindow.' + 'manualRouterForwardingText', + subs=[('${PORT}', + str(_ba.get_game_port()))]), + color=color_bad, + ) diff --git a/assets/src/ba_data/python/bastd/ui/gather/nearbytab.py b/assets/src/ba_data/python/bastd/ui/gather/nearbytab.py new file mode 100644 index 00000000..66f500be --- /dev/null +++ b/assets/src/ba_data/python/bastd/ui/gather/nearbytab.py @@ -0,0 +1,136 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines the nearby tab in the gather UI.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +import _ba +from bastd.ui.gather.bases import GatherTab + +if TYPE_CHECKING: + from typing import Optional, Dict, Any + from bastd.ui.gather import GatherWindow + + +class NetScanner: + """Class for scanning for games on the lan.""" + + def __init__(self, scrollwidget: ba.Widget, tab_button: ba.Widget, + width: float): + self._scrollwidget = scrollwidget + self._tab_button = tab_button + self._columnwidget = ba.columnwidget(parent=self._scrollwidget, + border=2, + margin=0, + left_border=10) + ba.widget(edit=self._columnwidget, up_widget=tab_button) + self._width = width + self._last_selected_host: Optional[Dict[str, Any]] = None + + self._update_timer = ba.Timer(1.0, + ba.WeakCall(self.update), + timetype=ba.TimeType.REAL, + repeat=True) + # Go ahead and run a few *almost* immediately so we don't + # have to wait a second. + self.update() + ba.timer(0.25, ba.WeakCall(self.update), timetype=ba.TimeType.REAL) + + def __del__(self) -> None: + _ba.end_host_scanning() + + def _on_select(self, host: Dict[str, Any]) -> None: + self._last_selected_host = host + + def _on_activate(self, host: Dict[str, Any]) -> None: + _ba.connect_to_party(host['address']) + + def update(self) -> None: + """(internal)""" + t_scale = 1.6 + for child in self._columnwidget.get_children(): + child.delete() + + # Grab this now this since adding widgets will change it. + last_selected_host = self._last_selected_host + hosts = _ba.host_scan_cycle() + for i, host in enumerate(hosts): + txt3 = ba.textwidget(parent=self._columnwidget, + size=(self._width / t_scale, 30), + selectable=True, + color=(1, 1, 1), + on_select_call=ba.Call(self._on_select, host), + on_activate_call=ba.Call( + self._on_activate, host), + click_activate=True, + text=host['display_string'], + h_align='left', + v_align='center', + corner_scale=t_scale, + maxwidth=(self._width / t_scale) * 0.93) + if host == last_selected_host: + ba.containerwidget(edit=self._columnwidget, + selected_child=txt3, + visible_child=txt3) + if i == 0: + ba.widget(edit=txt3, up_widget=self._tab_button) + + +class NearbyGatherTab(GatherTab): + """The nearby tab in the gather UI""" + + def __init__(self, window: GatherWindow) -> None: + super().__init__(window) + self._net_scanner: Optional[NetScanner] = None + self._container: Optional[ba.Widget] = None + + def on_activate( + self, + parent_widget: ba.Widget, + tab_button: ba.Widget, + region_width: float, + region_height: float, + region_left: float, + region_bottom: float, + ) -> ba.Widget: + c_width = region_width + c_height = region_height - 20 + sub_scroll_height = c_height - 85 + sub_scroll_width = 650 + self._container = ba.containerwidget( + parent=parent_widget, + position=(region_left, + region_bottom + (region_height - c_height) * 0.5), + size=(c_width, c_height), + background=False, + selection_loops_to_parent=True) + v = c_height - 30 + ba.textwidget(parent=self._container, + position=(c_width * 0.5, v - 3), + color=(0.6, 1.0, 0.6), + scale=1.3, + size=(0, 0), + maxwidth=c_width * 0.9, + h_align='center', + v_align='center', + text=ba.Lstr(resource='gatherWindow.' + 'localNetworkDescriptionText')) + v -= 15 + v -= sub_scroll_height + 23 + scrollw = ba.scrollwidget(parent=self._container, + position=((region_width - sub_scroll_width) * + 0.5, v), + size=(sub_scroll_width, sub_scroll_height)) + + self._net_scanner = NetScanner(scrollw, + tab_button, + width=sub_scroll_width) + + ba.widget(edit=scrollw, autoselect=True, up_widget=tab_button) + return self._container + + def on_deactivate(self) -> None: + self._net_scanner = None diff --git a/assets/src/ba_data/python/bastd/ui/gather/publictab.py b/assets/src/ba_data/python/bastd/ui/gather/publictab.py new file mode 100644 index 00000000..6e2a02ea --- /dev/null +++ b/assets/src/ba_data/python/bastd/ui/gather/publictab.py @@ -0,0 +1,1040 @@ +# Released under the MIT License. See LICENSE for details. +# +# pylint: disable=too-many-lines +"""Defines the public tab in the gather UI.""" + +from __future__ import annotations + +import time +import threading +from typing import TYPE_CHECKING, cast + +import _ba +import ba +from bastd.ui.gather.bases import GatherTab + +if TYPE_CHECKING: + from typing import Callable, Any, Optional, Dict, Union, Tuple + from bastd.ui.gather import GatherWindow + + +class PublicGatherTab(GatherTab): + """The public tab in the gather UI""" + + def __init__(self, window: GatherWindow) -> None: + super().__init__(window) + self._container: Optional[ba.Widget] = None + self._internet_join_text: Optional[ba.Widget] = None + self._internet_host_text: Optional[ba.Widget] = None + self._internet_local_address: Optional[str] = None + self._last_public_party_connect_attempt_time: Optional[float] = None + self._internet_tab: Optional[str] = None + self._public_party_list_selection: Optional[Tuple[str, str]] = None + self._refreshing_public_party_list: Optional[bool] = None + self._update_timer: Optional[ba.Timer] = None + self._internet_host_scrollwidget: Optional[ba.Widget] = None + self._internet_host_name_text: Optional[ba.Widget] = None + self._internet_host_toggle_button: Optional[ba.Widget] = None + self._internet_join_last_refresh_time = -99999.0 + self._internet_join_party_name_label: Optional[ba.Widget] = None + self._internet_join_party_language_label: Optional[ba.Widget] = None + self._internet_join_party_size_label: Optional[ba.Widget] = None + self._internet_join_party_ping_label: Optional[ba.Widget] = None + self._internet_host_columnwidget: Optional[ba.Widget] = None + self._internet_join_status_text: Optional[ba.Widget] = None + self._internet_host_name_label_text: Optional[ba.Widget] = None + self._internet_host_max_party_size_label: Optional[ba.Widget] = None + self._internet_host_max_party_size_value: Optional[ba.Widget] = None + self._internet_host_max_party_size_minus_button: ( + Optional[ba.Widget]) = None + self._internet_host_max_party_size_plus_button: ( + Optional[ba.Widget]) = None + self._internet_host_status_text: Optional[ba.Widget] = None + self._internet_host_dedicated_server_info_text: ( + Optional[ba.Widget]) = None + self._internet_lock_icon: Optional[ba.Widget] = None + self._public_parties: Dict[str, Dict[str, Any]] = {} + self._last_public_party_list_rebuild_time: Optional[float] = None + self._first_public_party_list_rebuild_time: Optional[float] = None + self._next_public_party_entry_index = 0 + + def on_activate( + self, + parent_widget: ba.Widget, + tab_button: ba.Widget, + region_width: float, + region_height: float, + region_left: float, + region_bottom: float, + ) -> ba.Widget: + c_width = region_width + c_height = region_height - 20 + self._container = ba.containerwidget( + parent=parent_widget, + position=(region_left, + region_bottom + (region_height - c_height) * 0.5), + size=(c_width, c_height), + background=False, + selection_loops_to_parent=True) + v = c_height - 30 + self._internet_join_text = txt = ba.textwidget( + parent=self._container, + position=(c_width * 0.5 - 245, v - 13), + color=(0.6, 1.0, 0.6), + scale=1.3, + size=(200, 30), + maxwidth=250, + h_align='left', + v_align='center', + click_activate=True, + selectable=True, + autoselect=True, + on_activate_call=lambda: self._set_internet_tab('join', + region_width, + region_height, + region_left, + region_bottom, + playsound=True), + text=ba.Lstr(resource='gatherWindow.' + 'joinPublicPartyDescriptionText')) + ba.widget(edit=txt, up_widget=tab_button) + self._internet_host_text = txt = ba.textwidget( + parent=self._container, + position=(c_width * 0.5 + 45, v - 13), + color=(0.6, 1.0, 0.6), + scale=1.3, + size=(200, 30), + maxwidth=250, + h_align='left', + v_align='center', + click_activate=True, + selectable=True, + autoselect=True, + on_activate_call=lambda: self._set_internet_tab('host', + region_width, + region_height, + region_left, + region_bottom, + playsound=True), + text=ba.Lstr(resource='gatherWindow.' + 'hostPublicPartyDescriptionText')) + ba.widget(edit=txt, + left_widget=self._internet_join_text, + up_widget=tab_button) + ba.widget(edit=self._internet_join_text, right_widget=txt) + + # Attempt to fetch our local address so we have it for + # error messages. + self._internet_local_address = None + + class AddrFetchThread(threading.Thread): + """Thread for fetching an address in the bg.""" + + def __init__(self, call: Callable[[Any], Any]): + super().__init__() + self._call = call + + 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() + + AddrFetchThread(ba.WeakCall( + self._internet_fetch_local_addr_cb)).start() + + assert self._internet_tab is not None + self._set_internet_tab(self._internet_tab, region_width, region_height, + region_left, region_bottom) + self._update_timer = ba.Timer(0.2, + ba.WeakCall(self._update_internet_tab), + repeat=True, + timetype=ba.TimeType.REAL) + + # Also update it immediately so we don't have to wait for the + # initial query. + self._update_internet_tab() + return self._container + + def on_deactivate(self) -> None: + self._update_timer = None + + def save_state(self) -> None: + ba.app.ui.window_states[self.__class__.__name__] = { + 'internet_tab': self._internet_tab + } + + def restore_state(self) -> None: + winstate = ba.app.ui.window_states.get(self.__class__.__name__, {}) + self._internet_tab = winstate.get('internet_tab', 'join') + + def _set_internet_tab(self, + value: str, + region_width: float, + region_height: float, + region_left: float, + region_bottom: float, + playsound: bool = False) -> None: + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements + assert self._container + del region_left, region_bottom # Unused + if playsound: + ba.playsound(ba.getsound('click01')) + + # If we're switching in from elsewhere, reset our selection. + # (prevents selecting something way down the list if we switched away + # and came back) + if self._internet_tab != value: + self._public_party_list_selection = None + + self._internet_tab = value + active_color = (0.6, 1.0, 0.6) + inactive_color = (0.5, 0.4, 0.5) + ba.textwidget( + edit=self._internet_join_text, + color=active_color if value == 'join' else inactive_color) + ba.textwidget( + edit=self._internet_host_text, + color=active_color if value == 'host' else inactive_color) + + # Clear anything in existence. + for widget in [ + self._internet_host_scrollwidget, + self._internet_host_name_text, + self._internet_host_toggle_button, + self._internet_host_name_label_text, + self._internet_host_status_text, + self._internet_join_party_size_label, + self._internet_join_party_name_label, + self._internet_join_party_language_label, + self._internet_join_party_ping_label, + self._internet_host_max_party_size_label, + self._internet_host_max_party_size_value, + self._internet_host_max_party_size_minus_button, + self._internet_host_max_party_size_plus_button, + self._internet_join_status_text, + self._internet_host_dedicated_server_info_text + ]: + if widget is not None: + widget.delete() + + c_width = region_width + c_height = region_height - 20 + sub_scroll_height = c_height - 90 + sub_scroll_width = 830 + v = c_height - 35 + v -= 25 + is_public_enabled = _ba.get_public_party_enabled() + if value == 'join': + # Reset this so we do an immediate refresh query. + self._internet_join_last_refresh_time = -99999.0 + + # Reset our list of public parties. + self._public_parties = {} + self._last_public_party_list_rebuild_time = 0 + self._first_public_party_list_rebuild_time = None + self._internet_join_party_name_label = ba.textwidget( + text=ba.Lstr(resource='nameText'), + parent=self._container, + size=(0, 0), + position=(90, v - 8), + maxwidth=60, + scale=0.6, + color=(0.5, 0.5, 0.5), + flatness=1.0, + shadow=0.0, + h_align='center', + v_align='center') + if bool(False): + self._internet_join_party_language_label = ba.textwidget( + text=ba.Lstr( + resource='settingsWindowAdvanced.languageText'), + parent=self._container, + size=(0, 0), + position=(662, v - 8), + maxwidth=100, + scale=0.6, + color=(0.5, 0.5, 0.5), + flatness=1.0, + shadow=0.0, + h_align='center', + v_align='center') + self._internet_join_party_size_label = ba.textwidget( + text=ba.Lstr(resource='gatherWindow.' + 'partySizeText'), + parent=self._container, + size=(0, 0), + position=(755, v - 8), + maxwidth=60, + scale=0.6, + color=(0.5, 0.5, 0.5), + flatness=1.0, + shadow=0.0, + h_align='center', + v_align='center') + self._internet_join_party_ping_label = ba.textwidget( + text=ba.Lstr(resource='gatherWindow.' + 'pingText'), + parent=self._container, + size=(0, 0), + position=(825, v - 8), + maxwidth=60, + scale=0.6, + color=(0.5, 0.5, 0.5), + flatness=1.0, + shadow=0.0, + h_align='center', + v_align='center') + v -= sub_scroll_height + 23 + + self._internet_host_scrollwidget = scrollw = ba.scrollwidget( + parent=self._container, + simple_culling_v=10, + position=((region_width - sub_scroll_width) * 0.5, v), + size=(sub_scroll_width, sub_scroll_height)) + ba.widget(edit=scrollw, autoselect=True) + colw = self._internet_host_columnwidget = ba.containerwidget( + parent=scrollw, background=False, size=(400, 400)) + ba.containerwidget(edit=scrollw, claims_left_right=True) + ba.containerwidget(edit=colw, claims_left_right=True) + + self._internet_join_status_text = ba.textwidget( + parent=self._container, + text=ba.Lstr(value='${A}...', + subs=[('${A}', + ba.Lstr(resource='store.loadingText'))]), + size=(0, 0), + scale=0.9, + flatness=1.0, + shadow=0.0, + h_align='center', + v_align='top', + maxwidth=c_width, + color=(0.6, 0.6, 0.6), + position=(c_width * 0.5, c_height * 0.5)) + + if value == 'host': + v -= 30 + party_name_text = ba.Lstr( + resource='gatherWindow.partyNameText', + fallback_resource='editGameListWindow.nameText') + self._internet_host_name_label_text = ba.textwidget( + parent=self._container, + size=(0, 0), + h_align='right', + v_align='center', + maxwidth=200, + scale=0.8, + color=ba.app.ui.infotextcolor, + position=(210, v - 9), + text=party_name_text) + self._internet_host_name_text = ba.textwidget( + parent=self._container, + editable=True, + size=(535, 40), + position=(230, v - 30), + text=ba.app.config.get('Public Party Name', ''), + maxwidth=494, + shadow=0.3, + flatness=1.0, + description=party_name_text, + autoselect=True, + v_align='center', + corner_scale=1.0) + + v -= 60 + self._internet_host_max_party_size_label = ba.textwidget( + parent=self._container, + size=(0, 0), + h_align='right', + v_align='center', + maxwidth=200, + scale=0.8, + color=ba.app.ui.infotextcolor, + position=(210, v - 9), + text=ba.Lstr(resource='maxPartySizeText', + fallback_resource='maxConnectionsText')) + self._internet_host_max_party_size_value = ba.textwidget( + parent=self._container, + size=(0, 0), + h_align='center', + v_align='center', + scale=1.2, + color=(1, 1, 1), + position=(240, v - 9), + text=str(_ba.get_public_party_max_size())) + btn1 = self._internet_host_max_party_size_minus_button = ( + ba.buttonwidget( + parent=self._container, + size=(40, 40), + on_activate_call=ba.WeakCall( + self._on_max_public_party_size_minus_press), + position=(280, v - 26), + label='-', + autoselect=True)) + btn2 = self._internet_host_max_party_size_plus_button = ( + ba.buttonwidget(parent=self._container, + size=(40, 40), + on_activate_call=ba.WeakCall( + self._on_max_public_party_size_plus_press), + position=(350, v - 26), + label='+', + autoselect=True)) + v -= 50 + v -= 70 + if is_public_enabled: + label = ba.Lstr( + resource='gatherWindow.makePartyPrivateText', + fallback_resource='gatherWindow.stopAdvertisingText') + else: + label = ba.Lstr( + resource='gatherWindow.makePartyPublicText', + fallback_resource='gatherWindow.startAdvertisingText') + self._internet_host_toggle_button = ba.buttonwidget( + parent=self._container, + label=label, + size=(400, 80), + on_activate_call=self._on_stop_internet_advertising_press + if is_public_enabled else + self._on_start_internet_advertizing_press, + position=(c_width * 0.5 - 200, v), + autoselect=True, + up_widget=btn2) + ba.widget(edit=self._internet_host_name_text, down_widget=btn2) + ba.widget(edit=btn2, up_widget=self._internet_host_name_text) + ba.widget(edit=btn1, up_widget=self._internet_host_name_text) + ba.widget(edit=self._internet_join_text, + down_widget=self._internet_host_name_text) + v -= 10 + self._internet_host_status_text = ba.textwidget( + parent=self._container, + text=ba.Lstr(resource='gatherWindow.' + 'partyStatusNotPublicText'), + size=(0, 0), + scale=0.7, + flatness=1.0, + shadow=0.0, + h_align='center', + v_align='top', + maxwidth=c_width, + color=(0.6, 0.6, 0.6), + position=(c_width * 0.5, v)) + v -= 90 + self._internet_host_dedicated_server_info_text = ba.textwidget( + parent=self._container, + text=ba.Lstr(resource='gatherWindow.' + 'dedicatedServerInfoText'), + size=(0, 0), + scale=0.7, + flatness=1.0, + shadow=0.0, + h_align='center', + v_align='center', + maxwidth=c_width * 0.9, + color=ba.app.ui.infotextcolor, + position=(c_width * 0.5, v)) + + # If public sharing is already on, + # launch a status-check immediately. + if _ba.get_public_party_enabled(): + self._do_internet_status_check() + + # Now add a lock icon overlay for if we don't have pro. + icon = self._internet_lock_icon + if icon and self._internet_lock_icon: + self._internet_lock_icon.delete() # Kill any existing. + self._internet_lock_icon = ba.imagewidget( + parent=self._container, + position=(c_width * 0.5 - 60, c_height * 0.5 - 50), + size=(120, 120), + opacity=0.0 if not self._is_internet_locked() else 0.5, + texture=ba.gettexture('lock')) + + def _rebuild_public_party_list(self) -> None: + # pylint: disable=too-many-branches + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements + cur_time = ba.time(ba.TimeType.REAL) + if self._first_public_party_list_rebuild_time is None: + self._first_public_party_list_rebuild_time = cur_time + + # Update faster for the first few seconds; + # then ease off to keep the list from jumping around. + since_first = cur_time - self._first_public_party_list_rebuild_time + wait_time = (1.0 if since_first < 2.0 else + 2.5 if since_first < 10.0 else 5.0) + assert self._last_public_party_list_rebuild_time is not None + if cur_time - self._last_public_party_list_rebuild_time < wait_time: + return + self._last_public_party_list_rebuild_time = cur_time + + # First off, check for the existence of our column widget; + # if we don't have this, we're done. + columnwidget = self._internet_host_columnwidget + if not columnwidget: + return + + with ba.Context('ui'): + + # Now kill and recreate all widgets. + for widget in columnwidget.get_children(): + widget.delete() + + # Sort - show queue-enabled ones first and sort by lowest ping. + ordered_parties = sorted( + list(self._public_parties.values()), + key=lambda p: ( + p['queue'] is None, # Show non-queued last. + p['ping'] if p['ping'] is not None else 999999, + p['index'])) + existing_selection = self._public_party_list_selection + first = True + + 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_public_party_list = True + + ba.pushcall(refresh_on) + + # Janky - allow escaping if there's nothing in us. + ba.containerwidget(edit=self._internet_host_scrollwidget, + claims_up_down=(len(ordered_parties) > 0)) + + for i, party in enumerate(ordered_parties): + hpos = 20 + vpos = sub_scroll_height - lineheight * i - 50 + party['name_widget'] = ba.textwidget( + text=ba.Lstr(value=party['name']), + parent=columnwidget, + size=(sub_scroll_width * 0.63, 20), + position=(0 + hpos, 4 + vpos), + selectable=True, + on_select_call=ba.WeakCall( + self._set_public_party_selection, + (party['address'], 'name')), + on_activate_call=ba.WeakCall( + self._on_public_party_activate, party), + click_activate=True, + maxwidth=sub_scroll_width * 0.45, + corner_scale=1.4, + autoselect=True, + color=(1, 1, 1, 0.3 if party['ping'] is None else 1.0), + h_align='left', + v_align='center') + ba.widget(edit=party['name_widget'], + left_widget=self._internet_join_text, + show_buffer_top=64.0, + show_buffer_bottom=64.0) + if existing_selection == (party['address'], 'name'): + ba.containerwidget(edit=columnwidget, + selected_child=party['name_widget']) + if bool(False): + party['language_widget'] = ba.textwidget( + text=ba.Lstr(translate=('languages', + party['language'])), + parent=columnwidget, + size=(0, 0), + position=(sub_scroll_width * 0.73 + hpos, 20 + vpos), + maxwidth=sub_scroll_width * 0.13, + scale=0.7, + color=(0.8, 0.8, 0.8), + h_align='center', + v_align='center') + if party['stats_addr'] != '': + url = party['stats_addr'].replace( + '${ACCOUNT}', + _ba.get_account_misc_read_val_2( + 'resolvedAccountID', 'UNKNOWN')) + party['stats_button'] = ba.buttonwidget( + color=(0.3, 0.6, 0.94), + textcolor=(1.0, 1.0, 1.0), + label=ba.Lstr(resource='statsText'), + parent=columnwidget, + autoselect=True, + on_activate_call=ba.Call(ba.open_url, url), + on_select_call=ba.WeakCall( + self._set_public_party_selection, + (party['address'], 'stats_button')), + size=(120, 40), + position=(sub_scroll_width * 0.66 + hpos, 1 + vpos), + scale=0.9) + if existing_selection == (party['address'], + 'stats_button'): + ba.containerwidget( + edit=columnwidget, + selected_child=party['stats_button']) + else: + if 'stats_button' in party: + del party['stats_button'] + + if first: + if 'stats_button' in party: + ba.widget(edit=party['stats_button'], + up_widget=self._internet_join_text) + if 'name_widget' in party: + ba.widget(edit=party['name_widget'], + up_widget=self._internet_join_text) + first = False + + party['size_widget'] = ba.textwidget( + text=str(party['size']) + '/' + str(party['size_max']), + parent=columnwidget, + size=(0, 0), + position=(sub_scroll_width * 0.86 + hpos, 20 + vpos), + scale=0.7, + color=(0.8, 0.8, 0.8), + h_align='right', + v_align='center') + party['ping_widget'] = ba.textwidget( + parent=columnwidget, + size=(0, 0), + position=(sub_scroll_width * 0.94 + hpos, 20 + vpos), + scale=0.7, + h_align='right', + v_align='center') + if party['ping'] is None: + ba.textwidget(edit=party['ping_widget'], + text='-', + color=(0.5, 0.5, 0.5)) + else: + ping_good = _ba.get_account_misc_read_val('pingGood', 100) + ping_med = _ba.get_account_misc_read_val('pingMed', 500) + ba.textwidget(edit=party['ping_widget'], + text=str(party['ping']), + color=(0, 1, + 0) if party['ping'] <= ping_good else + (1, 1, 0) if party['ping'] <= ping_med else + (1, 0, 0)) + + # So our selection callbacks can start firing.. + def refresh_on2() -> None: + self._refreshing_public_party_list = False + + ba.pushcall(refresh_on2) + + def _on_public_party_query_result( + self, result: Optional[Dict[str, Any]]) -> None: + with ba.Context('ui'): + # Any time we get any result at all, kill our loading status. + status_text = self._internet_join_status_text + if status_text: + # Don't show results if not signed in + # (probably didn't get any anyway). + if _ba.get_account_state() != 'signed_in': + ba.textwidget(edit=status_text, + text=ba.Lstr(resource='notSignedInText')) + else: + if result is None: + ba.textwidget(edit=status_text, + text=ba.Lstr(resource='errorText')) + else: + ba.textwidget(edit=status_text, text='') + + if result is not None: + parties_in = result['l'] + else: + parties_in = [] + + for partyval in list(self._public_parties.values()): + partyval['claimed'] = False + + for party_in in parties_in: + # Party is indexed by (ADDR)_(PORT) + party_key = party_in['a'] + '_' + str(party_in['p']) + party = self._public_parties.get(party_key) + if party is None: + # If this party is new to us, init it. + index = self._next_public_party_entry_index + self._next_public_party_entry_index = index + 1 + party = self._public_parties[party_key] = { + 'address': + party_in.get('a'), + 'next_ping_time': + ba.time(ba.TimeType.REAL) + 0.001 * party_in['pd'], + 'ping': + None, + 'index': + index, + } + + # Now, new or not, update its values. + party['queue'] = party_in.get('q') + party['port'] = party_in.get('p') + party['name'] = party_in['n'] + party['size'] = party_in['s'] + party['language'] = party_in['l'] + party['size_max'] = party_in['sm'] + party['claimed'] = True + # (server provides this in milliseconds; we use seconds) + party['ping_interval'] = 0.001 * party_in['pi'] + party['stats_addr'] = party_in['sa'] + + # Prune unclaimed party entries. + self._public_parties = { + key: val + for key, val in list(self._public_parties.items()) + if val['claimed'] + } + + self._rebuild_public_party_list() + + def _update_internet_tab(self) -> None: + # pylint: disable=too-many-statements + + # Special case: if a party-queue window is up, don't do any of this + # (keeps things smoother). + if ba.app.ui.have_party_queue_window: + return + + # If we've got a party-name text widget, keep its value plugged + # into our public host name. + text = self._internet_host_name_text + if text: + name = cast(str, + ba.textwidget(query=self._internet_host_name_text)) + _ba.set_public_party_name(name) + + # Show/hide the lock icon depending on if we've got pro. + icon = self._internet_lock_icon + if icon: + if self._is_internet_locked(): + ba.imagewidget(edit=icon, opacity=0.5) + else: + ba.imagewidget(edit=icon, opacity=0.0) + + if self._internet_tab == 'join': + now = ba.time(ba.TimeType.REAL) + if (now - self._internet_join_last_refresh_time > 0.001 * + _ba.get_account_misc_read_val('pubPartyRefreshMS', 10000)): + self._internet_join_last_refresh_time = now + app = ba.app + _ba.add_transaction( + { + 'type': 'PUBLIC_PARTY_QUERY', + 'proto': app.protocol_version, + 'lang': app.lang.language + }, + callback=ba.WeakCall(self._on_public_party_query_result)) + _ba.run_transactions() + + # Go through our existing public party entries firing off pings + # for any that have timed out. + for party in list(self._public_parties.values()): + if (party['next_ping_time'] <= now + and ba.app.ping_thread_count < 15): + + # Make sure to fully catch up and not to multi-ping if + # we're way behind somehow. + while party['next_ping_time'] <= now: + # Crank the interval up for high-latency parties to + # save us some work. + mult = 1 + if party['ping'] is not None: + mult = (10 if party['ping'] > 300 else + 5 if party['ping'] > 150 else 2) + party[ + 'next_ping_time'] += party['ping_interval'] * mult + + class PingThread(threading.Thread): + """Thread for sending out pings.""" + + def __init__(self, address: str, port: int, + call: Callable[[str, int, Optional[int]], + Optional[int]]): + super().__init__() + self._address = address + self._port = port + self._call = call + + def run(self) -> None: + # pylint: disable=too-many-branches + ba.app.ping_thread_count += 1 + 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) + sock.close() + ping = int((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) + ba.app.ping_thread_count -= 1 + + PingThread(party['address'], party['port'], + ba.WeakCall(self._ping_callback)).start() + + def _ping_callback(self, address: str, port: Optional[int], + result: Optional[int]) -> None: + # Look for a widget corresponding to this target. + # If we find one, update our list. + party = self._public_parties.get(address + '_' + str(port)) + if party is not None: + # We now smooth ping a bit to reduce jumping around in the list + # (only where pings are relatively good). + current_ping = party.get('ping') + if (current_ping is not None and result is not None + and result < 150): + smoothing = 0.7 + party['ping'] = int(smoothing * current_ping + + (1.0 - smoothing) * result) + else: + party['ping'] = result + + # This can happen if we switch away and then back to the + # client tab while pings are in flight. + if 'ping_widget' not in party: + pass + elif party['ping_widget']: + self._rebuild_public_party_list() + + def _internet_fetch_local_addr_cb(self, val: str) -> None: + self._internet_local_address = str(val) + + def _on_public_party_accessible_response( + self, data: Optional[Dict[str, Any]]) -> None: + + # If we've got status text widgets, update them. + text = self._internet_host_status_text + if text: + if data is None: + ba.textwidget( + edit=text, + text=ba.Lstr(resource='gatherWindow.' + 'partyStatusNoConnectionText'), + color=(1, 0, 0), + ) + else: + if not data.get('accessible', False): + ex_line: Union[str, ba.Lstr] + if self._internet_local_address is not None: + ex_line = ba.Lstr( + value='\n${A} ${B}', + subs=[('${A}', + ba.Lstr(resource='gatherWindow.' + 'manualYourLocalAddressText')), + ('${B}', self._internet_local_address)]) + else: + ex_line = '' + ba.textwidget( + edit=text, + text=ba.Lstr( + value='${A}\n${B}${C}', + subs=[('${A}', + ba.Lstr(resource='gatherWindow.' + 'partyStatusNotJoinableText')), + ('${B}', + ba.Lstr(resource='gatherWindow.' + 'manualRouterForwardingText', + subs=[('${PORT}', + str(_ba.get_game_port()))])), + ('${C}', ex_line)]), + color=(1, 0, 0)) + else: + ba.textwidget(edit=text, + text=ba.Lstr(resource='gatherWindow.' + 'partyStatusJoinableText'), + color=(0, 1, 0)) + + def _do_internet_status_check(self) -> None: + from ba.internal import master_server_get + ba.textwidget(edit=self._internet_host_status_text, + color=(1, 1, 0), + text=ba.Lstr(resource='gatherWindow.' + 'partyStatusCheckingText')) + master_server_get('bsAccessCheck', {'b': ba.app.build_number}, + callback=ba.WeakCall( + self._on_public_party_accessible_response)) + + def _on_start_internet_advertizing_press(self) -> None: + from bastd.ui.account import show_sign_in_prompt + from bastd.ui.purchase import PurchaseWindow + if _ba.get_account_state() != 'signed_in': + show_sign_in_prompt() + return + + # Requires sign-in and pro. + if self._is_internet_locked(): + if _ba.get_account_state() != 'signed_in': + show_sign_in_prompt() + else: + PurchaseWindow(items=['pro']) + return + + name = cast(str, ba.textwidget(query=self._internet_host_name_text)) + if name == '': + ba.screenmessage(ba.Lstr(resource='internal.invalidNameErrorText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + _ba.set_public_party_name(name) + cfg = ba.app.config + cfg['Public Party Name'] = name + cfg.commit() + ba.playsound(ba.getsound('shieldUp')) + _ba.set_public_party_enabled(True) + + # In GUI builds we want to authenticate clients only when hosting + # public parties. + _ba.set_authenticate_clients(True) + + self._do_internet_status_check() + ba.buttonwidget( + edit=self._internet_host_toggle_button, + label=ba.Lstr( + resource='gatherWindow.makePartyPrivateText', + fallback_resource='gatherWindow.stopAdvertisingText'), + on_activate_call=self._on_stop_internet_advertising_press) + + def _on_stop_internet_advertising_press(self) -> None: + _ba.set_public_party_enabled(False) + + # In GUI builds we want to authenticate clients only when hosting + # public parties. + _ba.set_authenticate_clients(False) + ba.playsound(ba.getsound('shieldDown')) + text = self._internet_host_status_text + if text: + ba.textwidget( + edit=text, + text=ba.Lstr(resource='gatherWindow.' + 'partyStatusNotPublicText'), + color=(0.6, 0.6, 0.6), + ) + ba.buttonwidget( + edit=self._internet_host_toggle_button, + label=ba.Lstr( + resource='gatherWindow.makePartyPublicText', + fallback_resource='gatherWindow.startAdvertisingText'), + on_activate_call=self._on_start_internet_advertizing_press) + + def _is_internet_locked(self) -> bool: + if _ba.get_account_misc_read_val('ilck', False): + return not ba.app.accounts.have_pro() + return False + + def _on_public_party_activate(self, party: Dict[str, Any]) -> None: + from bastd.ui.purchase import PurchaseWindow + from bastd.ui.account import show_sign_in_prompt + if party['queue'] is not None: + from bastd.ui.partyqueue import PartyQueueWindow + ba.playsound(ba.getsound('swish')) + PartyQueueWindow(party['queue'], party['address'], party['port']) + else: + address = party['address'] + port = party['port'] + if self._is_internet_locked(): + if _ba.get_account_state() != 'signed_in': + show_sign_in_prompt() + else: + PurchaseWindow(items=['pro']) + return + + # Rate limit this a bit. + now = time.time() + last_connect_time = self._last_public_party_connect_attempt_time + if last_connect_time is None or now - last_connect_time > 2.0: + _ba.connect_to_party(address, port=port) + self._last_public_party_connect_attempt_time = now + + def _set_public_party_selection(self, sel: Tuple[str, str]) -> None: + if self._refreshing_public_party_list: + return + self._public_party_list_selection = sel + + def _on_max_public_party_size_minus_press(self) -> None: + val = _ba.get_public_party_max_size() + val -= 1 + if val < 1: + val = 1 + _ba.set_public_party_max_size(val) + ba.textwidget(edit=self._internet_host_max_party_size_value, + text=str(val)) + + def _on_max_public_party_size_plus_press(self) -> None: + val = _ba.get_public_party_max_size() + val += 1 + _ba.set_public_party_max_size(val) + ba.textwidget(edit=self._internet_host_max_party_size_value, + text=str(val)) diff --git a/ballisticacore-cmake/.idea/dictionaries/ericf.xml b/ballisticacore-cmake/.idea/dictionaries/ericf.xml index 92811cfe..f47ef954 100644 --- a/ballisticacore-cmake/.idea/dictionaries/ericf.xml +++ b/ballisticacore-cmake/.idea/dictionaries/ericf.xml @@ -4,6 +4,7 @@ NOMINMAX aabb abcdefghijklmnopqrstuvwxyz + abouttab absval accel accountid @@ -360,6 +361,7 @@ gettotalrefcount gles glext + googleplaytab gpgs gqualstr grav @@ -491,6 +493,7 @@ magua mainmenu mallocs + manualtab maskhigh maskuv maximus @@ -535,6 +538,7 @@ mystatspage mywidget ndebug + nearbytab nearval needwindow negativex @@ -654,6 +658,7 @@ pton ptrs ptype + publictab pulseaudio punchmomentumlinear punchthrough @@ -815,6 +820,8 @@ symbolification syscalls tabdefs + tabtype + tabtypes talloc tegra telefonaktiebolaget diff --git a/docs/ba_module.md b/docs/ba_module.md index b169ace3..b844c4fc 100644 --- a/docs/ba_module.md +++ b/docs/ba_module.md @@ -1,5 +1,5 @@ -

last updated on 2020-10-19 for Ballistica version 1.5.27 build 20224

+

last updated on 2020-10-20 for Ballistica version 1.5.27 build 20224

This page documents the Python classes and functions in the 'ba' module, which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please let me know. Happy modding!


diff --git a/tools/efro/call.py b/tools/efro/call.py index f5bedfc2..89080206 100644 --- a/tools/efro/call.py +++ b/tools/efro/call.py @@ -49,7 +49,7 @@ class CallbackSet(Generic[CT]): # Define Call() which can be used in type-checking call-wrappers that behave # similarly to functools.partial (in that they take a callable and some -# positional arguments to be passed to it) +# positional arguments to be passed to it). # In type-checking land, We define several different _CallXArg classes # corresponding to different argument counts and define Call() as an @@ -61,7 +61,7 @@ class CallbackSet(Generic[CT]): # class _MyCallWrapper: # # if TYPE_CHECKING: -# MyCallWrapper = bafoundation.executils.Call +# MyCallWrapper = efro.call.Call # else: # MyCallWrapper = _MyCallWrapper @@ -196,7 +196,9 @@ if TYPE_CHECKING: # 2 arg call; no args bundled. # noinspection PyPep8Naming @overload - def Call(call: Callable[[In1T, In2T], OutT]) -> _CallNoArgs[OutT]: + def Call( + call: Callable[[In1T, In2T], + OutT]) -> _Call2Args[In1T, In2T, OutT]: ... # 3 arg call; 3 args bundled.