baclassic

Components for the classic BombSquad experience.

This package/feature-set contains functionality related to the classic BombSquad experience. Note that much legacy BombSquad code is still a bit tangled and thus this feature-set is largely inseperable from scenev1 and uiv1. Future feature-sets will be designed in a more modular way.

 1# Released under the MIT License. See LICENSE for details.
 2#
 3"""Components for the classic BombSquad experience.
 4
 5This package/feature-set contains functionality related to the classic
 6BombSquad experience. Note that much legacy BombSquad code is still a
 7bit tangled and thus this feature-set is largely inseperable from
 8scenev1 and uiv1. Future feature-sets will be designed in a more modular
 9way.
10"""
11
12# ba_meta require api 9
13
14# Note: Code relying on classic should import things from here *only*
15# for type-checking and use the versions in ba*.app.classic at runtime;
16# that way type-checking will cleanly cover the classic-not-present case
17# (ba*.app.classic being None).
18import logging
19
20from efro.util import set_canonical_module_names
21
22from baclassic._appmode import ClassicAppMode
23from baclassic._appsubsystem import ClassicAppSubsystem
24from baclassic._achievement import Achievement, AchievementSubsystem
25from baclassic._chest import (
26    ChestAppearanceDisplayInfo,
27    CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT,
28    CHEST_APPEARANCE_DISPLAY_INFOS,
29)
30from baclassic._displayitem import show_display_item
31
32__all__ = [
33    'ChestAppearanceDisplayInfo',
34    'CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT',
35    'CHEST_APPEARANCE_DISPLAY_INFOS',
36    'ClassicAppMode',
37    'ClassicAppSubsystem',
38    'Achievement',
39    'AchievementSubsystem',
40    'show_display_item',
41]
42
43# We want stuff here to show up as packagename.Foo instead of
44# packagename._submodule.Foo.
45set_canonical_module_names(globals())
46
47# Sanity check: we want to keep ballistica's dependencies and
48# bootstrapping order clearly defined; let's check a few particular
49# modules to make sure they never directly or indirectly import us
50# before their own execs complete.
51if __debug__:
52    for _mdl in 'babase', '_babase':
53        if not hasattr(__import__(_mdl), '_REACHED_END_OF_MODULE'):
54            logging.warning(
55                '%s was imported before %s finished importing;'
56                ' should not happen.',
57                __name__,
58                _mdl,
59            )
@dataclass
class ChestAppearanceDisplayInfo:
16@dataclass
17class ChestAppearanceDisplayInfo:
18    """Info about how to locally display chest appearances."""
19
20    # NOTE TO SELF: Don't rename these attrs; the C++ layer is hard
21    # coded to look for them.
22
23    texclosed: str
24    texclosedtint: str
25    texopen: str
26    texopentint: str
27    color: tuple[float, float, float]
28    tint: tuple[float, float, float]
29    tint2: tuple[float, float, float]

Info about how to locally display chest appearances.

ChestAppearanceDisplayInfo( texclosed: str, texclosedtint: str, texopen: str, texopentint: str, color: tuple[float, float, float], tint: tuple[float, float, float], tint2: tuple[float, float, float])
texclosed: str
texclosedtint: str
texopen: str
texopentint: str
color: tuple[float, float, float]
tint: tuple[float, float, float]
tint2: tuple[float, float, float]
CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT = ChestAppearanceDisplayInfo(texclosed='chestIcon', texclosedtint='chestIconTint', texopen='chestOpenIcon', texopentint='chestOpenIconTint', color=(1, 1, 1), tint=(1, 1, 1), tint2=(1, 1, 1))
CHEST_APPEARANCE_DISPLAY_INFOS = {<ClassicChestAppearance.L2: 'l2'>: ChestAppearanceDisplayInfo(texclosed='chestIcon', texclosedtint='chestIconTint', texopen='chestOpenIcon', texopentint='chestOpenIconTint', color=(0.8, 1.0, 0.93), tint=(0.65, 1.0, 0.8), tint2=(0.65, 1.0, 0.8)), <ClassicChestAppearance.L3: 'l3'>: ChestAppearanceDisplayInfo(texclosed='chestIcon', texclosedtint='chestIconTint', texopen='chestOpenIcon', texopentint='chestOpenIconTint', color=(0.75, 0.9, 1.3), tint=(0.7, 1, 1.9), tint2=(0.7, 1, 1.9)), <ClassicChestAppearance.L4: 'l4'>: ChestAppearanceDisplayInfo(texclosed='chestIcon', texclosedtint='chestIconTint', texopen='chestOpenIcon', texopentint='chestOpenIconTint', color=(0.7, 1.0, 1.4), tint=(1.4, 1.6, 2.0), tint2=(1.4, 1.6, 2.0)), <ClassicChestAppearance.L5: 'l5'>: ChestAppearanceDisplayInfo(texclosed='chestIcon', texclosedtint='chestIconTint', texopen='chestOpenIcon', texopentint='chestOpenIconTint', color=(0.75, 0.5, 2.4), tint=(1.0, 0.8, 0.0), tint2=(1.0, 0.8, 0.0)), <ClassicChestAppearance.L6: 'l6'>: ChestAppearanceDisplayInfo(texclosed='chestIcon', texclosedtint='chestIconTint', texopen='chestOpenIcon', texopentint='chestOpenIconTint', color=(1.1, 0.8, 0.0), tint=(2, 2, 2), tint2=(2, 2, 2))}
class ClassicAppMode(babase._appmode.AppMode):
 29class ClassicAppMode(babase.AppMode):
 30    """AppMode for the classic BombSquad experience."""
 31
 32    def __init__(self) -> None:
 33        self._on_primary_account_changed_callback: (
 34            CallbackRegistration | None
 35        ) = None
 36        self._on_connectivity_changed_callback: CallbackRegistration | None = (
 37            None
 38        )
 39        self._test_sub: babase.CloudSubscription | None = None
 40        self._account_data_sub: babase.CloudSubscription | None = None
 41
 42        self._have_account_values = False
 43        self._have_connectivity = False
 44
 45    @override
 46    @classmethod
 47    def get_app_experience(cls) -> AppExperience:
 48        return AppExperience.MELEE
 49
 50    @override
 51    @classmethod
 52    def _can_handle_intent(cls, intent: babase.AppIntent) -> bool:
 53        # We support default and exec intents currently.
 54        return isinstance(
 55            intent, babase.AppIntentExec | babase.AppIntentDefault
 56        )
 57
 58    @override
 59    def handle_intent(self, intent: babase.AppIntent) -> None:
 60        if isinstance(intent, babase.AppIntentExec):
 61            _baclassic.classic_app_mode_handle_app_intent_exec(intent.code)
 62            return
 63        assert isinstance(intent, babase.AppIntentDefault)
 64        _baclassic.classic_app_mode_handle_app_intent_default()
 65
 66    @override
 67    def on_activate(self) -> None:
 68
 69        # Let the native layer do its thing.
 70        _baclassic.classic_app_mode_activate()
 71
 72        app = babase.app
 73        plus = app.plus
 74        assert plus is not None
 75
 76        # Wire up the root ui to do what we want.
 77        ui = app.ui_v1
 78        ui.root_ui_calls[ui.RootUIElement.ACCOUNT_BUTTON] = (
 79            self._root_ui_account_press
 80        )
 81        ui.root_ui_calls[ui.RootUIElement.MENU_BUTTON] = (
 82            self._root_ui_menu_press
 83        )
 84        ui.root_ui_calls[ui.RootUIElement.SQUAD_BUTTON] = (
 85            self._root_ui_squad_press
 86        )
 87        ui.root_ui_calls[ui.RootUIElement.SETTINGS_BUTTON] = (
 88            self._root_ui_settings_press
 89        )
 90        ui.root_ui_calls[ui.RootUIElement.STORE_BUTTON] = (
 91            self._root_ui_store_press
 92        )
 93        ui.root_ui_calls[ui.RootUIElement.INVENTORY_BUTTON] = (
 94            self._root_ui_inventory_press
 95        )
 96        ui.root_ui_calls[ui.RootUIElement.GET_TOKENS_BUTTON] = (
 97            self._root_ui_get_tokens_press
 98        )
 99        ui.root_ui_calls[ui.RootUIElement.INBOX_BUTTON] = (
100            self._root_ui_inbox_press
101        )
102        ui.root_ui_calls[ui.RootUIElement.TICKETS_METER] = (
103            self._root_ui_tickets_meter_press
104        )
105        ui.root_ui_calls[ui.RootUIElement.TOKENS_METER] = (
106            self._root_ui_tokens_meter_press
107        )
108        ui.root_ui_calls[ui.RootUIElement.TROPHY_METER] = (
109            self._root_ui_trophy_meter_press
110        )
111        ui.root_ui_calls[ui.RootUIElement.LEVEL_METER] = (
112            self._root_ui_level_meter_press
113        )
114        ui.root_ui_calls[ui.RootUIElement.ACHIEVEMENTS_BUTTON] = (
115            self._root_ui_achievements_press
116        )
117        ui.root_ui_calls[ui.RootUIElement.CHEST_SLOT_0] = partial(
118            self._root_ui_chest_slot_pressed, 0
119        )
120        ui.root_ui_calls[ui.RootUIElement.CHEST_SLOT_1] = partial(
121            self._root_ui_chest_slot_pressed, 1
122        )
123        ui.root_ui_calls[ui.RootUIElement.CHEST_SLOT_2] = partial(
124            self._root_ui_chest_slot_pressed, 2
125        )
126        ui.root_ui_calls[ui.RootUIElement.CHEST_SLOT_3] = partial(
127            self._root_ui_chest_slot_pressed, 3
128        )
129
130        # We want to be informed when connectivity changes.
131        self._on_connectivity_changed_callback = (
132            plus.cloud.on_connectivity_changed_callbacks.register(
133                self._update_for_connectivity_change
134            )
135        )
136        # We want to be informed when primary account changes.
137        self._on_primary_account_changed_callback = (
138            plus.accounts.on_primary_account_changed_callbacks.register(
139                self._update_for_primary_account
140            )
141        )
142        # Establish subscriptions/etc. for any current primary account.
143        self._update_for_primary_account(plus.accounts.primary)
144        self._have_connectivity = plus.cloud.is_connected()
145        self._update_for_connectivity_change(self._have_connectivity)
146
147    @override
148    def on_deactivate(self) -> None:
149
150        classic = babase.app.classic
151
152        # Stop being informed of account changes.
153        self._on_primary_account_changed_callback = None
154
155        # Remove anything following any current account.
156        self._update_for_primary_account(None)
157
158        # Save where we were in the UI so we return there next time.
159        if classic is not None:
160            classic.save_ui_state()
161
162        # Let the native layer do its thing.
163        _baclassic.classic_app_mode_deactivate()
164
165    @override
166    def on_app_active_changed(self) -> None:
167        # If we've gone inactive, bring up the main menu, which has the
168        # side effect of pausing the action (when possible).
169        if not babase.app.active:
170            babase.invoke_main_menu()
171
172    def _update_for_primary_account(
173        self, account: babase.AccountV2Handle | None
174    ) -> None:
175        """Update subscriptions/etc. for a new primary account state."""
176        assert babase.in_logic_thread()
177        plus = babase.app.plus
178
179        assert plus is not None
180
181        if account is not None:
182            babase.set_ui_account_state(True, account.tag)
183        else:
184            babase.set_ui_account_state(False)
185
186        # For testing subscription functionality.
187        if os.environ.get('BA_SUBSCRIPTION_TEST') == '1':
188            if account is None:
189                self._test_sub = None
190            else:
191                with account:
192                    self._test_sub = plus.cloud.subscribe_test(
193                        self._on_sub_test_update
194                    )
195        else:
196            self._test_sub = None
197
198        if account is None:
199            self._account_data_sub = None
200            _baclassic.set_root_ui_account_values(
201                tickets=-1,
202                tokens=-1,
203                league_rank=-1,
204                league_type='',
205                achievements_percent_text='',
206                level_text='',
207                xp_text='',
208                inbox_count_text='',
209                gold_pass=False,
210                chest_0_appearance='',
211                chest_1_appearance='',
212                chest_2_appearance='',
213                chest_3_appearance='',
214                chest_0_unlock_time=-1.0,
215                chest_1_unlock_time=-1.0,
216                chest_2_unlock_time=-1.0,
217                chest_3_unlock_time=-1.0,
218                chest_0_ad_allow_time=-1.0,
219                chest_1_ad_allow_time=-1.0,
220                chest_2_ad_allow_time=-1.0,
221                chest_3_ad_allow_time=-1.0,
222            )
223            self._have_account_values = False
224            self._update_ui_live_state()
225
226        else:
227            with account:
228                self._account_data_sub = (
229                    plus.cloud.subscribe_classic_account_data(
230                        self._on_classic_account_data_change
231                    )
232                )
233
234    def _update_for_connectivity_change(self, connected: bool) -> None:
235        """Update when the app's connectivity state changes."""
236        self._have_connectivity = connected
237        self._update_ui_live_state()
238
239    def _update_ui_live_state(self) -> None:
240        # We want to show ui elements faded if we don't have a live
241        # connection to the master-server OR if we haven't received a
242        # set of account values from them yet. If we just plug in raw
243        # connectivity state here we get UI stuff un-fading a moment or
244        # two before values appear (since the subscriptions have not
245        # sent us any values yet) which looks odd.
246        _baclassic.set_root_ui_have_live_values(
247            self._have_connectivity and self._have_account_values
248        )
249
250    def _on_sub_test_update(self, val: int | None) -> None:
251        print(f'GOT SUB TEST UPDATE: {val}')
252
253    def _on_classic_account_data_change(
254        self, val: bacommon.bs.ClassicAccountLiveData
255    ) -> None:
256        # print('ACCOUNT CHANGED:', val)
257        achp = round(val.achievements / max(val.achievements_total, 1) * 100.0)
258        ibc = str(val.inbox_count)
259        if val.inbox_count_is_max:
260            ibc += '+'
261
262        chest0 = val.chests.get('0')
263        chest1 = val.chests.get('1')
264        chest2 = val.chests.get('2')
265        chest3 = val.chests.get('3')
266
267        _baclassic.set_root_ui_account_values(
268            tickets=val.tickets,
269            tokens=val.tokens,
270            league_rank=(-1 if val.league_rank is None else val.league_rank),
271            league_type=(
272                '' if val.league_type is None else val.league_type.value
273            ),
274            achievements_percent_text=f'{achp}%',
275            level_text=str(val.level),
276            xp_text=f'{val.xp}/{val.xpmax}',
277            inbox_count_text=ibc,
278            gold_pass=val.gold_pass,
279            chest_0_appearance=(
280                '' if chest0 is None else chest0.appearance.value
281            ),
282            chest_1_appearance=(
283                '' if chest1 is None else chest1.appearance.value
284            ),
285            chest_2_appearance=(
286                '' if chest2 is None else chest2.appearance.value
287            ),
288            chest_3_appearance=(
289                '' if chest3 is None else chest3.appearance.value
290            ),
291            chest_0_unlock_time=(
292                -1.0 if chest0 is None else chest0.unlock_time.timestamp()
293            ),
294            chest_1_unlock_time=(
295                -1.0 if chest1 is None else chest1.unlock_time.timestamp()
296            ),
297            chest_2_unlock_time=(
298                -1.0 if chest2 is None else chest2.unlock_time.timestamp()
299            ),
300            chest_3_unlock_time=(
301                -1.0 if chest3 is None else chest3.unlock_time.timestamp()
302            ),
303            chest_0_ad_allow_time=(
304                -1.0
305                if chest0 is None or chest0.ad_allow_time is None
306                else chest0.ad_allow_time.timestamp()
307            ),
308            chest_1_ad_allow_time=(
309                -1.0
310                if chest1 is None or chest1.ad_allow_time is None
311                else chest1.ad_allow_time.timestamp()
312            ),
313            chest_2_ad_allow_time=(
314                -1.0
315                if chest2 is None or chest2.ad_allow_time is None
316                else chest2.ad_allow_time.timestamp()
317            ),
318            chest_3_ad_allow_time=(
319                -1.0
320                if chest3 is None or chest3.ad_allow_time is None
321                else chest3.ad_allow_time.timestamp()
322            ),
323        )
324
325        # Note that we have values and updated faded state accordingly.
326        self._have_account_values = True
327        self._update_ui_live_state()
328
329    def _root_ui_menu_press(self) -> None:
330        from babase import push_back_press
331
332        ui = babase.app.ui_v1
333
334        # If *any* main-window is up, kill it and resume play.
335        old_window = ui.get_main_window()
336        if old_window is not None:
337
338            classic = babase.app.classic
339            assert classic is not None
340            classic.resume()
341
342            ui.clear_main_window()
343            return
344
345        # Otherwise
346        push_back_press()
347
348    def _root_ui_account_press(self) -> None:
349        from bauiv1lib.account.settings import AccountSettingsWindow
350
351        self._auxiliary_window_nav(
352            win_type=AccountSettingsWindow,
353            win_create_call=lambda: AccountSettingsWindow(
354                origin_widget=bauiv1.get_special_widget('account_button')
355            ),
356        )
357
358    def _root_ui_squad_press(self) -> None:
359        btn = bauiv1.get_special_widget('squad_button')
360        center = btn.get_screen_space_center()
361        if bauiv1.app.classic is not None:
362            bauiv1.app.classic.party_icon_activate(center)
363        else:
364            logging.warning('party_icon_activate: no classic.')
365
366    def _root_ui_settings_press(self) -> None:
367        from bauiv1lib.settings.allsettings import AllSettingsWindow
368
369        self._auxiliary_window_nav(
370            win_type=AllSettingsWindow,
371            win_create_call=lambda: AllSettingsWindow(
372                origin_widget=bauiv1.get_special_widget('settings_button')
373            ),
374        )
375
376    def _auxiliary_window_nav(
377        self,
378        win_type: type[bauiv1.MainWindow],
379        win_create_call: Callable[[], bauiv1.MainWindow],
380    ) -> None:
381        """Navigate to or away from an Auxiliary window.
382
383        Auxiliary windows can be thought of as 'side quests' in the
384        window hierarchy; places such as settings windows or league
385        ranking windows that the user might want to visit without losing
386        their place in the regular hierarchy.
387        """
388        # pylint: disable=unidiomatic-typecheck
389
390        ui = babase.app.ui_v1
391
392        current_main_window = ui.get_main_window()
393
394        # Scan our ancestors for auxiliary states matching our type as
395        # well as auxiliary states in general.
396        aux_matching_state: bauiv1.MainWindowState | None = None
397        aux_state: bauiv1.MainWindowState | None = None
398
399        if current_main_window is None:
400            raise RuntimeError(
401                'Not currently handling no-top-level-window case.'
402            )
403
404        state = current_main_window.main_window_back_state
405        while state is not None:
406            assert state.window_type is not None
407            if state.is_auxiliary:
408                if state.window_type is win_type:
409                    aux_matching_state = state
410                else:
411                    aux_state = state
412
413            state = state.parent
414
415        # If there's an ancestor auxiliary window-state matching our
416        # type, back out past it (example: poking settings, navigating
417        # down a level or two, and then poking settings again should
418        # back out of settings).
419        if aux_matching_state is not None:
420            current_main_window.main_window_back_state = (
421                aux_matching_state.parent
422            )
423            current_main_window.main_window_back()
424            return
425
426        # If there's an ancestory auxiliary state *not* matching our
427        # type, crop the state and swap in our new auxiliary UI
428        # (example: poking settings, then poking account, then poking
429        # back should end up where things were before the settings
430        # poke).
431        if aux_state is not None:
432            # Blow away the window stack and build a fresh one.
433            ui.clear_main_window()
434            ui.set_main_window(
435                win_create_call(),
436                from_window=False,  # Disable from-check.
437                back_state=aux_state.parent,
438                suppress_warning=True,
439                is_auxiliary=True,
440            )
441            return
442
443        # Ok, no auxiliary states found. Now if current window is
444        # auxiliary and the type matches, simply do a back.
445        if (
446            current_main_window.main_window_is_auxiliary
447            and type(current_main_window) is win_type
448        ):
449            current_main_window.main_window_back()
450            return
451
452        # If current window is auxiliary but type doesn't match,
453        # swap it out for our new auxiliary UI.
454        if current_main_window.main_window_is_auxiliary:
455            ui.clear_main_window()
456            ui.set_main_window(
457                win_create_call(),
458                from_window=False,  # Disable from-check.
459                back_state=current_main_window.main_window_back_state,
460                suppress_warning=True,
461                is_auxiliary=True,
462            )
463            return
464
465        # Ok, no existing auxiliary stuff was found period. Just
466        # navigate forward to this UI.
467        current_main_window.main_window_replace(
468            win_create_call(), is_auxiliary=True
469        )
470
471    def _root_ui_achievements_press(self) -> None:
472        from bauiv1lib.achievements import AchievementsWindow
473
474        if not self._ensure_signed_in_v1():
475            return
476
477        wait_for_connectivity(
478            on_connected=lambda: self._auxiliary_window_nav(
479                win_type=AchievementsWindow,
480                win_create_call=lambda: AchievementsWindow(
481                    origin_widget=bauiv1.get_special_widget(
482                        'achievements_button'
483                    )
484                ),
485            )
486        )
487
488    def _root_ui_inbox_press(self) -> None:
489        from bauiv1lib.inbox import InboxWindow
490
491        if not self._ensure_signed_in():
492            return
493
494        wait_for_connectivity(
495            on_connected=lambda: self._auxiliary_window_nav(
496                win_type=InboxWindow,
497                win_create_call=lambda: InboxWindow(
498                    origin_widget=bauiv1.get_special_widget('inbox_button')
499                ),
500            )
501        )
502
503    def _root_ui_store_press(self) -> None:
504        from bauiv1lib.store.browser import StoreBrowserWindow
505
506        if not self._ensure_signed_in_v1():
507            return
508
509        wait_for_connectivity(
510            on_connected=lambda: self._auxiliary_window_nav(
511                win_type=StoreBrowserWindow,
512                win_create_call=lambda: StoreBrowserWindow(
513                    origin_widget=bauiv1.get_special_widget('store_button')
514                ),
515            )
516        )
517
518    def _root_ui_tickets_meter_press(self) -> None:
519        from bauiv1lib.resourcetypeinfo import ResourceTypeInfoWindow
520
521        ResourceTypeInfoWindow(
522            'tickets', origin_widget=bauiv1.get_special_widget('tickets_meter')
523        )
524
525    def _root_ui_tokens_meter_press(self) -> None:
526        from bauiv1lib.resourcetypeinfo import ResourceTypeInfoWindow
527
528        ResourceTypeInfoWindow(
529            'tokens', origin_widget=bauiv1.get_special_widget('tokens_meter')
530        )
531
532    def _root_ui_trophy_meter_press(self) -> None:
533        from bauiv1lib.league.rankwindow import LeagueRankWindow
534
535        if not self._ensure_signed_in_v1():
536            return
537
538        self._auxiliary_window_nav(
539            win_type=LeagueRankWindow,
540            win_create_call=lambda: LeagueRankWindow(
541                origin_widget=bauiv1.get_special_widget('trophy_meter')
542            ),
543        )
544
545    def _root_ui_level_meter_press(self) -> None:
546        from bauiv1lib.resourcetypeinfo import ResourceTypeInfoWindow
547
548        ResourceTypeInfoWindow(
549            'xp', origin_widget=bauiv1.get_special_widget('level_meter')
550        )
551
552    def _root_ui_inventory_press(self) -> None:
553        from bauiv1lib.inventory import InventoryWindow
554
555        if not self._ensure_signed_in_v1():
556            return
557
558        self._auxiliary_window_nav(
559            win_type=InventoryWindow,
560            win_create_call=lambda: InventoryWindow(
561                origin_widget=bauiv1.get_special_widget('inventory_button')
562            ),
563        )
564
565    def _ensure_signed_in(self) -> bool:
566        """Make sure we're signed in (requiring modern v2 accounts)."""
567        plus = bauiv1.app.plus
568        if plus is None:
569            bauiv1.screenmessage('This requires plus.', color=(1, 0, 0))
570            bauiv1.getsound('error').play()
571            return False
572        if plus.accounts.primary is None:
573            show_sign_in_prompt()
574            return False
575        return True
576
577    def _ensure_signed_in_v1(self) -> bool:
578        """Make sure we're signed in (allowing legacy v1-only accounts)."""
579        plus = bauiv1.app.plus
580        if plus is None:
581            bauiv1.screenmessage('This requires plus.', color=(1, 0, 0))
582            bauiv1.getsound('error').play()
583            return False
584        if plus.get_v1_account_state() != 'signed_in':
585            show_sign_in_prompt()
586            return False
587        return True
588
589    def _root_ui_get_tokens_press(self) -> None:
590        from bauiv1lib.gettokens import GetTokensWindow
591
592        if not self._ensure_signed_in():
593            return
594
595        self._auxiliary_window_nav(
596            win_type=GetTokensWindow,
597            win_create_call=lambda: GetTokensWindow(
598                origin_widget=bauiv1.get_special_widget('get_tokens_button')
599            ),
600        )
601
602    def _root_ui_chest_slot_pressed(self, index: int) -> None:
603        from bauiv1lib.chest import (
604            ChestWindow0,
605            ChestWindow1,
606            ChestWindow2,
607            ChestWindow3,
608        )
609
610        widgetid: Literal[
611            'chest_0_button',
612            'chest_1_button',
613            'chest_2_button',
614            'chest_3_button',
615        ]
616        winclass: type[ChestWindow]
617        if index == 0:
618            widgetid = 'chest_0_button'
619            winclass = ChestWindow0
620        elif index == 1:
621            widgetid = 'chest_1_button'
622            winclass = ChestWindow1
623        elif index == 2:
624            widgetid = 'chest_2_button'
625            winclass = ChestWindow2
626        elif index == 3:
627            widgetid = 'chest_3_button'
628            winclass = ChestWindow3
629        else:
630            raise RuntimeError(f'Invalid index {index}')
631
632        wait_for_connectivity(
633            on_connected=lambda: self._auxiliary_window_nav(
634                win_type=winclass,
635                win_create_call=lambda: winclass(
636                    index=index,
637                    origin_widget=bauiv1.get_special_widget(widgetid),
638                ),
639            )
640        )

