Split Gather UI code out into more manageable chunks

This commit is contained in:
Eric Froemling 2020-10-21 15:36:25 -07:00
parent 6bcd827ced
commit b248d743f0
16 changed files with 2172 additions and 1978 deletions

View File

@ -17,6 +17,7 @@
<w>abeb</w>
<w>abishort</w>
<w>abot</w>
<w>abouttab</w>
<w>abtn</w>
<w>accesstime</w>
<w>accountname</w>
@ -877,6 +878,7 @@
<w>gnode</w>
<w>goles</w>
<w>goodlist</w>
<w>googleplaytab</w>
<w>googlevr</w>
<w>goosey</w>
<w>gotresponse</w>
@ -1209,6 +1211,7 @@
<w>malformatted</w>
<w>mallimportedby</w>
<w>mandir</w>
<w>manualtab</w>
<w>mapdata</w>
<w>mapdef</w>
<w>mapdefs</w>
@ -1340,6 +1343,7 @@
<w>ncpu</w>
<w>ndbm</w>
<w>ndkpath</w>
<w>nearbytab</w>
<w>neededsettings</w>
<w>ness</w>
<w>netlink</w>
@ -1607,6 +1611,7 @@
<w>pthreads</w>
<w>ptrans</w>
<w>ptype</w>
<w>publictab</w>
<w>pubsync</w>
<w>pucknode</w>
<w>pulllist</w>
@ -2010,6 +2015,8 @@
<w>sysctl</w>
<w>syslogmodule</w>
<w>tabdefs</w>
<w>tabtype</w>
<w>tabtypes</w>
<w>tabval</w>
<w>tagargs</w>
<w>tagversion</w>

View File

@ -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.

View File

@ -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",

View File

@ -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 \

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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())

View File

@ -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()

View File

@ -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."""

View File

@ -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)

View File

@ -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,
)

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,7 @@
<w>NOMINMAX</w>
<w>aabb</w>
<w>abcdefghijklmnopqrstuvwxyz</w>
<w>abouttab</w>
<w>absval</w>
<w>accel</w>
<w>accountid</w>
@ -360,6 +361,7 @@
<w>gettotalrefcount</w>
<w>gles</w>
<w>glext</w>
<w>googleplaytab</w>
<w>gpgs</w>
<w>gqualstr</w>
<w>grav</w>
@ -491,6 +493,7 @@
<w>magua</w>
<w>mainmenu</w>
<w>mallocs</w>
<w>manualtab</w>
<w>maskhigh</w>
<w>maskuv</w>
<w>maximus</w>
@ -535,6 +538,7 @@
<w>mystatspage</w>
<w>mywidget</w>
<w>ndebug</w>
<w>nearbytab</w>
<w>nearval</w>
<w>needwindow</w>
<w>negativex</w>
@ -654,6 +658,7 @@
<w>pton</w>
<w>ptrs</w>
<w>ptype</w>
<w>publictab</w>
<w>pulseaudio</w>
<w>punchmomentumlinear</w>
<w>punchthrough</w>
@ -815,6 +820,8 @@
<w>symbolification</w>
<w>syscalls</w>
<w>tabdefs</w>
<w>tabtype</w>
<w>tabtypes</w>
<w>talloc</w>
<w>tegra</w>
<w>telefonaktiebolaget</w>

View File

@ -1,5 +1,5 @@
<!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND -->
<h4><em>last updated on 2020-10-19 for Ballistica version 1.5.27 build 20224</em></h4>
<h4><em>last updated on 2020-10-20 for Ballistica version 1.5.27 build 20224</em></h4>
<p>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 <a href="mailto:support@froemling.net">let me know</a>. Happy modding!</p>
<hr>

View File

@ -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:
# <runtime class defined here>
# 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.