def on_copy(result: bool) -> None: if not result: screenmessage(Language.SUCCESS) with ba.Context('ui'): watch_window._my_replay_selected = None watch_window._refresh_my_replays() elif result == 1: screenmessage(Language.UNSUCCESS)
def handle_result(result: Optional[Dict[str, Any]]) -> None: with ba.Context('ui'): if result is None: ba.screenmessage(ba.Lstr(resource='errorText'), color=(1, 0, 0)) ba.playsound(ba.getsound('error')) else: ShowFriendCodeWindow(result)
def _start_preloads(self) -> None: # FIXME: The func that calls us back doesn't save/restore state # or check for a dead activity so we have to do that ourself. if self.expired: return with ba.Context(self): _preload1() ba.timer(0.5, lambda: ba.setmusic(ba.MusicType.MENU))
def _import_replay_press(self) -> None: from bastd.ui.fileselector import FileSelectorWindow user_dir = _ba.app.python_directory_user import_dir = (user_dir + '/replays') if not os.path.exists(import_dir): os.mkdir(import_dir) with ba.Context('ui'): FileSelectorWindow(import_dir, self._import_my_replay, True, ['brp'], False)
def screenmessage(message: Language) -> None: if message not in Language: raise ValueError() if message == Language.SUCCESS: color = (0, 1.0, 0) elif message == Language.UNSUCCESS: color = (1.0, 0, 0) else: color = (1.0, 1.0, 1.0) with ba.Context('ui'): ba.screenmessage(message=message.value, color=color)
def _launch(self) -> None: if self._launched: return self._launched = True launched = False # If they gave us an existing activity, just restart it. if self._tournament_activity is not None: try: ba.timer(0.1, lambda: ba.playsound(ba.getsound('cashRegister')), timetype=ba.TimeType.REAL) with ba.Context(self._tournament_activity): self._tournament_activity.end({'outcome': 'restart'}, force=True) ba.timer(0.3, self._transition_out, timetype=ba.TimeType.REAL) launched = True ba.screenmessage(ba.Lstr(translate=('serverResponses', 'Entering tournament...')), color=(0, 1, 0)) # We can hit exceptions here if _tournament_activity ends before # our restart attempt happens. # In this case we'll fall back to launching a new session. # This is not ideal since players will have to rejoin, etc., # but it works for now. except Exception: ba.print_exception('Error restarting tournament activity.') # If we had no existing activity (or were unable to restart it) # launch a new session. if not launched: ba.timer(0.1, lambda: ba.playsound(ba.getsound('cashRegister')), timetype=ba.TimeType.REAL) ba.timer( 1.0, lambda: ba.app.launch_coop_game( self._tournament_info['game'], args={ 'min_players': self._tournament_info['minPlayers'], 'max_players': self._tournament_info['maxPlayers'], 'tournament_id': self._tournament_id }), timetype=ba.TimeType.REAL) ba.timer(0.7, self._transition_out, timetype=ba.TimeType.REAL) ba.screenmessage(ba.Lstr(translate=('serverResponses', 'Entering tournament...')), color=(0, 1, 0))
def _got_news(self, news: str) -> None: # Run this stuff in the context of our activity since we # need to make nodes and stuff.. should fix the serverget # call so it. activity = self._activity() if activity is None or activity.expired: return with ba.Context(activity): self._phrases: List[str] = [] # Show upcoming achievements in non-vr versions # (currently too hard to read in vr). self._used_phrases = ( ['__ACH__'] if not ba.app.vr_mode else []) + [s for s in news.split('<br>\n') if s != ''] self._phrase_change_timer = ba.Timer( (self._message_duration + self._message_spacing), ba.WeakCall(self._change_phrase), repeat=True) scl = 1.2 if (ba.app.ui.uiscale is ba.UIScale.SMALL or ba.app.vr_mode) else 0.8 color2 = ((1, 1, 1, 1) if ba.app.vr_mode else (0.7, 0.65, 0.75, 1.0)) shadow = (1.0 if ba.app.vr_mode else 0.4) self._text = ba.NodeActor( ba.newnode('text', attrs={ 'v_attach': 'top', 'h_attach': 'center', 'h_align': 'center', 'vr_depth': -20, 'shadow': shadow, 'flatness': 0.8, 'v_align': 'top', 'color': color2, 'scale': scl, 'maxwidth': 900.0 / scl, 'position': (0, -10) })) self._change_phrase()
def _notify_handlers(cmd: str, playerdata: PlayerData): args = cmd.split() for handler in _handlers: if args[0] in handler.commands and playerdata.status in handler.statuses: if _lastrun.get(playerdata.id, {}).get( args[0], 0) + handler.statuses[playerdata.status] < int( datetime.now().timestamp()): try: with ba.Context(get_foreground_host_activity()): handler.callback(playerdata, args) except Exception: ba.print_exception( f'Error while processing command {cmd} from {playerdata.id}' ) chatmessage(get_locale('command_some_error')) lastrun = _lastrun.get(playerdata.id, {}) lastrun[args[0]] = int(datetime.now().timestamp()) _lastrun[playerdata.id] = lastrun else: chatmessage(get_locale('commands_too_many'))
def _smooth_update(self) -> None: if not self._ticket_count_text: self._smooth_update_timer = None return finished = False # if we're going down, do it immediately assert self._smooth_ticket_count is not None if int(self._smooth_ticket_count) >= self._ticket_count: self._smooth_ticket_count = float(self._ticket_count) finished = True else: # we're going up; start a sound if need be self._smooth_ticket_count = min( self._smooth_ticket_count + 1.0 * self._smooth_increase_speed, self._ticket_count) if int(self._smooth_ticket_count) >= self._ticket_count: finished = True self._smooth_ticket_count = float(self._ticket_count) elif self._ticking_node is None: with ba.Context('ui'): self._ticking_node = ba.newnode( 'sound', attrs={ 'sound': ba.getsound('scoreIncrease'), 'positional': False }) ba.textwidget(edit=self._ticket_count_text, text=str(int(self._smooth_ticket_count))) # if we've reached the target, kill the timer/sound/etc if finished: self._smooth_update_timer = None if self._ticking_node is not None: self._ticking_node.delete() self._ticking_node = None ba.playsound(ba.getsound('cashRegister2'))
def show_offer() -> bool: """(internal)""" try: from bastd.ui import feedback app = ba.app # Space things out a bit so we don't hit the poor user with an ad and # then an in-game offer. has_been_long_enough_since_ad = True if (app.ads.last_ad_completion_time is not None and (ba.time(ba.TimeType.REAL) - app.ads.last_ad_completion_time < 30.0)): has_been_long_enough_since_ad = False if app.special_offer is not None and has_been_long_enough_since_ad: # Special case: for pro offers, store this in our prefs so we # can re-show it if the user kills us (set phasers to 'NAG'!!!). if app.special_offer.get('item') == 'pro_fullprice': cfg = app.config cfg['pendingSpecialOffer'] = { 'a': _ba.get_public_login_id(), 'o': app.special_offer } cfg.commit() with ba.Context('ui'): if app.special_offer['item'] == 'rating': feedback.ask_for_rating() else: SpecialOfferWindow(app.special_offer) app.special_offer = None return True except Exception: ba.print_exception('Error showing offer.') return False
def _die(self, immediate: bool = False) -> None: session = self._session() if session is None and self.node: # If session is gone, our node should be too, # since it was part of the session's scene. # Let's make sure that's the case. # (since otherwise we have no way to kill it) ba.print_error('got None session on Background _die' ' (and node still exists!)') elif session is not None: with ba.Context(session): if not self._dying and self.node: self._dying = True if immediate: self.node.delete() else: ba.animate(self.node, 'opacity', { 0.0: 1.0, self.fade_time: 0.0 }, loop=False) ba.timer(self.fade_time + 0.1, self.node.delete)
def on_transition_in(self) -> None: super().on_transition_in() random.seed(123) self._logo_node: Optional[ba.Node] = None self._custom_logo_tex_name: Optional[str] = None self._word_actors: List[ba.Actor] = [] app = ba.app # FIXME: We shouldn't be doing things conditionally based on whether # the host is VR mode or not (clients may differ in that regard). # Any differences need to happen at the engine level so everyone # sees things in their own optimal way. vr_mode = ba.app.vr_mode if not ba.app.toolbar_test: color = ((1.0, 1.0, 1.0, 1.0) if vr_mode else (0.5, 0.6, 0.5, 0.6)) # FIXME: Need a node attr for vr-specific-scale. scale = (0.9 if (app.ui.uiscale is ba.UIScale.SMALL or vr_mode) else 0.7) self.my_name = ba.NodeActor( ba.newnode('text', attrs={ 'v_attach': 'bottom', 'h_align': 'center', 'color': color, 'flatness': 1.0, 'shadow': 1.0 if vr_mode else 0.5, 'scale': scale, 'position': (0, 10), 'vr_depth': -10, 'text': '\xa9 2011-2020 Eric Froemling' })) # Throw up some text that only clients can see so they know that the # host is navigating menus while they're just staring at an # empty-ish screen. tval = ba.Lstr(resource='hostIsNavigatingMenusText', subs=[('${HOST}', _ba.get_account_display_string())]) self._host_is_navigating_text = ba.NodeActor( ba.newnode('text', attrs={ 'text': tval, 'client_only': True, 'position': (0, -200), 'flatness': 1.0, 'h_align': 'center' })) if not ba.app.main_menu_did_initial_transition and hasattr( self, 'my_name'): assert self.my_name.node ba.animate(self.my_name.node, 'opacity', {2.3: 0, 3.0: 1.0}) # FIXME: We shouldn't be doing things conditionally based on whether # the host is vr mode or not (clients may not be or vice versa). # Any differences need to happen at the engine level so everyone sees # things in their own optimal way. vr_mode = app.vr_mode uiscale = app.ui.uiscale # In cases where we're doing lots of dev work lets always show the # build number. force_show_build_number = False if not ba.app.toolbar_test: if app.debug_build or app.test_build or force_show_build_number: if app.debug_build: text = ba.Lstr(value='${V} (${B}) (${D})', subs=[ ('${V}', app.version), ('${B}', str(app.build_number)), ('${D}', ba.Lstr(resource='debugText')), ]) else: text = ba.Lstr(value='${V} (${B})', subs=[ ('${V}', app.version), ('${B}', str(app.build_number)), ]) else: text = ba.Lstr(value='${V}', subs=[('${V}', app.version)]) scale = 0.9 if (uiscale is ba.UIScale.SMALL or vr_mode) else 0.7 color = (1, 1, 1, 1) if vr_mode else (0.5, 0.6, 0.5, 0.7) self.version = ba.NodeActor( ba.newnode( 'text', attrs={ 'v_attach': 'bottom', 'h_attach': 'right', 'h_align': 'right', 'flatness': 1.0, 'vr_depth': -10, 'shadow': 1.0 if vr_mode else 0.5, 'color': color, 'scale': scale, 'position': (-260, 10) if vr_mode else (-10, 10), 'text': text })) if not ba.app.main_menu_did_initial_transition: assert self.version.node ba.animate(self.version.node, 'opacity', {2.3: 0, 3.0: 1.0}) # Show the iircade logo on our iircade build. if app.iircade_mode: img = ba.NodeActor( ba.newnode('image', attrs={ 'texture': ba.gettexture('iircadeLogo'), 'attach': 'center', 'scale': (250, 250), 'position': (0, 0), 'tilt_translate': 0.21, 'absolute_scale': True })).autoretain() imgdelay = 0.0 if app.main_menu_did_initial_transition else 1.0 ba.animate(img.node, 'opacity', { imgdelay + 1.5: 0.0, imgdelay + 2.5: 1.0 }) # Throw in test build info. self.beta_info = self.beta_info_2 = None if app.test_build and not (app.demo_mode or app.arcade_mode): pos = ((230, 125) if (app.demo_mode or app.arcade_mode) else (230, 35)) self.beta_info = ba.NodeActor( ba.newnode('text', attrs={ 'v_attach': 'center', 'h_align': 'center', 'color': (1, 1, 1, 1), 'shadow': 0.5, 'flatness': 0.5, 'scale': 1, 'vr_depth': -60, 'position': pos, 'text': ba.Lstr(resource='testBuildText') })) if not ba.app.main_menu_did_initial_transition: assert self.beta_info.node ba.animate(self.beta_info.node, 'opacity', {1.3: 0, 1.8: 1.0}) model = ba.getmodel('thePadLevel') trees_model = ba.getmodel('trees') bottom_model = ba.getmodel('thePadLevelBottom') color_texture = ba.gettexture('thePadLevelColor') trees_texture = ba.gettexture('treesColor') bgtex = ba.gettexture('menuBG') bgmodel = ba.getmodel('thePadBG') # Load these last since most platforms don't use them. vr_bottom_fill_model = ba.getmodel('thePadVRFillBottom') vr_top_fill_model = ba.getmodel('thePadVRFillTop') gnode = self.globalsnode gnode.camera_mode = 'rotate' tint = (1.14, 1.1, 1.0) gnode.tint = tint gnode.ambient_color = (1.06, 1.04, 1.03) gnode.vignette_outer = (0.45, 0.55, 0.54) gnode.vignette_inner = (0.99, 0.98, 0.98) self.bottom = ba.NodeActor( ba.newnode('terrain', attrs={ 'model': bottom_model, 'lighting': False, 'reflection': 'soft', 'reflection_scale': [0.45], 'color_texture': color_texture })) self.vr_bottom_fill = ba.NodeActor( ba.newnode('terrain', attrs={ 'model': vr_bottom_fill_model, 'lighting': False, 'vr_only': True, 'color_texture': color_texture })) self.vr_top_fill = ba.NodeActor( ba.newnode('terrain', attrs={ 'model': vr_top_fill_model, 'vr_only': True, 'lighting': False, 'color_texture': bgtex })) self.terrain = ba.NodeActor( ba.newnode('terrain', attrs={ 'model': model, 'color_texture': color_texture, 'reflection': 'soft', 'reflection_scale': [0.3] })) self.trees = ba.NodeActor( ba.newnode('terrain', attrs={ 'model': trees_model, 'lighting': False, 'reflection': 'char', 'reflection_scale': [0.1], 'color_texture': trees_texture })) self.bgterrain = ba.NodeActor( ba.newnode('terrain', attrs={ 'model': bgmodel, 'color': (0.92, 0.91, 0.9), 'lighting': False, 'background': True, 'color_texture': bgtex })) self._ts = 0.86 self._language: Optional[str] = None self._update_timer = ba.Timer(1.0, self._update, repeat=True) self._update() # Hopefully this won't hitch but lets space these out anyway. _ba.add_clean_frame_callback(ba.WeakCall(self._start_preloads)) random.seed() # On the main menu, also show our news. class News: """Wrangles news display.""" def __init__(self, activity: ba.Activity): self._valid = True self._message_duration = 10.0 self._message_spacing = 2.0 self._text: Optional[ba.NodeActor] = None self._activity = weakref.ref(activity) # If we're signed in, fetch news immediately. # Otherwise wait until we are signed in. self._fetch_timer: Optional[ba.Timer] = ba.Timer( 1.0, ba.WeakCall(self._try_fetching_news), repeat=True) self._try_fetching_news() # We now want to wait until we're signed in before fetching news. def _try_fetching_news(self) -> None: if _ba.get_account_state() == 'signed_in': self._fetch_news() self._fetch_timer = None def _fetch_news(self) -> None: ba.app.main_menu_last_news_fetch_time = time.time() # UPDATE - We now just pull news from MRVs. news = _ba.get_account_misc_read_val('n', None) if news is not None: self._got_news(news) def _change_phrase(self) -> None: from bastd.actor.text import Text # If our news is way out of date, lets re-request it; # otherwise, rotate our phrase. assert ba.app.main_menu_last_news_fetch_time is not None if time.time() - ba.app.main_menu_last_news_fetch_time > 600.0: self._fetch_news() self._text = None else: if self._text is not None: if not self._phrases: for phr in self._used_phrases: self._phrases.insert(0, phr) val = self._phrases.pop() if val == '__ACH__': vrmode = app.vr_mode Text(ba.Lstr(resource='nextAchievementsText'), color=((1, 1, 1, 1) if vrmode else (0.95, 0.9, 1, 0.4)), host_only=True, maxwidth=200, position=(-300, -35), h_align=Text.HAlign.RIGHT, transition=Text.Transition.FADE_IN, scale=0.9 if vrmode else 0.7, flatness=1.0 if vrmode else 0.6, shadow=1.0 if vrmode else 0.5, h_attach=Text.HAttach.CENTER, v_attach=Text.VAttach.TOP, transition_delay=1.0, transition_out_delay=self._message_duration ).autoretain() achs = [ a for a in app.achievements if not a.complete ] if achs: ach = achs.pop( random.randrange(min(4, len(achs)))) ach.create_display( -180, -35, 1.0, outdelay=self._message_duration, style='news') if achs: ach = achs.pop( random.randrange(min(8, len(achs)))) ach.create_display( 180, -35, 1.25, outdelay=self._message_duration, style='news') else: spc = self._message_spacing keys = { spc: 0.0, spc + 1.0: 1.0, spc + self._message_duration - 1.0: 1.0, spc + self._message_duration: 0.0 } assert self._text.node ba.animate(self._text.node, 'opacity', keys) # {k: v # for k, v in list(keys.items())}) self._text.node.text = val def _got_news(self, news: str) -> None: # Run this stuff in the context of our activity since we # need to make nodes and stuff.. should fix the serverget # call so it. activity = self._activity() if activity is None or activity.expired: return with ba.Context(activity): self._phrases: List[str] = [] # Show upcoming achievements in non-vr versions # (currently too hard to read in vr). self._used_phrases = ( ['__ACH__'] if not ba.app.vr_mode else []) + [s for s in news.split('<br>\n') if s != ''] self._phrase_change_timer = ba.Timer( (self._message_duration + self._message_spacing), ba.WeakCall(self._change_phrase), repeat=True) scl = 1.2 if (ba.app.ui.uiscale is ba.UIScale.SMALL or ba.app.vr_mode) else 0.8 color2 = ((1, 1, 1, 1) if ba.app.vr_mode else (0.7, 0.65, 0.75, 1.0)) shadow = (1.0 if ba.app.vr_mode else 0.4) self._text = ba.NodeActor( ba.newnode('text', attrs={ 'v_attach': 'top', 'h_attach': 'center', 'h_align': 'center', 'vr_depth': -20, 'shadow': shadow, 'flatness': 0.8, 'v_align': 'top', 'color': color2, 'scale': scl, 'maxwidth': 900.0 / scl, 'position': (0, -10) })) self._change_phrase() if not (app.demo_mode or app.arcade_mode) and not app.toolbar_test: self._news = News(self) # Bring up the last place we were, or start at the main menu otherwise. with ba.Context('ui'): from bastd.ui import specialoffer if bool(False): uicontroller = ba.app.ui.controller assert uicontroller is not None uicontroller.show_main_menu() else: main_menu_location = ba.app.ui.get_main_menu_location() # When coming back from a kiosk-mode game, jump to # the kiosk start screen. if ba.app.demo_mode or ba.app.arcade_mode: # pylint: disable=cyclic-import from bastd.ui.kiosk import KioskWindow ba.app.ui.set_main_menu_window( KioskWindow().get_root_widget()) # ..or in normal cases go back to the main menu else: if main_menu_location == 'Gather': # pylint: disable=cyclic-import from bastd.ui.gather import GatherWindow ba.app.ui.set_main_menu_window( GatherWindow(transition=None).get_root_widget()) elif main_menu_location == 'Watch': # pylint: disable=cyclic-import from bastd.ui.watch import WatchWindow ba.app.ui.set_main_menu_window( WatchWindow(transition=None).get_root_widget()) elif main_menu_location == 'Team Game Select': # pylint: disable=cyclic-import from bastd.ui.playlist.browser import ( PlaylistBrowserWindow) ba.app.ui.set_main_menu_window( PlaylistBrowserWindow( sessiontype=ba.DualTeamSession, transition=None).get_root_widget()) elif main_menu_location == 'Free-for-All Game Select': # pylint: disable=cyclic-import from bastd.ui.playlist.browser import ( PlaylistBrowserWindow) ba.app.ui.set_main_menu_window( PlaylistBrowserWindow( sessiontype=ba.FreeForAllSession, transition=None).get_root_widget()) elif main_menu_location == 'Coop Select': # pylint: disable=cyclic-import from bastd.ui.coop.browser import CoopBrowserWindow ba.app.ui.set_main_menu_window( CoopBrowserWindow( transition=None).get_root_widget()) else: # pylint: disable=cyclic-import from bastd.ui.mainmenu import MainMenuWindow ba.app.ui.set_main_menu_window( MainMenuWindow(transition=None).get_root_widget()) # attempt to show any pending offers immediately. # If that doesn't work, try again in a few seconds # (we may not have heard back from the server) # ..if that doesn't work they'll just have to wait # until the next opportunity. if not specialoffer.show_offer(): def try_again() -> None: if not specialoffer.show_offer(): # Try one last time.. ba.timer(2.0, specialoffer.show_offer, timetype=ba.TimeType.REAL) ba.timer(2.0, try_again, timetype=ba.TimeType.REAL) ba.app.main_menu_did_initial_transition = True
def __init__(self, fade_time: float = 0.5, start_faded: bool = False, show_logo: bool = False): super().__init__() self._dying = False self.fade_time = fade_time # We're special in that we create our node in the session # scene instead of the activity scene. # This way we can overlap multiple activities for fades # and whatnot. session = ba.getsession() self._session = weakref.ref(session) with ba.Context(session): self.node = ba.newnode('image', delegate=self, attrs={ 'fill_screen': True, 'texture': ba.gettexture('bg'), 'tilt_translate': -0.3, 'has_alpha_channel': False, 'color': (1, 1, 1) }) if not start_faded: ba.animate(self.node, 'opacity', { 0.0: 0.0, self.fade_time: 1.0 }, loop=False) if show_logo: logo_texture = ba.gettexture('logo') logo_model = ba.getmodel('logo') logo_model_transparent = ba.getmodel('logoTransparent') self.logo = ba.newnode('image', owner=self.node, attrs={ 'texture': logo_texture, 'model_opaque': logo_model, 'model_transparent': logo_model_transparent, 'scale': (0.7, 0.7), 'vr_depth': -250, 'color': (0.15, 0.15, 0.15), 'position': (0, 0), 'tilt_translate': -0.05, 'absolute_scale': False }) self.node.connectattr('opacity', self.logo, 'opacity') # add jitter/pulse for a stop-motion-y look unless we're in VR # in which case stillness is better if not ba.app.vr_mode: self.cmb = ba.newnode('combine', owner=self.node, attrs={'size': 2}) for attr in ['input0', 'input1']: ba.animate(self.cmb, attr, { 0.0: 0.693, 0.05: 0.7, 0.5: 0.693 }, loop=True) self.cmb.connectattr('output', self.logo, 'scale') cmb = ba.newnode('combine', owner=self.node, attrs={'size': 2}) cmb.connectattr('output', self.logo, 'position') # Gen some random keys for that stop-motion-y look. keys = {} timeval = 0.0 for _i in range(10): keys[timeval] = (random.random() - 0.5) * 0.0015 timeval += random.random() * 0.1 ba.animate(cmb, 'input0', keys, loop=True) keys = {} timeval = 0.0 for _i in range(10): keys[timeval] = (random.random() - 0.5) * 0.0015 + 0.05 timeval += random.random() * 0.1 ba.animate(cmb, 'input1', keys, loop=True)
def on_copy(result: bool = False) -> None: if result: with ba.Context('ui'): self._my_replay_selected = None self._refresh_my_replays()
def open_fileselector(path: str, callback: Callable = None) -> None: with ba.Context('ui'): FileSelectorWindow(path, callback, True, ['brp'], False)
def set_next_map(session, game_map): session._next_game_spec = game_map with ba.Context(session): session._instantiate_next_game()
def _smooth_update(self) -> None: # pylint: disable=too-many-branches # pylint: disable=too-many-statements try: if not self._button: return if self._ticking_node is None: with ba.Context('ui'): self._ticking_node = ba.newnode( 'sound', attrs={ 'sound': ba.getsound('scoreIncrease'), 'positional': False }) self._bg_flash = (not self._bg_flash) color_used = ((self._color[0] * 2, self._color[1] * 2, self._color[2] * 2) if self._bg_flash else self._color) textcolor_used = ((1, 1, 1) if self._bg_flash else self._textcolor) header_color_used = ((1, 1, 1) if self._bg_flash else self._header_color) if self._rank is not None: assert self._smooth_rank is not None self._smooth_rank -= 1.0 * self._smooth_increase_speed finished = (int(self._smooth_rank) <= self._rank) elif self._smooth_percent is not None: self._smooth_percent += 1.0 * self._smooth_increase_speed assert self._percent is not None finished = (int(self._smooth_percent) >= self._percent) else: finished = True if finished: if self._rank is not None: self._smooth_rank = float(self._rank) elif self._percent is not None: self._smooth_percent = float(self._percent) color_used = self._color textcolor_used = self._textcolor self._smooth_update_timer = None if self._ticking_node is not None: self._ticking_node.delete() self._ticking_node = None ba.playsound(ba.getsound('cashRegister2')) assert self._improvement_text is not None diff_text = ba.textwidget( parent=self._parent, size=(0, 0), h_align='center', v_align='center', text='+' + self._improvement_text + '!', position=(self._position[0] + self._size[0] * 0.5 * self._scale, self._position[1] + self._size[1] * -0.2 * self._scale), color=(0, 1, 0), flatness=1.0, shadow=0.0, scale=self._scale * 0.7) def safe_delete(widget: ba.Widget) -> None: if widget: widget.delete() ba.timer(2.0, ba.Call(safe_delete, diff_text), timetype=ba.TimeType.REAL) status_text: Union[str, ba.Lstr] if self._rank is not None: assert self._smooth_rank is not None status_text = ba.Lstr(resource='numberText', subs=[('${NUMBER}', str(int(self._smooth_rank)))]) elif self._smooth_percent is not None: status_text = str(int(self._smooth_percent)) + '%' else: status_text = '-' ba.textwidget(edit=self._value_text, text=status_text, color=textcolor_used) ba.textwidget(edit=self._title_text, color=header_color_used) ba.buttonwidget(edit=self._button, color=color_used) except Exception: ba.print_exception('Error doing smooth update.') self._smooth_update_timer = None
def _on_public_party_query_result( self, result: Optional[Dict[str, Any]]) -> None: with ba.Context('ui'): # Any time we get any result at all, kill our loading status. status_text = self._join_status_text if status_text: # Don't show results if not signed in # (probably didn't get any anyway). if _ba.get_account_state() != 'signed_in': ba.textwidget(edit=status_text, text=ba.Lstr(resource='notSignedInText')) else: if result is None: ba.textwidget(edit=status_text, text=ba.Lstr(resource='errorText')) else: ba.textwidget(edit=status_text, text='') if result is not None: self._have_valid_server_list = True parties_in = result['l'] else: self._have_valid_server_list = False parties_in = [] for partyval in list(self._parties.values()): partyval.claimed = False for party_in in parties_in: addr = party_in['a'] assert isinstance(addr, str) port = party_in['p'] assert isinstance(port, int) party_key = f'{addr}_{port}' party = self._parties.get(party_key) if party is None: # If this party is new to us, init it. party = self._parties[party_key] = PartyEntry( address=addr, next_ping_time=ba.time(ba.TimeType.REAL) + 0.001 * party_in['pd'], index=self._next_entry_index) self._next_entry_index += 1 assert isinstance(party.address, str) assert isinstance(party.next_ping_time, float) # Now, new or not, update its values. party.queue = party_in.get('q') assert isinstance(party.queue, (str, type(None))) party.port = port party.name = party_in['n'] assert isinstance(party.name, str) party.size = party_in['s'] assert isinstance(party.size, int) party.size_max = party_in['sm'] assert isinstance(party.size_max, int) party.claimed = True # (server provides this in milliseconds; we use seconds) party.ping_interval = 0.001 * party_in['pi'] assert isinstance(party.ping_interval, float) party.stats_addr = party_in['sa'] assert isinstance(party.stats_addr, (str, type(None))) # Prune unclaimed party entries. self._parties = { key: val for key, val in list(self._parties.items()) if val.claimed } self._update_server_list()
def _update_server_list(self) -> None: cur_time = ba.time(ba.TimeType.REAL) if self._first_server_list_rebuild_time is None: self._first_server_list_rebuild_time = cur_time # We get called quite often (for each ping response, etc) so we want # to limit our rebuilds to keep the UI responsive. # Let's update faster for the first few seconds, # then ease off to keep the list from jumping around. since_first = cur_time - self._first_server_list_rebuild_time wait_time = (1.0 if since_first < 2.0 else 2.5 if since_first < 10.0 else 5.0) if (not self._server_list_dirty and self._last_server_list_update_time is not None and cur_time - self._last_server_list_update_time < wait_time): return # If we somehow got here without the required UI being in place... columnwidget = self._join_list_column if not columnwidget: return self._last_server_list_update_time = cur_time self._server_list_dirty = False with ba.Context('ui'): # Now kill and recreate all widgets. for widget in columnwidget.get_children(): widget.delete() ordered_parties = self._get_ordered_parties() # If we've got a filter, filter them. if self._filter_value: # Let's do case-insensitive searching. filterval = self._filter_value.lower() ordered_parties = [ p for p in ordered_parties if filterval in p.name.lower() ] sub_scroll_width = 830 lineheight = 42 sub_scroll_height = lineheight * len(ordered_parties) + 50 ba.containerwidget(edit=columnwidget, size=(sub_scroll_width, sub_scroll_height)) # Ew; this rebuilding generates deferred selection callbacks # so we need to generated deferred ignore notices for ourself. def refresh_on() -> None: self._refreshing_list = True ba.pushcall(refresh_on) # Janky - allow escaping if there's nothing in us. ba.containerwidget(edit=self._host_scrollwidget, claims_up_down=(len(ordered_parties) > 0)) self._build_server_entry_lines(lineheight, ordered_parties, sub_scroll_height, sub_scroll_width) # So our selection callbacks can start firing.. def refresh_off() -> None: self._refreshing_list = False ba.pushcall(refresh_off)