AppMode for the classic BombSquad experience.

@override
@classmethod
def get_app_experience(cls) -> bacommon.app.AppExperience:
45    @override
46    @classmethod
47    def get_app_experience(cls) -> AppExperience:
48        return AppExperience.MELEE

Return the overall experience provided by this mode.

@override
def handle_intent(self, intent: babase.AppIntent) -> None:
58    @override
59    def handle_intent(self, intent: babase.AppIntent) -> None:
60        if isinstance(intent, babase.AppIntentExec):
61            _baclassic.classic_app_mode_handle_app_intent_exec(intent.code)
62            return
63        assert isinstance(intent, babase.AppIntentDefault)
64        _baclassic.classic_app_mode_handle_app_intent_default()

Handle an intent.

@override
def on_activate(self) -> None:
 66    @override
 67    def on_activate(self) -> None:
 68
 69        # Let the native layer do its thing.
 70        _baclassic.classic_app_mode_activate()
 71
 72        app = babase.app
 73        plus = app.plus
 74        assert plus is not None
 75
 76        # Wire up the root ui to do what we want.
 77        ui = app.ui_v1
 78        ui.root_ui_calls[ui.RootUIElement.ACCOUNT_BUTTON] = (
 79            self._root_ui_account_press
 80        )
 81        ui.root_ui_calls[ui.RootUIElement.MENU_BUTTON] = (
 82            self._root_ui_menu_press
 83        )
 84        ui.root_ui_calls[ui.RootUIElement.SQUAD_BUTTON] = (
 85            self._root_ui_squad_press
 86        )
 87        ui.root_ui_calls[ui.RootUIElement.SETTINGS_BUTTON] = (
 88            self._root_ui_settings_press
 89        )
 90        ui.root_ui_calls[ui.RootUIElement.STORE_BUTTON] = (
 91            self._root_ui_store_press
 92        )
 93        ui.root_ui_calls[ui.RootUIElement.INVENTORY_BUTTON] = (
 94            self._root_ui_inventory_press
 95        )
 96        ui.root_ui_calls[ui.RootUIElement.GET_TOKENS_BUTTON] = (
 97            self._root_ui_get_tokens_press
 98        )
 99        ui.root_ui_calls[ui.RootUIElement.INBOX_BUTTON] = (
100            self._root_ui_inbox_press
101        )
102        ui.root_ui_calls[ui.RootUIElement.TICKETS_METER] = (
103            self._root_ui_tickets_meter_press
104        )
105        ui.root_ui_calls[ui.RootUIElement.TOKENS_METER] = (
106            self._root_ui_tokens_meter_press
107        )
108        ui.root_ui_calls[ui.RootUIElement.TROPHY_METER] = (
109            self._root_ui_trophy_meter_press
110        )
111        ui.root_ui_calls[ui.RootUIElement.LEVEL_METER] = (
112            self._root_ui_level_meter_press
113        )
114        ui.root_ui_calls[ui.RootUIElement.ACHIEVEMENTS_BUTTON] = (
115            self._root_ui_achievements_press
116        )
117        ui.root_ui_calls[ui.RootUIElement.CHEST_SLOT_0] = partial(
118            self._root_ui_chest_slot_pressed, 0
119        )
120        ui.root_ui_calls[ui.RootUIElement.CHEST_SLOT_1] = partial(
121            self._root_ui_chest_slot_pressed, 1
122        )
123        ui.root_ui_calls[ui.RootUIElement.CHEST_SLOT_2] = partial(
124            self._root_ui_chest_slot_pressed, 2
125        )
126        ui.root_ui_calls[ui.RootUIElement.CHEST_SLOT_3] = partial(
127            self._root_ui_chest_slot_pressed, 3
128        )
129
130        # We want to be informed when connectivity changes.
131        self._on_connectivity_changed_callback = (
132            plus.cloud.on_connectivity_changed_callbacks.register(
133                self._update_for_connectivity_change
134            )
135        )
136        # We want to be informed when primary account changes.
137        self._on_primary_account_changed_callback = (
138            plus.accounts.on_primary_account_changed_callbacks.register(
139                self._update_for_primary_account
140            )
141        )
142        # Establish subscriptions/etc. for any current primary account.
143        self._update_for_primary_account(plus.accounts.primary)
144        self._have_connectivity = plus.cloud.is_connected()
145        self._update_for_connectivity_change(self._have_connectivity)

Called when the mode is being activated.

@override
def on_deactivate(self) -> None:
147    @override
148    def on_deactivate(self) -> None:
149
150        classic = babase.app.classic
151
152        # Stop being informed of account changes.
153        self._on_primary_account_changed_callback = None
154
155        # Remove anything following any current account.
156        self._update_for_primary_account(None)
157
158        # Save where we were in the UI so we return there next time.
159        if classic is not None:
160            classic.save_ui_state()
161
162        # Let the native layer do its thing.
163        _baclassic.classic_app_mode_deactivate()

Called when the mode is being deactivated.

@override
def on_app_active_changed(self) -> None:
165    @override
166    def on_app_active_changed(self) -> None:
167        # If we've gone inactive, bring up the main menu, which has the
168        # side effect of pausing the action (when possible).
169        if not babase.app.active:
170            babase.invoke_main_menu()

Called when ba*.app.active changes while this mode is active.

The app-mode may want to take action such as pausing a running game in such cases.

class ClassicAppSubsystem(babase._appsubsystem.AppSubsystem):
 38class ClassicAppSubsystem(babase.AppSubsystem):
 39    """Subsystem for classic functionality in the app.
 40
 41    The single shared instance of this app can be accessed at
 42    babase.app.classic. Note that it is possible for babase.app.classic to
 43    be None if the classic package is not present, and code should handle
 44    that case gracefully.
 45    """
 46
 47    # pylint: disable=too-many-public-methods
 48
 49    # noinspection PyUnresolvedReferences
 50    from baclassic._music import MusicPlayMode
 51
 52    def __init__(self) -> None:
 53        super().__init__()
 54        self._env = babase.env()
 55
 56        self.accounts = AccountV1Subsystem()
 57        self.ads = AdsSubsystem()
 58        self.ach = AchievementSubsystem()
 59        self.store = StoreSubsystem()
 60        self.music = MusicSubsystem()
 61
 62        # Co-op Campaigns.
 63        self.campaigns: dict[str, bascenev1.Campaign] = {}
 64        self.custom_coop_practice_games: list[str] = []
 65
 66        # Lobby.
 67        self.lobby_random_profile_index: int = 1
 68        self.lobby_random_char_index_offset = random.randrange(1000)
 69        self.lobby_account_profile_device_id: int | None = None
 70
 71        # Misc.
 72        self.tips: list[str] = []
 73        self.stress_test_update_timer: babase.AppTimer | None = None
 74        self.stress_test_update_timer_2: babase.AppTimer | None = None
 75        self.value_test_defaults: dict = {}
 76        self.special_offer: dict | None = None
 77        self.ping_thread_count = 0
 78        self.allow_ticket_purchases: bool = True
 79
 80        # Main Menu.
 81        self.main_menu_did_initial_transition = False
 82        self.main_menu_last_news_fetch_time: float | None = None
 83
 84        # Spaz.
 85        self.spaz_appearances: dict[str, spazappearance.Appearance] = {}
 86        self.last_spaz_turbo_warn_time = babase.AppTime(-99999.0)
 87
 88        # Server Mode.
 89        self.server: ServerController | None = None
 90
 91        self.log_have_new = False
 92        self.log_upload_timer_started = False
 93        self.printed_live_object_warning = False
 94
 95        # We include this extra hash with shared input-mapping names so
 96        # that we don't share mappings between differently-configured
 97        # systems. For instance, different android devices may give different
 98        # key values for the same controller type so we keep their mappings
 99        # distinct.
