bauiv1lib.gather.privatetab
Defines the Private tab in the gather UI.
1# Released under the MIT License. See LICENSE for details. 2# 3# pylint: disable=too-many-lines 4"""Defines the Private tab in the gather UI.""" 5 6from __future__ import annotations 7 8import os 9import copy 10import time 11import logging 12from enum import Enum 13from dataclasses import dataclass 14from typing import TYPE_CHECKING, cast, override 15 16from efro.error import CommunicationError 17from efro.dataclassio import dataclass_from_dict, dataclass_to_dict 18import bacommon.cloud 19from bacommon.net import ( 20 PrivateHostingState, 21 PrivateHostingConfig, 22 PrivatePartyConnectResult, 23) 24from bauiv1lib.gather import GatherTab 25 26from bauiv1lib.gettokens import GetTokensWindow, show_get_tokens_prompt 27import bascenev1 as bs 28import bauiv1 as bui 29 30if TYPE_CHECKING: 31 from typing import Any 32 33 from bauiv1lib.gather import GatherWindow 34 35 36# Print a bit of info about queries, etc. 37DEBUG_SERVER_COMMUNICATION = os.environ.get('BA_DEBUG_PPTABCOM') == '1' 38 39 40class SubTabType(Enum): 41 """Available sub-tabs.""" 42 43 JOIN = 'join' 44 HOST = 'host' 45 46 47@dataclass 48class State: 49 """Our core state that persists while the app is running.""" 50 51 sub_tab: SubTabType = SubTabType.JOIN 52 53 54class PrivateGatherTab(GatherTab): 55 """The private tab in the gather UI""" 56 57 def __init__(self, window: GatherWindow) -> None: 58 super().__init__(window) 59 self._container: bui.Widget | None = None 60 self._state: State = State() 61 self._last_datacode_refresh_time: float | None = None 62 self._hostingstate = PrivateHostingState() 63 self._v2state: bacommon.cloud.BSPrivatePartyResponse | None = None 64 self._join_sub_tab_text: bui.Widget | None = None 65 self._host_sub_tab_text: bui.Widget | None = None 66 self._update_timer: bui.AppTimer | None = None 67 self._join_party_code_text: bui.Widget | None = None 68 self._c_width: float = 0.0 69 self._c_height: float = 0.0 70 self._last_hosting_state_query_time: float | None = None 71 self._last_v2_state_query_time: float | None = None 72 self._waiting_for_initial_state = True 73 self._waiting_for_start_stop_response = True 74 self._host_playlist_button: bui.Widget | None = None 75 self._host_copy_button: bui.Widget | None = None 76 self._host_connect_button: bui.Widget | None = None 77 self._host_start_stop_button: bui.Widget | None = None 78 self._get_tokens_button: bui.Widget | None = None 79 self._token_count_text: bui.Widget | None = None 80 self._showing_not_signed_in_screen = False 81 self._create_time = time.time() 82 self._last_action_send_time: float | None = None 83 self._connect_press_time: float | None = None 84 try: 85 self._hostingconfig = self._build_hosting_config() 86 except Exception: 87 logging.exception('Error building hosting config.') 88 self._hostingconfig = PrivateHostingConfig() 89 90 @override 91 def on_activate( 92 self, 93 parent_widget: bui.Widget, 94 tab_button: bui.Widget, 95 region_width: float, 96 region_height: float, 97 region_left: float, 98 region_bottom: float, 99 ) -> bui.Widget: 100 self._c_width = region_width 101 self._c_height = region_height - 20 102 self._container = bui.containerwidget( 103 parent=parent_widget, 104 position=( 105 region_left, 106 region_bottom + (region_height - self._c_height) * 0.5, 107 ), 108 size=(self._c_width, self._c_height), 109 background=False, 110 selection_loops_to_parent=True, 111 ) 112 v = self._c_height - 30.0 113 self._join_sub_tab_text = bui.textwidget( 114 parent=self._container, 115 position=(self._c_width * 0.5 - 245, v - 13), 116 color=(0.6, 1.0, 0.6), 117 scale=1.3, 118 size=(200, 30), 119 maxwidth=250, 120 h_align='left', 121 v_align='center', 122 click_activate=True, 123 selectable=True, 124 autoselect=True, 125 on_activate_call=lambda: self._set_sub_tab( 126 SubTabType.JOIN, 127 playsound=True, 128 ), 129 text=bui.Lstr(resource='gatherWindow.privatePartyJoinText'), 130 glow_type='uniform', 131 ) 132 self._host_sub_tab_text = bui.textwidget( 133 parent=self._container, 134 position=(self._c_width * 0.5 + 45, v - 13), 135 color=(0.6, 1.0, 0.6), 136 scale=1.3, 137 size=(200, 30), 138 maxwidth=250, 139 h_align='left', 140 v_align='center', 141 click_activate=True, 142 selectable=True, 143 autoselect=True, 144 on_activate_call=lambda: self._set_sub_tab( 145 SubTabType.HOST, 146 playsound=True, 147 ), 148 text=bui.Lstr(resource='gatherWindow.privatePartyHostText'), 149 glow_type='uniform', 150 ) 151 bui.widget(edit=self._join_sub_tab_text, up_widget=tab_button) 152 bui.widget( 153 edit=self._host_sub_tab_text, 154 left_widget=self._join_sub_tab_text, 155 up_widget=tab_button, 156 ) 157 bui.widget( 158 edit=self._join_sub_tab_text, right_widget=self._host_sub_tab_text 159 ) 160 161 self._update_timer = bui.AppTimer( 162 1.0, bui.WeakCall(self._update), repeat=True 163 ) 164 165 # Prevent taking any action until we've updated our state. 166 self._waiting_for_initial_state = True 167 168 # Force some immediate refreshes. 169 self._last_datacode_refresh_time = None 170 self._last_v2_state_query_time = None 171 self._last_action_send_time = None # Ensure we don't ignore response. 172 self._last_hosting_state_query_time = None 173 self._update() 174 175 self._set_sub_tab(self._state.sub_tab) 176 177 return self._container 178 179 def _build_hosting_config(self) -> PrivateHostingConfig: 180 # pylint: disable=too-many-branches 181 # pylint: disable=too-many-locals 182 from bauiv1lib.playlist import PlaylistTypeVars 183 from bascenev1 import filter_playlist 184 185 hcfg = PrivateHostingConfig() 186 cfg = bui.app.config 187 sessiontypestr = cfg.get('Private Party Host Session Type', 'ffa') 188 if not isinstance(sessiontypestr, str): 189 raise RuntimeError(f'Invalid sessiontype {sessiontypestr}') 190 hcfg.session_type = sessiontypestr 191 192 sessiontype: type[bs.Session] 193 if hcfg.session_type == 'ffa': 194 sessiontype = bs.FreeForAllSession 195 elif hcfg.session_type == 'teams': 196 sessiontype = bs.DualTeamSession 197 else: 198 raise RuntimeError(f'Invalid sessiontype: {hcfg.session_type}') 199 pvars = PlaylistTypeVars(sessiontype) 200 201 playlist_name = bui.app.config.get( 202 f'{pvars.config_name} Playlist Selection' 203 ) 204 if not isinstance(playlist_name, str): 205 playlist_name = '__default__' 206 hcfg.playlist_name = ( 207 pvars.default_list_name.evaluate() 208 if playlist_name == '__default__' 209 else playlist_name 210 ) 211 212 playlist: list[dict[str, Any]] | None = None 213 if playlist_name != '__default__': 214 playlist = cfg.get(f'{pvars.config_name} Playlists', {}).get( 215 playlist_name 216 ) 217 if playlist is None: 218 playlist = pvars.get_default_list_call() 219 220 hcfg.playlist = filter_playlist( 221 playlist, sessiontype, name=playlist_name 222 ) 223 224 randomize = cfg.get(f'{pvars.config_name} Playlist Randomize') 225 if not isinstance(randomize, bool): 226 randomize = False 227 hcfg.randomize = randomize 228 229 tutorial = cfg.get('Show Tutorial') 230 if not isinstance(tutorial, bool): 231 tutorial = True 232 hcfg.tutorial = tutorial 233 234 if hcfg.session_type == 'teams': 235 ctn: list[str] | None = cfg.get('Custom Team Names') 236 if ctn is not None: 237 ctn_any: Any = ctn # Actual value may not match type checker. 238 if ( 239 isinstance(ctn_any, (list, tuple)) 240 and len(ctn) == 2 241 and all(isinstance(x, str) for x in ctn) 242 ): 243 hcfg.custom_team_names = (ctn[0], ctn[1]) 244 else: 245 print(f'Found invalid custom-team-names data: {ctn}') 246 247 ctc: list[list[float]] | None = cfg.get('Custom Team Colors') 248 if ctc is not None: 249 ctc_any: Any = ctc # Actual value may not match type checker. 250 if ( 251 isinstance(ctc_any, (list, tuple)) 252 and len(ctc) == 2 253 and all(isinstance(x, (list, tuple)) for x in ctc) 254 and all(len(x) == 3 for x in ctc) 255 ): 256 hcfg.custom_team_colors = ( 257 (ctc[0][0], ctc[0][1], ctc[0][2]), 258 (ctc[1][0], ctc[1][1], ctc[1][2]), 259 ) 260 else: 261 print(f'Found invalid custom-team-colors data: {ctc}') 262 263 return hcfg 264 265 @override 266 def on_deactivate(self) -> None: 267 self._update_timer = None 268 269 def _update_currency_ui(self) -> None: 270 # Keep currency count up to date if applicable. 271 plus = bui.app.plus 272 assert plus is not None 273 274 if self._v2state is not None: 275 t_str = str(self._v2state.tokens) 276 else: 277 t_str = '-' 278 279 if self._get_tokens_button: 280 bui.buttonwidget( 281 edit=self._get_tokens_button, 282 label=bui.charstr(bui.SpecialChar.TOKEN) + t_str, 283 ) 284 if self._token_count_text: 285 bui.textwidget( 286 edit=self._token_count_text, 287 text=bui.charstr(bui.SpecialChar.TOKEN) + t_str, 288 ) 289 290 def _update(self) -> None: 291 """Periodic updating.""" 292 293 plus = bui.app.plus 294 assert plus is not None 295 296 now = bui.apptime() 297 298 self._update_currency_ui() 299 300 if self._state.sub_tab is SubTabType.HOST: 301 # If we're not signed in, just refresh to show that. 302 if ( 303 plus.get_v1_account_state() != 'signed_in' 304 or plus.accounts.primary is None 305 ) and not self._showing_not_signed_in_screen: 306 self._refresh_sub_tab() 307 else: 308 # Query an updated v1 state periodically. 309 if ( 310 self._last_hosting_state_query_time is None 311 or now - self._last_hosting_state_query_time > 15.0 312 ): 313 self._debug_server_comm('querying private party state') 314 if plus.get_v1_account_state() == 'signed_in': 315 plus.add_v1_account_transaction( 316 { 317 'type': 'PRIVATE_PARTY_QUERY', 318 'expire_time': time.time() + 20, 319 }, 320 callback=bui.WeakCall( 321 self._idle_hosting_state_response 322 ), 323 ) 324 plus.run_v1_account_transactions() 325 else: 326 self._idle_hosting_state_response(None) 327 self._last_hosting_state_query_time = now 328 329 # Query an updated v2 state periodically. 330 if ( 331 self._last_v2_state_query_time is None 332 or now - self._last_v2_state_query_time > 12.0 333 ): 334 self._debug_server_comm('querying pp v2 state') 335 336 if plus.accounts.primary is not None: 337 with plus.accounts.primary: 338 plus.cloud.send_message_cb( 339 bacommon.cloud.BSPrivatePartyMessage( 340 need_datacode=( 341 self._last_datacode_refresh_time is None 342 or time.monotonic() 343 - self._last_datacode_refresh_time 344 > 30.0 345 ) 346 ), 347 on_response=bui.WeakCall( 348 self._on_private_party_query_response 349 ), 350 ) 351 352 self._last_v2_state_query_time = now 353 354 def _on_private_party_query_response( 355 self, response: bacommon.cloud.BSPrivatePartyResponse | Exception 356 ) -> None: 357 if isinstance(response, Exception): 358 self._debug_server_comm('got pp v2 state response (err)') 359 # We expect comm errors sometimes. Make noise on anything else. 360 if not isinstance(response, CommunicationError): 361 logging.exception('Error on private-party-query-response') 362 return 363 364 # Ignore if something went wrong server-side. 365 if not response.success: 366 self._debug_server_comm('got pp v2 state response (serverside err)') 367 return 368 369 self._debug_server_comm('got pp v2 state response') 370 371 existing_datacode = ( 372 None if self._v2state is None else self._v2state.datacode 373 ) 374 375 self._v2state = response 376 if self._v2state.datacode is None: 377 # We don't fetch datacode each time; preserve our existing 378 # if we didn't. 379 self._v2state.datacode = existing_datacode 380 else: 381 # If we *did* fetch it, note the time. 382 self._last_datacode_refresh_time = time.monotonic() 383 384 def _idle_hosting_state_response( 385 self, result: dict[str, Any] | None 386 ) -> None: 387 # This simply passes through to our standard response handler. 388 # The one exception is if we've recently sent an action to the 389 # server (start/stop hosting/etc.) In that case we want to 390 # ignore idle background updates and wait for the response to 391 # our action. (this keeps the button showing 'one moment...' 392 # until the change takes effect, etc.) 393 if ( 394 self._last_action_send_time is not None 395 and time.time() - self._last_action_send_time < 5.0 396 ): 397 self._debug_server_comm( 398 'ignoring private party state response due to recent action' 399 ) 400 return 401 self._hosting_state_response(result) 402 403 def _hosting_state_response(self, result: dict[str, Any] | None) -> None: 404 # Its possible for this to come back to us after our UI is dead; 405 # ignore in that case. 406 if not self._container: 407 return 408 409 state: PrivateHostingState | None = None 410 if result is not None: 411 self._debug_server_comm('got private party state response') 412 try: 413 state = dataclass_from_dict( 414 PrivateHostingState, result, discard_unknown_attrs=True 415 ) 416 except Exception: 417 logging.exception('Got invalid PrivateHostingState data') 418 else: 419 self._debug_server_comm('private party state response errored') 420 421 # Hmm I guess let's just ignore failed responses?... Or should 422 # we show some sort of error state to the user?... 423 if result is None or state is None: 424 return 425 426 self._waiting_for_initial_state = False 427 self._waiting_for_start_stop_response = False 428 self._hostingstate = state 429 self._refresh_sub_tab() 430 431 def _set_sub_tab(self, value: SubTabType, playsound: bool = False) -> None: 432 assert self._container 433 if playsound: 434 bui.getsound('click01').play() 435 436 # If switching from join to host, force some refreshes. 437 if self._state.sub_tab is SubTabType.JOIN and value is SubTabType.HOST: 438 # Prevent taking any action until we've gotten a fresh 439 # state. 440 self._waiting_for_initial_state = True 441 442 # Get some refreshes going immediately. 443 self._last_hosting_state_query_time = None 444 self._last_action_send_time = None # So we don't ignore response. 445 self._last_datacode_refresh_time = None 446 self._last_v2_state_query_time = None 447 self._update() 448 449 self._state.sub_tab = value 450 active_color = (0.6, 1.0, 0.6) 451 inactive_color = (0.5, 0.4, 0.5) 452 bui.textwidget( 453 edit=self._join_sub_tab_text, 454 color=active_color if value is SubTabType.JOIN else inactive_color, 455 ) 456 bui.textwidget( 457 edit=self._host_sub_tab_text, 458 color=active_color if value is SubTabType.HOST else inactive_color, 459 ) 460 461 self._refresh_sub_tab() 462 463 # Kick off an update to get any needed messages sent/etc. 464 bui.pushcall(self._update) 465 466 def _selwidgets(self) -> list[bui.Widget | None]: 467 """An indexed list of widgets we can use for saving/restoring sel.""" 468 return [ 469 self._host_playlist_button, 470 self._host_copy_button, 471 self._host_connect_button, 472 self._host_start_stop_button, 473 self._get_tokens_button, 474 ] 475 476 def _refresh_sub_tab(self) -> None: 477 assert self._container 478 479 # Store an index for our current selection so we can reselect 480 # the equivalent recreated widget if possible. 481 selindex: int | None = None 482 selchild = self._container.get_selected_child() 483 if selchild is not None: 484 try: 485 selindex = self._selwidgets().index(selchild) 486 except ValueError: 487 pass 488 489 # Clear anything existing in the old sub-tab. 490 for widget in self._container.get_children(): 491 if widget and widget not in { 492 self._host_sub_tab_text, 493 self._join_sub_tab_text, 494 }: 495 widget.delete() 496 497 if self._state.sub_tab is SubTabType.JOIN: 498 self._build_join_tab() 499 elif self._state.sub_tab is SubTabType.HOST: 500 self._build_host_tab() 501 else: 502 raise RuntimeError('Invalid state.') 503 504 # Select the new equivalent widget if there is one. 505 if selindex is not None: 506 selwidget = self._selwidgets()[selindex] 507 if selwidget: 508 bui.containerwidget( 509 edit=self._container, selected_child=selwidget 510 ) 511 512 def _build_join_tab(self) -> None: 513 bui.textwidget( 514 parent=self._container, 515 position=(self._c_width * 0.5, self._c_height - 140), 516 color=(0.5, 0.46, 0.5), 517 scale=1.5, 518 size=(0, 0), 519 maxwidth=250, 520 h_align='center', 521 v_align='center', 522 text=bui.Lstr(resource='gatherWindow.partyCodeText'), 523 ) 524 525 self._join_party_code_text = bui.textwidget( 526 parent=self._container, 527 position=(self._c_width * 0.5 - 150, self._c_height - 250), 528 flatness=1.0, 529 scale=1.5, 530 size=(300, 50), 531 editable=True, 532 max_chars=20, 533 description=bui.Lstr(resource='gatherWindow.partyCodeText'), 534 autoselect=True, 535 h_align='left', 536 v_align='center', 537 text='', 538 ) 539 btn = bui.buttonwidget( 540 parent=self._container, 541 size=(300, 70), 542 label=bui.Lstr(resource='gatherWindow.' 'manualConnectText'), 543 position=(self._c_width * 0.5 - 150, self._c_height - 350), 544 on_activate_call=self._join_connect_press, 545 autoselect=True, 546 ) 547 bui.textwidget( 548 edit=self._join_party_code_text, on_return_press_call=btn.activate 549 ) 550 551 def _on_get_tokens_press(self) -> None: 552 if self._waiting_for_start_stop_response: 553 return 554 555 # Bring up get-tickets window and then kill ourself (we're on 556 # the overlay layer so we'd show up above it). 557 GetTokensWindow(origin_widget=self._get_tokens_button) 558 559 def _build_host_tab(self) -> None: 560 # pylint: disable=too-many-branches 561 # pylint: disable=too-many-statements 562 assert bui.app.classic is not None 563 564 plus = bui.app.plus 565 assert plus is not None 566 567 hostingstate = self._hostingstate 568 569 havegoldpass = self._v2state is not None and self._v2state.gold_pass 570 571 # We use both v1 and v2 account functionality here (sigh). So 572 # make sure we're signed in on both ends. 573 574 # Make sure the V1 side is good to go. 575 if plus.get_v1_account_state() != 'signed_in': 576 bui.textwidget( 577 parent=self._container, 578 size=(0, 0), 579 h_align='center', 580 v_align='center', 581 maxwidth=self._c_width * 0.8, 582 scale=0.8, 583 color=(0.6, 0.56, 0.6), 584 position=(self._c_width * 0.5, self._c_height * 0.5), 585 text=bui.Lstr(resource='notSignedInErrorText'), 586 ) 587 self._showing_not_signed_in_screen = True 588 return 589 590 # Make sure the V2 side is good to go. 591 if plus.accounts.primary is None: 592 bui.textwidget( 593 parent=self._container, 594 size=(0, 0), 595 h_align='center', 596 v_align='center', 597 maxwidth=self._c_width * 0.8, 598 scale=0.8, 599 color=(0.6, 0.56, 0.6), 600 position=(self._c_width * 0.5, self._c_height * 0.5), 601 text=bui.Lstr(resource='v2AccountRequiredText'), 602 ) 603 self._showing_not_signed_in_screen = True 604 return 605 606 self._showing_not_signed_in_screen = False 607 608 # At first we don't want to show anything until we've gotten a 609 # state. Update: In this situation we now simply show our 610 # existing state but give the start/stop button a loading 611 # message and disallow its use. This keeps things a lot less 612 # jumpy looking and allows selecting playlists/etc without 613 # having to wait for the server each time back to the ui. 614 if self._waiting_for_initial_state and bool(False): 615 bui.textwidget( 616 parent=self._container, 617 size=(0, 0), 618 h_align='center', 619 v_align='center', 620 maxwidth=200, 621 scale=0.8, 622 color=(0.6, 0.56, 0.6), 623 position=(self._c_width * 0.5, self._c_height * 0.5), 624 text=bui.Lstr( 625 value='${A}...', 626 subs=[('${A}', bui.Lstr(resource='store.loadingText'))], 627 ), 628 ) 629 return 630 631 # If we're not currently hosting and hosting requires tokens, 632 # Show our count (possibly with a link to purchase more). 633 if ( 634 not self._waiting_for_initial_state 635 and hostingstate.party_code is None 636 and hostingstate.tickets_to_host_now != 0 637 and not havegoldpass 638 ): 639 if not bui.app.ui_v1.use_toolbars: 640 641 # Currently have no allow_token_purchases value like 642 # we had with tickets; just assuming we always allow. 643 if bool(True): 644 # if bui.app.classic.allow_ticket_purchases: 645 self._get_tokens_button = bui.buttonwidget( 646 parent=self._container, 647 position=( 648 self._c_width - 210 + 125, 649 self._c_height - 44, 650 ), 651 autoselect=True, 652 scale=0.6, 653 size=(120, 60), 654 textcolor=(1.0, 0.6, 0.0), 655 label=bui.charstr(bui.SpecialChar.TOKEN), 656 color=(0.65, 0.5, 0.8), 657 on_activate_call=self._on_get_tokens_press, 658 ) 659 else: 660 self._token_count_text = bui.textwidget( 661 parent=self._container, 662 scale=0.6, 663 position=( 664 self._c_width - 210 + 125, 665 self._c_height - 44, 666 ), 667 color=(1.0, 0.6, 0.0), 668 h_align='center', 669 v_align='center', 670 ) 671 672 # Set initial token count. 673 self._update_currency_ui() 674 675 v = self._c_height - 90 676 if hostingstate.party_code is None: 677 bui.textwidget( 678 parent=self._container, 679 size=(0, 0), 680 h_align='center', 681 v_align='center', 682 maxwidth=self._c_width * 0.9, 683 scale=0.7, 684 flatness=1.0, 685 color=(0.5, 0.46, 0.5), 686 position=(self._c_width * 0.5, v), 687 text=bui.Lstr( 688 resource='gatherWindow.privatePartyCloudDescriptionText' 689 ), 690 ) 691 692 v -= 100 693 if hostingstate.party_code is None: 694 # We've got no current party running; show options to set 695 # one up. 696 bui.textwidget( 697 parent=self._container, 698 size=(0, 0), 699 h_align='right', 700 v_align='center', 701 maxwidth=200, 702 scale=0.8, 703 color=(0.6, 0.56, 0.6), 704 position=(self._c_width * 0.5 - 210, v), 705 text=bui.Lstr(resource='playlistText'), 706 ) 707 self._host_playlist_button = bui.buttonwidget( 708 parent=self._container, 709 size=(400, 70), 710 color=(0.6, 0.5, 0.6), 711 textcolor=(0.8, 0.75, 0.8), 712 label=self._hostingconfig.playlist_name, 713 on_activate_call=self._playlist_press, 714 position=(self._c_width * 0.5 - 200, v - 35), 715 up_widget=self._host_sub_tab_text, 716 autoselect=True, 717 ) 718 719 # If it appears we're coming back from playlist selection, 720 # re-select our playlist button. 721 if bui.app.ui_v1.selecting_private_party_playlist: 722 bui.containerwidget( 723 edit=self._container, 724 selected_child=self._host_playlist_button, 725 ) 726 bui.app.ui_v1.selecting_private_party_playlist = False 727 else: 728 # We've got a current party; show its info. 729 bui.textwidget( 730 parent=self._container, 731 size=(0, 0), 732 h_align='center', 733 v_align='center', 734 maxwidth=600, 735 scale=0.9, 736 color=(0.7, 0.64, 0.7), 737 position=(self._c_width * 0.5, v + 90), 738 text=bui.Lstr(resource='gatherWindow.partyServerRunningText'), 739 ) 740 bui.textwidget( 741 parent=self._container, 742 size=(0, 0), 743 h_align='center', 744 v_align='center', 745 maxwidth=600, 746 scale=0.7, 747 color=(0.7, 0.64, 0.7), 748 position=(self._c_width * 0.5, v + 50), 749 text=bui.Lstr(resource='gatherWindow.partyCodeText'), 750 ) 751 bui.textwidget( 752 parent=self._container, 753 size=(0, 0), 754 h_align='center', 755 v_align='center', 756 scale=2.0, 757 color=(0.0, 1.0, 0.0), 758 position=(self._c_width * 0.5, v + 10), 759 text=hostingstate.party_code, 760 ) 761 762 # Also action buttons to copy it and connect to it. 763 if bui.clipboard_is_supported(): 764 cbtnoffs = 10 765 self._host_copy_button = bui.buttonwidget( 766 parent=self._container, 767 size=(140, 40), 768 color=(0.6, 0.5, 0.6), 769 textcolor=(0.8, 0.75, 0.8), 770 label=bui.Lstr(resource='gatherWindow.copyCodeText'), 771 on_activate_call=self._host_copy_press, 772 position=(self._c_width * 0.5 - 150, v - 70), 773 autoselect=True, 774 ) 775 else: 776 cbtnoffs = -70 777 self._host_connect_button = bui.buttonwidget( 778 parent=self._container, 779 size=(140, 40), 780 color=(0.6, 0.5, 0.6), 781 textcolor=(0.8, 0.75, 0.8), 782 label=bui.Lstr(resource='gatherWindow.manualConnectText'), 783 on_activate_call=self._host_connect_press, 784 position=(self._c_width * 0.5 + cbtnoffs, v - 70), 785 autoselect=True, 786 ) 787 788 v -= 120 789 790 # Line above the main action button: 791 792 # If we don't want to show anything until we get a state: 793 if self._waiting_for_initial_state: 794 pass 795 elif hostingstate.unavailable_error is not None: 796 # If hosting is unavailable, show the associated reason. 797 bui.textwidget( 798 parent=self._container, 799 size=(0, 0), 800 h_align='center', 801 v_align='center', 802 maxwidth=self._c_width * 0.9, 803 scale=0.7, 804 flatness=1.0, 805 color=(1.0, 0.0, 0.0), 806 position=(self._c_width * 0.5, v), 807 text=bui.Lstr( 808 translate=( 809 'serverResponses', 810 hostingstate.unavailable_error, 811 ) 812 ), 813 ) 814 elif havegoldpass: 815 # If we have a gold pass, none of the 816 # timing/free-server-availability info below is relevant to 817 # us. 818 pass 819 elif hostingstate.free_host_minutes_remaining is not None: 820 # If we've been pre-approved to start/stop for free, show 821 # that. 822 bui.textwidget( 823 parent=self._container, 824 size=(0, 0), 825 h_align='center', 826 v_align='center', 827 maxwidth=self._c_width * 0.9, 828 scale=0.7, 829 flatness=1.0, 830 color=( 831 (0.7, 0.64, 0.7) 832 if hostingstate.party_code 833 else (0.0, 1.0, 0.0) 834 ), 835 position=(self._c_width * 0.5, v), 836 text=bui.Lstr( 837 resource='gatherWindow.startStopHostingMinutesText', 838 subs=[ 839 ( 840 '${MINUTES}', 841 f'{hostingstate.free_host_minutes_remaining:.0f}', 842 ) 843 ], 844 ), 845 ) 846 else: 847 # Otherwise tell whether the free cloud server is available 848 # or will be at some point. 849 if hostingstate.party_code is None: 850 if hostingstate.tickets_to_host_now == 0: 851 bui.textwidget( 852 parent=self._container, 853 size=(0, 0), 854 h_align='center', 855 v_align='center', 856 maxwidth=self._c_width * 0.9, 857 scale=0.7, 858 flatness=1.0, 859 color=(0.0, 1.0, 0.0), 860 position=(self._c_width * 0.5, v), 861 text=bui.Lstr( 862 resource=( 863 'gatherWindow.freeCloudServerAvailableNowText' 864 ) 865 ), 866 ) 867 else: 868 if hostingstate.minutes_until_free_host is None: 869 bui.textwidget( 870 parent=self._container, 871 size=(0, 0), 872 h_align='center', 873 v_align='center', 874 maxwidth=self._c_width * 0.9, 875 scale=0.7, 876 flatness=1.0, 877 color=(1.0, 0.6, 0.0), 878 position=(self._c_width * 0.5, v), 879 text=bui.Lstr( 880 resource=( 881 'gatherWindow' 882 '.freeCloudServerNotAvailableText' 883 ) 884 ), 885 ) 886 else: 887 availmins = hostingstate.minutes_until_free_host 888 bui.textwidget( 889 parent=self._container, 890 size=(0, 0), 891 h_align='center', 892 v_align='center', 893 maxwidth=self._c_width * 0.9, 894 scale=0.7, 895 flatness=1.0, 896 color=(1.0, 0.6, 0.0), 897 position=(self._c_width * 0.5, v), 898 text=bui.Lstr( 899 resource='gatherWindow.' 900 'freeCloudServerAvailableMinutesText', 901 subs=[('${MINUTES}', f'{availmins:.0f}')], 902 ), 903 ) 904 905 v -= 100 906 907 if ( 908 self._waiting_for_start_stop_response 909 or self._waiting_for_initial_state 910 ): 911 btnlabel = bui.Lstr(resource='oneMomentText') 912 else: 913 if hostingstate.unavailable_error is not None: 914 btnlabel = bui.Lstr( 915 resource='gatherWindow.hostingUnavailableText' 916 ) 917 elif hostingstate.party_code is None: 918 ticon = bui.charstr(bui.SpecialChar.TOKEN) 919 nowtokens = hostingstate.tokens_to_host_now 920 if nowtokens > 0 and not havegoldpass: 921 btnlabel = bui.Lstr( 922 resource='gatherWindow.startHostingPaidText', 923 subs=[('${COST}', f'{ticon}{nowtokens}')], 924 ) 925 else: 926 btnlabel = bui.Lstr( 927 resource='gatherWindow.startHostingText' 928 ) 929 else: 930 btnlabel = bui.Lstr(resource='gatherWindow.stopHostingText') 931 932 disabled = ( 933 hostingstate.unavailable_error is not None 934 or self._waiting_for_initial_state 935 ) 936 waiting = self._waiting_for_start_stop_response 937 self._host_start_stop_button = bui.buttonwidget( 938 parent=self._container, 939 size=(400, 80), 940 color=( 941 (0.6, 0.6, 0.6) 942 if disabled 943 else (0.5, 1.0, 0.5) if waiting else None 944 ), 945 enable_sound=False, 946 label=btnlabel, 947 textcolor=((0.7, 0.7, 0.7) if disabled else None), 948 position=(self._c_width * 0.5 - 200, v), 949 on_activate_call=self._start_stop_button_press, 950 autoselect=True, 951 ) 952 953 def _playlist_press(self) -> None: 954 assert self._host_playlist_button is not None 955 self.window.playlist_select(origin_widget=self._host_playlist_button) 956 957 def _host_copy_press(self) -> None: 958 assert self._hostingstate.party_code is not None 959 bui.clipboard_set_text(self._hostingstate.party_code) 960 bui.screenmessage(bui.Lstr(resource='gatherWindow.copyCodeConfirmText')) 961 962 def _host_connect_press(self) -> None: 963 assert self._hostingstate.party_code is not None 964 self._connect_to_party_code(self._hostingstate.party_code) 965 966 def _debug_server_comm(self, msg: str) -> None: 967 if DEBUG_SERVER_COMMUNICATION: 968 print( 969 f'PPTABCOM: {msg} at time ' 970 f'{time.time()-self._create_time:.2f}' 971 ) 972 973 def _connect_to_party_code(self, code: str) -> None: 974 # Ignore attempted followup sends for a few seconds. (this will 975 # reset if we get a response) 976 plus = bui.app.plus 977 assert plus is not None 978 979 now = time.time() 980 if ( 981 self._connect_press_time is not None 982 and now - self._connect_press_time < 5.0 983 ): 984 self._debug_server_comm( 985 'not sending private party connect (too soon)' 986 ) 987 return 988 self._connect_press_time = now 989 990 self._debug_server_comm('sending private party connect') 991 plus.add_v1_account_transaction( 992 { 993 'type': 'PRIVATE_PARTY_CONNECT', 994 'expire_time': time.time() + 20, 995 'code': code, 996 }, 997 callback=bui.WeakCall(self._connect_response), 998 ) 999 plus.run_v1_account_transactions() 1000 1001 def _start_stop_button_press(self) -> None: 1002 plus = bui.app.plus 1003 assert plus is not None 1004 if ( 1005 self._waiting_for_start_stop_response 1006 or self._waiting_for_initial_state 1007 ): 1008 return 1009 1010 if plus.get_v1_account_state() != 'signed_in': 1011 bui.screenmessage(bui.Lstr(resource='notSignedInErrorText')) 1012 bui.getsound('error').play() 1013 self._refresh_sub_tab() 1014 return 1015 1016 if self._hostingstate.unavailable_error is not None: 1017 bui.getsound('error').play() 1018 return 1019 1020 bui.getsound('click01').play() 1021 1022 # We need our v2 info for this. 1023 if self._v2state is None or self._v2state.datacode is None: 1024 bui.screenmessage( 1025 bui.Lstr(resource='internal.unavailableNoConnectionText'), 1026 color=(1, 0, 0), 1027 ) 1028 bui.getsound('error').play() 1029 return 1030 1031 # If we're not hosting, start. 1032 if self._hostingstate.party_code is None: 1033 # If there's a token cost, make sure we have enough tokens 1034 # or a gold pass. 1035 if self._hostingstate.tokens_to_host_now > 0: 1036 1037 if ( 1038 not self._v2state.gold_pass 1039 and self._v2state.tokens 1040 < self._hostingstate.tokens_to_host_now 1041 ): 1042 show_get_tokens_prompt() 1043 bui.getsound('error').play() 1044 return 1045 1046 self._last_action_send_time = time.time() 1047 plus.add_v1_account_transaction( 1048 { 1049 'type': 'PRIVATE_PARTY_START', 1050 'config': dataclass_to_dict(self._hostingconfig), 1051 'region_pings': bui.app.net.zone_pings, 1052 'expire_time': time.time() + 20, 1053 'datacode': self._v2state.datacode, 1054 }, 1055 callback=bui.WeakCall(self._hosting_state_response), 1056 ) 1057 plus.run_v1_account_transactions() 1058 1059 else: 1060 self._last_action_send_time = time.time() 1061 plus.add_v1_account_transaction( 1062 { 1063 'type': 'PRIVATE_PARTY_STOP', 1064 'expire_time': time.time() + 20, 1065 }, 1066 callback=bui.WeakCall(self._hosting_state_response), 1067 ) 1068 plus.run_v1_account_transactions() 1069 bui.getsound('click01').play() 1070 1071 self._waiting_for_start_stop_response = True 1072 self._refresh_sub_tab() 1073 1074 def _join_connect_press(self) -> None: 1075 # Error immediately if its an empty code. 1076 code: str | None = None 1077 if self._join_party_code_text: 1078 code = cast(str, bui.textwidget(query=self._join_party_code_text)) 1079 if not code: 1080 bui.screenmessage( 1081 bui.Lstr(translate=('serverResponses', 'Invalid code.')), 1082 color=(1, 0, 0), 1083 ) 1084 bui.getsound('error').play() 1085 return 1086 1087 self._connect_to_party_code(code) 1088 1089 def _connect_response(self, result: dict[str, Any] | None) -> None: 1090 try: 1091 self._connect_press_time = None 1092 if result is None: 1093 raise RuntimeError() 1094 cresult = dataclass_from_dict( 1095 PrivatePartyConnectResult, result, discard_unknown_attrs=True 1096 ) 1097 if cresult.error is not None: 1098 self._debug_server_comm('got error connect response') 1099 bui.screenmessage( 1100 bui.Lstr(translate=('serverResponses', cresult.error)), 1101 (1, 0, 0), 1102 ) 1103 bui.getsound('error').play() 1104 return 1105 self._debug_server_comm('got valid connect response') 1106 assert cresult.address4 is not None and cresult.port is not None 1107 bs.connect_to_party(cresult.address4, port=cresult.port) 1108 except Exception: 1109 self._debug_server_comm('got connect response error') 1110 bui.getsound('error').play() 1111 1112 @override 1113 def save_state(self) -> None: 1114 assert bui.app.classic is not None 1115 bui.app.ui_v1.window_states[type(self)] = copy.deepcopy(self._state) 1116 1117 @override 1118 def restore_state(self) -> None: 1119 assert bui.app.classic is not None 1120 state = bui.app.ui_v1.window_states.get(type(self)) 1121 if state is None: 1122 state = State() 1123 assert isinstance(state, State) 1124 self._state = state
DEBUG_SERVER_COMMUNICATION =
False
class
SubTabType(enum.Enum):
Available sub-tabs.
JOIN =
<SubTabType.JOIN: 'join'>
HOST =
<SubTabType.HOST: 'host'>
Inherited Members
- enum.Enum
- name
- value
@dataclass
class
State:
48@dataclass 49class State: 50 """Our core state that persists while the app is running.""" 51 52 sub_tab: SubTabType = SubTabType.JOIN
Our core state that persists while the app is running.
State( sub_tab: SubTabType = <SubTabType.JOIN: 'join'>)
55class PrivateGatherTab(GatherTab): 56 """The private tab in the gather UI""" 57 58 def __init__(self, window: GatherWindow) -> None: 59 super().__init__(window) 60 self._container: bui.Widget | None = None 61 self._state: State = State() 62 self._last_datacode_refresh_time: float | None = None 63 self._hostingstate = PrivateHostingState() 64 self._v2state: bacommon.cloud.BSPrivatePartyResponse | None = None 65 self._join_sub_tab_text: bui.Widget | None = None 66 self._host_sub_tab_text: bui.Widget | None = None 67 self._update_timer: bui.AppTimer | None = None 68 self._join_party_code_text: bui.Widget | None = None 69 self._c_width: float = 0.0 70 self._c_height: float = 0.0 71 self._last_hosting_state_query_time: float | None = None 72 self._last_v2_state_query_time: float | None = None 73 self._waiting_for_initial_state = True 74 self._waiting_for_start_stop_response = True 75 self._host_playlist_button: bui.Widget | None = None 76 self._host_copy_button: bui.Widget | None = None 77 self._host_connect_button: bui.Widget | None = None 78 self._host_start_stop_button: bui.Widget | None = None 79 self._get_tokens_button: bui.Widget | None = None 80 self._token_count_text: bui.Widget | None = None 81 self._showing_not_signed_in_screen = False 82 self._create_time = time.time() 83 self._last_action_send_time: float | None = None 84 self._connect_press_time: float | None = None 85 try: 86 self._hostingconfig = self._build_hosting_config() 87 except Exception: 88 logging.exception('Error building hosting config.') 89 self._hostingconfig = PrivateHostingConfig() 90 91 @override 92 def on_activate( 93 self, 94 parent_widget: bui.Widget, 95 tab_button: bui.Widget, 96 region_width: float, 97 region_height: float, 98 region_left: float, 99 region_bottom: float, 100 ) -> bui.Widget: 101 self._c_width = region_width 102 self._c_height = region_height - 20 103 self._container = bui.containerwidget( 104 parent=parent_widget, 105 position=( 106 region_left, 107 region_bottom + (region_height - self._c_height) * 0.5, 108 ), 109 size=(self._c_width, self._c_height), 110 background=False, 111 selection_loops_to_parent=True, 112 ) 113 v = self._c_height - 30.0 114 self._join_sub_tab_text = bui.textwidget( 115 parent=self._container, 116 position=(self._c_width * 0.5 - 245, v - 13), 117 color=(0.6, 1.0, 0.6), 118 scale=1.3, 119 size=(200, 30), 120 maxwidth=250, 121 h_align='left', 122 v_align='center', 123 click_activate=True, 124 selectable=True, 125 autoselect=True, 126 on_activate_call=lambda: self._set_sub_tab( 127 SubTabType.JOIN, 128 playsound=True, 129 ), 130 text=bui.Lstr(resource='gatherWindow.privatePartyJoinText'), 131 glow_type='uniform', 132 ) 133 self._host_sub_tab_text = bui.textwidget( 134 parent=self._container, 135 position=(self._c_width * 0.5 + 45, v - 13), 136 color=(0.6, 1.0, 0.6), 137 scale=1.3, 138 size=(200, 30), 139 maxwidth=250, 140 h_align='left', 141 v_align='center', 142 click_activate=True, 143 selectable=True, 144 autoselect=True, 145 on_activate_call=lambda: self._set_sub_tab( 146 SubTabType.HOST, 147 playsound=True, 148 ), 149 text=bui.Lstr(resource='gatherWindow.privatePartyHostText'), 150 glow_type='uniform', 151 ) 152 bui.widget(edit=self._join_sub_tab_text, up_widget=tab_button) 153 bui.widget( 154 edit=self._host_sub_tab_text, 155 left_widget=self._join_sub_tab_text, 156 up_widget=tab_button, 157 ) 158 bui.widget( 159 edit=self._join_sub_tab_text, right_widget=self._host_sub_tab_text 160 ) 161 162 self._update_timer = bui.AppTimer( 163 1.0, bui.WeakCall(self._update), repeat=True 164 ) 165 166 # Prevent taking any action until we've updated our state. 167 self._waiting_for_initial_state = True 168 169 # Force some immediate refreshes. 170 self._last_datacode_refresh_time = None 171 self._last_v2_state_query_time = None 172 self._last_action_send_time = None # Ensure we don't ignore response. 173 self._last_hosting_state_query_time = None 174 self._update() 175 176 self._set_sub_tab(self._state.sub_tab) 177 178 return self._container 179 180 def _build_hosting_config(self) -> PrivateHostingConfig: 181 # pylint: disable=too-many-branches 182 # pylint: disable=too-many-locals 183 from bauiv1lib.playlist import PlaylistTypeVars 184 from bascenev1 import filter_playlist 185 186 hcfg = PrivateHostingConfig() 187 cfg = bui.app.config 188 sessiontypestr = cfg.get('Private Party Host Session Type', 'ffa') 189 if not isinstance(sessiontypestr, str): 190 raise RuntimeError(f'Invalid sessiontype {sessiontypestr}') 191 hcfg.session_type = sessiontypestr 192 193 sessiontype: type[bs.Session] 194 if hcfg.session_type == 'ffa': 195 sessiontype = bs.FreeForAllSession 196 elif hcfg.session_type == 'teams': 197 sessiontype = bs.DualTeamSession 198 else: 199 raise RuntimeError(f'Invalid sessiontype: {hcfg.session_type}') 200 pvars = PlaylistTypeVars(sessiontype) 201 202 playlist_name = bui.app.config.get( 203 f'{pvars.config_name} Playlist Selection' 204 ) 205 if not isinstance(playlist_name, str): 206 playlist_name = '__default__' 207 hcfg.playlist_name = ( 208 pvars.default_list_name.evaluate() 209 if playlist_name == '__default__' 210 else playlist_name 211 ) 212 213 playlist: list[dict[str, Any]] | None = None 214 if playlist_name != '__default__': 215 playlist = cfg.get(f'{pvars.config_name} Playlists', {}).get( 216 playlist_name 217 ) 218 if playlist is None: 219 playlist = pvars.get_default_list_call() 220 221 hcfg.playlist = filter_playlist( 222 playlist, sessiontype, name=playlist_name 223 ) 224 225 randomize = cfg.get(f'{pvars.config_name} Playlist Randomize') 226 if not isinstance(randomize, bool): 227 randomize = False 228 hcfg.randomize = randomize 229 230 tutorial = cfg.get('Show Tutorial') 231 if not isinstance(tutorial, bool): 232 tutorial = True 233 hcfg.tutorial = tutorial 234 235 if hcfg.session_type == 'teams': 236 ctn: list[str] | None = cfg.get('Custom Team Names') 237 if ctn is not None: 238 ctn_any: Any = ctn # Actual value may not match type checker. 239 if ( 240 isinstance(ctn_any, (list, tuple)) 241 and len(ctn) == 2 242 and all(isinstance(x, str) for x in ctn) 243 ): 244 hcfg.custom_team_names = (ctn[0], ctn[1]) 245 else: 246 print(f'Found invalid custom-team-names data: {ctn}') 247 248 ctc: list[list[float]] | None = cfg.get('Custom Team Colors') 249 if ctc is not None: 250 ctc_any: Any = ctc # Actual value may not match type checker. 251 if ( 252 isinstance(ctc_any, (list, tuple)) 253 and len(ctc) == 2 254 and all(isinstance(x, (list, tuple)) for x in ctc) 255 and all(len(x) == 3 for x in ctc) 256 ): 257 hcfg.custom_team_colors = ( 258 (ctc[0][0], ctc[0][1], ctc[0][2]), 259 (ctc[1][0], ctc[1][1], ctc[1][2]), 260 ) 261 else: 262 print(f'Found invalid custom-team-colors data: {ctc}') 263 264 return hcfg 265 266 @override 267 def on_deactivate(self) -> None: 268 self._update_timer = None 269 270 def _update_currency_ui(self) -> None: 271 # Keep currency count up to date if applicable. 272 plus = bui.app.plus 273 assert plus is not None 274 275 if self._v2state is not None: 276 t_str = str(self._v2state.tokens) 277 else: 278 t_str = '-' 279 280 if self._get_tokens_button: 281 bui.buttonwidget( 282 edit=self._get_tokens_button, 283 label=bui.charstr(bui.SpecialChar.TOKEN) + t_str, 284 ) 285 if self._token_count_text: 286 bui.textwidget( 287 edit=self._token_count_text, 288 text=bui.charstr(bui.SpecialChar.TOKEN) + t_str, 289 ) 290 291 def _update(self) -> None: 292 """Periodic updating.""" 293 294 plus = bui.app.plus 295 assert plus is not None 296 297 now = bui.apptime() 298 299 self._update_currency_ui() 300 301 if self._state.sub_tab is SubTabType.HOST: 302 # If we're not signed in, just refresh to show that. 303 if ( 304 plus.get_v1_account_state() != 'signed_in' 305 or plus.accounts.primary is None 306 ) and not self._showing_not_signed_in_screen: 307 self._refresh_sub_tab() 308 else: 309 # Query an updated v1 state periodically. 310 if ( 311 self._last_hosting_state_query_time is None 312 or now - self._last_hosting_state_query_time > 15.0 313 ): 314 self._debug_server_comm('querying private party state') 315 if plus.get_v1_account_state() == 'signed_in': 316 plus.add_v1_account_transaction( 317 { 318 'type': 'PRIVATE_PARTY_QUERY', 319 'expire_time': time.time() + 20, 320 }, 321 callback=bui.WeakCall( 322 self._idle_hosting_state_response 323 ), 324 ) 325 plus.run_v1_account_transactions() 326 else: 327 self._idle_hosting_state_response(None) 328 self._last_hosting_state_query_time = now 329 330 # Query an updated v2 state periodically. 331 if ( 332 self._last_v2_state_query_time is None 333 or now - self._last_v2_state_query_time > 12.0 334 ): 335 self._debug_server_comm('querying pp v2 state') 336 337 if plus.accounts.primary is not None: 338 with plus.accounts.primary: 339 plus.cloud.send_message_cb( 340 bacommon.cloud.BSPrivatePartyMessage( 341 need_datacode=( 342 self._last_datacode_refresh_time is None 343 or time.monotonic() 344 - self._last_datacode_refresh_time 345 > 30.0 346 ) 347 ), 348 on_response=bui.WeakCall( 349 self._on_private_party_query_response 350 ), 351 ) 352 353 self._last_v2_state_query_time = now 354 355 def _on_private_party_query_response( 356 self, response: bacommon.cloud.BSPrivatePartyResponse | Exception 357 ) -> None: 358 if isinstance(response, Exception): 359 self._debug_server_comm('got pp v2 state response (err)') 360 # We expect comm errors sometimes. Make noise on anything else. 361 if not isinstance(response, CommunicationError): 362 logging.exception('Error on private-party-query-response') 363 return 364 365 # Ignore if something went wrong server-side. 366 if not response.success: 367 self._debug_server_comm('got pp v2 state response (serverside err)') 368 return 369 370 self._debug_server_comm('got pp v2 state response') 371 372 existing_datacode = ( 373 None if self._v2state is None else self._v2state.datacode 374 ) 375 376 self._v2state = response 377 if self._v2state.datacode is None: 378 # We don't fetch datacode each time; preserve our existing 379 # if we didn't. 380 self._v2state.datacode = existing_datacode 381 else: 382 # If we *did* fetch it, note the time. 383 self._last_datacode_refresh_time = time.monotonic() 384 385 def _idle_hosting_state_response( 386 self, result: dict[str, Any] | None 387 ) -> None: 388 # This simply passes through to our standard response handler. 389 # The one exception is if we've recently sent an action to the 390 # server (start/stop hosting/etc.) In that case we want to 391 # ignore idle background updates and wait for the response to 392 # our action. (this keeps the button showing 'one moment...' 393 # until the change takes effect, etc.) 394 if ( 395 self._last_action_send_time is not None 396 and time.time() - self._last_action_send_time < 5.0 397 ): 398 self._debug_server_comm( 399 'ignoring private party state response due to recent action' 400 ) 401 return 402 self._hosting_state_response(result) 403 404 def _hosting_state_response(self, result: dict[str, Any] | None) -> None: 405 # Its possible for this to come back to us after our UI is dead; 406 # ignore in that case. 407 if not self._container: 408 return 409 410 state: PrivateHostingState | None = None 411 if result is not None: 412 self._debug_server_comm('got private party state response') 413 try: 414 state = dataclass_from_dict( 415 PrivateHostingState, result, discard_unknown_attrs=True 416 ) 417 except Exception: 418 logging.exception('Got invalid PrivateHostingState data') 419 else: 420 self._debug_server_comm('private party state response errored') 421 422 # Hmm I guess let's just ignore failed responses?... Or should 423 # we show some sort of error state to the user?... 424 if result is None or state is None: 425 return 426 427 self._waiting_for_initial_state = False 428 self._waiting_for_start_stop_response = False 429 self._hostingstate = state 430 self._refresh_sub_tab() 431 432 def _set_sub_tab(self, value: SubTabType, playsound: bool = False) -> None: 433 assert self._container 434 if playsound: 435 bui.getsound('click01').play() 436 437 # If switching from join to host, force some refreshes. 438 if self._state.sub_tab is SubTabType.JOIN and value is SubTabType.HOST: 439 # Prevent taking any action until we've gotten a fresh 440 # state. 441 self._waiting_for_initial_state = True 442 443 # Get some refreshes going immediately. 444 self._last_hosting_state_query_time = None 445 self._last_action_send_time = None # So we don't ignore response. 446 self._last_datacode_refresh_time = None 447 self._last_v2_state_query_time = None 448 self._update() 449 450 self._state.sub_tab = value 451 active_color = (0.6, 1.0, 0.6) 452 inactive_color = (0.5, 0.4, 0.5) 453 bui.textwidget( 454 edit=self._join_sub_tab_text, 455 color=active_color if value is SubTabType.JOIN else inactive_color, 456 ) 457 bui.textwidget( 458 edit=self._host_sub_tab_text, 459 color=active_color if value is SubTabType.HOST else inactive_color, 460 ) 461 462 self._refresh_sub_tab() 463 464 # Kick off an update to get any needed messages sent/etc. 465 bui.pushcall(self._update) 466 467 def _selwidgets(self) -> list[bui.Widget | None]: 468 """An indexed list of widgets we can use for saving/restoring sel.""" 469 return [ 470 self._host_playlist_button, 471 self._host_copy_button, 472 self._host_connect_button, 473 self._host_start_stop_button, 474 self._get_tokens_button, 475 ] 476 477 def _refresh_sub_tab(self) -> None: 478 assert self._container 479 480 # Store an index for our current selection so we can reselect 481 # the equivalent recreated widget if possible. 482 selindex: int | None = None 483 selchild = self._container.get_selected_child() 484 if selchild is not None: 485 try: 486 selindex = self._selwidgets().index(selchild) 487 except ValueError: 488 pass 489 490 # Clear anything existing in the old sub-tab. 491 for widget in self._container.get_children(): 492 if widget and widget not in { 493 self._host_sub_tab_text, 494 self._join_sub_tab_text, 495 }: 496 widget.delete() 497 498 if self._state.sub_tab is SubTabType.JOIN: 499 self._build_join_tab() 500 elif self._state.sub_tab is SubTabType.HOST: 501 self._build_host_tab() 502 else: 503 raise RuntimeError('Invalid state.') 504 505 # Select the new equivalent widget if there is one. 506 if selindex is not None: 507 selwidget = self._selwidgets()[selindex] 508 if selwidget: 509 bui.containerwidget( 510 edit=self._container, selected_child=selwidget 511 ) 512 513 def _build_join_tab(self) -> None: 514 bui.textwidget( 515 parent=self._container, 516 position=(self._c_width * 0.5, self._c_height - 140), 517 color=(0.5, 0.46, 0.5), 518 scale=1.5, 519 size=(0, 0), 520 maxwidth=250, 521 h_align='center', 522 v_align='center', 523 text=bui.Lstr(resource='gatherWindow.partyCodeText'), 524 ) 525 526 self._join_party_code_text = bui.textwidget( 527 parent=self._container, 528 position=(self._c_width * 0.5 - 150, self._c_height - 250), 529 flatness=1.0, 530 scale=1.5, 531 size=(300, 50), 532 editable=True, 533 max_chars=20, 534 description=bui.Lstr(resource='gatherWindow.partyCodeText'), 535 autoselect=True, 536 h_align='left', 537 v_align='center', 538 text='', 539 ) 540 btn = bui.buttonwidget( 541 parent=self._container, 542 size=(300, 70), 543 label=bui.Lstr(resource='gatherWindow.' 'manualConnectText'), 544 position=(self._c_width * 0.5 - 150, self._c_height - 350), 545 on_activate_call=self._join_connect_press, 546 autoselect=True, 547 ) 548 bui.textwidget( 549 edit=self._join_party_code_text, on_return_press_call=btn.activate 550 ) 551 552 def _on_get_tokens_press(self) -> None: 553 if self._waiting_for_start_stop_response: 554 return 555 556 # Bring up get-tickets window and then kill ourself (we're on 557 # the overlay layer so we'd show up above it). 558 GetTokensWindow(origin_widget=self._get_tokens_button) 559 560 def _build_host_tab(self) -> None: 561 # pylint: disable=too-many-branches 562 # pylint: disable=too-many-statements 563 assert bui.app.classic is not None 564 565 plus = bui.app.plus 566 assert plus is not None 567 568 hostingstate = self._hostingstate 569 570 havegoldpass = self._v2state is not None and self._v2state.gold_pass 571 572 # We use both v1 and v2 account functionality here (sigh). So 573 # make sure we're signed in on both ends. 574 575 # Make sure the V1 side is good to go. 576 if plus.get_v1_account_state() != 'signed_in': 577 bui.textwidget( 578 parent=self._container, 579 size=(0, 0), 580 h_align='center', 581 v_align='center', 582 maxwidth=self._c_width * 0.8, 583 scale=0.8, 584 color=(0.6, 0.56, 0.6), 585 position=(self._c_width * 0.5, self._c_height * 0.5), 586 text=bui.Lstr(resource='notSignedInErrorText'), 587 ) 588 self._showing_not_signed_in_screen = True 589 return 590 591 # Make sure the V2 side is good to go. 592 if plus.accounts.primary is None: 593 bui.textwidget( 594 parent=self._container, 595 size=(0, 0), 596 h_align='center', 597 v_align='center', 598 maxwidth=self._c_width * 0.8, 599 scale=0.8, 600 color=(0.6, 0.56, 0.6), 601 position=(self._c_width * 0.5, self._c_height * 0.5), 602 text=bui.Lstr(resource='v2AccountRequiredText'), 603 ) 604 self._showing_not_signed_in_screen = True 605 return 606 607 self._showing_not_signed_in_screen = False 608 609 # At first we don't want to show anything until we've gotten a 610 # state. Update: In this situation we now simply show our 611 # existing state but give the start/stop button a loading 612 # message and disallow its use. This keeps things a lot less 613 # jumpy looking and allows selecting playlists/etc without 614 # having to wait for the server each time back to the ui. 615 if self._waiting_for_initial_state and bool(False): 616 bui.textwidget( 617 parent=self._container, 618 size=(0, 0), 619 h_align='center', 620 v_align='center', 621 maxwidth=200, 622 scale=0.8, 623 color=(0.6, 0.56, 0.6), 624 position=(self._c_width * 0.5, self._c_height * 0.5), 625 text=bui.Lstr( 626 value='${A}...', 627 subs=[('${A}', bui.Lstr(resource='store.loadingText'))], 628 ), 629 ) 630 return 631 632 # If we're not currently hosting and hosting requires tokens, 633 # Show our count (possibly with a link to purchase more). 634 if ( 635 not self._waiting_for_initial_state 636 and hostingstate.party_code is None 637 and hostingstate.tickets_to_host_now != 0 638 and not havegoldpass 639 ): 640 if not bui.app.ui_v1.use_toolbars: 641 642 # Currently have no allow_token_purchases value like 643 # we had with tickets; just assuming we always allow. 644 if bool(True): 645 # if bui.app.classic.allow_ticket_purchases: 646 self._get_tokens_button = bui.buttonwidget( 647 parent=self._container, 648 position=( 649 self._c_width - 210 + 125, 650 self._c_height - 44, 651 ), 652 autoselect=True, 653 scale=0.6, 654 size=(120, 60), 655 textcolor=(1.0, 0.6, 0.0), 656 label=bui.charstr(bui.SpecialChar.TOKEN), 657 color=(0.65, 0.5, 0.8), 658 on_activate_call=self._on_get_tokens_press, 659 ) 660 else: 661 self._token_count_text = bui.textwidget( 662 parent=self._container, 663 scale=0.6, 664 position=( 665 self._c_width - 210 + 125, 666 self._c_height - 44, 667 ), 668 color=(1.0, 0.6, 0.0), 669 h_align='center', 670 v_align='center', 671 ) 672 673 # Set initial token count. 674 self._update_currency_ui() 675 676 v = self._c_height - 90 677 if hostingstate.party_code is None: 678 bui.textwidget( 679 parent=self._container, 680 size=(0, 0), 681 h_align='center', 682 v_align='center', 683 maxwidth=self._c_width * 0.9, 684 scale=0.7, 685 flatness=1.0, 686 color=(0.5, 0.46, 0.5), 687 position=(self._c_width * 0.5, v), 688 text=bui.Lstr( 689 resource='gatherWindow.privatePartyCloudDescriptionText' 690 ), 691 ) 692 693 v -= 100 694 if hostingstate.party_code is None: 695 # We've got no current party running; show options to set 696 # one up. 697 bui.textwidget( 698 parent=self._container, 699 size=(0, 0), 700 h_align='right', 701 v_align='center', 702 maxwidth=200, 703 scale=0.8, 704 color=(0.6, 0.56, 0.6), 705 position=(self._c_width * 0.5 - 210, v), 706 text=bui.Lstr(resource='playlistText'), 707 ) 708 self._host_playlist_button = bui.buttonwidget( 709 parent=self._container, 710 size=(400, 70), 711 color=(0.6, 0.5, 0.6), 712 textcolor=(0.8, 0.75, 0.8), 713 label=self._hostingconfig.playlist_name, 714 on_activate_call=self._playlist_press, 715 position=(self._c_width * 0.5 - 200, v - 35), 716 up_widget=self._host_sub_tab_text, 717 autoselect=True, 718 ) 719 720 # If it appears we're coming back from playlist selection, 721 # re-select our playlist button. 722 if bui.app.ui_v1.selecting_private_party_playlist: 723 bui.containerwidget( 724 edit=self._container, 725 selected_child=self._host_playlist_button, 726 ) 727 bui.app.ui_v1.selecting_private_party_playlist = False 728 else: 729 # We've got a current party; show its info. 730 bui.textwidget( 731 parent=self._container, 732 size=(0, 0), 733 h_align='center', 734 v_align='center', 735 maxwidth=600, 736 scale=0.9, 737 color=(0.7, 0.64, 0.7), 738 position=(self._c_width * 0.5, v + 90), 739 text=bui.Lstr(resource='gatherWindow.partyServerRunningText'), 740 ) 741 bui.textwidget( 742 parent=self._container, 743 size=(0, 0), 744 h_align='center', 745 v_align='center', 746 maxwidth=600, 747 scale=0.7, 748 color=(0.7, 0.64, 0.7), 749 position=(self._c_width * 0.5, v + 50), 750 text=bui.Lstr(resource='gatherWindow.partyCodeText'), 751 ) 752 bui.textwidget( 753 parent=self._container, 754 size=(0, 0), 755 h_align='center', 756 v_align='center', 757 scale=2.0, 758 color=(0.0, 1.0, 0.0), 759 position=(self._c_width * 0.5, v + 10), 760 text=hostingstate.party_code, 761 ) 762 763 # Also action buttons to copy it and connect to it. 764 if bui.clipboard_is_supported(): 765 cbtnoffs = 10 766 self._host_copy_button = bui.buttonwidget( 767 parent=self._container, 768 size=(140, 40), 769 color=(0.6, 0.5, 0.6), 770 textcolor=(0.8, 0.75, 0.8), 771 label=bui.Lstr(resource='gatherWindow.copyCodeText'), 772 on_activate_call=self._host_copy_press, 773 position=(self._c_width * 0.5 - 150, v - 70), 774 autoselect=True, 775 ) 776 else: 777 cbtnoffs = -70 778 self._host_connect_button = bui.buttonwidget( 779 parent=self._container, 780 size=(140, 40), 781 color=(0.6, 0.5, 0.6), 782 textcolor=(0.8, 0.75, 0.8), 783 label=bui.Lstr(resource='gatherWindow.manualConnectText'), 784 on_activate_call=self._host_connect_press, 785 position=(self._c_width * 0.5 + cbtnoffs, v - 70), 786 autoselect=True, 787 ) 788 789 v -= 120 790 791 # Line above the main action button: 792 793 # If we don't want to show anything until we get a state: 794 if self._waiting_for_initial_state: 795 pass 796 elif hostingstate.unavailable_error is not None: 797 # If hosting is unavailable, show the associated reason. 798 bui.textwidget( 799 parent=self._container, 800 size=(0, 0), 801 h_align='center', 802 v_align='center', 803 maxwidth=self._c_width * 0.9, 804 scale=0.7, 805 flatness=1.0, 806 color=(1.0, 0.0, 0.0), 807 position=(self._c_width * 0.5, v), 808 text=bui.Lstr( 809 translate=( 810 'serverResponses', 811 hostingstate.unavailable_error, 812 ) 813 ), 814 ) 815 elif havegoldpass: 816 # If we have a gold pass, none of the 817 # timing/free-server-availability info below is relevant to 818 # us. 819 pass 820 elif hostingstate.free_host_minutes_remaining is not None: 821 # If we've been pre-approved to start/stop for free, show 822 # that. 823 bui.textwidget( 824 parent=self._container, 825 size=(0, 0), 826 h_align='center', 827 v_align='center', 828 maxwidth=self._c_width * 0.9, 829 scale=0.7, 830 flatness=1.0, 831 color=( 832 (0.7, 0.64, 0.7) 833 if hostingstate.party_code 834 else (0.0, 1.0, 0.0) 835 ), 836 position=(self._c_width * 0.5, v), 837 text=bui.Lstr( 838 resource='gatherWindow.startStopHostingMinutesText', 839 subs=[ 840 ( 841 '${MINUTES}', 842 f'{hostingstate.free_host_minutes_remaining:.0f}', 843 ) 844 ], 845 ), 846 ) 847 else: 848 # Otherwise tell whether the free cloud server is available 849 # or will be at some point. 850 if hostingstate.party_code is None: 851 if hostingstate.tickets_to_host_now == 0: 852 bui.textwidget( 853 parent=self._container, 854 size=(0, 0), 855 h_align='center', 856 v_align='center', 857 maxwidth=self._c_width * 0.9, 858 scale=0.7, 859 flatness=1.0, 860 color=(0.0, 1.0, 0.0), 861 position=(self._c_width * 0.5, v), 862 text=bui.Lstr( 863 resource=( 864 'gatherWindow.freeCloudServerAvailableNowText' 865 ) 866 ), 867 ) 868 else: 869 if hostingstate.minutes_until_free_host is None: 870 bui.textwidget( 871 parent=self._container, 872 size=(0, 0), 873 h_align='center', 874 v_align='center', 875 maxwidth=self._c_width * 0.9, 876 scale=0.7, 877 flatness=1.0, 878 color=(1.0, 0.6, 0.0), 879 position=(self._c_width * 0.5, v), 880 text=bui.Lstr( 881 resource=( 882 'gatherWindow' 883 '.freeCloudServerNotAvailableText' 884 ) 885 ), 886 ) 887 else: 888 availmins = hostingstate.minutes_until_free_host 889 bui.textwidget( 890 parent=self._container, 891 size=(0, 0), 892 h_align='center', 893 v_align='center', 894 maxwidth=self._c_width * 0.9, 895 scale=0.7, 896 flatness=1.0, 897 color=(1.0, 0.6, 0.0), 898 position=(self._c_width * 0.5, v), 899 text=bui.Lstr( 900 resource='gatherWindow.' 901 'freeCloudServerAvailableMinutesText', 902 subs=[('${MINUTES}', f'{availmins:.0f}')], 903 ), 904 ) 905 906 v -= 100 907 908 if ( 909 self._waiting_for_start_stop_response 910 or self._waiting_for_initial_state 911 ): 912 btnlabel = bui.Lstr(resource='oneMomentText') 913 else: 914 if hostingstate.unavailable_error is not None: 915 btnlabel = bui.Lstr( 916 resource='gatherWindow.hostingUnavailableText' 917 ) 918 elif hostingstate.party_code is None: 919 ticon = bui.charstr(bui.SpecialChar.TOKEN) 920 nowtokens = hostingstate.tokens_to_host_now 921 if nowtokens > 0 and not havegoldpass: 922 btnlabel = bui.Lstr( 923 resource='gatherWindow.startHostingPaidText', 924 subs=[('${COST}', f'{ticon}{nowtokens}')], 925 ) 926 else: 927 btnlabel = bui.Lstr( 928 resource='gatherWindow.startHostingText' 929 ) 930 else: 931 btnlabel = bui.Lstr(resource='gatherWindow.stopHostingText') 932 933 disabled = ( 934 hostingstate.unavailable_error is not None 935 or self._waiting_for_initial_state 936 ) 937 waiting = self._waiting_for_start_stop_response 938 self._host_start_stop_button = bui.buttonwidget( 939 parent=self._container, 940 size=(400, 80), 941 color=( 942 (0.6, 0.6, 0.6) 943 if disabled 944 else (0.5, 1.0, 0.5) if waiting else None 945 ), 946 enable_sound=False, 947 label=btnlabel, 948 textcolor=((0.7, 0.7, 0.7) if disabled else None), 949 position=(self._c_width * 0.5 - 200, v), 950 on_activate_call=self._start_stop_button_press, 951 autoselect=True, 952 ) 953 954 def _playlist_press(self) -> None: 955 assert self._host_playlist_button is not None 956 self.window.playlist_select(origin_widget=self._host_playlist_button) 957 958 def _host_copy_press(self) -> None: 959 assert self._hostingstate.party_code is not None 960 bui.clipboard_set_text(self._hostingstate.party_code) 961 bui.screenmessage(bui.Lstr(resource='gatherWindow.copyCodeConfirmText')) 962 963 def _host_connect_press(self) -> None: 964 assert self._hostingstate.party_code is not None 965 self._connect_to_party_code(self._hostingstate.party_code) 966 967 def _debug_server_comm(self, msg: str) -> None: 968 if DEBUG_SERVER_COMMUNICATION: 969 print( 970 f'PPTABCOM: {msg} at time ' 971 f'{time.time()-self._create_time:.2f}' 972 ) 973 974 def _connect_to_party_code(self, code: str) -> None: 975 # Ignore attempted followup sends for a few seconds. (this will 976 # reset if we get a response) 977 plus = bui.app.plus 978 assert plus is not None 979 980 now = time.time() 981 if ( 982 self._connect_press_time is not None 983 and now - self._connect_press_time < 5.0 984 ): 985 self._debug_server_comm( 986 'not sending private party connect (too soon)' 987 ) 988 return 989 self._connect_press_time = now 990 991 self._debug_server_comm('sending private party connect') 992 plus.add_v1_account_transaction( 993 { 994 'type': 'PRIVATE_PARTY_CONNECT', 995 'expire_time': time.time() + 20, 996 'code': code, 997 }, 998 callback=bui.WeakCall(self._connect_response), 999 ) 1000 plus.run_v1_account_transactions() 1001 1002 def _start_stop_button_press(self) -> None: 1003 plus = bui.app.plus 1004 assert plus is not None 1005 if ( 1006 self._waiting_for_start_stop_response 1007 or self._waiting_for_initial_state 1008 ): 1009 return 1010 1011 if plus.get_v1_account_state() != 'signed_in': 1012 bui.screenmessage(bui.Lstr(resource='notSignedInErrorText')) 1013 bui.getsound('error').play() 1014 self._refresh_sub_tab() 1015 return 1016 1017 if self._hostingstate.unavailable_error is not None: 1018 bui.getsound('error').play() 1019 return 1020 1021 bui.getsound('click01').play() 1022 1023 # We need our v2 info for this. 1024 if self._v2state is None or self._v2state.datacode is None: 1025 bui.screenmessage( 1026 bui.Lstr(resource='internal.unavailableNoConnectionText'), 1027 color=(1, 0, 0), 1028 ) 1029 bui.getsound('error').play() 1030 return 1031 1032 # If we're not hosting, start. 1033 if self._hostingstate.party_code is None: 1034 # If there's a token cost, make sure we have enough tokens 1035 # or a gold pass. 1036 if self._hostingstate.tokens_to_host_now > 0: 1037 1038 if ( 1039 not self._v2state.gold_pass 1040 and self._v2state.tokens 1041 < self._hostingstate.tokens_to_host_now 1042 ): 1043 show_get_tokens_prompt() 1044 bui.getsound('error').play() 1045 return 1046 1047 self._last_action_send_time = time.time() 1048 plus.add_v1_account_transaction( 1049 { 1050 'type': 'PRIVATE_PARTY_START', 1051 'config': dataclass_to_dict(self._hostingconfig), 1052 'region_pings': bui.app.net.zone_pings, 1053 'expire_time': time.time() + 20, 1054 'datacode': self._v2state.datacode, 1055 }, 1056 callback=bui.WeakCall(self._hosting_state_response), 1057 ) 1058 plus.run_v1_account_transactions() 1059 1060 else: 1061 self._last_action_send_time = time.time() 1062 plus.add_v1_account_transaction( 1063 { 1064 'type': 'PRIVATE_PARTY_STOP', 1065 'expire_time': time.time() + 20, 1066 }, 1067 callback=bui.WeakCall(self._hosting_state_response), 1068 ) 1069 plus.run_v1_account_transactions() 1070 bui.getsound('click01').play() 1071 1072 self._waiting_for_start_stop_response = True 1073 self._refresh_sub_tab() 1074 1075 def _join_connect_press(self) -> None: 1076 # Error immediately if its an empty code. 1077 code: str | None = None 1078 if self._join_party_code_text: 1079 code = cast(str, bui.textwidget(query=self._join_party_code_text)) 1080 if not code: 1081 bui.screenmessage( 1082 bui.Lstr(translate=('serverResponses', 'Invalid code.')), 1083 color=(1, 0, 0), 1084 ) 1085 bui.getsound('error').play() 1086 return 1087 1088 self._connect_to_party_code(code) 1089 1090 def _connect_response(self, result: dict[str, Any] | None) -> None: 1091 try: 1092 self._connect_press_time = None 1093 if result is None: 1094 raise RuntimeError() 1095 cresult = dataclass_from_dict( 1096 PrivatePartyConnectResult, result, discard_unknown_attrs=True 1097 ) 1098 if cresult.error is not None: 1099 self._debug_server_comm('got error connect response') 1100 bui.screenmessage( 1101 bui.Lstr(translate=('serverResponses', cresult.error)), 1102 (1, 0, 0), 1103 ) 1104 bui.getsound('error').play() 1105 return 1106 self._debug_server_comm('got valid connect response') 1107 assert cresult.address4 is not None and cresult.port is not None 1108 bs.connect_to_party(cresult.address4, port=cresult.port) 1109 except Exception: 1110 self._debug_server_comm('got connect response error') 1111 bui.getsound('error').play() 1112 1113 @override 1114 def save_state(self) -> None: 1115 assert bui.app.classic is not None 1116 bui.app.ui_v1.window_states[type(self)] = copy.deepcopy(self._state) 1117 1118 @override 1119 def restore_state(self) -> None: 1120 assert bui.app.classic is not None 1121 state = bui.app.ui_v1.window_states.get(type(self)) 1122 if state is None: 1123 state = State() 1124 assert isinstance(state, State) 1125 self._state = state
The private tab in the gather UI
PrivateGatherTab(window: bauiv1lib.gather.GatherWindow)
58 def __init__(self, window: GatherWindow) -> None: 59 super().__init__(window) 60 self._container: bui.Widget | None = None 61 self._state: State = State() 62 self._last_datacode_refresh_time: float | None = None 63 self._hostingstate = PrivateHostingState() 64 self._v2state: bacommon.cloud.BSPrivatePartyResponse | None = None 65 self._join_sub_tab_text: bui.Widget | None = None 66 self._host_sub_tab_text: bui.Widget | None = None 67 self._update_timer: bui.AppTimer | None = None 68 self._join_party_code_text: bui.Widget | None = None 69 self._c_width: float = 0.0 70 self._c_height: float = 0.0 71 self._last_hosting_state_query_time: float | None = None 72 self._last_v2_state_query_time: float | None = None 73 self._waiting_for_initial_state = True 74 self._waiting_for_start_stop_response = True 75 self._host_playlist_button: bui.Widget | None = None 76 self._host_copy_button: bui.Widget | None = None 77 self._host_connect_button: bui.Widget | None = None 78 self._host_start_stop_button: bui.Widget | None = None 79 self._get_tokens_button: bui.Widget | None = None 80 self._token_count_text: bui.Widget | None = None 81 self._showing_not_signed_in_screen = False 82 self._create_time = time.time() 83 self._last_action_send_time: float | None = None 84 self._connect_press_time: float | None = None 85 try: 86 self._hostingconfig = self._build_hosting_config() 87 except Exception: 88 logging.exception('Error building hosting config.') 89 self._hostingconfig = PrivateHostingConfig()
@override
def
on_activate( self, parent_widget: _bauiv1.Widget, tab_button: _bauiv1.Widget, region_width: float, region_height: float, region_left: float, region_bottom: float) -> _bauiv1.Widget:
91 @override 92 def on_activate( 93 self, 94 parent_widget: bui.Widget, 95 tab_button: bui.Widget, 96 region_width: float, 97 region_height: float, 98 region_left: float, 99 region_bottom: float, 100 ) -> bui.Widget: 101 self._c_width = region_width 102 self._c_height = region_height - 20 103 self._container = bui.containerwidget( 104 parent=parent_widget, 105 position=( 106 region_left, 107 region_bottom + (region_height - self._c_height) * 0.5, 108 ), 109 size=(self._c_width, self._c_height), 110 background=False, 111 selection_loops_to_parent=True, 112 ) 113 v = self._c_height - 30.0 114 self._join_sub_tab_text = bui.textwidget( 115 parent=self._container, 116 position=(self._c_width * 0.5 - 245, v - 13), 117 color=(0.6, 1.0, 0.6), 118 scale=1.3, 119 size=(200, 30), 120 maxwidth=250, 121 h_align='left', 122 v_align='center', 123 click_activate=True, 124 selectable=True, 125 autoselect=True, 126 on_activate_call=lambda: self._set_sub_tab( 127 SubTabType.JOIN, 128 playsound=True, 129 ), 130 text=bui.Lstr(resource='gatherWindow.privatePartyJoinText'), 131 glow_type='uniform', 132 ) 133 self._host_sub_tab_text = bui.textwidget( 134 parent=self._container, 135 position=(self._c_width * 0.5 + 45, v - 13), 136 color=(0.6, 1.0, 0.6), 137 scale=1.3, 138 size=(200, 30), 139 maxwidth=250, 140 h_align='left', 141 v_align='center', 142 click_activate=True, 143 selectable=True, 144 autoselect=True, 145 on_activate_call=lambda: self._set_sub_tab( 146 SubTabType.HOST, 147 playsound=True, 148 ), 149 text=bui.Lstr(resource='gatherWindow.privatePartyHostText'), 150 glow_type='uniform', 151 ) 152 bui.widget(edit=self._join_sub_tab_text, up_widget=tab_button) 153 bui.widget( 154 edit=self._host_sub_tab_text, 155 left_widget=self._join_sub_tab_text, 156 up_widget=tab_button, 157 ) 158 bui.widget( 159 edit=self._join_sub_tab_text, right_widget=self._host_sub_tab_text 160 ) 161 162 self._update_timer = bui.AppTimer( 163 1.0, bui.WeakCall(self._update), repeat=True 164 ) 165 166 # Prevent taking any action until we've updated our state. 167 self._waiting_for_initial_state = True 168 169 # Force some immediate refreshes. 170 self._last_datacode_refresh_time = None 171 self._last_v2_state_query_time = None 172 self._last_action_send_time = None # Ensure we don't ignore response. 173 self._last_hosting_state_query_time = None 174 self._update() 175 176 self._set_sub_tab(self._state.sub_tab) 177 178 return self._container
Called when the tab becomes the active one.
The tab should create and return a container widget covering the specified region.
@override
def
save_state(self) -> None:
1113 @override 1114 def save_state(self) -> None: 1115 assert bui.app.classic is not None 1116 bui.app.ui_v1.window_states[type(self)] = copy.deepcopy(self._state)
Called when the parent window is saving state.
@override
def
restore_state(self) -> None:
1118 @override 1119 def restore_state(self) -> None: 1120 assert bui.app.classic is not None 1121 state = bui.app.ui_v1.window_states.get(type(self)) 1122 if state is None: 1123 state = State() 1124 assert isinstance(state, State) 1125 self._state = state
Called when the parent window is restoring state.