# Released under the MIT License. See LICENSE for details. # """UI functionality for advanced settings.""" from __future__ import annotations from typing import TYPE_CHECKING import ba import ba.internal from bastd.ui import popup as popup_ui if TYPE_CHECKING: from typing import Any class AdvancedSettingsWindow(ba.Window): """Window for editing advanced game settings.""" def __init__( self, transition: str = 'in_right', origin_widget: ba.Widget | None = None, ): # pylint: disable=too-many-statements from ba.internal import master_server_get import threading # Preload some modules we use in a background thread so we won't # have a visual hitch when the user taps them. threading.Thread(target=self._preload_modules).start() app = ba.app # If they provided an origin-widget, scale up from that. scale_origin: tuple[float, float] | None if origin_widget is not None: self._transition_out = 'out_scale' scale_origin = origin_widget.get_screen_space_center() transition = 'in_scale' else: self._transition_out = 'out_right' scale_origin = None uiscale = ba.app.ui.uiscale self._width = 870.0 if uiscale is ba.UIScale.SMALL else 670.0 x_inset = 100 if uiscale is ba.UIScale.SMALL else 0 self._height = ( 390.0 if uiscale is ba.UIScale.SMALL else 450.0 if uiscale is ba.UIScale.MEDIUM else 520.0 ) self._spacing = 32 self._menu_open = False top_extra = 10 if uiscale is ba.UIScale.SMALL else 0 super().__init__( root_widget=ba.containerwidget( size=(self._width, self._height + top_extra), transition=transition, toolbar_visibility='menu_minimal', scale_origin_stack_offset=scale_origin, scale=( 2.06 if uiscale is ba.UIScale.SMALL else 1.4 if uiscale is ba.UIScale.MEDIUM else 1.0 ), stack_offset=(0, -25) if uiscale is ba.UIScale.SMALL else (0, 0), ) ) self._prev_lang = '' self._prev_lang_list: list[str] = [] self._complete_langs_list: list | None = None self._complete_langs_error = False self._language_popup: popup_ui.PopupMenu | None = None # In vr-mode, the internal keyboard is currently the *only* option, # so no need to show this. self._show_always_use_internal_keyboard = ( not app.vr_mode and not app.iircade_mode ) self._scroll_width = self._width - (100 + 2 * x_inset) self._scroll_height = self._height - 115.0 self._sub_width = self._scroll_width * 0.95 self._sub_height = 724.0 if self._show_always_use_internal_keyboard: self._sub_height += 62 self._show_disable_gyro = app.platform in {'ios', 'android'} if self._show_disable_gyro: self._sub_height += 42 self._do_vr_test_button = app.vr_mode self._do_net_test_button = True self._extra_button_spacing = self._spacing * 2.5 if self._do_vr_test_button: self._sub_height += self._extra_button_spacing if self._do_net_test_button: self._sub_height += self._extra_button_spacing self._sub_height += self._spacing * 2.0 # plugins self._r = 'settingsWindowAdvanced' if app.ui.use_toolbars and uiscale is ba.UIScale.SMALL: ba.containerwidget( edit=self._root_widget, on_cancel_call=self._do_back ) self._back_button = None else: self._back_button = ba.buttonwidget( parent=self._root_widget, position=(53 + x_inset, self._height - 60), size=(140, 60), scale=0.8, autoselect=True, label=ba.Lstr(resource='backText'), button_type='back', on_activate_call=self._do_back, ) ba.containerwidget( edit=self._root_widget, cancel_button=self._back_button ) self._title_text = ba.textwidget( parent=self._root_widget, position=(0, self._height - 52), size=(self._width, 25), text=ba.Lstr(resource=f'{self._r}.titleText'), color=app.ui.title_color, h_align='center', v_align='top', ) if self._back_button is not None: ba.buttonwidget( edit=self._back_button, button_type='backSmall', size=(60, 60), label=ba.charstr(ba.SpecialChar.BACK), ) self._scrollwidget = ba.scrollwidget( parent=self._root_widget, position=(50 + x_inset, 50), simple_culling_v=20.0, highlight=False, size=(self._scroll_width, self._scroll_height), selection_loops_to_parent=True, ) ba.widget(edit=self._scrollwidget, right_widget=self._scrollwidget) self._subcontainer = ba.containerwidget( parent=self._scrollwidget, size=(self._sub_width, self._sub_height), background=False, selection_loops_to_parent=True, ) self._rebuild() # Rebuild periodically to pick up language changes/additions/etc. self._rebuild_timer = ba.Timer( 1.0, ba.WeakCall(self._rebuild), repeat=True, timetype=ba.TimeType.REAL, ) # Fetch the list of completed languages. master_server_get( 'bsLangGetCompleted', {'b': app.build_number}, callback=ba.WeakCall(self._completed_langs_cb), ) # noinspection PyUnresolvedReferences @staticmethod def _preload_modules() -> None: """Preload modules we use (called in bg thread).""" from bastd.ui import config as _unused1 from ba import modutils as _unused2 from bastd.ui.settings import vrtesting as _unused3 from bastd.ui.settings import nettesting as _unused4 from bastd.ui import appinvite as _unused5 from bastd.ui import account as _unused6 from bastd.ui import promocode as _unused7 from bastd.ui import debug as _unused8 from bastd.ui.settings import plugins as _unused9 def _update_lang_status(self) -> None: if self._complete_langs_list is not None: up_to_date = ba.app.lang.language in self._complete_langs_list ba.textwidget( edit=self._lang_status_text, text='' if ba.app.lang.language == 'Test' else ba.Lstr( resource=f'{self._r}.translationNoUpdateNeededText' ) if up_to_date else ba.Lstr(resource=f'{self._r}.translationUpdateNeededText'), color=(0.2, 1.0, 0.2, 0.8) if up_to_date else (1.0, 0.2, 0.2, 0.8), ) else: ba.textwidget( edit=self._lang_status_text, text=ba.Lstr(resource=f'{self._r}.translationFetchErrorText') if self._complete_langs_error else ba.Lstr( resource=f'{self._r}.translationFetchingStatusText' ), color=(1.0, 0.5, 0.2) if self._complete_langs_error else (0.7, 0.7, 0.7), ) def _rebuild(self) -> None: # pylint: disable=too-many-statements # pylint: disable=too-many-branches # pylint: disable=too-many-locals from bastd.ui.config import ConfigCheckBox from ba.modutils import show_user_scripts available_languages = ba.app.lang.available_languages # Don't rebuild if the menu is open or if our language and # language-list hasn't changed. # NOTE - although we now support widgets updating their own # translations, we still change the label formatting on the language # menu based on the language so still need this. ...however we could # make this more limited to it only rebuilds that one menu instead # of everything. if self._menu_open or ( self._prev_lang == ba.app.config.get('Lang', None) and self._prev_lang_list == available_languages ): return self._prev_lang = ba.app.config.get('Lang', None) self._prev_lang_list = available_languages # Clear out our sub-container. children = self._subcontainer.get_children() for child in children: child.delete() v = self._sub_height - 35 v -= self._spacing * 1.2 # Update our existing back button and title. if self._back_button is not None: ba.buttonwidget( edit=self._back_button, label=ba.Lstr(resource='backText') ) ba.buttonwidget( edit=self._back_button, label=ba.charstr(ba.SpecialChar.BACK) ) ba.textwidget( edit=self._title_text, text=ba.Lstr(resource=f'{self._r}.titleText') ) this_button_width = 410 self._promo_code_button = ba.buttonwidget( parent=self._subcontainer, position=(self._sub_width / 2 - this_button_width / 2, v - 14), size=(this_button_width, 60), autoselect=True, label=ba.Lstr(resource=f'{self._r}.enterPromoCodeText'), text_scale=1.0, on_activate_call=self._on_promo_code_press, ) if self._back_button is not None: ba.widget( edit=self._promo_code_button, up_widget=self._back_button, left_widget=self._back_button, ) v -= self._extra_button_spacing * 0.8 ba.textwidget( parent=self._subcontainer, position=(200, v + 10), size=(0, 0), text=ba.Lstr(resource=f'{self._r}.languageText'), maxwidth=150, scale=0.95, color=ba.app.ui.title_color, h_align='right', v_align='center', ) languages = ba.app.lang.available_languages cur_lang = ba.app.config.get('Lang', None) if cur_lang is None: cur_lang = 'Auto' # We have a special dict of language names in that language # so we don't have to go digging through each full language. try: import json with open('ba_data/data/langdata.json', encoding='utf-8') as infile: lang_names_translated = json.loads(infile.read())[ 'lang_names_translated' ] except Exception: ba.print_exception('Error reading lang data.') lang_names_translated = {} langs_translated = {} for lang in languages: langs_translated[lang] = lang_names_translated.get(lang, lang) langs_full = {} for lang in languages: lang_translated = ba.Lstr(translate=('languages', lang)).evaluate() if langs_translated[lang] == lang_translated: langs_full[lang] = lang_translated else: langs_full[lang] = ( langs_translated[lang] + ' (' + lang_translated + ')' ) self._language_popup = popup_ui.PopupMenu( parent=self._subcontainer, position=(210, v - 19), width=150, opening_call=ba.WeakCall(self._on_menu_open), closing_call=ba.WeakCall(self._on_menu_close), autoselect=False, on_value_change_call=ba.WeakCall(self._on_menu_choice), choices=['Auto'] + languages, button_size=(250, 60), choices_display=( [ ba.Lstr( value=( ba.Lstr(resource='autoText').evaluate() + ' (' + ba.Lstr( translate=( 'languages', ba.app.lang.default_language, ) ).evaluate() + ')' ) ) ] + [ba.Lstr(value=langs_full[l]) for l in languages] ), current_choice=cur_lang, ) v -= self._spacing * 1.8 ba.textwidget( parent=self._subcontainer, position=(self._sub_width * 0.5, v + 10), size=(0, 0), text=ba.Lstr( resource=f'{self._r}.helpTranslateText', subs=[('${APP_NAME}', ba.Lstr(resource='titleText'))], ), maxwidth=self._sub_width * 0.9, max_height=55, flatness=1.0, scale=0.65, color=(0.4, 0.9, 0.4, 0.8), h_align='center', v_align='center', ) v -= self._spacing * 1.9 this_button_width = 410 self._translation_editor_button = ba.buttonwidget( parent=self._subcontainer, position=(self._sub_width / 2 - this_button_width / 2, v - 24), size=(this_button_width, 60), label=ba.Lstr( resource=f'{self._r}.translationEditorButtonText', subs=[('${APP_NAME}', ba.Lstr(resource='titleText'))], ), autoselect=True, on_activate_call=ba.Call( ba.open_url, 'https://legacy.ballistica.net/translate' ), ) self._lang_status_text = ba.textwidget( parent=self._subcontainer, position=(self._sub_width * 0.5, v - 40), size=(0, 0), text='', flatness=1.0, scale=0.63, h_align='center', v_align='center', maxwidth=400.0, ) self._update_lang_status() v -= 40 lang_inform = ba.internal.get_v1_account_misc_val('langInform', False) self._language_inform_checkbox = cbw = ba.checkboxwidget( parent=self._subcontainer, position=(50, v - 50), size=(self._sub_width - 100, 30), autoselect=True, maxwidth=430, textcolor=(0.8, 0.8, 0.8), value=lang_inform, text=ba.Lstr(resource=f'{self._r}.translationInformMe'), on_value_change_call=ba.WeakCall(self._on_lang_inform_value_change), ) ba.widget( edit=self._translation_editor_button, down_widget=cbw, up_widget=self._language_popup.get_button(), ) v -= self._spacing * 3.0 self._kick_idle_players_check_box = ConfigCheckBox( parent=self._subcontainer, position=(50, v), size=(self._sub_width - 100, 30), configkey='Kick Idle Players', displayname=ba.Lstr(resource=f'{self._r}.kickIdlePlayersText'), scale=1.0, maxwidth=430, ) v -= 42 self._show_game_ping_check_box = ConfigCheckBox( parent=self._subcontainer, position=(50, v), size=(self._sub_width - 100, 30), configkey='Show Ping', displayname=ba.Lstr(resource=f'{self._r}.showInGamePingText'), scale=1.0, maxwidth=430, ) v -= 42 self._disable_camera_shake_check_box = ConfigCheckBox( parent=self._subcontainer, position=(50, v), size=(self._sub_width - 100, 30), configkey='Disable Camera Shake', displayname=ba.Lstr(resource=f'{self._r}.disableCameraShakeText'), scale=1.0, maxwidth=430, ) self._disable_gyro_check_box: ConfigCheckBox | None = None if self._show_disable_gyro: v -= 42 self._disable_gyro_check_box = ConfigCheckBox( parent=self._subcontainer, position=(50, v), size=(self._sub_width - 100, 30), configkey='Disable Camera Gyro', displayname=ba.Lstr( resource=f'{self._r}.disableCameraGyroscopeMotionText' ), scale=1.0, maxwidth=430, ) self._always_use_internal_keyboard_check_box: ConfigCheckBox | None if self._show_always_use_internal_keyboard: v -= 42 self._always_use_internal_keyboard_check_box = ConfigCheckBox( parent=self._subcontainer, position=(50, v), size=(self._sub_width - 100, 30), configkey='Always Use Internal Keyboard', autoselect=True, displayname=ba.Lstr( resource=f'{self._r}.alwaysUseInternalKeyboardText' ), scale=1.0, maxwidth=430, ) ba.textwidget( parent=self._subcontainer, position=(90, v - 10), size=(0, 0), text=ba.Lstr( resource=( f'{self._r}.alwaysUseInternalKeyboardDescriptionText' ) ), maxwidth=400, flatness=1.0, scale=0.65, color=(0.4, 0.9, 0.4, 0.8), h_align='left', v_align='center', ) v -= 20 else: self._always_use_internal_keyboard_check_box = None v -= self._spacing * 2.1 this_button_width = 410 self._modding_guide_button = ba.buttonwidget( parent=self._subcontainer, position=(self._sub_width / 2 - this_button_width / 2, v - 10), size=(this_button_width, 60), autoselect=True, label=ba.Lstr(resource=f'{self._r}.moddingGuideText'), text_scale=1.0, on_activate_call=ba.Call( ba.open_url, 'https://ballistica.net/wiki/modding-guide' ), ) if self._show_always_use_internal_keyboard: assert self._always_use_internal_keyboard_check_box is not None ba.widget( edit=self._always_use_internal_keyboard_check_box.widget, down_widget=self._modding_guide_button, ) ba.widget( edit=self._modding_guide_button, up_widget=self._always_use_internal_keyboard_check_box.widget, ) else: ba.widget( edit=self._modding_guide_button, up_widget=self._kick_idle_players_check_box.widget, ) ba.widget( edit=self._kick_idle_players_check_box.widget, down_widget=self._modding_guide_button, ) v -= self._spacing * 2.0 self._show_user_mods_button = ba.buttonwidget( parent=self._subcontainer, position=(self._sub_width / 2 - this_button_width / 2, v - 10), size=(this_button_width, 60), autoselect=True, label=ba.Lstr(resource=f'{self._r}.showUserModsText'), text_scale=1.0, on_activate_call=show_user_scripts, ) v -= self._spacing * 2.0 self._plugins_button = ba.buttonwidget( parent=self._subcontainer, position=(self._sub_width / 2 - this_button_width / 2, v - 10), size=(this_button_width, 60), autoselect=True, label=ba.Lstr(resource='pluginsText'), text_scale=1.0, on_activate_call=self._on_plugins_button_press, ) v -= self._spacing * 0.6 self._vr_test_button: ba.Widget | None if self._do_vr_test_button: v -= self._extra_button_spacing self._vr_test_button = ba.buttonwidget( parent=self._subcontainer, position=(self._sub_width / 2 - this_button_width / 2, v - 14), size=(this_button_width, 60), autoselect=True, label=ba.Lstr(resource=f'{self._r}.vrTestingText'), text_scale=1.0, on_activate_call=self._on_vr_test_press, ) else: self._vr_test_button = None self._net_test_button: ba.Widget | None if self._do_net_test_button: v -= self._extra_button_spacing self._net_test_button = ba.buttonwidget( parent=self._subcontainer, position=(self._sub_width / 2 - this_button_width / 2, v - 14), size=(this_button_width, 60), autoselect=True, label=ba.Lstr(resource=f'{self._r}.netTestingText'), text_scale=1.0, on_activate_call=self._on_net_test_press, ) else: self._net_test_button = None v -= 70 self._benchmarks_button = ba.buttonwidget( parent=self._subcontainer, position=(self._sub_width / 2 - this_button_width / 2, v - 14), size=(this_button_width, 60), autoselect=True, label=ba.Lstr(resource=f'{self._r}.benchmarksText'), text_scale=1.0, on_activate_call=self._on_benchmark_press, ) for child in self._subcontainer.get_children(): ba.widget(edit=child, show_buffer_bottom=30, show_buffer_top=20) if ba.app.ui.use_toolbars: pbtn = ba.internal.get_special_widget('party_button') ba.widget(edit=self._scrollwidget, right_widget=pbtn) if self._back_button is None: ba.widget( edit=self._scrollwidget, left_widget=ba.internal.get_special_widget('back_button'), ) self._restore_state() def _show_restart_needed(self, value: Any) -> None: del value # Unused. ba.screenmessage( ba.Lstr(resource=f'{self._r}.mustRestartText'), color=(1, 1, 0) ) def _on_lang_inform_value_change(self, val: bool) -> None: ba.internal.add_transaction( {'type': 'SET_MISC_VAL', 'name': 'langInform', 'value': val} ) ba.internal.run_transactions() def _on_vr_test_press(self) -> None: from bastd.ui.settings.vrtesting import VRTestingWindow self._save_state() ba.containerwidget(edit=self._root_widget, transition='out_left') ba.app.ui.set_main_menu_window( VRTestingWindow(transition='in_right').get_root_widget() ) def _on_net_test_press(self) -> None: from bastd.ui.settings.nettesting import NetTestingWindow # Net-testing requires a signed in v1 account. if ba.internal.get_v1_account_state() != 'signed_in': ba.screenmessage( ba.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0) ) ba.playsound(ba.getsound('error')) return self._save_state() ba.containerwidget(edit=self._root_widget, transition='out_left') ba.app.ui.set_main_menu_window( NetTestingWindow(transition='in_right').get_root_widget() ) def _on_friend_promo_code_press(self) -> None: from bastd.ui import appinvite from bastd.ui import account if ba.internal.get_v1_account_state() != 'signed_in': account.show_sign_in_prompt() return appinvite.handle_app_invites_press() def _on_plugins_button_press(self) -> None: from bastd.ui.settings.plugins import PluginWindow self._save_state() ba.containerwidget(edit=self._root_widget, transition='out_left') ba.app.ui.set_main_menu_window( PluginWindow(origin_widget=self._plugins_button).get_root_widget() ) def _on_promo_code_press(self) -> None: from bastd.ui.promocode import PromoCodeWindow from bastd.ui.account import show_sign_in_prompt # We have to be logged in for promo-codes to work. if ba.internal.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return self._save_state() ba.containerwidget(edit=self._root_widget, transition='out_left') ba.app.ui.set_main_menu_window( PromoCodeWindow( origin_widget=self._promo_code_button ).get_root_widget() ) def _on_benchmark_press(self) -> None: from bastd.ui.debug import DebugWindow self._save_state() ba.containerwidget(edit=self._root_widget, transition='out_left') ba.app.ui.set_main_menu_window( DebugWindow(transition='in_right').get_root_widget() ) def _save_state(self) -> None: # pylint: disable=too-many-branches try: sel = self._root_widget.get_selected_child() if sel == self._scrollwidget: sel = self._subcontainer.get_selected_child() if sel == self._vr_test_button: sel_name = 'VRTest' elif sel == self._net_test_button: sel_name = 'NetTest' elif sel == self._promo_code_button: sel_name = 'PromoCode' elif sel == self._benchmarks_button: sel_name = 'Benchmarks' elif sel == self._kick_idle_players_check_box.widget: sel_name = 'KickIdlePlayers' elif sel == self._show_game_ping_check_box.widget: sel_name = 'ShowPing' elif sel == self._disable_camera_shake_check_box.widget: sel_name = 'DisableCameraShake' elif ( self._always_use_internal_keyboard_check_box is not None and sel == self._always_use_internal_keyboard_check_box.widget ): sel_name = 'AlwaysUseInternalKeyboard' elif ( self._disable_gyro_check_box is not None and sel == self._disable_gyro_check_box.widget ): sel_name = 'DisableGyro' elif ( self._language_popup is not None and sel == self._language_popup.get_button() ): sel_name = 'Languages' elif sel == self._translation_editor_button: sel_name = 'TranslationEditor' elif sel == self._show_user_mods_button: sel_name = 'ShowUserMods' elif sel == self._plugins_button: sel_name = 'Plugins' elif sel == self._modding_guide_button: sel_name = 'ModdingGuide' elif sel == self._language_inform_checkbox: sel_name = 'LangInform' else: raise ValueError(f'unrecognized selection \'{sel}\'') elif sel == self._back_button: sel_name = 'Back' else: raise ValueError(f'unrecognized selection \'{sel}\'') ba.app.ui.window_states[type(self)] = {'sel_name': sel_name} except Exception: ba.print_exception(f'Error saving state for {self.__class__}') def _restore_state(self) -> None: # pylint: disable=too-many-branches try: sel_name = ba.app.ui.window_states.get(type(self), {}).get( 'sel_name' ) if sel_name == 'Back': sel = self._back_button else: ba.containerwidget( edit=self._root_widget, selected_child=self._scrollwidget ) if sel_name == 'VRTest': sel = self._vr_test_button elif sel_name == 'NetTest': sel = self._net_test_button elif sel_name == 'PromoCode': sel = self._promo_code_button elif sel_name == 'Benchmarks': sel = self._benchmarks_button elif sel_name == 'KickIdlePlayers': sel = self._kick_idle_players_check_box.widget elif sel_name == 'ShowPing': sel = self._show_game_ping_check_box.widget elif sel_name == 'DisableCameraShake': sel = self._disable_camera_shake_check_box.widget elif ( sel_name == 'AlwaysUseInternalKeyboard' and self._always_use_internal_keyboard_check_box is not None ): sel = self._always_use_internal_keyboard_check_box.widget elif ( sel_name == 'DisableGyro' and self._disable_gyro_check_box is not None ): sel = self._disable_gyro_check_box.widget elif ( sel_name == 'Languages' and self._language_popup is not None ): sel = self._language_popup.get_button() elif sel_name == 'TranslationEditor': sel = self._translation_editor_button elif sel_name == 'ShowUserMods': sel = self._show_user_mods_button elif sel_name == 'Plugins': sel = self._plugins_button elif sel_name == 'ModdingGuide': sel = self._modding_guide_button elif sel_name == 'LangInform': sel = self._language_inform_checkbox else: sel = None if sel is not None: ba.containerwidget( edit=self._subcontainer, selected_child=sel, visible_child=sel, ) except Exception: ba.print_exception(f'Error restoring state for {self.__class__}') def _on_menu_open(self) -> None: self._menu_open = True def _on_menu_close(self) -> None: self._menu_open = False def _on_menu_choice(self, choice: str) -> None: ba.app.lang.setlanguage(None if choice == 'Auto' else choice) self._save_state() ba.timer(0.1, ba.WeakCall(self._rebuild), timetype=ba.TimeType.REAL) def _completed_langs_cb(self, results: dict[str, Any] | None) -> None: if results is not None and results['langs'] is not None: self._complete_langs_list = results['langs'] self._complete_langs_error = False else: self._complete_langs_list = None self._complete_langs_error = True ba.timer( 0.001, ba.WeakCall(self._update_lang_status), timetype=ba.TimeType.REAL, ) def _do_back(self) -> None: from bastd.ui.settings.allsettings import AllSettingsWindow self._save_state() ba.containerwidget( edit=self._root_widget, transition=self._transition_out ) ba.app.ui.set_main_menu_window( AllSettingsWindow(transition='in_left').get_root_widget() )