100        self.input_map_hash: str | None = None
101
102        # Maps.
103        self.maps: dict[str, type[bascenev1.Map]] = {}
104
105        # Gameplay.
106        self.teams_series_length = 7  # deprecated, left for old mods
107        self.ffa_series_length = 24  # deprecated, left for old mods
108        self.coop_session_args: dict = {}
109
110        # UI.
111        self.first_main_menu = True  # FIXME: Move to mainmenu class.
112        self.did_menu_intro = False  # FIXME: Move to mainmenu class.
113        self.main_menu_window_refresh_check_count = 0  # FIXME: Mv to mainmenu.
114        self.invite_confirm_windows: list[Any] = []  # FIXME: Don't use Any.
115        self.party_window: weakref.ref[PartyWindow] | None = None
116        self.main_menu_resume_callbacks: list = []
117        self.saved_ui_state: bauiv1.MainWindowState | None = None
118
119        # Store.
120        self.store_layout: dict[str, list[dict[str, Any]]] | None = None
121        self.store_items: dict[str, dict] | None = None
122        self.pro_sale_start_time: int | None = None
123        self.pro_sale_start_val: int | None = None
124
125    def add_main_menu_close_callback(self, call: Callable[[], Any]) -> None:
126        """(internal)"""
127
128        # If there's no main window up, just call immediately.
129        if not babase.app.ui_v1.has_main_window():
130            with babase.ContextRef.empty():
131                call()
132        else:
133            self.main_menu_resume_callbacks.append(call)
134
135    @property
136    def platform(self) -> str:
137        """Name of the current platform.
138
139        Examples are: 'mac', 'windows', android'.
140        """
141        assert isinstance(self._env['platform'], str)
142        return self._env['platform']
143
144    def scene_v1_protocol_version(self) -> int:
145        """(internal)"""
146        return bascenev1.protocol_version()
147
148    @property
149    def subplatform(self) -> str:
150        """String for subplatform.
151
152        Can be empty. For the 'android' platform, subplatform may
153        be 'google', 'amazon', etc.
154        """
155        assert isinstance(self._env['subplatform'], str)
156        return self._env['subplatform']
157
158    @property
159    def legacy_user_agent_string(self) -> str:
160        """String containing various bits of info about OS/device/etc."""
161        assert isinstance(self._env['legacy_user_agent_string'], str)
162        return self._env['legacy_user_agent_string']
163
164    @override
165    def on_app_loading(self) -> None:
166        from bascenev1lib.actor import spazappearance
167        from bascenev1lib import maps as stdmaps
168
169        plus = babase.app.plus
170        assert plus is not None
171
172        env = babase.app.env
173        cfg = babase.app.config
174
175        self.music.on_app_loading()
176
177        # Non-test, non-debug builds should generally be blessed; warn if not.
178        # (so I don't accidentally release a build that can't play tourneys)
179        if not env.debug and not env.test and not plus.is_blessed():
180            babase.screenmessage('WARNING: NON-BLESSED BUILD', color=(1, 0, 0))
181
182        stdmaps.register_all_maps()
183
184        spazappearance.register_appearances()
185        bascenev1.init_campaigns()
186
187        launch_count = cfg.get('launchCount', 0)
188        launch_count += 1
189
190        # So we know how many times we've run the game at various
191        # version milestones.
192        for key in ('lc14173', 'lc14292'):
193            cfg.setdefault(key, launch_count)
194
195        cfg['launchCount'] = launch_count
196        cfg.commit()
197
198        # If there's a leftover log file, attempt to upload it to the
199        # master-server and/or get rid of it.
200        babase.handle_leftover_v1_cloud_log_file()
201
202        self.accounts.on_app_loading()
203
204    @override
205    def on_app_suspend(self) -> None:
206        self.accounts.on_app_suspend()
207
208    @override
209    def on_app_unsuspend(self) -> None:
210        self.accounts.on_app_unsuspend()
211        self.music.on_app_unsuspend()
212
213    @override
214    def on_app_shutdown(self) -> None:
215        self.music.on_app_shutdown()
216
217    def pause(self) -> None:
218        """Pause the game due to a user request or menu popping up.
219
220        If there's a foreground host-activity that says it's pausable, tell it
221        to pause. Note: we now no longer pause if there are connected clients.
222        """
223        activity: bascenev1.Activity | None = (
224            bascenev1.get_foreground_host_activity()
225        )
226        if (
227            activity is not None
228            and activity.allow_pausing
229            and not bascenev1.have_connected_clients()
230        ):
231            from babase import Lstr
232            from bascenev1 import NodeActor
233
234            # FIXME: Shouldn't be touching scene stuff here;
235            #  should just pass the request on to the host-session.
236            with activity.context:
237                globs = activity.globalsnode
238                if not globs.paused:
239                    bascenev1.getsound('refWhistle').play()
240                    globs.paused = True
241
242                # FIXME: This should not be an attr on Actor.
243                activity.paused_text = NodeActor(
244                    bascenev1.newnode(
245                        'text',
246                        attrs={
247                            'text': Lstr(resource='pausedByHostText'),
248                            'client_only': True,
249                            'flatness': 1.0,
250                            'h_align': 'center',
251                        },
252                    )
253                )
254
255    def resume(self) -> None:
256        """Resume the game due to a user request or menu closing.
257
258        If there's a foreground host-activity that's currently paused, tell it
259        to resume.
260        """
261
262        # FIXME: Shouldn't be touching scene stuff here;
263        #  should just pass the request on to the host-session.
264        activity = bascenev1.get_foreground_host_activity()
265        if activity is not None:
266            with activity.context:
267                globs = activity.globalsnode
268                if globs.paused:
269                    bascenev1.getsound('refWhistle').play()
270                    globs.paused = False
271
272                    # FIXME: This should not be an actor attr.
273                    activity.paused_text = None
274
275    def add_coop_practice_level(self, level: bascenev1.Level) -> None:
276        """Adds an individual level to the 'practice' section in Co-op."""
277
278        # Assign this level to our catch-all campaign.
279        self.campaigns['Challenges'].addlevel(level)
280
281        # Make note to add it to our challenges UI.
282        self.custom_coop_practice_games.append(f'Challenges:{level.name}')
283
284    def launch_coop_game(
285        self, game: str, force: bool = False, args: dict | None = None
286    ) -> bool:
287        """High level way to launch a local co-op session."""
288        # pylint: disable=cyclic-import
289        from bauiv1lib.coop.level import CoopLevelLockedWindow
290
291        assert babase.app.classic is not None
292
293        if args is None:
294            args = {}
295        if game == '':
296            raise ValueError('empty game name')
297        campaignname, levelname = game.split(':')
298        campaign = babase.app.classic.getcampaign(campaignname)
299
300        # If this campaign is sequential, make sure we've completed the
301        # one before this.
302        if campaign.sequential and not force:
303            for level in campaign.levels:
304                if level.name == levelname:
305                    break
306                if not level.complete:
307                    CoopLevelLockedWindow(
308                        campaign.getlevel(levelname).displayname,
309                        campaign.getlevel(level.name).displayname,
310                    )
311                    return False
312
313        # Save where we are in the UI to come back to when done.
314        babase.app.classic.save_ui_state()
315
316        # Ok, we're good to go.
317        self.coop_session_args = {
318            'campaign': campaignname,
319            'level': levelname,
320        }
321        for arg_name, arg_val in list(args.items()):
322            self.coop_session_args[arg_name] = arg_val
323
324        def _fade_end() -> None:
325            from bascenev1 import CoopSession
326
327            try:
328                bascenev1.new_host_session(CoopSession)
329            except Exception:
330                logging.exception('Error creating coopsession after fade end.')
331                from bascenev1lib.mainmenu import MainMenuSession
332
333                bascenev1.new_host_session(MainMenuSession)
334
335        babase.fade_screen(False, endcall=_fade_end)
336        return True
337
338    def return_to_main_menu_session_gracefully(
339        self, reset_ui: bool = True
340    ) -> None:
341        """Attempt to cleanly get back to the main menu."""
342        # pylint: disable=cyclic-import
343        from baclassic import _benchmark
344        from bascenev1lib.mainmenu import MainMenuSession
345
346        plus = babase.app.plus
347        assert plus is not None
348
349        if reset_ui:
350            babase.app.ui_v1.clear_main_window()
351
352        if isinstance(bascenev1.get_foreground_host_session(), MainMenuSession):
353            # It may be possible we're on the main menu but the screen is faded
354            # so fade back in.
355            babase.fade_screen(True)
356            return
357
358        _benchmark.stop_stress_test()  # Stop stress-test if in progress.
359
360        # If we're in a host-session, tell them to end.
361        # This lets them tear themselves down gracefully.
362        host_session: bascenev1.Session | None = (
363            bascenev1.get_foreground_host_session()
364        )
365        if host_session is not None:
366            # Kick off a little transaction so we'll hopefully have all the
367            # latest account state when we get back to the menu.
368            plus.add_v1_account_transaction(
369                {'type': 'END_SESSION', 'sType': str(type(host_session))}
370            )
371            plus.run_v1_account_transactions()
372
373            host_session.end()
374
375        # Otherwise just force the issue.
376        else:
377            babase.pushcall(
378                babase.Call(bascenev1.new_host_session, MainMenuSession)
379            )
380
381    def getmaps(self, playtype: str) -> list[str]:
382        """Return a list of bascenev1.Map types supporting a playtype str.
383
384        Category: **Asset Functions**
385
386        Maps supporting a given playtype must provide a particular set of
387        features and lend themselves to a certain style of play.
388
389        Play Types:
390
391        'melee'
392          General fighting map.
393          Has one or more 'spawn' locations.
394
395        'team_flag'
396          For games such as Capture The Flag where each team spawns by a flag.
397          Has two or more 'spawn' locations, each with a corresponding 'flag'
398          location (based on index).
399
400        'single_flag'
401          For games such as King of the Hill or Keep Away where multiple teams
402          are fighting over a single flag.
403          Has two or more 'spawn' locations and 1 'flag_default' location.
404
405        'conquest'
406          For games such as Conquest where flags are spread throughout the map
407          - has 2+ 'flag' locations, 2+ 'spawn_by_flag' locations.
408
409        'king_of_the_hill' - has 2+ 'spawn' locations,
410           1+ 'flag_default' locations, and 1+ 'powerup_spawn' locations
411
412        'hockey'
413          For hockey games.
414          Has two 'goal' locations, corresponding 'spawn' locations, and one
415          'flag_default' location (for where puck spawns)
416
417        'football'
418          For football games.
419          Has two 'goal' locations, corresponding 'spawn' locations, and one
420          'flag_default' location (for where flag/ball/etc. spawns)
421
422        'race'
423          For racing games where players much touch each region in order.
424          Has two or more 'race_point' locations.
425        """
426        return sorted(
427            key
428            for key, val in self.maps.items()
429            if playtype in val.get_play_types()
430        )
431
432    def game_begin_analytics(self) -> None:
433        """(internal)"""
434        from baclassic import _analytics
435
436        _analytics.game_begin_analytics()
437
438    @classmethod
439    def json_prep(cls, data: Any) -> Any:
440        """Return a json-friendly version of the provided data.
441
442        This converts any tuples to lists and any bytes to strings
443        (interpreted as utf-8, ignoring errors). Logs errors (just once)
444        if any data is modified/discarded/unsupported.
445        """
446
447        if isinstance(data, dict):
448            return dict(
449                (cls.json_prep(key), cls.json_prep(value))
450                for key, value in list(data.items())
451            )
452        if isinstance(data, list):
453            return [cls.json_prep(element) for element in data]
454        if isinstance(data, tuple):
455            logging.exception('json_prep encountered tuple')
456            return [cls.json_prep(element) for element in data]
457        if isinstance(data, bytes):
458            try:
459                return data.decode(errors='ignore')
460            except Exception:
461                logging.exception('json_prep encountered utf-8 decode error')
462                return data.decode(errors='ignore')
463        if not isinstance(data, (str, float, bool, type(None), int)):
464            logging.exception(
465                'got unsupported type in json_prep: %s', type(data)
466            )
467        return data
468
469    def master_server_v1_get(
470        self,
471        request: str,
472        data: dict[str, Any],
473        callback: MasterServerCallback | None = None,
474        response_type: MasterServerResponseType = MasterServerResponseType.JSON,
475    ) -> None:
476        """Make a call to the master server via a http GET."""
477
478        MasterServerV1CallThread(
479            request, 'get', data, callback, response_type
480        ).start()
481
482    def master_server_v1_post(
483        self,
484        request: str,
485        data: dict[str, Any],
486        callback: MasterServerCallback | None = None,
487        response_type: MasterServerResponseType = MasterServerResponseType.JSON,
488    ) -> None:
489        """Make a call to the master server via a http POST."""
490        MasterServerV1CallThread(
491            request, 'post', data, callback, response_type
492        ).start()
493
494    def set_tournament_prize_image(
495        self, entry: dict[str, Any], index: int, image: bauiv1.Widget
496    ) -> None:
497        """Given a tournament entry, return strings for its prize levels."""
498        from baclassic import _tournament
499
500        return _tournament.set_tournament_prize_chest_image(entry, index, image)
501
502    def create_in_game_tournament_prize_image(
503        self,
504        entry: dict[str, Any],
505        index: int,
506        position: tuple[float, float],
507    ) -> None:
508        """Given a tournament entry, return strings for its prize levels."""
509        from baclassic import _tournament
510
511        _tournament.create_in_game_tournament_prize_image(
512            entry, index, position
513        )
514
515    def get_tournament_prize_strings(
516        self, entry: dict[str, Any], include_tickets: bool
517    ) -> list[str]:
518        """Given a tournament entry, return strings for its prize levels."""
519        from baclassic import _tournament
520
521        return _tournament.get_tournament_prize_strings(
522            entry, include_tickets=include_tickets
523        )
524
525    def getcampaign(self, name: str) -> bascenev1.Campaign:
526        """Return a campaign by name."""
527        return self.campaigns[name]
528
529    def get_next_tip(self) -> str:
530        """Returns the next tip to be displayed."""
531        if not self.tips:
532            for tip in get_all_tips():
533                self.tips.insert(random.randint(0, len(self.tips)), tip)
534        tip = self.tips.pop()
535        return tip
536
537    def run_cpu_benchmark(self) -> None:
538        """Kick off a benchmark to test cpu speeds."""
539        from baclassic._benchmark import run_cpu_benchmark
540
541        run_cpu_benchmark()
542
543    def run_media_reload_benchmark(self) -> None:
544        """Kick off a benchmark to test media reloading speeds."""
545        from baclassic._benchmark import run_media_reload_benchmark
546
547        run_media_reload_benchmark()
548
549    def run_stress_test(
550        self,
551        *,
552        playlist_type: str = 'Random',
553        playlist_name: str = '__default__',
554        player_count: int = 8,
555        round_duration: int = 30,
556        attract_mode: bool = False,
557    ) -> None:
558        """Run a stress test."""
559        from baclassic._benchmark import run_stress_test
560
561        run_stress_test(
562            playlist_type=playlist_type,
563            playlist_name=playlist_name,
564            player_count=player_count,
565            round_duration=round_duration,
566            attract_mode=attract_mode,
567        )
568
569    def get_input_device_mapped_value(
570        self,
571        device: bascenev1.InputDevice,
572        name: str,
573        default: bool = False,
574    ) -> Any:
575        """Return a mapped value for an input device.
576
577        This checks the user config and falls back to default values
578        where available.
579        """
580        return _input.get_input_device_mapped_value(
581            device.name, device.unique_identifier, name, default
582        )
583
584    def get_input_device_map_hash(
585        self, inputdevice: bascenev1.InputDevice
586    ) -> str:
587        """Given an input device, return hash based on its raw input values."""
588        del inputdevice  # unused currently
589        return _input.get_input_device_map_hash()
590
591    def get_input_device_config(
592        self, inputdevice: bascenev1.InputDevice, default: bool
593    ) -> tuple[dict, str]:
594        """Given an input device, return its config dict in the app config.
595
596        The dict will be created if it does not exist.
597        """
598        return _input.get_input_device_config(
599            inputdevice.name, inputdevice.unique_identifier, default
600        )
601
602    def get_player_colors(self) -> list[tuple[float, float, float]]:
603        """Return user-selectable player colors."""
604        return bascenev1.get_player_colors()
605
606    def get_player_profile_icon(self, profilename: str) -> str:
607        """Given a profile name, returns an icon string for it.
608
609        (non-account profiles only)
610        """
611        return bascenev1.get_player_profile_icon(profilename)
612
613    def get_player_profile_colors(
614        self,
615        profilename: str | None,
616        profiles: dict[str, dict[str, Any]] | None = None,
617    ) -> tuple[tuple[float, float, float], tuple[float, float, float]]:
618        """Given a profile, return colors for them."""
619        return bascenev1.get_player_profile_colors(profilename, profiles)
620
621    def get_foreground_host_session(self) -> bascenev1.Session | None:
622        """(internal)"""
623        return bascenev1.get_foreground_host_session()
624
625    def get_foreground_host_activity(self) -> bascenev1.Activity | None:
626        """(internal)"""
627        return bascenev1.get_foreground_host_activity()
628
629    def value_test(
630        self,
631        arg: str,
632        change: float | None = None,
633        absolute: float | None = None,
634    ) -> float:
635        """(internal)"""
636        return _baclassic.value_test(arg, change, absolute)
637
638    def set_master_server_source(self, source: int) -> None:
639        """(internal)"""
640        bascenev1.set_master_server_source(source)
641
642    def get_game_port(self) -> int:
643        """(internal)"""
644        return bascenev1.get_game_port()
645
646    def v2_upgrade_window(self, login_name: str, code: str) -> None:
647        """(internal)"""
648
649        from bauiv1lib.v2upgrade import V2UpgradeWindow
650
651        V2UpgradeWindow(login_name, code)
652
653    def account_link_code_window(self, data: dict[str, Any]) -> None:
654        """(internal)"""
655        from bauiv1lib.account.link import AccountLinkCodeWindow
656
657        AccountLinkCodeWindow(data)
658
659    def server_dialog(self, delay: float, data: dict[str, Any]) -> None:
660        """(internal)"""
661        from bauiv1lib.serverdialog import (
662            ServerDialogData,
663            ServerDialogWindow,
664        )
665
666        try:
667            sddata = dataclass_from_dict(ServerDialogData, data)
668        except Exception:
669            sddata = None
670            logging.warning(
671                'Got malformatted ServerDialogData: %s',
672                data,
673            )
674        if sddata is not None:
675            babase.apptimer(
676                delay,
677                babase.Call(ServerDialogWindow, sddata),
678            )
679
680    # def root_ui_ticket_icon_press(self) -> None:
681    #     """(internal)"""
682    #     from bauiv1lib.resourcetypeinfo import ResourceTypeInfoWindow
683
684    #     ResourceTypeInfoWindow(
685    #         origin_widget=bauiv1.get_special_widget('tickets_meter')
686    #     )
687
688    def show_url_window(self, address: str) -> None:
689        """(internal)"""
690        from bauiv1lib.url import ShowURLWindow
691
692        ShowURLWindow(address)
693
694    def quit_window(self, quit_type: babase.QuitType) -> None:
695        """(internal)"""
696        from bauiv1lib.confirm import QuitWindow
697
698        QuitWindow(quit_type)
699
700    def tournament_entry_window(
701        self,
702        tournament_id: str,
703        *,
704        tournament_activity: bascenev1.Activity | None = None,
705        position: tuple[float, float] = (0.0, 0.0),
706        delegate: Any = None,
707        scale: float | None = None,
708        offset: tuple[float, float] = (0.0, 0.0),
709        on_close_call: Callable[[], Any] | None = None,
710    ) -> None:
711        """(internal)"""
712        from bauiv1lib.tournamententry import TournamentEntryWindow
713
714        TournamentEntryWindow(
715            tournament_id,
716            tournament_activity,
717            position,
718            delegate,
719            scale,
720            offset,
721            on_close_call,
722        )
723
724    def get_main_menu_session(self) -> type[bascenev1.Session]:
725        """(internal)"""
726        from bascenev1lib.mainmenu import MainMenuSession
727
728        return MainMenuSession
729
730    def profile_browser_window(
731        self,
732        transition: str = 'in_right',
733        origin_widget: bauiv1.Widget | None = None,
734        # in_main_menu: bool = True,
735        selected_profile: str | None = None,
736    ) -> None:
737        """(internal)"""
738        from bauiv1lib.profile.browser import ProfileBrowserWindow
739
740        main_window = babase.app.ui_v1.get_main_window()
741        if main_window is not None:
742            logging.warning(
743                'profile_browser_window()'
744                ' called with existing main window; should not happen.'
745            )
746            return
747
748        babase.app.ui_v1.set_main_window(
749            ProfileBrowserWindow(
750                transition=transition,
751                selected_profile=selected_profile,
752                origin_widget=origin_widget,
753                minimal_toolbar=True,
754            ),
755            is_top_level=True,
756            suppress_warning=True,
757        )
758
759    def preload_map_preview_media(self) -> None:
760        """Preload media needed for map preview UIs.
761
762        Category: **Asset Functions**
763        """
764        try:
765            bauiv1.getmesh('level_select_button_opaque')
766            bauiv1.getmesh('level_select_button_transparent')
767            for maptype in list(self.maps.values()):
768                map_tex_name = maptype.get_preview_texture_name()
769                if map_tex_name is not None:
770                    bauiv1.gettexture(map_tex_name)
771        except Exception:
772            logging.exception('Error preloading map preview media.')
773
774    def party_icon_activate(self, origin: Sequence[float]) -> None:
775        """(internal)"""
776        from bauiv1lib.party import PartyWindow
777        from babase import app
778
779        assert app.env.gui
780
781        # Play explicit swish sound so it occurs due to keypresses/etc.
782        # This means we have to disable it for any button or else we get
783        # double.
784        bauiv1.getsound('swish').play()
785
786        # If it exists, dismiss it; otherwise make a new one.
787        party_window = (
788            None if self.party_window is None else self.party_window()
789        )
790        if party_window is not None:
791            party_window.close()
792        else:
793            self.party_window = weakref.ref(PartyWindow(origin=origin))
794
795    def device_menu_press(self, device_id: int | None) -> None:
796        """(internal)"""
797        from bauiv1lib.ingamemenu import InGameMenuWindow
798        from bauiv1 import set_ui_input_device
799
800        assert babase.app is not None
801        in_main_menu = babase.app.ui_v1.has_main_window()
802        if not in_main_menu:
803            set_ui_input_device(device_id)
804
805            # Hack(ish). We play swish sound here so it happens for
806            # device presses, but this means we need to disable default
807            # swish sounds for any menu buttons or we'll get double.
808            if babase.app.env.gui:
809                bauiv1.getsound('swish').play()
810
811            babase.app.ui_v1.set_main_window(
812                InGameMenuWindow(), is_top_level=True, suppress_warning=True
813            )
814
815    def save_ui_state(self) -> None:
816        """Store our current place in the UI."""
817        ui = babase.app.ui_v1
818        mainwindow = ui.get_main_window()
819        if mainwindow is not None:
820            self.saved_ui_state = ui.save_main_window_state(mainwindow)
821        else:
822            self.saved_ui_state = None
823
824    def invoke_main_menu_ui(self) -> None:
825        """Bring up main menu ui."""
826
827        # Bring up the last place we were, or start at the main menu
828        # otherwise.
829        app = bauiv1.app
830        env = app.env
831        with bascenev1.ContextRef.empty():
832            # from bauiv1lib import specialoffer
833
834            assert app.classic is not None
835            if app.env.headless:
836                # UI stuff fails now in headless builds; avoid it.
837                pass
838            else:
839
840                # When coming back from a kiosk-mode game, jump to the
841                # kiosk start screen.
842                if env.demo or env.arcade:
843                    # pylint: disable=cyclic-import
844                    from bauiv1lib.kiosk import KioskWindow
845
846                    app.ui_v1.set_main_window(
847                        KioskWindow(), is_top_level=True, suppress_warning=True
848                    )
849                else:
850                    # If there's a saved ui state, restore that.
851                    if self.saved_ui_state is not None:
852                        app.ui_v1.restore_main_window_state(self.saved_ui_state)
853                    else:
854                        # Otherwise start fresh at the main menu.
855                        from bauiv1lib.mainmenu import MainMenuWindow
856
857                        app.ui_v1.set_main_window(
858                            MainMenuWindow(transition=None),
859                            is_top_level=True,
860                            suppress_warning=True,
861                        )
862
863    @staticmethod
864    def run_bs_client_effects(effects: list[bacommon.bs.ClientEffect]) -> None:
865        """Run client effects sent from the master server."""
866        from baclassic._clienteffect import run_bs_client_effects
867
868        run_bs_client_effects(effects)
869
870    @staticmethod
871    def basic_client_ui_button_label_str(
872        label: bacommon.bs.BasicClientUI.ButtonLabel,
873    ) -> babase.Lstr:
874        """Given a client-ui label, return an Lstr."""
875        import bacommon.bs
876
877        cls = bacommon.bs.BasicClientUI.ButtonLabel
878        if label is cls.UNKNOWN:
879            # Server should not be sending us unknown stuff; make noise
880            # if they do.
881            logging.error(
882                'Got BasicClientUI.ButtonLabel.UNKNOWN; should not happen.'
883            )
884            return babase.Lstr(value='<error>')
885
886        rsrc: str | None = None
887        if label is cls.OK:
888            rsrc = 'okText'
889        elif label is cls.APPLY:
890            rsrc = 'applyText'
891        elif label is cls.CANCEL:
892            rsrc = 'cancelText'
893        elif label is cls.ACCEPT:
894            rsrc = 'gatherWindow.partyInviteAcceptText'
895        elif label is cls.DECLINE:
896            rsrc = 'gatherWindow.partyInviteDeclineText'
897        elif label is cls.IGNORE:
898            rsrc = 'gatherWindow.partyInviteIgnoreText'
899        elif label is cls.CLAIM:
900            rsrc = 'claimText'
901        elif label is cls.DISCARD:
902            rsrc = 'discardText'
903        else:
904            assert_never(label)
905
906        return babase.Lstr(resource=rsrc)

Subsystem for classic functionality in the app.

The single shared instance of this app can be accessed at babase.app.classic. Note that it is possible for babase.app.classic to be None if the classic package is not present, and code should handle that case gracefully.

accounts
ads
ach
store
music
campaigns: dict[str, bascenev1.Campaign]
custom_coop_practice_games: list[str]
lobby_random_profile_index: int
lobby_random_char_index_offset
lobby_account_profile_device_id: int | None
tips: list[str]
stress_test_update_timer: _babase.AppTimer | None
stress_test_update_timer_2: _babase.AppTimer | None
value_test_defaults: dict
special_offer: dict | None
ping_thread_count
allow_ticket_purchases: bool
main_menu_did_initial_transition
main_menu_last_news_fetch_time: float | None
spaz_appearances: dict[str, bascenev1lib.actor.spazappearance.Appearance]
last_spaz_turbo_warn_time
server: baclassic._servermode.ServerController | None
log_have_new
log_upload_timer_started
printed_live_object_warning
input_map_hash: str | None
maps: dict[str, type[bascenev1.Map]]
teams_series_length
ffa_series_length
coop_session_args: dict
first_main_menu
did_menu_intro
main_menu_window_refresh_check_count
invite_confirm_windows: list[typing.Any]
party_window: weakref.ReferenceType[bauiv1lib.party.PartyWindow] | None
main_menu_resume_callbacks: list
saved_ui_state: bauiv1.MainWindowState | None
store_layout: dict[str, list[dict[str, typing.Any]]] | None
store_items: dict[str, dict] | None
pro_sale_start_time: int | None
pro_sale_start_val: int | None
platform: str
135    @property
136    def platform(self) -> str:
137        """Name of the current platform.
138
139        Examples are: 'mac', 'windows', android'.
140        """
141        assert isinstance(self._env['platform'], str)
142        return self._env['platform']

Name of the current platform.

Examples are: 'mac', 'windows', android'.

subplatform: str
148    @property
149    def subplatform(self) -> str:
150        """String for subplatform.
151
152        Can be empty. For the 'android' platform, subplatform may
153        be 'google', 'amazon', etc.
154        """
155        assert isinstance(self._env['subplatform'], str)
156        return self._env['subplatform']

String for subplatform.

Can be empty. For the 'android' platform, subplatform may be 'google', 'amazon', etc.

legacy_user_agent_string: str
158    @property
159    def legacy_user_agent_string(self) -> str:
160        """String containing various bits of info about OS/device/etc."""
161        assert isinstance(self._env['legacy_user_agent_string'], str)
162        return self._env['legacy_user_agent_string']

String containing various bits of info about OS/device/etc.

@override
def on_app_loading(self) -> None:
164    @override
165    def on_app_loading(self) -> None:
166        from bascenev1lib.actor import spazappearance
167        from bascenev1lib import maps as stdmaps
168
169        plus = babase.app.plus
170        assert plus is not None
171
172        env = babase.app.env
173        cfg = babase.app.config
174
175        self.music.on_app_loading()
176
177        # Non-test, non-debug builds should generally be blessed; warn if not.
178        # (so I don't accidentally release a build that can't play tourneys)
179        if not env.debug and not env.test and not plus.is_blessed():
180            babase.screenmessage('WARNING: NON-BLESSED BUILD', color=(1, 0, 0))
181
182        stdmaps.register_all_maps()
183
184        spazappearance.register_appearances()
185        bascenev1.init_campaigns()
186
187        launch_count = cfg.get('launchCount', 0)
188        launch_count += 1
189
190        # So we know how many times we've run the game at various
191        # version milestones.
192        for key in ('lc14173', 'lc14292'):
193            cfg.setdefault(key, launch_count)
194
195        cfg['launchCount'] = launch_count
196        cfg.commit()
197
198        # If there's a leftover log file, attempt to upload it to the
199        # master-server and/or get rid of it.
200        babase.handle_leftover_v1_cloud_log_file()
201
202        self.accounts.on_app_loading()

