# Released under the MIT License. See LICENSE for details.
#
"""Contains ClassicAppMode."""
from __future__ import annotations
import os
import logging
from functools import partial
from typing import TYPE_CHECKING, override
from bacommon.app import AppExperience
import babase
import bauiv1
import _baclassic
if TYPE_CHECKING:
from typing import Callable, Any, Literal
from efro.call import CallbackRegistration
import bacommon.cloud
from bauiv1lib.chest import ChestWindow
# ba_meta export babase.AppMode
class ClassicAppMode(babase.AppMode):
"""AppMode for the classic BombSquad experience."""
def __init__(self) -> None:
self._on_primary_account_changed_callback: (
CallbackRegistration | None
) = None
self._test_sub: babase.CloudSubscription | None = None
self._account_data_sub: babase.CloudSubscription | None = None
[docs]
@override
@classmethod
def get_app_experience(cls) -> AppExperience:
return AppExperience.MELEE
@override
@classmethod
def _supports_intent(cls, intent: babase.AppIntent) -> bool:
# We support default and exec intents currently.
return isinstance(
intent, babase.AppIntentExec | babase.AppIntentDefault
)
[docs]
@override
def handle_intent(self, intent: babase.AppIntent) -> None:
if isinstance(intent, babase.AppIntentExec):
_baclassic.classic_app_mode_handle_app_intent_exec(intent.code)
return
assert isinstance(intent, babase.AppIntentDefault)
_baclassic.classic_app_mode_handle_app_intent_default()
[docs]
@override
def on_activate(self) -> None:
# Let the native layer do its thing.
_baclassic.classic_app_mode_activate()
app = babase.app
plus = app.plus
assert plus is not None
# Wire up the root ui to do what we want.
ui = app.ui_v1
ui.root_ui_calls[ui.RootUIElement.ACCOUNT_BUTTON] = (
self._root_ui_account_press
)
ui.root_ui_calls[ui.RootUIElement.MENU_BUTTON] = (
self._root_ui_menu_press
)
ui.root_ui_calls[ui.RootUIElement.SQUAD_BUTTON] = (
self._root_ui_squad_press
)
ui.root_ui_calls[ui.RootUIElement.SETTINGS_BUTTON] = (
self._root_ui_settings_press
)
ui.root_ui_calls[ui.RootUIElement.STORE_BUTTON] = (
self._root_ui_store_press
)
ui.root_ui_calls[ui.RootUIElement.INVENTORY_BUTTON] = (
self._root_ui_inventory_press
)
ui.root_ui_calls[ui.RootUIElement.GET_TOKENS_BUTTON] = (
self._root_ui_get_tokens_press
)
ui.root_ui_calls[ui.RootUIElement.INBOX_BUTTON] = (
self._root_ui_inbox_press
)
ui.root_ui_calls[ui.RootUIElement.TICKETS_METER] = (
self._root_ui_tickets_meter_press
)
ui.root_ui_calls[ui.RootUIElement.TOKENS_METER] = (
self._root_ui_tokens_meter_press
)
ui.root_ui_calls[ui.RootUIElement.TROPHY_METER] = (
self._root_ui_trophy_meter_press
)
ui.root_ui_calls[ui.RootUIElement.LEVEL_METER] = (
self._root_ui_level_meter_press
)
ui.root_ui_calls[ui.RootUIElement.ACHIEVEMENTS_BUTTON] = (
self._root_ui_achievements_press
)
ui.root_ui_calls[ui.RootUIElement.CHEST_SLOT_0] = partial(
self._root_ui_chest_slot_pressed, 0
)
ui.root_ui_calls[ui.RootUIElement.CHEST_SLOT_1] = partial(
self._root_ui_chest_slot_pressed, 1
)
ui.root_ui_calls[ui.RootUIElement.CHEST_SLOT_2] = partial(
self._root_ui_chest_slot_pressed, 2
)
ui.root_ui_calls[ui.RootUIElement.CHEST_SLOT_3] = partial(
self._root_ui_chest_slot_pressed, 3
)
# We want to be informed when primary account changes.
self._on_primary_account_changed_callback = (
plus.accounts.on_primary_account_changed_callbacks.register(
self.update_for_primary_account
)
)
# Establish subscriptions/etc. for any current primary account.
self.update_for_primary_account(plus.accounts.primary)
[docs]
@override
def on_deactivate(self) -> None:
classic = babase.app.classic
# Stop being informed of account changes.
self._on_primary_account_changed_callback = None
# Remove anything following any current account.
self.update_for_primary_account(None)
# Save where we were in the UI so we return there next time.
if classic is not None:
classic.save_ui_state()
# Let the native layer do its thing.
_baclassic.classic_app_mode_deactivate()
[docs]
@override
def on_app_active_changed(self) -> None:
# If we've gone inactive, bring up the main menu, which has the
# side effect of pausing the action (when possible).
if not babase.app.active:
babase.invoke_main_menu()
[docs]
def update_for_primary_account(
self, account: babase.AccountV2Handle | None
) -> None:
"""Update subscriptions/etc. for a new primary account state."""
assert babase.in_logic_thread()
plus = babase.app.plus
assert plus is not None
if account is not None:
babase.set_ui_account_state(True, account.tag)
else:
babase.set_ui_account_state(False)
# For testing subscription functionality.
if os.environ.get('BA_SUBSCRIPTION_TEST') == '1':
if account is None:
self._test_sub = None
else:
with account:
self._test_sub = plus.cloud.subscribe_test(
self._on_sub_test_update
)
else:
self._test_sub = None
if account is None:
self._account_data_sub = None
_baclassic.set_root_ui_values(
tickets_text='-',
tokens_text='-',
league_rank_text='-',
league_type='',
achievements_percent_text='-',
level_text='-',
xp_text='-',
inbox_count_text='',
gold_pass=False,
chest_0_appearance='',
chest_1_appearance='',
chest_2_appearance='',
chest_3_appearance='',
)
else:
with account:
self._account_data_sub = (
plus.cloud.subscribe_classic_account_data(
self._on_classic_account_data_change
)
)
def _on_sub_test_update(self, val: int | None) -> None:
print(f'GOT SUB TEST UPDATE: {val}')
def _on_classic_account_data_change(
self, val: bacommon.cloud.BSClassicAccountLiveData
) -> None:
# print('ACCOUNT CHANGED:', val)
achp = round(val.achievements / max(val.achievements_total, 1) * 100.0)
ibc = str(val.inbox_count)
if val.inbox_count_is_max:
ibc += '+'
chest0 = val.chests.get('0')
chest1 = val.chests.get('1')
chest2 = val.chests.get('2')
chest3 = val.chests.get('3')
_baclassic.set_root_ui_values(
tickets_text=str(val.tickets),
tokens_text=str(val.tokens),
league_rank_text=(
'-' if val.league_rank is None else f'#{val.league_rank}'
),
league_type=(
'' if val.league_type is None else val.league_type.value
),
achievements_percent_text=f'{achp}%',
level_text=str(val.level),
xp_text=f'{val.xp}/{val.xpmax}',
inbox_count_text=ibc,
gold_pass=val.gold_pass,
chest_0_appearance=(
'' if chest0 is None else chest0.appearance.value
),
chest_1_appearance=(
'' if chest1 is None else chest1.appearance.value
),
chest_2_appearance=(
'' if chest2 is None else chest2.appearance.value
),
chest_3_appearance=(
'' if chest3 is None else chest3.appearance.value
),
)
def _root_ui_menu_press(self) -> None:
from babase import push_back_press
ui = babase.app.ui_v1
# If *any* main-window is up, kill it and resume play.
old_window = ui.get_main_window()
if old_window is not None:
classic = babase.app.classic
assert classic is not None
classic.resume()
ui.clear_main_window()
return
push_back_press()
def _root_ui_account_press(self) -> None:
from bauiv1lib.account.settings import AccountSettingsWindow
self._auxiliary_window_nav(
win_type=AccountSettingsWindow,
win_create_call=lambda: AccountSettingsWindow(
origin_widget=bauiv1.get_special_widget('account_button')
),
)
def _root_ui_squad_press(self) -> None:
btn = bauiv1.get_special_widget('squad_button')
center = btn.get_screen_space_center()
if bauiv1.app.classic is not None:
bauiv1.app.classic.party_icon_activate(center)
else:
logging.warning('party_icon_activate: no classic.')
def _root_ui_settings_press(self) -> None:
from bauiv1lib.settings.allsettings import AllSettingsWindow
self._auxiliary_window_nav(
win_type=AllSettingsWindow,
win_create_call=lambda: AllSettingsWindow(
origin_widget=bauiv1.get_special_widget('settings_button')
),
)
def _auxiliary_window_nav(
self,
win_type: type[bauiv1.MainWindow],
win_create_call: Callable[[], bauiv1.MainWindow],
) -> None:
"""Navigate to or away from an Auxiliary window.
Auxiliary windows can be thought of as 'side quests' in the
window hierarchy; places such as settings windows or league
ranking windows that the user might want to visit without losing
their place in the regular hierarchy.
"""
# pylint: disable=unidiomatic-typecheck
ui = babase.app.ui_v1
current_main_window = ui.get_main_window()
# Scan our ancestors for auxiliary states matching our type as
# well as auxiliary states in general.
aux_matching_state: bauiv1.MainWindowState | None = None
aux_state: bauiv1.MainWindowState | None = None
if current_main_window is None:
raise RuntimeError(
'Not currently handling no-top-level-window case.'
)
state = current_main_window.main_window_back_state
while state is not None:
assert state.window_type is not None
if state.is_auxiliary:
if state.window_type is win_type:
aux_matching_state = state
else:
aux_state = state
state = state.parent
# If there's an ancestor auxiliary window-state matching our
# type, back out past it (example: poking settings, navigating
# down a level or two, and then poking settings again should
# back out of settings).
if aux_matching_state is not None:
current_main_window.main_window_back_state = (
aux_matching_state.parent
)
current_main_window.main_window_back()
return
# If there's an ancestory auxiliary state *not* matching our
# type, crop the state and swap in our new auxiliary UI
# (example: poking settings, then poking account, then poking
# back should end up where things were before the settings
# poke).
if aux_state is not None:
# Blow away the window stack and build a fresh one.
ui.clear_main_window()
ui.set_main_window(
win_create_call(),
from_window=False, # Disable from-check.
back_state=aux_state.parent,
suppress_warning=True,
is_auxiliary=True,
)
return
# Ok, no auxiliary states found. Now if current window is
# auxiliary and the type matches, simply do a back.
if (
current_main_window.main_window_is_auxiliary
and type(current_main_window) is win_type
):
current_main_window.main_window_back()
return
# If current window is auxiliary but type doesn't match,
# swap it out for our new auxiliary UI.
if current_main_window.main_window_is_auxiliary:
ui.clear_main_window()
ui.set_main_window(
win_create_call(),
from_window=False, # Disable from-check.
back_state=current_main_window.main_window_back_state,
suppress_warning=True,
is_auxiliary=True,
)
return
# Ok, no existing auxiliary stuff was found period. Just
# navigate forward to this UI.
current_main_window.main_window_replace(
win_create_call(), is_auxiliary=True
)
def _root_ui_achievements_press(self) -> None:
from bauiv1lib.achievements import AchievementsWindow
self._auxiliary_window_nav(
win_type=AchievementsWindow,
win_create_call=lambda: AchievementsWindow(
origin_widget=bauiv1.get_special_widget('achievements_button')
),
)
def _root_ui_inbox_press(self) -> None:
from bauiv1lib.connectivity import wait_for_connectivity
from bauiv1lib.inbox import InboxWindow
wait_for_connectivity(
on_connected=lambda: self._auxiliary_window_nav(
win_type=InboxWindow,
win_create_call=lambda: InboxWindow(
origin_widget=bauiv1.get_special_widget('inbox_button')
),
)
)
def _root_ui_store_press(self) -> None:
from bauiv1lib.store.browser import StoreBrowserWindow
self._auxiliary_window_nav(
win_type=StoreBrowserWindow,
win_create_call=lambda: StoreBrowserWindow(
origin_widget=bauiv1.get_special_widget('store_button')
),
)
def _root_ui_tickets_meter_press(self) -> None:
from bauiv1lib.resourcetypeinfo import ResourceTypeInfoWindow
ResourceTypeInfoWindow(
'tickets', origin_widget=bauiv1.get_special_widget('tickets_meter')
)
def _root_ui_tokens_meter_press(self) -> None:
from bauiv1lib.resourcetypeinfo import ResourceTypeInfoWindow
ResourceTypeInfoWindow(
'tokens', origin_widget=bauiv1.get_special_widget('tokens_meter')
)
def _root_ui_trophy_meter_press(self) -> None:
from bauiv1lib.account import show_sign_in_prompt
from bauiv1lib.league.rankwindow import LeagueRankWindow
plus = bauiv1.app.plus
assert plus is not None
if plus.get_v1_account_state() != 'signed_in':
show_sign_in_prompt()
return
self._auxiliary_window_nav(
win_type=LeagueRankWindow,
win_create_call=lambda: LeagueRankWindow(
origin_widget=bauiv1.get_special_widget('trophy_meter')
),
)
def _root_ui_level_meter_press(self) -> None:
from bauiv1lib.resourcetypeinfo import ResourceTypeInfoWindow
ResourceTypeInfoWindow(
'xp', origin_widget=bauiv1.get_special_widget('level_meter')
)
def _root_ui_inventory_press(self) -> None:
from bauiv1lib.inventory import InventoryWindow
self._auxiliary_window_nav(
win_type=InventoryWindow,
win_create_call=lambda: InventoryWindow(
origin_widget=bauiv1.get_special_widget('inventory_button')
),
)
def _root_ui_get_tokens_press(self) -> None:
from bauiv1lib.gettokens import GetTokensWindow
self._auxiliary_window_nav(
win_type=GetTokensWindow,
win_create_call=lambda: GetTokensWindow(
origin_widget=bauiv1.get_special_widget('get_tokens_button')
),
)
def _root_ui_chest_slot_pressed(self, index: int) -> None:
from bauiv1lib.chest import (
ChestWindow0,
ChestWindow1,
ChestWindow2,
ChestWindow3,
)
from bauiv1lib.connectivity import wait_for_connectivity
widgetid: Literal[
'chest_0_button',
'chest_1_button',
'chest_2_button',
'chest_3_button',
]
winclass: type[ChestWindow]
if index == 0:
widgetid = 'chest_0_button'
winclass = ChestWindow0
elif index == 1:
widgetid = 'chest_1_button'
winclass = ChestWindow1
elif index == 2:
widgetid = 'chest_2_button'
winclass = ChestWindow2
elif index == 3:
widgetid = 'chest_3_button'
winclass = ChestWindow3
else:
raise RuntimeError(f'Invalid index {index}')
wait_for_connectivity(
on_connected=lambda: self._auxiliary_window_nav(
win_type=winclass,
win_create_call=lambda: winclass(
index=index,
origin_widget=bauiv1.get_special_widget(widgetid),
),
)
)