Called when the app reaches the loading state.

Note that subsystems created after the app switches to the loading state will not receive this callback. Subsystems created by plugins are an example of this.

@override
def on_app_suspend(self) -> None:
204    @override
205    def on_app_suspend(self) -> None:
206        self.accounts.on_app_suspend()

Called when the app enters the suspended state.

@override
def on_app_unsuspend(self) -> None:
208    @override
209    def on_app_unsuspend(self) -> None:
210        self.accounts.on_app_unsuspend()
211        self.music.on_app_unsuspend()

Called when the app exits the suspended state.

@override
def on_app_shutdown(self) -> None:
213    @override
214    def on_app_shutdown(self) -> None:
215        self.music.on_app_shutdown()

Called when the app begins shutting down.

def pause(self) -> None:
217    def pause(self) -> None:
218        """Pause the game due to a user request or menu popping up.
219
220        If there's a foreground host-activity that says it's pausable, tell it
221        to pause. Note: we now no longer pause if there are connected clients.
222        """
223        activity: bascenev1.Activity | None = (
224            bascenev1.get_foreground_host_activity()
225        )
226        if (
227            activity is not None
228            and activity.allow_pausing
229            and not bascenev1.have_connected_clients()
230        ):
231            from babase import Lstr
232            from bascenev1 import NodeActor
233
234            # FIXME: Shouldn't be touching scene stuff here;
235            #  should just pass the request on to the host-session.
236            with activity.context:
237                globs = activity.globalsnode
238                if not globs.paused:
239                    bascenev1.getsound('refWhistle').play()
240                    globs.paused = True
241
242                # FIXME: This should not be an attr on Actor.
243                activity.paused_text = NodeActor(
244                    bascenev1.newnode(
245                        'text',
246                        attrs={
247                            'text': Lstr(resource='pausedByHostText'),
248                            'client_only': True,
249                            'flatness': 1.0,
250                            'h_align': 'center',
251                        },
252                    )
253                )

Pause the game due to a user request or menu popping up.

If there's a foreground host-activity that says it's pausable, tell it to pause. Note: we now no longer pause if there are connected clients.

def resume(self) -> None:
255    def resume(self) -> None:
256        """Resume the game due to a user request or menu closing.
257
258        If there's a foreground host-activity that's currently paused, tell it
259        to resume.
260        """
261
262        # FIXME: Shouldn't be touching scene stuff here;
263        #  should just pass the request on to the host-session.
264        activity = bascenev1.get_foreground_host_activity()
265        if activity is not None:
266            with activity.context:
267                globs = activity.globalsnode
268                if globs.paused:
269                    bascenev1.getsound('refWhistle').play()
270                    globs.paused = False
271
272                    # FIXME: This should not be an actor attr.
273                    activity.paused_text = None

Resume the game due to a user request or menu closing.

If there's a foreground host-activity that's currently paused, tell it to resume.

def add_coop_practice_level(self, level: bascenev1.Level) -> None:
275    def add_coop_practice_level(self, level: bascenev1.Level) -> None:
276        """Adds an individual level to the 'practice' section in Co-op."""
277
278        # Assign this level to our catch-all campaign.
279        self.campaigns['Challenges'].addlevel(level)
280
281        # Make note to add it to our challenges UI.
282        self.custom_coop_practice_games.append(f'Challenges:{level.name}')

Adds an individual level to the 'practice' section in Co-op.

def launch_coop_game(self, game: str, force: bool = False, args: dict | None = None) -> bool:
284    def launch_coop_game(
285        self, game: str, force: bool = False, args: dict | None = None
286    ) -> bool:
287        """High level way to launch a local co-op session."""
288        # pylint: disable=cyclic-import
289        from bauiv1lib.coop.level import CoopLevelLockedWindow
290
291        assert babase.app.classic is not None
292
293        if args is None:
294            args = {}
295        if game == '':
296            raise ValueError('empty game name')
297        campaignname, levelname = game.split(':')
298        campaign = babase.app.classic.getcampaign(campaignname)
299
300        # If this campaign is sequential, make sure we've completed the
301        # one before this.
302        if campaign.sequential and not force:
303            for level in campaign.levels:
304                if level.name == levelname:
305                    break
306                if not level.complete:
307                    CoopLevelLockedWindow(
308                        campaign.getlevel(levelname).displayname,
309                        campaign.getlevel(level.name).displayname,
310                    )
311                    return False
312
313        # Save where we are in the UI to come back to when done.
314        babase.app.classic.save_ui_state()
315
316        # Ok, we're good to go.
317        self.coop_session_args = {
318            'campaign': campaignname,
319            'level': levelname,
320        }
321        for arg_name, arg_val in list(args.items()):
322            self.coop_session_args[arg_name] = arg_val
323
324        def _fade_end() -> None:
325            from bascenev1 import CoopSession
326
327            try:
328                bascenev1.new_host_session(CoopSession)
329            except Exception:
330                logging.exception('Error creating coopsession after fade end.')
331                from bascenev1lib.mainmenu import MainMenuSession
332
333                bascenev1.new_host_session(MainMenuSession)
334
335        babase.fade_screen(False, endcall=_fade_end)
336        return True

High level way to launch a local co-op session.

def return_to_main_menu_session_gracefully(self, reset_ui: bool = True) -> None:
338    def return_to_main_menu_session_gracefully(
339        self, reset_ui: bool = True
340    ) -> None:
341        """Attempt to cleanly get back to the main menu."""
342        # pylint: disable=cyclic-import
343        from baclassic import _benchmark
344        from bascenev1lib.mainmenu import MainMenuSession
345
346        plus = babase.app.plus
347        assert plus is not None
348
349        if reset_ui:
350            babase.app.ui_v1.clear_main_window()
351
352        if isinstance(bascenev1.get_foreground_host_session(), MainMenuSession):
353            # It may be possible we're on the main menu but the screen is faded
354            # so fade back in.
355            babase.fade_screen(True)
356            return
357
358        _benchmark.stop_stress_test()  # Stop stress-test if in progress.
359
360        # If we're in a host-session, tell them to end.
361        # This lets them tear themselves down gracefully.
362        host_session: bascenev1.Session | None = (
363            bascenev1.get_foreground_host_session()
364        )
365        if host_session is not None:
366            # Kick off a little transaction so we'll hopefully have all the
367            # latest account state when we get back to the menu.
368            plus.add_v1_account_transaction(
369                {'type': 'END_SESSION', 'sType': str(type(host_session))}
370            )
371            plus.run_v1_account_transactions()
372
373            host_session.end()
374
375        # Otherwise just force the issue.
376        else:
377            babase.pushcall(
378                babase.Call(bascenev1.new_host_session, MainMenuSession)
379            )

Attempt to cleanly get back to the main menu.

def getmaps(self, playtype: str) -> list[str]:
381    def getmaps(self, playtype: str) -> list[str]:
382        """Return a list of bascenev1.Map types supporting a playtype str.
383
384        Category: **Asset Functions**
385
386        Maps supporting a given playtype must provide a particular set of
387        features and lend themselves to a certain style of play.
388
389        Play Types:
390
391        'melee'
392          General fighting map.
393          Has one or more 'spawn' locations.
394
395        'team_flag'
396          For games such as Capture The Flag where each team spawns by a flag.
397          Has two or more 'spawn' locations, each with a corresponding 'flag'
398          location (based on index).
399
400        'single_flag'
401          For games such as King of the Hill or Keep Away where multiple teams
402          are fighting over a single flag.
403          Has two or more 'spawn' locations and 1 'flag_default' location.
404
405        'conquest'
406          For games such as Conquest where flags are spread throughout the map
407          - has 2+ 'flag' locations, 2+ 'spawn_by_flag' locations.
408
409        'king_of_the_hill' - has 2+ 'spawn' locations,
410           1+ 'flag_default' locations, and 1+ 'powerup_spawn' locations
411
412        'hockey'
413          For hockey games.
414          Has two 'goal' locations, corresponding 'spawn' locations, and one
415          'flag_default' location (for where puck spawns)
416
417        'football'
418          For football games.
419          Has two 'goal' locations, corresponding 'spawn' locations, and one
420          'flag_default' location (for where flag/ball/etc. spawns)
421
422        'race'
423          For racing games where players much touch each region in order.
424          Has two or more 'race_point' locations.
425        """
426        return sorted(
427            key
428            for key, val in self.maps.items()
429            if playtype in val.get_play_types()
430        )

Return a list of bascenev1.Map types supporting a playtype str.

Category: Asset Functions

Maps supporting a given playtype must provide a particular set of features and lend themselves to a certain style of play.

Play Types:

'melee' General fighting map. Has one or more 'spawn' locations.

'team_flag' For games such as Capture The Flag where each team spawns by a flag. Has two or more 'spawn' locations, each with a corresponding 'flag' location (based on index).

'single_flag' For games such as King of the Hill or Keep Away where multiple teams are fighting over a single flag. Has two or more 'spawn' locations and 1 'flag_default' location.

'conquest' For games such as Conquest where flags are spread throughout the map

  • has 2+ 'flag' locations, 2+ 'spawn_by_flag' locations.

'king_of_the_hill' - has 2+ 'spawn' locations, 1+ 'flag_default' locations, and 1+ 'powerup_spawn' locations

'hockey' For hockey games. Has two 'goal' locations, corresponding 'spawn' locations, and one 'flag_default' location (for where puck spawns)

'football' For football games. Has two 'goal' locations, corresponding 'spawn' locations, and one 'flag_default' location (for where flag/ball/etc. spawns)

'race' For racing games where players much touch each region in order. Has two or more 'race_point' locations.

@classmethod
def json_prep(cls, data: Any) -> Any:
438    @classmethod
439    def json_prep(cls, data: Any) -> Any:
440        """Return a json-friendly version of the provided data.
441
442        This converts any tuples to lists and any bytes to strings
443        (interpreted as utf-8, ignoring errors). Logs errors (just once)
444        if any data is modified/discarded/unsupported.
445        """
446
447        if isinstance(data, dict):
448            return dict(
449                (cls.json_prep(key), cls.json_prep(value))
450                for key, value in list(data.items())
451            )
452        if isinstance(data, list):
453            return [cls.json_prep(element) for element in data]
454        if isinstance(data, tuple):
455            logging.exception('json_prep encountered tuple')
456            return [cls.json_prep(element) for element in data]
457        if isinstance(data, bytes):
458            try:
459                return data.decode(errors='ignore')
460            except Exception:
461                logging.exception('json_prep encountered utf-8 decode error')
462                return data.decode(errors='ignore')
463        if not isinstance(data, (str, float, bool, type(None), int)):
464            logging.exception(
465                'got unsupported type in json_prep: %s', type(data)
466            )
467        return data

Return a json-friendly version of the provided data.

This converts any tuples to lists and any bytes to strings (interpreted as utf-8, ignoring errors). Logs errors (just once) if any data is modified/discarded/unsupported.

def master_server_v1_get( self, request: str, data: dict[str, typing.Any], callback: Optional[Callable[[None | dict[str, Any]], NoneType]] = None, response_type: baclassic._net.MasterServerResponseType = <MasterServerResponseType.JSON: 0>) -> None:
469    def master_server_v1_get(
470        self,
471        request: str,
472        data: dict[str, Any],
473        callback: MasterServerCallback | None = None,
474        response_type: MasterServerResponseType = MasterServerResponseType.JSON,
475    ) -> None:
476        """Make a call to the master server via a http GET."""
477
478        MasterServerV1CallThread(
479            request, 'get', data, callback, response_type
480        ).start()

Make a call to the master server via a http GET.

def master_server_v1_post( self, request: str, data: dict[str, typing.Any], callback: Optional[Callable[[None | dict[str, Any]], NoneType]] = None, response_type: baclassic._net.MasterServerResponseType = <MasterServerResponseType.JSON: 0>) -> None:
482    def master_server_v1_post(
483        self,
484        request: str,
485        data: dict[str, Any],
486        callback: MasterServerCallback | None = None,
487        response_type: MasterServerResponseType = MasterServerResponseType.JSON,
488    ) -> None:
489        """Make a call to the master server via a http POST."""
490        MasterServerV1CallThread(
491            request, 'post', data, callback, response_type
492        ).start()

Make a call to the master server via a http POST.

def set_tournament_prize_image( self, entry: dict[str, typing.Any], index: int, image: _bauiv1.Widget) -> None:
494    def set_tournament_prize_image(
495        self, entry: dict[str, Any], index: int, image: bauiv1.Widget
496    ) -> None:
497        """Given a tournament entry, return strings for its prize levels."""
498        from baclassic import _tournament
499
500        return _tournament.set_tournament_prize_chest_image(entry, index, image)

Given a tournament entry, return strings for its prize levels.

def create_in_game_tournament_prize_image( self, entry: dict[str, typing.Any], index: int, position: tuple[float, float]) -> None:
502    def create_in_game_tournament_prize_image(
503        self,
504        entry: dict[str, Any],
505        index: int,
506        position: tuple[float, float],
507    ) -> None:
508        """Given a tournament entry, return strings for its prize levels."""
509        from baclassic import _tournament
510
511        _tournament.create_in_game_tournament_prize_image(
512            entry, index, position
513        )

Given a tournament entry, return strings for its prize levels.

def get_tournament_prize_strings(self, entry: dict[str, typing.Any], include_tickets: bool) -> list[str]:
515    def get_tournament_prize_strings(
516        self, entry: dict[str, Any], include_tickets: bool
517    ) -> list[str]:
518        """Given a tournament entry, return strings for its prize levels."""
519        from baclassic import _tournament
520
521        return _tournament.get_tournament_prize_strings(
522            entry, include_tickets=include_tickets
523        )

Given a tournament entry, return strings for its prize levels.

def getcampaign(self, name: str) -> bascenev1.Campaign:
525    def getcampaign(self, name: str) -> bascenev1.Campaign:
526        """Return a campaign by name."""
527        return self.campaigns[name]

Return a campaign by name.

def get_next_tip(self) -> str:
529    def get_next_tip(self) -> str:
530        """Returns the next tip to be displayed."""
531        if not self.tips:
532            for tip in get_all_tips():
533                self.tips.insert(random.randint(0, len(self.tips)), tip)
534        tip = self.tips.pop()
535        return tip

Returns the next tip to be displayed.

def run_cpu_benchmark(self) -> None:
537    def run_cpu_benchmark(self) -> None:
538        """Kick off a benchmark to test cpu speeds."""
539        from baclassic._benchmark import run_cpu_benchmark
540
541        run_cpu_benchmark()

Kick off a benchmark to test cpu speeds.

def run_media_reload_benchmark(self) -> None:
543    def run_media_reload_benchmark(self) -> None:
544        """Kick off a benchmark to test media reloading speeds."""
545        from baclassic._benchmark import run_media_reload_benchmark
546
547        run_media_reload_benchmark()

Kick off a benchmark to test media reloading speeds.

def run_stress_test( self, *, playlist_type: str = 'Random', playlist_name: str = '__default__', player_count: int = 8, round_duration: int = 30, attract_mode: bool = False) -> None:
549    def run_stress_test(
550        self,
551        *,
552        playlist_type: str = 'Random',
553        playlist_name: str = '__default__',
554        player_count: int = 8,
555        round_duration: int = 30,
556        attract_mode: bool = False,
557    ) -> None:
558        """Run a stress test."""
559        from baclassic._benchmark import run_stress_test
560
561        run_stress_test(
562            playlist_type=playlist_type,
563            playlist_name=playlist_name,
564            player_count=player_count,
565            round_duration=round_duration,
566            attract_mode=attract_mode,
567        )

Run a stress test.

def get_input_device_mapped_value( self, device: _bascenev1.InputDevice, name: str, default: bool = False) -> Any:
569    def get_input_device_mapped_value(
570        self,
571        device: bascenev1.InputDevice,
572        name: str,
573        default: bool = False,
574    ) -> Any:
575        """Return a mapped value for an input device.
576
577        This checks the user config and falls back to default values
578        where available.
579        """
580        return _input.get_input_device_mapped_value(
581            device.name, device.unique_identifier, name, default
582        )

Return a mapped value for an input device.

This checks the user config and falls back to default values where available.

def get_input_device_map_hash(self, inputdevice: _bascenev1.InputDevice) -> str:
584    def get_input_device_map_hash(
585        self, inputdevice: bascenev1.InputDevice
586    ) -> str:
587        """Given an input device, return hash based on its raw input values."""
588        del inputdevice  # unused currently
589        return _input.get_input_device_map_hash()

Given an input device, return hash based on its raw input values.

def get_input_device_config( self, inputdevice: _bascenev1.InputDevice, default: bool) -> tuple[dict, str]:
591    def get_input_device_config(
592        self, inputdevice: bascenev1.InputDevice, default: bool
593    ) -> tuple[dict, str]:
594        """Given an input device, return its config dict in the app config.
595
596        The dict will be created if it does not exist.
597        """
598        return _input.get_input_device_config(
599            inputdevice.name, inputdevice.unique_identifier, default
600        )

Given an input device, return its config dict in the app config.

The dict will be created if it does not exist.

def get_player_colors(self) -> list[tuple[float, float, float]]:
602    def get_player_colors(self) -> list[tuple[float, float, float]]:
603        """Return user-selectable player colors."""
604        return bascenev1.get_player_colors()

Return user-selectable player colors.

def get_player_profile_icon(self, profilename: str) -> str:
606    def get_player_profile_icon(self, profilename: str) -> str:
607        """Given a profile name, returns an icon string for it.
608
609        (non-account profiles only)
610        """
611        return bascenev1.get_player_profile_icon(profilename)

Given a profile name, returns an icon string for it.

(non-account profiles only)

def get_player_profile_colors( self, profilename: str | None, profiles: dict[str, dict[str, typing.Any]] | None = None) -> tuple[tuple[float, float, float], tuple[float, float, float]]:
613    def get_player_profile_colors(
614        self,
615        profilename: str | None,
616        profiles: dict[str, dict[str, Any]] | None = None,
617    ) -> tuple[tuple[float, float, float], tuple[float, float, float]]:
618        """Given a profile, return colors for them."""
619        return bascenev1.get_player_profile_colors(profilename, profiles)

Given a profile, return colors for them.

def preload_map_preview_media(self) -> None:
759    def preload_map_preview_media(self) -> None:
760        """Preload media needed for map preview UIs.
761
762        Category: **Asset Functions**
763        """
764        try:
765            bauiv1.getmesh('level_select_button_opaque')
766            bauiv1.getmesh('level_select_button_transparent')
767            for maptype in list(self.maps.values()):
768                map_tex_name = maptype.get_preview_texture_name()
769                if map_tex_name is not None:
770                    bauiv1.gettexture(map_tex_name)
771        except Exception:
772            logging.exception('Error preloading map preview media.')

Preload media needed for map preview UIs.

Category: Asset Functions

def save_ui_state(self) -> None:
815    def save_ui_state(self) -> None:
816        """Store our current place in the UI."""
817        ui = babase.app.ui_v1
818        mainwindow = ui.get_main_window()
819        if mainwindow is not None:
820            self.saved_ui_state = ui.save_main_window_state(mainwindow)
821        else:
822            self.saved_ui_state = None

Store our current place in the UI.

def invoke_main_menu_ui(self) -> None:
824    def invoke_main_menu_ui(self) -> None:
825        """Bring up main menu ui."""
826
827        # Bring up the last place we were, or start at the main menu
828        # otherwise.
829        app = bauiv1.app
830        env = app.env
831        with bascenev1.ContextRef.empty():
832            # from bauiv1lib import specialoffer
833
834            assert app.classic is not None
835            if app.env.headless:
836                # UI stuff fails now in headless builds; avoid it.
837                pass
838            else:
839
840                # When coming back from a kiosk-mode game, jump to the
841                # kiosk start screen.
842                if env.demo or env.arcade:
843                    # pylint: disable=cyclic-import
844                    from bauiv1lib.kiosk import KioskWindow
845
846                    app.ui_v1.set_main_window(
847                        KioskWindow(), is_top_level=True, suppress_warning=True
848                    )
849                else:
850                    # If there's a saved ui state, restore that.
851                    if self.saved_ui_state is not None:
852                        app.ui_v1.restore_main_window_state(self.saved_ui_state)
853                    else:
854                        # Otherwise start fresh at the main menu.
855                        from bauiv1lib.mainmenu import MainMenuWindow
856
857                        app.ui_v1.set_main_window(
858                            MainMenuWindow(transition=None),
859                            is_top_level=True,
860                            suppress_warning=True,
861                        )

Bring up main menu ui.

@staticmethod
def run_bs_client_effects(effects: list[bacommon.bs.ClientEffect]) -> None:
863    @staticmethod
864    def run_bs_client_effects(effects: list[bacommon.bs.ClientEffect]) -> None:
865        """Run client effects sent from the master server."""
866        from baclassic._clienteffect import run_bs_client_effects
867
868        run_bs_client_effects(effects)

Run client effects sent from the master server.

@staticmethod
def basic_client_ui_button_label_str(label: bacommon.bs.BasicClientUI.ButtonLabel) -> babase.Lstr:
870    @staticmethod
871    def basic_client_ui_button_label_str(
872        label: bacommon.bs.BasicClientUI.ButtonLabel,
873    ) -> babase.Lstr:
874        """Given a client-ui label, return an Lstr."""
875        import bacommon.bs
876
877        cls = bacommon.bs.BasicClientUI.ButtonLabel
878        if label is cls.UNKNOWN:
879            # Server should not be sending us unknown stuff; make noise
880            # if they do.
881            logging.error(
882                'Got BasicClientUI.ButtonLabel.UNKNOWN; should not happen.'
883            )
884            return babase.Lstr(value='<error>')
885
886        rsrc: str | None = None
887        if label is cls.OK:
888            rsrc = 'okText'
889        elif label is cls.APPLY:
890            rsrc = 'applyText'
891        elif label is cls.CANCEL:
892            rsrc = 'cancelText'
893        elif label is cls.ACCEPT:
894            rsrc = 'gatherWindow.partyInviteAcceptText'
895        elif label is cls.DECLINE:
896            rsrc = 'gatherWindow.partyInviteDeclineText'
897        elif label is cls.IGNORE:
898            rsrc = 'gatherWindow.partyInviteIgnoreText'
899        elif label is cls.CLAIM:
900            rsrc = 'claimText'
901        elif label is cls.DISCARD:
902            rsrc = 'discardText'
903        else:
904            assert_never(label)
905
906        return babase.Lstr(resource=rsrc)

Given a client-ui label, return an Lstr.

class ClassicAppSubsystem.MusicPlayMode(enum.Enum):
23class MusicPlayMode(Enum):
24    """Influences behavior when playing music.
25
26    Category: **Enums**
27    """
28
29    REGULAR = 'regular'
30    TEST = 'test'

Influences behavior when playing music.

Category: Enums

class Achievement:
 646class Achievement:
 647    """Represents attributes and state for an individual achievement.
 648
 649    Category: **App Classes**
 650    """
 651
 652    def __init__(
 653        self,
 654        name: str,
 655        icon_name: str,
 656        icon_color: Sequence[float],
 657        level_name: str,
 658        award: int,
 659        hard_mode_only: bool = False,
 660    ):
 661        # pylint: disable=too-many-positional-arguments
 662        self._name = name
 663        self._icon_name = icon_name
 664        self._icon_color: Sequence[float] = list(icon_color) + [1]
 665        self._level_name = level_name
 666        self._completion_banner_slot: int | None = None
 667        self._award = award
 668        self._hard_mode_only = hard_mode_only
 669
 670    @property
 671    def name(self) -> str:
 672        """The name of this achievement."""
 673        return self._name
 674
 675    @property
 676    def level_name(self) -> str:
 677        """The name of the level this achievement applies to."""
 678        return self._level_name
 679
 680    def get_icon_ui_texture(self, complete: bool) -> bauiv1.Texture:
 681        """Return the icon texture to display for this achievement"""
 682        return bauiv1.gettexture(
 683            self._icon_name if complete else 'achievementEmpty'
 684        )
 685
 686    def get_icon_texture(self, complete: bool) -> bascenev1.Texture:
 687        """Return the icon texture to display for this achievement"""
 688        return bascenev1.gettexture(
 689            self._icon_name if complete else 'achievementEmpty'
 690        )
 691
 692    def get_icon_color(self, complete: bool) -> Sequence[float]:
 693        """Return the color tint for this Achievement's icon."""
 694        if complete:
 695            return self._icon_color
 696        return 1.0, 1.0, 1.0, 0.6
 697
 698    @property
 699    def hard_mode_only(self) -> bool:
 700        """Whether this Achievement is only unlockable in hard-mode."""
 701        return self._hard_mode_only
 702
 703    @property
 704    def complete(self) -> bool:
 705        """Whether this Achievement is currently complete."""
 706        val: bool = self._getconfig()['Complete']
 707        assert isinstance(val, bool)
 708        return val
 709
 710    def announce_completion(self, sound: bool = True) -> None:
 711        """Kick off an announcement for this achievement's completion."""
 712
 713        app = babase.app
 714        plus = app.plus
 715        classic = app.classic
 716        if plus is None or classic is None:
 717            logging.warning('ach account_completion not available.')
 718            return
 719
 720        ach_ss = classic.ach
 721
 722        # Even though there are technically achievements when we're not
 723        # signed in, lets not show them (otherwise we tend to get
 724        # confusing 'controller connected' achievements popping up while
 725        # waiting to sign in which can be confusing).
 726        if plus.get_v1_account_state() != 'signed_in':
 727            return
 728
 729        # If we're being freshly complete, display/report it and whatnot.
 730        if (self, sound) not in ach_ss.achievements_to_display:
 731            ach_ss.achievements_to_display.append((self, sound))
 732
 733        # If there's no achievement display timer going, kick one off
 734        # (if one's already running it will pick this up before it dies).
 735
 736        # Need to check last time too; its possible our timer wasn't able to
 737        # clear itself if an activity died and took it down with it.
 738        if (
 739            ach_ss.achievement_display_timer is None
 740            or babase.apptime() - ach_ss.last_achievement_display_time > 2.0
 741        ) and bascenev1.getactivity(doraise=False) is not None:
 742            ach_ss.achievement_display_timer = bascenev1.BaseTimer(
 743                1.0, _display_next_achievement, repeat=True
 744            )
 745
 746            # Show the first immediately.
 747            _display_next_achievement()
 748
 749    def set_complete(self, complete: bool = True) -> None:
 750        """Set an achievement's completed state.
 751
 752        note this only sets local state; use a transaction to
 753        actually award achievements.
 754        """
 755        config = self._getconfig()
 756        if complete != config['Complete']:
 757            config['Complete'] = complete
 758
 759    @property
 760    def display_name(self) -> babase.Lstr:
 761        """Return a babase.Lstr for this Achievement's name."""
 762        name: babase.Lstr | str
 763        try:
 764            if self._level_name != '':
 765                campaignname, campaign_level = self._level_name.split(':')
 766                classic = babase.app.classic
 767                assert classic is not None
 768                name = (
 769                    classic.getcampaign(campaignname)
 770                    .getlevel(campaign_level)
 771                    .displayname
 772                )
 773            else:
 774                name = ''
 775        except Exception:
 776            name = ''
 777            logging.exception('Error calcing achievement display-name.')
 778        return babase.Lstr(
 779            resource='achievements.' + self._name + '.name',
 780            subs=[('${LEVEL}', name)],
 781        )
 782
 783    @property
 784    def description(self) -> babase.Lstr:
 785        """Get a babase.Lstr for the Achievement's brief description."""
 786        if (
 787            'description'
 788            in babase.app.lang.get_resource('achievements')[self._name]
 789        ):
 790            return babase.Lstr(
 791                resource='achievements.' + self._name + '.description'
 792            )
 793        return babase.Lstr(
 794            resource='achievements.' + self._name + '.descriptionFull'
 795        )
 796
 797    @property
 798    def description_complete(self) -> babase.Lstr:
 799        """Get a babase.Lstr for the Achievement's description when complete."""
 800        if (
 801            'descriptionComplete'
 802            in babase.app.lang.get_resource('achievements')[self._name]
 803        ):
 804            return babase.Lstr(
 805                resource='achievements.' + self._name + '.descriptionComplete'
 806            )
 807        return babase.Lstr(
 808            resource='achievements.' + self._name + '.descriptionFullComplete'
 809        )
 810
 811    @property
 812    def description_full(self) -> babase.Lstr:
 813        """Get a babase.Lstr for the Achievement's full description."""
 814        return babase.Lstr(
 815            resource='achievements.' + self._name + '.descriptionFull',
 816            subs=[
 817                (
 818                    '${LEVEL}',
 819                    babase.Lstr(
 820                        translate=(
 821                            'coopLevelNames',
 822                            ACH_LEVEL_NAMES.get(self._name, '?'),
 823                        )
 824                    ),
 825                )
 826            ],
 827        )
 828
 829    @property
 830    def description_full_complete(self) -> babase.Lstr:
 831        """Get a babase.Lstr for the Achievement's full desc. when completed."""
 832        return babase.Lstr(
 833            resource='achievements.' + self._name + '.descriptionFullComplete',
 834            subs=[
 835                (
 836                    '${LEVEL}',
 837                    babase.Lstr(
 838                        translate=(
 839                            'coopLevelNames',
 840                            ACH_LEVEL_NAMES.get(self._name, '?'),
 841                        )
 842                    ),
 843                )
 844            ],
 845        )
 846
 847    def get_award_ticket_value(self, include_pro_bonus: bool = False) -> int:
 848        """Get the ticket award value for this achievement."""
 849        plus = babase.app.plus
 850        if plus is None:
 851            return 0
 852        val: int = plus.get_v1_account_misc_read_val(
 853            'achAward.' + self._name, self._award
 854        ) * _get_ach_mult(include_pro_bonus)
 855        assert isinstance(val, int)
 856        return val
 857
 858    @property
 859    def power_ranking_value(self) -> int:
 860        """Get the power-ranking award value for this achievement."""
 861        plus = babase.app.plus
 862        if plus is None:
 863            return 0
 864        val: int = plus.get_v1_account_misc_read_val(
 865            'achLeaguePoints.' + self._name, self._award
 866        )
 867        assert isinstance(val, int)
 868        return val
 869
 870    def create_display(
 871        self,
 872        x: float,
 873        y: float,
 874        delay: float,
 875        *,
 876        outdelay: float | None = None,
 877        color: Sequence[float] | None = None,
 878        style: str = 'post_game',
 879    ) -> list[bascenev1.Actor]:
 880        """Create a display for the Achievement.
 881
 882        Shows the Achievement icon, name, and description.
 883        """
 884        # pylint: disable=cyclic-import
 885        from bascenev1 import CoopSession
 886        from bascenev1lib.actor.image import Image
 887        from bascenev1lib.actor.text import Text
 888
 889        # Yeah this needs cleaning up.
 890        if style == 'post_game':
 891            in_game_colors = False
 892            in_main_menu = False
 893            h_attach = Text.HAttach.CENTER
 894            v_attach = Text.VAttach.CENTER
 895            attach = Image.Attach.CENTER
 896        elif style == 'in_game':
 897            in_game_colors = True
 898            in_main_menu = False
 899            h_attach = Text.HAttach.LEFT
 900            v_attach = Text.VAttach.TOP
 901            attach = Image.Attach.TOP_LEFT
 902        elif style == 'news':
 903            in_game_colors = True
 904            in_main_menu = True
 905            h_attach = Text.HAttach.CENTER
 906            v_attach = Text.VAttach.TOP
 907            attach = Image.Attach.TOP_CENTER
 908        else:
 909            raise ValueError('invalid style "' + style + '"')
 910
 911        # Attempt to determine what campaign we're in
 912        # (so we know whether to show "hard mode only").
 913        if in_main_menu:
 914            hmo = False
 915        else:
 916            try:
 917                session = bascenev1.getsession()
 918                if isinstance(session, CoopSession):
 919                    campaign = session.campaign
 920                    assert campaign is not None
 921                    hmo = self._hard_mode_only and campaign.name == 'Easy'
 922                else:
 923                    hmo = False
 924            except Exception:
 925                logging.exception('Error determining campaign.')
 926                hmo = False
 927
 928        objs: list[bascenev1.Actor]
 929
 930        if in_game_colors:
 931            objs = []
 932            out_delay_fin = (delay + outdelay) if outdelay is not None else None
 933            if color is not None:
 934                cl1 = (2.0 * color[0], 2.0 * color[1], 2.0 * color[2], color[3])
 935                cl2 = color
 936            else:
 937                cl1 = (1.5, 1.5, 2, 1.0)
 938                cl2 = (0.8, 0.8, 1.0, 1.0)
 939
 940            if hmo:
 941                cl1 = (cl1[0], cl1[1], cl1[2], cl1[3] * 0.6)
 942                cl2 = (cl2[0], cl2[1], cl2[2], cl2[3] * 0.2)
 943
 944            objs.append(
 945                Image(
 946                    self.get_icon_texture(False),
 947                    host_only=True,
 948                    color=cl1,
 949                    position=(x - 25, y + 5),
 950                    attach=attach,
 951                    transition=Image.Transition.FADE_IN,
 952                    transition_delay=delay,
 953                    vr_depth=4,
 954                    transition_out_delay=out_delay_fin,
 955                    scale=(40, 40),
 956                ).autoretain()
 957            )
 958            txt = self.display_name
 959            txt_s = 0.85
 960            txt_max_w = 300
 961            objs.append(
 962                Text(
 963                    txt,
 964                    host_only=True,
 965                    maxwidth=txt_max_w,
 966                    position=(x, y + 2),
 967                    transition=Text.Transition.FADE_IN,
 968                    scale=txt_s,
 969                    flatness=0.6,
 970                    shadow=0.5,
 971                    h_attach=h_attach,
 972                    v_attach=v_attach,
 973                    color=cl2,
 974                    transition_delay=delay + 0.05,
 975                    transition_out_delay=out_delay_fin,
 976                ).autoretain()
 977            )
 978            txt2_s = 0.62
 979            txt2_max_w = 400
 980            objs.append(
 981                Text(
 982                    self.description_full if in_main_menu else self.description,
 983                    host_only=True,
 984                    maxwidth=txt2_max_w,
 985                    position=(x, y - 14),
 986                    transition=Text.Transition.FADE_IN,
 987                    vr_depth=-5,
 988                    h_attach=h_attach,
 989                    v_attach=v_attach,
 990                    scale=txt2_s,
 991                    flatness=1.0,
 992                    shadow=0.5,
 993                    color=cl2,
 994                    transition_delay=delay + 0.1,
 995                    transition_out_delay=out_delay_fin,
 996                ).autoretain()
 997            )
 998
 999            if hmo:
1000                txtactor = Text(
1001                    babase.Lstr(resource='difficultyHardOnlyText'),
1002                    host_only=True,
1003                    maxwidth=txt2_max_w * 0.7,
1004                    position=(x + 60, y + 5),
1005                    transition=Text.Transition.FADE_IN,
1006                    vr_depth=-5,
1007                    h_attach=h_attach,
1008                    v_attach=v_attach,
1009                    h_align=Text.HAlign.CENTER,
1010                    v_align=Text.VAlign.CENTER,
1011                    scale=txt_s * 0.8,
1012                    flatness=1.0,
1013                    shadow=0.5,
1014                    color=(1, 1, 0.6, 1),
1015                    transition_delay=delay + 0.1,
1016                    transition_out_delay=out_delay_fin,
1017                ).autoretain()
1018                txtactor.node.rotate = 10
1019                objs.append(txtactor)
1020
1021            # Ticket-award.
1022            award_x = -100
1023            objs.append(
1024                Text(
1025                    babase.charstr(babase.SpecialChar.TICKET),
1026                    host_only=True,
1027                    position=(x + award_x + 33, y + 7),
1028                    transition=Text.Transition.FADE_IN,
1029                    scale=1.5,
1030                    h_attach=h_attach,
1031                    v_attach=v_attach,
1032                    h_align=Text.HAlign.CENTER,
1033                    v_align=Text.VAlign.CENTER,
1034                    color=(1, 1, 1, 0.2 if hmo else 0.4),
1035                    transition_delay=delay + 0.05,
1036                    transition_out_delay=out_delay_fin,
1037                ).autoretain()
1038            )
1039            objs.append(
1040                Text(
1041                    '+' + str(self.get_award_ticket_value()),
1042                    host_only=True,
1043                    position=(x + award_x + 28, y + 16),
1044                    transition=Text.Transition.FADE_IN,
1045                    scale=0.7,
1046                    flatness=1,
1047                    h_attach=h_attach,
1048                    v_attach=v_attach,
1049                    h_align=Text.HAlign.CENTER,
1050                    v_align=Text.VAlign.CENTER,
1051                    color=cl2,
1052                    transition_delay=delay + 0.05,
1053                    transition_out_delay=out_delay_fin,
1054                ).autoretain()
1055            )
1056
1057        else:
1058            complete = self.complete
1059            objs = []
1060            c_icon = self.get_icon_color(complete)
1061            if hmo and not complete:
1062                c_icon = (c_icon[0], c_icon[1], c_icon[2], c_icon[3] * 0.3)
1063            objs.append(
1064                Image(
1065                    self.get_icon_texture(complete),
1066                    host_only=True,
1067                    color=c_icon,
1068                    position=(x - 25, y + 5),
1069                    attach=attach,
1070                    vr_depth=4,
1071                    transition=Image.Transition.IN_RIGHT,
1072                    transition_delay=delay,
1073                    transition_out_delay=None,
1074                    scale=(40, 40),
1075                ).autoretain()
1076            )
1077            if complete:
1078                objs.append(
1079                    Image(
1080                        bascenev1.gettexture('achievementOutline'),
1081                        host_only=True,
1082                        mesh_transparent=bascenev1.getmesh(
1083                            'achievementOutline'
1084                        ),
1085                        color=(2, 1.4, 0.4, 1),
1086                        vr_depth=8,
1087                        position=(x - 25, y + 5),
1088                        attach=attach,
1089                        transition=Image.Transition.IN_RIGHT,
1090                        transition_delay=delay,
1091                        transition_out_delay=None,
1092                        scale=(40, 40),
1093                    ).autoretain()
1094                )
1095            else:
1096                if not complete:
1097                    award_x = -100
1098                    objs.append(
1099                        Text(
1100                            babase.charstr(babase.SpecialChar.TICKET),
1101                            host_only=True,
1102                            position=(x + award_x + 33, y + 7),
1103                            transition=Text.Transition.IN_RIGHT,
1104                            scale=1.5,
1105                            h_attach=h_attach,
1106                            v_attach=v_attach,
1107                            h_align=Text.HAlign.CENTER,
1108                            v_align=Text.VAlign.CENTER,
1109                            color=(1, 1, 1, (0.1 if hmo else 0.2)),
1110                            transition_delay=delay + 0.05,
1111                            transition_out_delay=None,
1112                        ).autoretain()
1113                    )
1114                    objs.append(
1115                        Text(
1116                            '+' + str(self.get_award_ticket_value()),
1117                            host_only=True,
1118                            position=(x + award_x + 28, y + 16),
1119                            transition=Text.Transition.IN_RIGHT,
1120                            scale=0.7,
1121                            flatness=1,
1122                            h_attach=h_attach,
1123                            v_attach=v_attach,
1124                            h_align=Text.HAlign.CENTER,
1125                            v_align=Text.VAlign.CENTER,
1126                            color=(0.6, 0.6, 0.6, (0.2 if hmo else 0.4)),
1127                            transition_delay=delay + 0.05,
1128                            transition_out_delay=None,
1129                        ).autoretain()
1130                    )
1131
1132                    # Show 'hard-mode-only' only over incomplete achievements
1133                    # when that's the case.
1134                    if hmo:
1135                        txtactor = Text(
1136                            babase.Lstr(resource='difficultyHardOnlyText'),
1137                            host_only=True,
1138                            maxwidth=300 * 0.7,
1139                            position=(x + 60, y + 5),
1140                            transition=Text.Transition.FADE_IN,
1141                            vr_depth=-5,
1142                            h_attach=h_attach,
1143                            v_attach=v_attach,
1144                            h_align=Text.HAlign.CENTER,
1145                            v_align=Text.VAlign.CENTER,
1146                            scale=0.85 * 0.8,
1147                            flatness=1.0,
1148                            shadow=0.5,
1149                            color=(1, 1, 0.6, 1),
1150                            transition_delay=delay + 0.05,
1151                            transition_out_delay=None,
1152                        ).autoretain()
1153                        assert txtactor.node
1154                        txtactor.node.rotate = 10
1155                        objs.append(txtactor)
1156
1157            objs.append(
1158                Text(
1159                    self.display_name,
1160                    host_only=True,
1161                    maxwidth=300,
1162                    position=(x, y + 2),
1163                    transition=Text.Transition.IN_RIGHT,
1164                    scale=0.85,
1165                    flatness=0.6,
1166                    h_attach=h_attach,
1167                    v_attach=v_attach,
1168                    color=(
1169                        (0.8, 0.93, 0.8, 1.0)
1170                        if complete
1171                        else (0.6, 0.6, 0.6, (0.2 if hmo else 0.4))
1172                    ),
1173                    transition_delay=delay + 0.05,
1174                    transition_out_delay=None,
1175                ).autoretain()
1176            )
1177            objs.append(
1178                Text(
1179                    self.description_complete if complete else self.description,
1180                    host_only=True,
1181                    maxwidth=400,
1182                    position=(x, y - 14),
1183                    transition=Text.Transition.IN_RIGHT,
1184                    vr_depth=-5,
1185                    h_attach=h_attach,
1186                    v_attach=v_attach,
1187                    scale=0.62,
1188                    flatness=1.0,
1189                    color=(
1190                        (0.6, 0.6, 0.6, 1.0)
1191                        if complete
1192                        else (0.6, 0.6, 0.6, (0.2 if hmo else 0.4))
1193                    ),
1194                    transition_delay=delay + 0.1,
1195                    transition_out_delay=None,
1196                ).autoretain()
1197            )
1198        return objs
1199
1200    def _getconfig(self) -> dict[str, Any]:
1201        """
1202        Return the sub-dict in settings where this achievement's
1203        state is stored, creating it if need be.
1204        """
1205        val: dict[str, Any] = babase.app.config.setdefault(
1206            'Achievements', {}
1207        ).setdefault(self._name, {'Complete': False})
1208        assert isinstance(val, dict)
1209        return val
1210
1211    def _remove_banner_slot(self) -> None:
1212        classic = babase.app.classic
1213        assert classic is not None
1214        assert self._completion_banner_slot is not None
1215        classic.ach.achievement_completion_banner_slots.remove(
1216            self._completion_banner_slot
1217        )
1218        self._completion_banner_slot = None
1219
1220    def show_completion_banner(self, sound: bool = True) -> None:
1221        """Create the banner/sound for an acquired achievement announcement."""
1222        from bascenev1lib.actor.text import Text
1223        from bascenev1lib.actor.image import Image
1224
1225        app = babase.app
1226        assert app.classic is not None
1227        app.classic.ach.last_achievement_display_time = babase.apptime()
1228
1229        # Just piggy-back onto any current activity
1230        # (should we use the session instead?..)
1231        activity = bascenev1.getactivity(doraise=False)
1232
1233        # If this gets called while this achievement is occupying a slot
1234        # already, ignore it. (probably should never happen in real
1235        # life but whatevs).
1236        if self._completion_banner_slot is not None:
1237            return
1238
1239        if activity is None:
1240            print('show_completion_banner() called with no current activity!')
1241            return
1242
1243        if sound:
1244            bascenev1.getsound('achievement').play(host_only=True)
1245        else:
1246            bascenev1.timer(
1247                0.5, lambda: bascenev1.getsound('ding').play(host_only=True)
1248            )
1249
1250        in_time = 0.300
1251        out_time = 3.5
1252
1253        base_vr_depth = 200
1254
1255        # Find the first free slot.
1256        i = 0
1257        while True:
1258            if i not in app.classic.ach.achievement_completion_banner_slots:
1259                app.classic.ach.achievement_completion_banner_slots.add(i)
1260                self._completion_banner_slot = i
1261
1262                # Remove us from that slot when we close.
1263                # Use an app-timer in an empty context so the removal
1264                # runs even if our activity/session dies.
1265                with babase.ContextRef.empty():
1266                    babase.apptimer(
1267                        in_time + out_time, self._remove_banner_slot
1268                    )
1269                break
1270            i += 1
1271        assert self._completion_banner_slot is not None
1272        y_offs = 110 * self._completion_banner_slot
1273        objs: list[bascenev1.Actor] = []
1274        obj = Image(
1275            bascenev1.gettexture('shadow'),
1276            position=(-30, 30 + y_offs),
1277            front=True,
1278            attach=Image.Attach.BOTTOM_CENTER,
1279            transition=Image.Transition.IN_BOTTOM,
1280            vr_depth=base_vr_depth - 100,
1281            transition_delay=in_time,
1282            transition_out_delay=out_time,
1283            color=(0.0, 0.1, 0, 1),
1284            scale=(1000, 300),
1285        ).autoretain()
1286        objs.append(obj)
1287        assert obj.node
1288        obj.node.host_only = True
1289        obj = Image(
1290            bascenev1.gettexture('light'),
1291            position=(-180, 60 + y_offs),
1292            front=True,
1293            attach=Image.Attach.BOTTOM_CENTER,
1294            vr_depth=base_vr_depth,
1295            transition=Image.Transition.IN_BOTTOM,
1296            transition_delay=in_time,
1297            transition_out_delay=out_time,
1298            color=(1.8, 1.8, 1.0, 0.0),
1299            scale=(40, 300),
1300        ).autoretain()
1301        objs.append(obj)
1302        assert obj.node
1303        obj.node.host_only = True
1304        obj.node.premultiplied = True
1305        combine = bascenev1.newnode(
1306            'combine', owner=obj.node, attrs={'size': 2}
1307        )
1308        bascenev1.animate(
1309            combine,
1310            'input0',
1311            {
1312                in_time: 0,
1313                in_time + 0.4: 30,
1314                in_time + 0.5: 40,
1315                in_time + 0.6: 30,
1316                in_time + 2.0: 0,
1317            },
1318        )
1319        bascenev1.animate(
1320            combine,
1321            'input1',
1322            {
1323                in_time: 0,
1324                in_time + 0.4: 200,
1325                in_time + 0.5: 500,
1326                in_time + 0.6: 200,
1327                in_time + 2.0: 0,
1328            },
1329        )
1330        combine.connectattr('output', obj.node, 'scale')
1331        bascenev1.animate(obj.node, 'rotate', {0: 0.0, 0.35: 360.0}, loop=True)
1332        obj = Image(
1333            self.get_icon_texture(True),
1334            position=(-180, 60 + y_offs),
1335            attach=Image.Attach.BOTTOM_CENTER,
1336            front=True,
1337            vr_depth=base_vr_depth - 10,
1338            transition=Image.Transition.IN_BOTTOM,
1339            transition_delay=in_time,
1340            transition_out_delay=out_time,
1341            scale=(100, 100),
1342        ).autoretain()
1343        objs.append(obj)
1344        assert obj.node
1345        obj.node.host_only = True
1346
1347        # Flash.
1348        color = self.get_icon_color(True)
1349        combine = bascenev1.newnode(
1350            'combine', owner=obj.node, attrs={'size': 3}
1351        )
1352        keys = {
1353            in_time: 1.0 * color[0],
1354            in_time + 0.4: 1.5 * color[0],
1355            in_time + 0.5: 6.0 * color[0],
1356            in_time + 0.6: 1.5 * color[0],
1357            in_time + 2.0: 1.0 * color[0],
1358        }
1359        bascenev1.animate(combine, 'input0', keys)
1360        keys = {
1361            in_time: 1.0 * color[1],
1362            in_time + 0.4: 1.5 * color[1],
1363            in_time + 0.5: 6.0 * color[1],
1364            in_time + 0.6: 1.5 * color[1],
1365            in_time + 2.0: 1.0 * color[1],
1366        }
1367        bascenev1.animate(combine, 'input1', keys)
1368        keys = {
1369            in_time: 1.0 * color[2],
1370            in_time + 0.4: 1.5 * color[2],
1371            in_time + 0.5: 6.0 * color[2],
1372            in_time + 0.6: 1.5 * color[2],
1373            in_time + 2.0: 1.0 * color[2],
1374        }
1375        bascenev1.animate(combine, 'input2', keys)
1376        combine.connectattr('output', obj.node, 'color')
1377
1378        obj = Image(
1379            bascenev1.gettexture('achievementOutline'),
1380            mesh_transparent=bascenev1.getmesh('achievementOutline'),
1381            position=(-180, 60 + y_offs),
1382            front=True,
1383            attach=Image.Attach.BOTTOM_CENTER,
1384            vr_depth=base_vr_depth,
1385            transition=Image.Transition.IN_BOTTOM,
1386            transition_delay=in_time,
1387            transition_out_delay=out_time,
1388            scale=(100, 100),
1389        ).autoretain()
1390        assert obj.node
1391        obj.node.host_only = True
1392
1393        # Flash.
1394        color = (2, 1.4, 0.4, 1)
1395        combine = bascenev1.newnode(
1396            'combine', owner=obj.node, attrs={'size': 3}
1397        )
1398        keys = {
1399            in_time: 1.0 * color[0],
1400            in_time + 0.4: 1.5 * color[0],
1401            in_time + 0.5: 6.0 * color[0],
1402            in_time + 0.6: 1.5 * color[0],
1403            in_time + 2.0: 1.0 * color[0],
1404        }
1405        bascenev1.animate(combine, 'input0', keys)
1406        keys = {
1407            in_time: 1.0 * color[1],
1408            in_time + 0.4: 1.5 * color[1],
1409            in_time + 0.5: 6.0 * color[1],
1410            in_time + 0.6: 1.5 * color[1],
1411            in_time + 2.0: 1.0 * color[1],
1412        }
1413        bascenev1.animate(combine, 'input1', keys)
1414        keys = {
1415            in_time: 1.0 * color[2],
1416            in_time + 0.4: 1.5 * color[2],
1417            in_time + 0.5: 6.0 * color[2],
1418            in_time + 0.6: 1.5 * color[2],
1419            in_time + 2.0: 1.0 * color[2],
1420        }
1421        bascenev1.animate(combine, 'input2', keys)
1422        combine.connectattr('output', obj.node, 'color')
1423        objs.append(obj)
1424
1425        objt = Text(
1426            babase.Lstr(
1427                value='${A}:',
1428                subs=[('${A}', babase.Lstr(resource='achievementText'))],
1429            ),
1430            position=(-120, 91 + y_offs),
1431            front=True,
1432            v_attach=Text.VAttach.BOTTOM,
1433            vr_depth=base_vr_depth - 10,
1434            transition=Text.Transition.IN_BOTTOM,
1435            flatness=0.5,
1436            transition_delay=in_time,
1437            transition_out_delay=out_time,
1438            color=(1, 1, 1, 0.8),
1439            scale=0.65,
1440        ).autoretain()
1441        objs.append(objt)
1442        assert objt.node
1443        objt.node.host_only = True
1444
1445        objt = Text(
1446            self.display_name,
1447            position=(-120, 50 + y_offs),
1448            front=True,
1449            v_attach=Text.VAttach.BOTTOM,
1450            transition=Text.Transition.IN_BOTTOM,
1451            vr_depth=base_vr_depth,
1452            flatness=0.5,
1453            transition_delay=in_time,
1454            transition_out_delay=out_time,
1455            flash=True,
1456            color=(1, 0.8, 0, 1.0),
1457            scale=1.5,
1458        ).autoretain()
1459        objs.append(objt)
1460        assert objt.node
1461        objt.node.host_only = True
1462
1463        objt = Text(
1464            babase.charstr(babase.SpecialChar.TICKET),
1465            position=(-120 - 170 + 5, 75 + y_offs - 20),
1466            front=True,
1467            v_attach=Text.VAttach.BOTTOM,
1468            h_align=Text.HAlign.CENTER,
1469            v_align=Text.VAlign.CENTER,
1470            transition=Text.Transition.IN_BOTTOM,
1471            vr_depth=base_vr_depth,
1472            transition_delay=in_time,
1473            transition_out_delay=out_time,
1474            flash=True,
1475            color=(0.5, 0.5, 0.5, 1),
1476            scale=3.0,
1477        ).autoretain()
1478        objs.append(objt)
1479        assert objt.node
1480        objt.node.host_only = True
1481
1482        objt = Text(
1483            '+' + str(self.get_award_ticket_value()),
1484            position=(-120 - 180 + 5, 80 + y_offs - 20),
1485            v_attach=Text.VAttach.BOTTOM,
1486            front=True,
1487            h_align=Text.HAlign.CENTER,
1488            v_align=Text.VAlign.CENTER,
1489            transition=Text.Transition.IN_BOTTOM,
1490            vr_depth=base_vr_depth,
1491            flatness=0.5,
1492            shadow=1.0,
1493            transition_delay=in_time,
1494            transition_out_delay=out_time,
1495            flash=True,
1496            color=(0, 1, 0, 1),
1497            scale=1.5,
1498        ).autoretain()
1499        objs.append(objt)
1500        assert objt.node
1501        objt.node.host_only = True
1502
1503        # Add the 'x 2' if we've got pro.
1504        if app.classic.accounts.have_pro():
1505            objt = Text(
1506                'x 2',
1507                position=(-120 - 180 + 45, 80 + y_offs - 50),
1508                v_attach=Text.VAttach.BOTTOM,
1509                front=True,
1510                h_align=Text.HAlign.CENTER,
1511                v_align=Text.VAlign.CENTER,
1512                transition=Text.Transition.IN_BOTTOM,
1513                vr_depth=base_vr_depth,
1514                flatness=0.5,
1515                shadow=1.0,
1516                transition_delay=in_time,
1517                transition_out_delay=out_time,
1518                flash=True,
1519                color=(0.4, 0, 1, 1),
1520                scale=0.9,
1521            ).autoretain()
1522            objs.append(objt)
1523            assert objt.node
1524            objt.node.host_only = True
1525
1526        objt = Text(
1527            self.description_complete,
1528            position=(-120, 30 + y_offs),
1529            front=True,
1530            v_attach=Text.VAttach.BOTTOM,
1531            transition=Text.Transition.IN_BOTTOM,
1532            vr_depth=base_vr_depth - 10,
1533            flatness=0.5,
1534            transition_delay=in_time,
1535            transition_out_delay=out_time,
1536            color=(1.0, 0.7, 0.5, 1.0),
1537            scale=0.8,
1538        ).autoretain()
1539        objs.append(objt)
1540        assert objt.node
1541        objt.node.host_only = True
1542
1543        for actor in objs:
1544            bascenev1.timer(
1545                out_time + 1.000,
1546                babase.WeakCall(actor.handlemessage, bascenev1.DieMessage()),
1547            )

Represents attributes and state for an individual achievement.

Category: App Classes

Achievement( name: str, icon_name: str, icon_color: Sequence[float], level_name: str, award: int, hard_mode_only: bool = False)
652    def __init__(
653        self,
654        name: str,
655        icon_name: str,
656        icon_color: Sequence[float],
657        level_name: str,
658        award: int,
659        hard_mode_only: bool = False,
660    ):
661        # pylint: disable=too-many-positional-arguments
662        self._name = name
663        self._icon_name = icon_name
664        self._icon_color: Sequence[float] = list(icon_color) + [1]
665        self._level_name = level_name
666        self._completion_banner_slot: int | None = None
667        self._award = award
668        self._hard_mode_only = hard_mode_only
name: str
670    @property
671    def name(self) -> str:
672        """The name of this achievement."""
673        return self._name

The name of this achievement.

level_name: str
675    @property
676    def level_name(self) -> str:
677        """The name of the level this achievement applies to."""
678        return self._level_name

The name of the level this achievement applies to.

def get_icon_ui_texture(self, complete: bool) -> _bauiv1.Texture:
680    def get_icon_ui_texture(self, complete: bool) -> bauiv1.Texture:
681        """Return the icon texture to display for this achievement"""
682        return bauiv1.gettexture(
683            self._icon_name if complete else 'achievementEmpty'
684        )

Return the icon texture to display for this achievement

def get_icon_texture(self, complete: bool) -> _bascenev1.Texture:
686    def get_icon_texture(self, complete: bool) -> bascenev1.Texture:
687        """Return the icon texture to display for this achievement"""
688        return bascenev1.gettexture(
689            self._icon_name if complete else 'achievementEmpty'
690        )

Return the icon texture to display for this achievement

def get_icon_color(self, complete: bool) -> Sequence[float]:
692    def get_icon_color(self, complete: bool) -> Sequence[float]:
693        """Return the color tint for this Achievement's icon."""
694        if complete:
695            return self._icon_color
696        return 1.0, 1.0, 1.0, 0.6

Return the color tint for this Achievement's icon.

hard_mode_only: bool
698    @property
699    def hard_mode_only(self) -> bool:
700        """Whether this Achievement is only unlockable in hard-mode."""
701        return self._hard_mode_only

Whether this Achievement is only unlockable in hard-mode.

complete: bool
703    @property
704    def complete(self) -> bool:
705        """Whether this Achievement is currently complete."""
706        val: bool = self._getconfig()['Complete']
707        assert isinstance(val, bool)
708        return val

Whether this Achievement is currently complete.

def announce_completion(self, sound: bool = True) -> None:
710    def announce_completion(self, sound: bool = True) -> None:
711        """Kick off an announcement for this achievement's completion."""
712
713        app = babase.app
714        plus = app.plus
715        classic = app.classic
716        if plus is None or classic is None:
717            logging.warning('ach account_completion not available.')
718            return
719
720        ach_ss = classic.ach
721
722        # Even though there are technically achievements when we're not
723        # signed in, lets not show them (otherwise we tend to get
724        # confusing 'controller connected' achievements popping up while
725        # waiting to sign in which can be confusing).
726        if plus.get_v1_account_state() != 'signed_in':
727            return
728
729        # If we're being freshly complete, display/report it and whatnot.
730        if (self, sound) not in ach_ss.achievements_to_display:
731            ach_ss.achievements_to_display.append((self, sound))
732
733        # If there's no achievement display timer going, kick one off
734        # (if one's already running it will pick this up before it dies).
735
736        # Need to check last time too; its possible our timer wasn't able to
737        # clear itself if an activity died and took it down with it.
738        if (
739            ach_ss.achievement_display_timer is None
740            or babase.apptime() - ach_ss.last_achievement_display_time > 2.0
741        ) and bascenev1.getactivity(doraise=False) is not None:
742            ach_ss.achievement_display_timer = bascenev1.BaseTimer(
743                1.0, _display_next_achievement, repeat=True
744            )
745
746            # Show the first immediately.
747            _display_next_achievement()

Kick off an announcement for this achievement's completion.

def set_complete(self, complete: bool = True) -> None:
749    def set_complete(self, complete: bool = True) -> None:
750        """Set an achievement's completed state.
751
752        note this only sets local state; use a transaction to
753        actually award achievements.
754        """
755        config = self._getconfig()
756        if complete != config['Complete']:
757            config['Complete'] = complete

Set an achievement's completed state.

note this only sets local state; use a transaction to actually award achievements.

display_name: babase.Lstr
759    @property
760    def display_name(self) -> babase.Lstr:
761        """Return a babase.Lstr for this Achievement's name."""
762        name: babase.Lstr | str
763        try:
764            if self._level_name != '':
765                campaignname, campaign_level = self._level_name.split(':')
766                classic = babase.app.classic
767                assert classic is not None
768                name = (
769                    classic.getcampaign(campaignname)
770                    .getlevel(campaign_level)
771                    .displayname
772                )
773            else:
774                name = ''
775        except Exception:
776            name = ''
777            logging.exception('Error calcing achievement display-name.')
778        return babase.Lstr(
779            resource='achievements.' + self._name + '.name',
780            subs=[('${LEVEL}', name)],
781        )

Return a babase.Lstr for this Achievement's name.

description: babase.Lstr
783    @property
784    def description(self) -> babase.Lstr:
785        """Get a babase.Lstr for the Achievement's brief description."""
786        if (
787            'description'
788            in babase.app.lang.get_resource('achievements')[self._name]
789        ):
790            return babase.Lstr(
791                resource='achievements.' + self._name + '.description'
792            )
793        return babase.Lstr(
794            resource='achievements.' + self._name + '.descriptionFull'
795        )

Get a babase.Lstr for the Achievement's brief description.

description_complete: babase.Lstr
797    @property
798    def description_complete(self) -> babase.Lstr:
799        """Get a babase.Lstr for the Achievement's description when complete."""
800        if (
801            'descriptionComplete'
802            in babase.app.lang.get_resource('achievements')[self._name]
803        ):
804            return babase.Lstr(
805                resource='achievements.' + self._name + '.descriptionComplete'
806            )
807        return babase.Lstr(
808            resource='achievements.' + self._name + '.descriptionFullComplete'
809        )

Get a babase.Lstr for the Achievement's description when complete.

description_full: babase.Lstr
811    @property
812    def description_full(self) -> babase.Lstr:
813        """Get a babase.Lstr for the Achievement's full description."""
814        return babase.Lstr(
815            resource='achievements.' + self._name + '.descriptionFull',
816            subs=[
817                (
818                    '${LEVEL}',
819                    babase.Lstr(
820                        translate=(
821                            'coopLevelNames',
822                            ACH_LEVEL_NAMES.get(self._name, '?'),
823                        )
824                    ),
825                )
826            ],
827        )

Get a babase.Lstr for the Achievement's full description.

description_full_complete: babase.Lstr
829    @property
830    def description_full_complete(self) -> babase.Lstr:
831        """Get a babase.Lstr for the Achievement's full desc. when completed."""
832        return babase.Lstr(
833            resource='achievements.' + self._name + '.descriptionFullComplete',
834            subs=[
835                (
836                    '${LEVEL}',
837                    babase.Lstr(
838                        translate=(
839                            'coopLevelNames',
840                            ACH_LEVEL_NAMES.get(self._name, '?'),
841                        )
842                    ),
843                )
844            ],
845        )

Get a babase.Lstr for the Achievement's full desc. when completed.

def get_award_ticket_value(self, include_pro_bonus: bool = False) -> int:
847    def get_award_ticket_value(self, include_pro_bonus: bool = False) -> int:
848        """Get the ticket award value for this achievement."""
849        plus = babase.app.plus
850        if plus is None:
851            return 0
852        val: int = plus.get_v1_account_misc_read_val(
853            'achAward.' + self._name, self._award
854        ) * _get_ach_mult(include_pro_bonus)
855        assert isinstance(val, int)
856        return val

Get the ticket award value for this achievement.

power_ranking_value: int
858    @property
859    def power_ranking_value(self) -> int:
860        """Get the power-ranking award value for this achievement."""
861        plus = babase.app.plus
862        if plus is None:
863            return 0
864        val: int = plus.get_v1_account_misc_read_val(
865            'achLeaguePoints.' + self._name, self._award
866        )
867        assert isinstance(val, int)
868        return val

Get the power-ranking award value for this achievement.

def create_display( self, x: float, y: float, delay: float, *, outdelay: float | None = None, color: Optional[Sequence[float]] = None, style: str = 'post_game') -> list[bascenev1.Actor]:
 870    def create_display(
 871        self,
 872        x: float,
 873        y: float,
 874        delay: float,
 875        *,
 876        outdelay: float | None = None,
 877        color: Sequence[float] | None = None,
 878        style: str = 'post_game',
 879    ) -> list[bascenev1.Actor]:
 880        """Create a display for the Achievement.
 881
 882        Shows the Achievement icon, name, and description.
 883        """
 884        # pylint: disable=cyclic-import
 885        from bascenev1 import CoopSession
 886        from bascenev1lib.actor.image import Image
 887        from bascenev1lib.actor.text import Text
 888
 889        # Yeah this needs cleaning up.
 890        if style == 'post_game':
 891            in_game_colors = False
 892            in_main_menu = False
 893            h_attach = Text.HAttach.CENTER
 894            v_attach = Text.VAttach.CENTER
 895            attach = Image.Attach.CENTER
 896        elif style == 'in_game':
 897            in_game_colors = True
 898            in_main_menu = False
 899            h_attach = Text.HAttach.LEFT
 900            v_attach = Text.VAttach.TOP
 901            attach = Image.Attach.TOP_LEFT
 902        elif style == 'news':
 903            in_game_colors = True
 904            in_main_menu = True
 905            h_attach = Text.HAttach.CENTER
 906            v_attach = Text.VAttach.TOP
 907            attach = Image.Attach.TOP_CENTER
 908        else:
 909            raise ValueError('invalid style "' + style + '"')
 910
 911        # Attempt to determine what campaign we're in
 912        # (so we know whether to show "hard mode only").
 913        if in_main_menu:
 914            hmo = False
 915        else:
 916            try:
 917                session = bascenev1.getsession()
 918                if isinstance(session, CoopSession):
 919                    campaign = session.campaign
 920                    assert campaign is not None
 921                    hmo = self._hard_mode_only and campaign.name == 'Easy'
 922                else:
 923                    hmo = False
 924            except Exception:
 925                logging.exception('Error determining campaign.')
 926                hmo = False
 927
 928        objs: list[bascenev1.Actor]
 929
 930        if in_game_colors:
 931            objs = []
 932            out_delay_fin = (delay + outdelay) if outdelay is not None else None
 933            if color is not None:
 934                cl1 = (2.0 * color[0], 2.0 * color[1], 2.0 * color[2], color[3])
 935                cl2 = color
 936            else:
 937                cl1 = (1.5, 1.5, 2, 1.0)
 938                cl2 = (0.8, 0.8, 1.0, 1.0)
 939
 940            if hmo:
 941                cl1 = (cl1[0], cl1[1], cl1[2], cl1[3] * 0.6)
 942                cl2 = (cl2[0], cl2[1], cl2[2], cl2[3] * 0.2)
 943
 944            objs.append(
 945                Image(
 946                    self.get_icon_texture(False),
 947                    host_only=True,
 948                    color=cl1,
 949                    position=(x - 25, y + 5),
 950                    attach=attach,
 951                    transition=Image.Transition.FADE_IN,
 952                    transition_delay=delay,
 953                    vr_depth=4,
 954                    transition_out_delay=out_delay_fin,
 955                    scale=(40, 40),
 956                ).autoretain()
 957            )
 958            txt = self.display_name
 959            txt_s = 0.85
 960            txt_max_w = 300
 961            objs.append(
 962                Text(
 963                    txt,
 964                    host_only=True,
 965                    maxwidth=txt_max_w,
 966                    position=(x, y + 2),
 967                    transition=Text.Transition.FADE_IN,
 968                    scale=txt_s,
 969                    flatness=0.6,
 970                    shadow=0.5,
 971                    h_attach=h_attach,
 972                    v_attach=v_attach,
 973                    color=cl2,
 974                    transition_delay=delay + 0.05,
 975                    transition_out_delay=out_delay_fin,
 976                ).autoretain()
 977            )
 978            txt2_s = 0.62
 979            txt2_max_w = 400
 980            objs.append(
 981                Text(
 982                    self.description_full if in_main_menu else self.description,
 983                    host_only=True,
 984                    maxwidth=txt2_max_w,
 985                    position=(x, y - 14),
 986                    transition=Text.Transition.FADE_IN,
 987                    vr_depth=-5,
 988                    h_attach=h_attach,
 989                    v_attach=v_attach,
 990                    scale=txt2_s,
 991                    flatness=1.0,
 992                    shadow=0.5,
 993                    color=cl2,
 994                    transition_delay=delay + 0.1,
 995                    transition_out_delay=out_delay_fin,
 996                ).autoretain()
 997            )
 998
 999            if hmo:
1000                txtactor = Text(
1001                    babase.Lstr(resource='difficultyHardOnlyText'),
1002                    host_only=True,
1003                    maxwidth=txt2_max_w * 0.7,
1004                    position=(x + 60, y + 5),
1005                    transition=Text.Transition.FADE_IN,
1006                    vr_depth=-5,
1007                    h_attach=h_attach,
1008                    v_attach=v_attach,
1009                    h_align=Text.HAlign.CENTER,
1010                    v_align=Text.VAlign.CENTER,
1011                    scale=txt_s * 0.8,
1012                    flatness=1.0,
1013                    shadow=0.5,
1014                    color=(1, 1, 0.6, 1),
1015                    transition_delay=delay + 0.1,
1016                    transition_out_delay=out_delay_fin,
1017                ).autoretain()
1018                txtactor.node.rotate = 10
1019                objs.append(txtactor)
1020
1021            # Ticket-award.
1022            award_x = -100
1023            objs.append(
1024                Text(
1025                    babase.charstr(babase.SpecialChar.TICKET),
1026                    host_only=True,
1027                    position=(x + award_x + 33, y + 7),
1028                    transition=Text.Transition.FADE_IN,
1029                    scale=1.5,
1030                    h_attach=h_attach,
1031                    v_attach=v_attach,
1032                    h_align=Text.HAlign.CENTER,
1033                    v_align=Text.VAlign.CENTER,
1034                    color=(1, 1, 1, 0.2 if hmo else 0.4),
1035                    transition_delay=delay + 0.05,
1036                    transition_out_delay=out_delay_fin,
1037                ).autoretain()
1038            )
1039            objs.append(
1040                Text(
1041                    '+' + str(self.get_award_ticket_value()),
1042                    host_only=True,
1043                    position=(x + award_x + 28, y + 16),
1044                    transition=Text.Transition.FADE_IN,
1045                    scale=0.7,
1046                    flatness=1,
1047                    h_attach=h_attach,
1048                    v_attach=v_attach,
1049                    h_align=Text.HAlign.CENTER,
1050                    v_align=Text.VAlign.CENTER,
1051                    color=cl2,
1052                    transition_delay=delay + 0.05,
1053                    transition_out_delay=out_delay_fin,
1054                ).autoretain()
1055            )
1056
1057        else:
1058            complete = self.complete
1059            objs = []
1060            c_icon = self.get_icon_color(complete)
1061            if hmo and not complete:
1062                c_icon = (c_icon[0], c_icon[1], c_icon[2], c_icon[3] * 0.3)
1063            objs.append(
1064                Image(
1065                    self.get_icon_texture(complete),
1066                    host_only=True,
1067                    color=c_icon,
1068                    position=(x - 25, y + 5),
1069                    attach=attach,
1070                    vr_depth=4,
1071                    transition=Image.Transition.IN_RIGHT,
1072                    transition_delay=delay,
1073                    transition_out_delay=None,
1074                    scale=(40, 40),
1075                ).autoretain()
1076            )
1077            if complete:
1078                objs.append(
1079                    Image(
1080                        bascenev1.gettexture('achievementOutline'),
1081                        host_only=True,
1082                        mesh_transparent=bascenev1.getmesh(
1083                            'achievementOutline'
1084                        ),
1085                        color=(2, 1.4, 0.4, 1),
1086                        vr_depth=8,
1087                        position=(x - 25, y + 5),
1088                        attach=attach,
1089                        transition=Image.Transition.IN_RIGHT,
1090                        transition_delay=delay,
1091                        transition_out_delay=None,
1092                        scale=(40, 40),
1093                    ).autoretain()
1094                )
1095            else:
1096                if not complete:
1097                    award_x = -100
1098                    objs.append(
1099                        Text(
1100                            babase.charstr(babase.SpecialChar.TICKET),
1101                            host_only=True,
1102                            position=(x + award_x + 33, y + 7),
1103                            transition=Text.Transition.IN_RIGHT,
1104                            scale=1.5,
1105                            h_attach=h_attach,
1106                            v_attach=v_attach,
1107                            h_align=Text.HAlign.CENTER,
1108                            v_align=Text.VAlign.CENTER,
1109                            color=(1, 1, 1, (0.1 if hmo else 0.2)),
1110                            transition_delay=delay + 0.05,
1111                            transition_out_delay=None,
1112                        ).autoretain()
1113                    )
1114                    objs.append(
1115                        Text(
1116                            '+' + str(self.get_award_ticket_value()),
1117                            host_only=True,
1118                            position=(x + award_x + 28, y + 16),
1119                            transition=Text.Transition.IN_RIGHT,
1120                            scale=0.7,
1121                            flatness=1,
1122                            h_attach=h_attach,
1123                            v_attach=v_attach,
1124                            h_align=Text.HAlign.CENTER,
1125                            v_align=Text.VAlign.CENTER,
1126                            color=(0.6, 0.6, 0.6, (0.2 if hmo else 0.4)),
1127                            transition_delay=delay + 0.05,
1128                            transition_out_delay=None,
1129                        ).autoretain()
1130                    )
1131
1132                    # Show 'hard-mode-only' only over incomplete achievements
1133                    # when that's the case.
1134                    if hmo:
1135                        txtactor = Text(
1136                            babase.Lstr(resource='difficultyHardOnlyText'),
1137                            host_only=True,
1138                            maxwidth=300 * 0.7,
1139                            position=(x + 60, y + 5),
1140                            transition=Text.Transition.FADE_IN,
1141                            vr_depth=-5,
1142                            h_attach=h_attach,
1143                            v_attach=v_attach,
1144                            h_align=Text.HAlign.CENTER,
1145                            v_align=Text.VAlign.CENTER,
1146                            scale=0.85 * 0.8,
1147                            flatness=1.0,
1148                            shadow=0.5,
1149                            color=(1, 1, 0.6, 1),
1150                            transition_delay=delay + 0.05,
1151                            transition_out_delay=None,
1152                        ).autoretain()
1153                        assert txtactor.node
1154                        txtactor.node.rotate = 10
1155                        objs.append(txtactor)
1156
1157            objs.append(
1158                Text(
1159                    self.display_name,
1160                    host_only=True,
1161                    maxwidth=300,
1162                    position=(x, y + 2),
1163                    transition=Text.Transition.IN_RIGHT,
1164                    scale=0.85,
1165                    flatness=0.6,
1166                    h_attach=h_attach,
1167                    v_attach=v_attach,
1168                    color=(
1169                        (0.8, 0.93, 0.8, 1.0)
1170                        if complete
1171                        else (0.6, 0.6, 0.6, (0.2 if hmo else 0.4))
1172                    ),
1173                    transition_delay=delay + 0.05,
1174                    transition_out_delay=None,
1175                ).autoretain()
1176            )
1177            objs.append(
1178                Text(
1179                    self.description_complete if complete else self.description,
1180                    host_only=True,
1181                    maxwidth=400,
1182                    position=(x, y - 14),
1183                    transition=Text.Transition.IN_RIGHT,
1184                    vr_depth=-5,
1185                    h_attach=h_attach,
1186                    v_attach=v_attach,
1187                    scale=0.62,
1188                    flatness=1.0,
1189                    color=(
1190                        (0.6, 0.6, 0.6, 1.0)
1191                        if complete
1192                        else (0.6, 0.6, 0.6, (0.2 if hmo else 0.4))
1193                    ),
1194                    transition_delay=delay + 0.1,
1195                    transition_out_delay=None,
1196                ).autoretain()
1197            )
1198        return objs

Create a display for the Achievement.

Shows the Achievement icon, name, and description.

def show_completion_banner(self, sound: bool = True) -> None:
1220    def show_completion_banner(self, sound: bool = True) -> None:
1221        """Create the banner/sound for an acquired achievement announcement."""
1222        from bascenev1lib.actor.text import Text
1223        from bascenev1lib.actor.image import Image
1224
1225        app = babase.app
1226        assert app.classic is not None
1227        app.classic.ach.last_achievement_display_time = babase.apptime()
1228
1229        # Just piggy-back onto any current activity
1230        # (should we use the session instead?..)
1231        activity = bascenev1.getactivity(doraise=False)
1232
1233        # If this gets called while this achievement is occupying a slot
1234        # already, ignore it. (probably should never happen in real
1235        # life but whatevs).
1236        if self._completion_banner_slot is not None:
1237            return
1238
1239        if activity is None:
1240            print('show_completion_banner() called with no current activity!')
1241            return
1242
1243        if sound:
1244            bascenev1.getsound('achievement').play(host_only=True)
1245        else:
1246            bascenev1.timer(
1247                0.5, lambda: bascenev1.getsound('ding').play(host_only=True)
1248            )
1249
1250        in_time = 0.300
1251        out_time = 3.5
1252
1253        base_vr_depth = 200
1254
1255        # Find the first free slot.
1256        i = 0
1257        while True:
1258            if i not in app.classic.ach.achievement_completion_banner_slots:
1259                app.classic.ach.achievement_completion_banner_slots.add(i)
1260                self._completion_banner_slot = i
1261
1262                # Remove us from that slot when we close.
1263                # Use an app-timer in an empty context so the removal
1264                # runs even if our activity/session dies.
1265                with babase.ContextRef.empty():
1266                    babase.apptimer(
1267                        in_time + out_time, self._remove_banner_slot
1268                    )
1269                break
1270            i += 1
1271        assert self._completion_banner_slot is not None
1272        y_offs = 110 * self._completion_banner_slot
1273        objs: list[bascenev1.Actor] = []
1274        obj = Image(
1275            bascenev1.gettexture('shadow'),
1276            position=(-30, 30 + y_offs),
1277            front=True,
1278            attach=Image.Attach.BOTTOM_CENTER,
1279            transition=Image.Transition.IN_BOTTOM,
1280            vr_depth=base_vr_depth - 100,
1281            transition_delay=in_time,
1282            transition_out_delay=out_time,
1283            color=(0.0, 0.1, 0, 1),
1284            scale=(1000, 300),
1285        ).autoretain()
1286        objs.append(obj)
1287        assert obj.node
1288        obj.node.host_only = True
1289        obj = Image(
1290            bascenev1.gettexture('light'),
1291            position=(-180, 60 + y_offs),
1292            front=True,
1293            attach=Image.Attach.BOTTOM_CENTER,
1294            vr_depth=base_vr_depth,
1295            transition=Image.Transition.IN_BOTTOM,
1296            transition_delay=in_time,
1297            transition_out_delay=out_time,
1298            color=(1.8, 1.8, 1.0, 0.0),
1299            scale=(40, 300),
1300        ).autoretain()
1301        objs.append(obj)
1302        assert obj.node
1303        obj.node.host_only = True
1304        obj.node.premultiplied = True
1305        combine = bascenev1.newnode(
1306            'combine', owner=obj.node, attrs={'size': 2}
1307        )
1308        bascenev1.animate(
1309            combine,
1310            'input0',
1311            {
1312                in_time: 0,
1313                in_time + 0.4: 30,
1314                in_time + 0.5: 40,
1315                in_time + 0.6: 30,
1316                in_time + 2.0: 0,
1317            },
1318        )
1319        bascenev1.animate(
1320            combine,
1321            'input1',
1322            {
1323                in_time: 0,
1324                in_time + 0.4: 200,
1325                in_time + 0.5: 500,
1326                in_time + 0.6: 200,
1327                in_time + 2.0: 0,
1328            },
1329        )
1330        combine.connectattr('output', obj.node, 'scale')
1331        bascenev1.animate(obj.node, 'rotate', {0: 0.0, 0.35: 360.0}, loop=True)
1332        obj = Image(
1333            self.get_icon_texture(True),
1334            position=(-180, 60 + y_offs),
1335            attach=Image.Attach.BOTTOM_CENTER,
1336            front=True,
1337            vr_depth=base_vr_depth - 10,
1338            transition=Image.Transition.IN_BOTTOM,
1339            transition_delay=in_time,
1340            transition_out_delay=out_time,
1341            scale=(100, 100),
1342        ).autoretain()
1343        objs.append(obj)
1344        assert obj.node
1345        obj.node.host_only = True
1346
1347        # Flash.
1348        color = self.get_icon_color(True)
1349        combine = bascenev1.newnode(
1350            'combine', owner=obj.node, attrs={'size': 3}
1351        )
1352        keys = {
1353            in_time: 1.0 * color[0],
1354            in_time + 0.4: 1.5 * color[0],
1355            in_time + 0.5: 6.0 * color[0],
1356            in_time + 0.6: 1.5 * color[0],
1357            in_time + 2.0: 1.0 * color[0],
1358        }
1359        bascenev1.animate(combine, 'input0', keys)
1360        keys = {
1361            in_time: 1.0 * color[1],
1362            in_time + 0.4: 1.5 * color[1],
1363            in_time + 0.5: 6.0 * color[1],
1364            in_time + 0.6: 1.5 * color[1],
1365            in_time + 2.0: 1.0 * color[1],
1366        }
1367        bascenev1.animate(combine, 'input1', keys)
1368        keys = {
1369            in_time: 1.0 * color[2],
1370            in_time + 0.4: 1.5 * color[2],
1371            in_time + 0.5: 6.0 * color[2],
1372            in_time + 0.6: 1.5 * color[2],
1373            in_time + 2.0: 1.0 * color[2],
1374        }
1375        bascenev1.animate(combine, 'input2', keys)
1376        combine.connectattr('output', obj.node, 'color')
1377
1378        obj = Image(
1379            bascenev1.gettexture('achievementOutline'),
1380            mesh_transparent=bascenev1.getmesh('achievementOutline'),
1381            position=(-180, 60 + y_offs),
1382            front=True,
1383            attach=Image.Attach.BOTTOM_CENTER,
1384            vr_depth=base_vr_depth,
1385            transition=Image.Transition.IN_BOTTOM,
1386            transition_delay=in_time,
1387            transition_out_delay=out_time,
1388            scale=(100, 100),
1389        ).autoretain()
1390        assert obj.node
1391        obj.node.host_only = True
1392
1393        # Flash.
1394        color = (2, 1.4, 0.4, 1)
1395        combine = bascenev1.newnode(
1396            'combine', owner=obj.node, attrs={'size': 3}
1397        )
1398        keys = {
1399            in_time: 1.0 * color[0],
1400            in_time + 0.4: 1.5 * color[0],
1401            in_time + 0.5: 6.0 * color[0],
1402            in_time + 0.6: 1.5 * color[0],
1403            in_time + 2.0: 1.0 * color[0],
1404        }
1405        bascenev1.animate(combine, 'input0', keys)
1406        keys = {
1407            in_time: 1.0 * color[1],
1408            in_time + 0.4: 1.5 * color[1],
1409            in_time + 0.5: 6.0 * color[1],
1410            in_time + 0.6: 1.5 * color[1],
1411            in_time + 2.0: 1.0 * color[1],
1412        }
1413        bascenev1.animate(combine, 'input1', keys)
1414        keys = {
1415            in_time: 1.0 * color[2],
1416            in_time + 0.4: 1.5 * color[2],
1417            in_time + 0.5: 6.0 * color[2],
1418            in_time + 0.6: 1.5 * color[2],
1419            in_time + 2.0: 1.0 * color[2],
1420        }
1421        bascenev1.animate(combine, 'input2', keys)
1422        combine.connectattr('output', obj.node, 'color')
1423        objs.append(obj)
1424
1425        objt = Text(
1426            babase.Lstr(
1427                value='${A}:',
1428                subs=[('${A}', babase.Lstr(resource='achievementText'))],
1429            ),
1430            position=(-120, 91 + y_offs),
1431            front=True,
1432            v_attach=Text.VAttach.BOTTOM,
1433            vr_depth=base_vr_depth - 10,
1434            transition=Text.Transition.IN_BOTTOM,
1435            flatness=0.5,
1436            transition_delay=in_time,
1437            transition_out_delay=out_time,
1438            color=(1, 1, 1, 0.8),
1439            scale=0.65,
1440        ).autoretain()
1441        objs.append(objt)
1442        assert objt.node
1443        objt.node.host_only = True
1444
1445        objt = Text(
1446            self.display_name,
1447            position=(-120, 50 + y_offs),
1448            front=True,
1449            v_attach=Text.VAttach.BOTTOM,
1450            transition=Text.Transition.IN_BOTTOM,
1451            vr_depth=base_vr_depth,
1452            flatness=0.5,
1453            transition_delay=in_time,
1454            transition_out_delay=out_time,
1455            flash=True,
1456            color=(1, 0.8, 0, 1.0),
1457            scale=1.5,
1458        ).autoretain()
1459        objs.append(objt)
1460        assert objt.node
1461        objt.node.host_only = True
1462
1463        objt = Text(
1464            babase.charstr(babase.SpecialChar.TICKET),
1465            position=(-120 - 170 + 5, 75 + y_offs - 20),
1466            front=True,
1467            v_attach=Text.VAttach.BOTTOM,
1468            h_align=Text.HAlign.CENTER,
1469            v_align=Text.VAlign.CENTER,
1470            transition=Text.Transition.IN_BOTTOM,
1471            vr_depth=base_vr_depth,
1472            transition_delay=in_time,
1473            transition_out_delay=out_time,
1474            flash=True,
1475            color=(0.5, 0.5, 0.5, 1),
1476            scale=3.0,
1477        ).autoretain()
1478        objs.append(objt)
1479        assert objt.node
1480        objt.node.host_only = True
1481
1482        objt = Text(
1483            '+' + str(self.get_award_ticket_value()),
1484            position=(-120 - 180 + 5, 80 + y_offs - 20),
1485            v_attach=Text.VAttach.BOTTOM,
1486            front=True,
1487            h_align=Text.HAlign.CENTER,
1488            v_align=Text.VAlign.CENTER,
1489            transition=Text.Transition.IN_BOTTOM,
1490            vr_depth=base_vr_depth,
1491            flatness=0.5,
1492            shadow=1.0,
1493            transition_delay=in_time,
1494            transition_out_delay=out_time,
1495            flash=True,
1496            color=(0, 1, 0, 1),
1497            scale=1.5,
1498        ).autoretain()
1499        objs.append(objt)
1500        assert objt.node
1501        objt.node.host_only = True
1502
1503        # Add the 'x 2' if we've got pro.
1504        if app.classic.accounts.have_pro():
1505            objt = Text(
1506                'x 2',
1507                position=(-120 - 180 + 45, 80 + y_offs - 50),
1508                v_attach=Text.VAttach.BOTTOM,
1509                front=True,
1510                h_align=Text.HAlign.CENTER,
1511                v_align=Text.VAlign.CENTER,
1512                transition=Text.Transition.IN_BOTTOM,
1513                vr_depth=base_vr_depth,
1514                flatness=0.5,
1515                shadow=1.0,
1516                transition_delay=in_time,
1517                transition_out_delay=out_time,
1518                flash=True,
1519                color=(0.4, 0, 1, 1),
1520                scale=0.9,
1521            ).autoretain()
1522            objs.append(objt)
1523            assert objt.node
1524            objt.node.host_only = True
1525
1526        objt = Text(
1527            self.description_complete,
1528            position=(-120, 30 + y_offs),
1529            front=True,
1530            v_attach=Text.VAttach.BOTTOM,
1531            transition=Text.Transition.IN_BOTTOM,
1532            vr_depth=base_vr_depth - 10,
1533            flatness=0.5,
1534            transition_delay=in_time,
1535            transition_out_delay=out_time,
1536            color=(1.0, 0.7, 0.5, 1.0),
1537            scale=0.8,
1538        ).autoretain()
1539        objs.append(objt)
1540        assert objt.node
1541        objt.node.host_only = True
1542
1543        for actor in objs:
1544            bascenev1.timer(
1545                out_time + 1.000,
1546                babase.WeakCall(actor.handlemessage, bascenev1.DieMessage()),
1547            )

Create the banner/sound for an acquired achievement announcement.

class AchievementSubsystem:
 69class AchievementSubsystem:
 70    """Subsystem for achievement handling.
 71
 72    Category: **App Classes**
 73
 74    Access the single shared instance of this class at 'ba.app.ach'.
 75    """
 76
 77    def __init__(self) -> None:
 78        self.achievements: list[Achievement] = []
 79        self.achievements_to_display: list[
 80            tuple[baclassic.Achievement, bool]
 81        ] = []
 82        self.achievement_display_timer: bascenev1.BaseTimer | None = None
 83        self.last_achievement_display_time: float = 0.0
 84        self.achievement_completion_banner_slots: set[int] = set()
 85        self._init_achievements()
 86
 87    def _init_achievements(self) -> None:
 88        """Fill in available achievements."""
 89
 90        achs = self.achievements
 91
 92        # 5
 93        achs.append(
 94            Achievement('In Control', 'achievementInControl', (1, 1, 1), '', 5)
 95        )
 96        # 15
 97        achs.append(
 98            Achievement(
 99                'Sharing is Caring',
100                'achievementSharingIsCaring',
101                (1, 1, 1),
102                '',
103                15,
104            )
105        )
106        # 10
107        achs.append(
108            Achievement(
109                'Dual Wielding', 'achievementDualWielding', (1, 1, 1), '', 10
110            )
111        )
112
113        # 10
114        achs.append(
115            Achievement(
116                'Free Loader', 'achievementFreeLoader', (1, 1, 1), '', 10
117            )
118        )
119        # 20
120        achs.append(
121            Achievement(
122                'Team Player', 'achievementTeamPlayer', (1, 1, 1), '', 20
123            )
124        )
125
126        # 5
127        achs.append(
128            Achievement(
129                'Onslaught Training Victory',
130                'achievementOnslaught',
131                (1, 1, 1),
132                'Default:Onslaught Training',
133                5,
134            )
135        )
136        # 5
137        achs.append(
138            Achievement(
139                'Off You Go Then',
140                'achievementOffYouGo',
141                (1, 1.1, 1.3),
142                'Default:Onslaught Training',
143                5,
144            )
145        )
146        # 10
147        achs.append(
148            Achievement(
149                'Boxer',
150                'achievementBoxer',
151                (1, 0.6, 0.6),
152                'Default:Onslaught Training',
153                10,
154                hard_mode_only=True,
155            )
156        )
157
158        # 10
159        achs.append(
160            Achievement(
161                'Rookie Onslaught Victory',
162                'achievementOnslaught',
163                (0.5, 1.4, 0.6),
164                'Default:Rookie Onslaught',
165                10,
166            )
167        )
168        # 10
169        achs.append(
170            Achievement(
171                'Mine Games',
172                'achievementMine',
173                (1, 1, 1.4),
174                'Default:Rookie Onslaught',
175                10,
176            )
177        )
178        # 15
179        achs.append(
180            Achievement(
181                'Flawless Victory',
182                'achievementFlawlessVictory',
183                (1, 1, 1),
184                'Default:Rookie Onslaught',
185                15,
186                hard_mode_only=True,
187            )
188        )
189
190        # 10
191        achs.append(
192            Achievement(
193                'Rookie Football Victory',
194                'achievementFootballVictory',
195                (1.0, 1, 0.6),
196                'Default:Rookie Football',
197                10,
198            )
199        )
200        # 10
201        achs.append(
202            Achievement(
203                'Super Punch',
204                'achievementSuperPunch',
205                (1, 1, 1.8),
206                'Default:Rookie Football',
207                10,
208            )
209        )
210        # 15
211        achs.append(
212            Achievement(
213                'Rookie Football Shutout',
214                'achievementFootballShutout',
215                (1, 1, 1),
216                'Default:Rookie Football',
217                15,
218                hard_mode_only=True,
219            )
220        )
221
222        # 15
223        achs.append(
224            Achievement(
225                'Pro Onslaught Victory',
226                'achievementOnslaught',
227                (0.3, 1, 2.0),
228                'Default:Pro Onslaught',
229                15,
230            )
231        )
232        # 15
233        achs.append(
234            Achievement(
235                'Boom Goes the Dynamite',
236                'achievementTNT',
237                (1.4, 1.2, 0.8),
238                'Default:Pro Onslaught',
239                15,
240            )
241        )
242        # 20
243        achs.append(
244            Achievement(
245                'Pro Boxer',
246                'achievementBoxer',
247                (2, 2, 0),
248                'Default:Pro Onslaught',
249                20,
250                hard_mode_only=True,
251            )
252        )
253
254        # 15
255        achs.append(
256            Achievement(
257                'Pro Football Victory',
258                'achievementFootballVictory',
259                (1.3, 1.3, 2.0),
260                'Default:Pro Football',
261                15,
262            )
263        )
264        # 15
265        achs.append(
266            Achievement(
267                'Super Mega Punch',
268                'achievementSuperPunch',
269                (2, 1, 0.6),
270                'Default:Pro Football',
271                15,
272            )
273        )
274        # 20
275        achs.append(
276            Achievement(
277                'Pro Football Shutout',
278                'achievementFootballShutout',
279                (0.7, 0.7, 2.0),
280                'Default:Pro Football',
281                20,
282                hard_mode_only=True,
283            )
284        )
285
286        # 15
287        achs.append(
288            Achievement(
289                'Pro Runaround Victory',
290                'achievementRunaround',
291                (1, 1, 1),
292                'Default:Pro Runaround',
293                15,
294            )
295        )
296        # 20
297        achs.append(
298            Achievement(
299                'Precision Bombing',
300                'achievementCrossHair',
301                (1, 1, 1.3),
302                'Default:Pro Runaround',
303                20,
304                hard_mode_only=True,
305            )
306        )
307        # 25
308        achs.append(
309            Achievement(
310                'The Wall',
311                'achievementWall',
312                (1, 0.7, 0.7),
313                'Default:Pro Runaround',
314                25,
315                hard_mode_only=True,
316            )
317        )
318
319        # 30
320        achs.append(
321            Achievement(
322                'Uber Onslaught Victory',
323                'achievementOnslaught',
324                (2, 2, 1),
325                'Default:Uber Onslaught',
326                30,
327            )
328        )
329        # 30
330        achs.append(
331            Achievement(
332                'Gold Miner',
333                'achievementMine',
334                (2, 1.6, 0.2),
335                'Default:Uber Onslaught',
336                30,
337                hard_mode_only=True,
338            )
339        )
340        # 30
341        achs.append(
342            Achievement(
343                'TNT Terror',
344                'achievementTNT',
345                (2, 1.8, 0.3),
346                'Default:Uber Onslaught',
347                30,
348                hard_mode_only=True,
349            )
350        )
351
352        # 30
353        achs.append(
354            Achievement(
355                'Uber Football Victory',
356                'achievementFootballVictory',
357                (1.8, 1.4, 0.3),
358                'Default:Uber Football',
359                30,
360            )
361        )
362        # 30
363        achs.append(
364            Achievement(
365                'Got the Moves',
366                'achievementGotTheMoves',
367                (2, 1, 0),
368                'Default:Uber Football',
369                30,
370                hard_mode_only=True,
371            )
372        )
373        # 40
374        achs.append(
375            Achievement(
376                'Uber Football Shutout',
377                'achievementFootballShutout',
378                (2, 2, 0),
379                'Default:Uber Football',
380                40,
381                hard_mode_only=True,
382            )
383        )
384
385        # 30
386        achs.append(
387            Achievement(
388                'Uber Runaround Victory',
389                'achievementRunaround',
390                (1.5, 1.2, 0.2),
391                'Default:Uber Runaround',
392                30,
393            )
394        )
395        # 40
396        achs.append(
397            Achievement(
398                'The Great Wall',
399                'achievementWall',
400                (2, 1.7, 0.4),
401                'Default:Uber Runaround',
402                40,
403                hard_mode_only=True,
404            )
405        )
406        # 40
407        achs.append(
408            Achievement(
409                'Stayin\' Alive',
410                'achievementStayinAlive',
411                (2, 2, 1),
412                'Default:Uber Runaround',
413                40,
414                hard_mode_only=True,
415            )
416        )
417
418        # 20
419        achs.append(
420            Achievement(
421                'Last Stand Master',
422                'achievementMedalSmall',
423                (2, 1.5, 0.3),
424                'Default:The Last Stand',
425                20,
426                hard_mode_only=True,
427            )
428        )
429        # 40
430        achs.append(
431            Achievement(
432                'Last Stand Wizard',
433                'achievementMedalMedium',
434                (2, 1.5, 0.3),
435                'Default:The Last Stand',
436                40,
437                hard_mode_only=True,
438            )
439        )
440        # 60
441        achs.append(
442            Achievement(
443                'Last Stand God',
444                'achievementMedalLarge',
445                (2, 1.5, 0.3),
446                'Default:The Last Stand',
447                60,
448                hard_mode_only=True,
449            )
450        )
451
452        # 5
453        achs.append(
454            Achievement(
455                'Onslaught Master',
456                'achievementMedalSmall',
457                (0.7, 1, 0.7),
458                'Challenges:Infinite Onslaught',
459                5,
460            )
461        )
462        # 15
463        achs.append(
464            Achievement(
465                'Onslaught Wizard',
466                'achievementMedalMedium',
467                (0.7, 1.0, 0.7),
468                'Challenges:Infinite Onslaught',
469                15,
470            )
471        )
472        # 30
473        achs.append(
474            Achievement(
475                'Onslaught God',
476                'achievementMedalLarge',
477                (0.7, 1.0, 0.7),
478                'Challenges:Infinite Onslaught',
479                30,
480            )
481        )
482
483        # 5
484        achs.append(
485            Achievement(
486                'Runaround Master',
487                'achievementMedalSmall',
488                (1.0, 1.0, 1.2),
489                'Challenges:Infinite Runaround',
490                5,
491            )
492        )
493        # 15
494        achs.append(
495            Achievement(
496                'Runaround Wizard',
497                'achievementMedalMedium',
498                (1.0, 1.0, 1.2),
499                'Challenges:Infinite Runaround',
500                15,
501            )
502        )
503        # 30
504        achs.append(
505            Achievement(
506                'Runaround God',
507                'achievementMedalLarge',
508                (1.0, 1.0, 1.2),
509                'Challenges:Infinite Runaround',
510                30,
511            )
512        )
513
514    def award_local_achievement(self, achname: str) -> None:
515        """For non-game-based achievements such as controller-connection."""
516        plus = babase.app.plus
517        if plus is None:
518            logging.warning('achievements require plus feature-set')
519            return
520        try:
521            ach = self.get_achievement(achname)
522            if not ach.complete:
523                # Report new achievements to the game-service.
524                plus.report_achievement(achname)
525
526                # And to our account.
527                plus.add_v1_account_transaction(
528                    {'type': 'ACHIEVEMENT', 'name': achname}
529                )
530
531                # Now attempt to show a banner.
532                self.display_achievement_banner(achname)
533
534        except Exception:
535            logging.exception('Error in award_local_achievement.')
536
537    def display_achievement_banner(self, achname: str) -> None:
538        """Display a completion banner for an achievement.
539
540        (internal)
541
542        Used for server-driven achievements.
543        """
544        try:
545            # FIXME: Need to get these using the UI context or some other
546            #  purely local context somehow instead of trying to inject these
547            #  into whatever activity happens to be active
548            #  (since that won't work while in client mode).
549            activity = bascenev1.get_foreground_host_activity()
550            if activity is not None:
551                with activity.context:
552                    self.get_achievement(achname).announce_completion()
553        except Exception:
554            logging.exception('Error in display_achievement_banner.')
555
556    def set_completed_achievements(self, achs: Sequence[str]) -> None:
557        """Set the current state of completed achievements.
558
559        (internal)
560
561        All achievements not included here will be set incomplete.
562        """
563
564        # Note: This gets called whenever game-center/game-circle/etc tells
565        # us which achievements we currently have.  We always defer to them,
566        # even if that means we have to un-set an achievement we think we have.
567
568        cfg = babase.app.config
569        cfg['Achievements'] = {}
570        for a_name in achs:
571            self.get_achievement(a_name).set_complete(True)
572        cfg.commit()
573
574    def get_achievement(self, name: str) -> Achievement:
575        """Return an Achievement by name."""
576        achs = [a for a in self.achievements if a.name == name]
577        assert len(achs) < 2
578        if not achs:
579            raise ValueError("Invalid achievement name: '" + name + "'")
580        return achs[0]
581
582    def achievements_for_coop_level(self, level_name: str) -> list[Achievement]:
583        """Given a level name, return achievements available for it."""
584
585        # For the Easy campaign we return achievements for the Default
586        # campaign too. (want the user to see what achievements are part of the
587        # level even if they can't unlock them all on easy mode).
588        return [
589            a
590            for a in self.achievements
591            if a.level_name
592            in (level_name, level_name.replace('Easy', 'Default'))
593        ]
594
595    def _test(self) -> None:
596        """For testing achievement animations."""
597
598        def testcall1() -> None:
599            self.achievements[0].announce_completion()
600            self.achievements[1].announce_completion()
601            self.achievements[2].announce_completion()
602
603        def testcall2() -> None:
604            self.achievements[3].announce_completion()
605            self.achievements[4].announce_completion()
606            self.achievements[5].announce_completion()
607
608        bascenev1.basetimer(3.0, testcall1)
609        bascenev1.basetimer(7.0, testcall2)

Subsystem for achievement handling.

Category: App Classes

Access the single shared instance of this class at 'ba.app.ach'.

achievements: list[Achievement]
achievements_to_display: list[tuple[Achievement, bool]]
achievement_display_timer: _bascenev1.BaseTimer | None
last_achievement_display_time: float
achievement_completion_banner_slots: set[int]
def award_local_achievement(self, achname: str) -> None:
514    def award_local_achievement(self, achname: str) -> None:
515        """For non-game-based achievements such as controller-connection."""
516        plus = babase.app.plus
517        if plus is None:
518            logging.warning('achievements require plus feature-set')
519            return
520        try:
521            ach = self.get_achievement(achname)
522            if not ach.complete:
523                # Report new achievements to the game-service.
524                plus.report_achievement(achname)
525
526                # And to our account.
527                plus.add_v1_account_transaction(
528                    {'type': 'ACHIEVEMENT', 'name': achname}
529                )
530
531                # Now attempt to show a banner.
532                self.display_achievement_banner(achname)
533
534        except Exception:
535            logging.exception('Error in award_local_achievement.')

For non-game-based achievements such as controller-connection.

def get_achievement(self, name: str) -> Achievement:
574    def get_achievement(self, name: str) -> Achievement:
575        """Return an Achievement by name."""
576        achs = [a for a in self.achievements if a.name == name]
577        assert len(achs) < 2
578        if not achs:
579            raise ValueError("Invalid achievement name: '" + name + "'")
580        return achs[0]

Return an Achievement by name.

def achievements_for_coop_level(self, level_name: str) -> list[Achievement]:
582    def achievements_for_coop_level(self, level_name: str) -> list[Achievement]:
583        """Given a level name, return achievements available for it."""
584
585        # For the Easy campaign we return achievements for the Default
586        # campaign too. (want the user to see what achievements are part of the
587        # level even if they can't unlock them all on easy mode).
588        return [
589            a
590            for a in self.achievements
591            if a.level_name
592            in (level_name, level_name.replace('Easy', 'Default'))
593        ]

Given a level name, return achievements available for it.

def show_display_item( itemwrapper: bacommon.bs.DisplayItemWrapper, parent: _bauiv1.Widget, pos: tuple[float, float], width: float) -> None:
 18def show_display_item(
 19    itemwrapper: bacommon.bs.DisplayItemWrapper,
 20    parent: bauiv1.Widget,
 21    pos: tuple[float, float],
 22    width: float,
 23) -> None:
 24    """Create ui to depict a display-item."""
 25
 26    height = width * 0.666
 27
 28    # Silent no-op if our parent ui is dead.
 29    if not parent:
 30        return
 31
 32    img: str | None = None
 33    img_y_offs = 0.0
 34    text_y_offs = 0.0
 35    show_text = True
 36
 37    if isinstance(itemwrapper.item, bacommon.bs.TicketsDisplayItem):
 38        img = 'tickets'
 39        img_y_offs = width * 0.11
 40        text_y_offs = width * -0.15
 41    elif isinstance(itemwrapper.item, bacommon.bs.TokensDisplayItem):
 42        img = 'coin'
 43        img_y_offs = width * 0.11
 44        text_y_offs = width * -0.15
 45    elif isinstance(itemwrapper.item, bacommon.bs.ChestDisplayItem):
 46        from baclassic._chest import (
 47            CHEST_APPEARANCE_DISPLAY_INFOS,
 48            CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT,
 49        )
 50
 51        img = None
 52        show_text = False
 53        c_info = CHEST_APPEARANCE_DISPLAY_INFOS.get(
 54            itemwrapper.item.appearance, CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT
 55        )
 56        c_size = width * 0.85
 57        bauiv1.imagewidget(
 58            parent=parent,
 59            position=(pos[0] - c_size * 0.5, pos[1] - c_size * 0.5),
 60            color=c_info.color,
 61            size=(c_size, c_size),
 62            texture=bauiv1.gettexture(c_info.texclosed),
 63            tint_texture=bauiv1.gettexture(c_info.texclosedtint),
 64            tint_color=c_info.tint,
 65            tint2_color=c_info.tint2,
 66        )
 67
 68    # Enable this for testing spacing.
 69    if bool(False):
 70        bauiv1.imagewidget(
 71            parent=parent,
 72            position=(
 73                pos[0] - width * 0.5,
 74                pos[1] - height * 0.5,
 75            ),
 76            size=(width, height),
 77            texture=bauiv1.gettexture('white'),
 78            color=(0, 1, 0),
 79            opacity=0.1,
 80        )
 81
 82    imgsize = width * 0.33
 83    if img is not None:
 84        bauiv1.imagewidget(
 85            parent=parent,
 86            position=(
 87                pos[0] - imgsize * 0.5,
 88                pos[1] + img_y_offs - imgsize * 0.5,
 89            ),
 90            size=(imgsize, imgsize),
 91            texture=bauiv1.gettexture(img),
 92        )
 93    if show_text:
 94        subs = itemwrapper.description_subs
 95        if subs is None:
 96            subs = []
 97        bauiv1.textwidget(
 98            parent=parent,
 99            position=(pos[0], pos[1] + text_y_offs),
100            scale=width * 0.006,
101            size=(0, 0),
102            text=bauiv1.Lstr(
103                translate=('serverResponses', itemwrapper.description),
104                subs=pairs_from_flat(subs),
105            ),
106            maxwidth=width * 0.9,
107            color=(0.0, 1.0, 0.0),
108            h_align='center',
109            v_align='center',
110        )

Create ui to depict a display-item.