Source code for bauiv1lib.ingamemenu

# Released under the MIT License. See LICENSE for details.
#
"""Implements the in-gmae menu window."""

from __future__ import annotations

from typing import TYPE_CHECKING, override
import logging

import bauiv1 as bui
import bascenev1 as bs

if TYPE_CHECKING:
    from typing import Any, Callable


[docs] class InGameMenuWindow(bui.MainWindow): """The menu that can be invoked while in a game.""" def __init__( self, transition: str | None = 'in_right', origin_widget: bui.Widget | None = None, ): # Make a vanilla container; we'll modify it to our needs in # refresh. super().__init__( root_widget=bui.containerwidget( toolbar_visibility=('menu_in_game') ), transition=transition, origin_widget=origin_widget, ) # Grab this stuff in case it changes. self._is_demo = bui.app.env.demo self._is_arcade = bui.app.env.arcade self._p_index = 0 self._use_autoselect = True self._button_width = 200.0 self._button_height = 45.0 self._width = 100.0 self._height = 100.0 self._refresh()
[docs] @override def get_main_window_state(self) -> bui.MainWindowState: cls = type(self) return bui.BasicMainWindowState( create_call=lambda transition, origin_widget: cls( transition=transition, origin_widget=origin_widget ) )
def _refresh(self) -> None: # Clear everything that was there. children = self._root_widget.get_children() for child in children: child.delete() self._r = 'mainMenu' self._input_device = input_device = bs.get_ui_input_device() # Are we connected to a local player? self._input_player = input_device.player if input_device else None # Are we connected to a remote player?. self._connected_to_remote_player = ( input_device.is_attached_to_player() if (input_device and self._input_player is None) else False ) positions: list[tuple[float, float, float]] = [] self._p_index = 0 self._refresh_in_game(positions) h, v, scale = positions[self._p_index] self._p_index += 1 # If we're in a replay, we have a 'Leave Replay' button. if bs.is_in_replay(): bui.buttonwidget( parent=self._root_widget, position=(h - self._button_width * 0.5 * scale, v), scale=scale, size=(self._button_width, self._button_height), autoselect=self._use_autoselect, label=bui.Lstr(resource='replayEndText'), on_activate_call=self._confirm_end_replay, ) elif bs.get_foreground_host_session() is not None: bui.buttonwidget( parent=self._root_widget, position=(h - self._button_width * 0.5 * scale, v), scale=scale, size=(self._button_width, self._button_height), autoselect=self._use_autoselect, label=bui.Lstr( resource=self._r + ( '.endTestText' if self._is_benchmark() else '.endGameText' ) ), on_activate_call=( self._confirm_end_test if self._is_benchmark() else self._confirm_end_game ), ) else: # Assume we're in a client-session. bui.buttonwidget( parent=self._root_widget, position=(h - self._button_width * 0.5 * scale, v), scale=scale, size=(self._button_width, self._button_height), autoselect=self._use_autoselect, label=bui.Lstr(resource=f'{self._r}.leavePartyText'), on_activate_call=self._confirm_leave_party, ) # Add speed-up/slow-down buttons for replays. Ideally this # should be part of a fading-out playback bar like most media # players but this works for now. if bs.is_in_replay(): b_size = 50.0 b_buffer_1 = 50.0 b_buffer_2 = 10.0 t_scale = 0.75 assert bui.app.classic is not None uiscale = bui.app.ui_v1.uiscale if uiscale is bui.UIScale.SMALL: b_size *= 0.6 b_buffer_1 *= 0.8 b_buffer_2 *= 1.0 v_offs = -40 t_scale = 0.5 elif uiscale is bui.UIScale.MEDIUM: v_offs = -70 else: v_offs = -100 self._replay_speed_text = bui.textwidget( parent=self._root_widget, text=bui.Lstr( resource='watchWindow.playbackSpeedText', subs=[('${SPEED}', str(1.23))], ), position=(h, v + v_offs + 15 * t_scale), h_align='center', v_align='center', size=(0, 0), scale=t_scale, ) # Update to current value. self._change_replay_speed(0) # Keep updating in a timer in case it gets changed elsewhere. self._change_replay_speed_timer = bui.AppTimer( 0.25, bui.WeakCall(self._change_replay_speed, 0), repeat=True ) btn = bui.buttonwidget( parent=self._root_widget, position=( h - b_size - b_buffer_1, v - b_size - b_buffer_2 + v_offs, ), button_type='square', size=(b_size, b_size), label='', autoselect=True, on_activate_call=bui.Call(self._change_replay_speed, -1), ) bui.textwidget( parent=self._root_widget, draw_controller=btn, text='-', position=( h - b_size * 0.5 - b_buffer_1, v - b_size * 0.5 - b_buffer_2 + 5 * t_scale + v_offs, ), h_align='center', v_align='center', size=(0, 0), scale=3.0 * t_scale, ) btn = bui.buttonwidget( parent=self._root_widget, position=(h + b_buffer_1, v - b_size - b_buffer_2 + v_offs), button_type='square', size=(b_size, b_size), label='', autoselect=True, on_activate_call=bui.Call(self._change_replay_speed, 1), ) bui.textwidget( parent=self._root_widget, draw_controller=btn, text='+', position=( h + b_size * 0.5 + b_buffer_1, v - b_size * 0.5 - b_buffer_2 + 5 * t_scale + v_offs, ), h_align='center', v_align='center', size=(0, 0), scale=3.0 * t_scale, ) self._pause_resume_button = btn = bui.buttonwidget( parent=self._root_widget, position=(h - b_size * 0.5, v - b_size - b_buffer_2 + v_offs), button_type='square', size=(b_size, b_size), label=bui.charstr( bui.SpecialChar.PLAY_BUTTON if bs.is_replay_paused() else bui.SpecialChar.PAUSE_BUTTON ), autoselect=True, on_activate_call=bui.Call(self._pause_or_resume_replay), ) btn = bui.buttonwidget( parent=self._root_widget, position=( h - b_size * 1.5 - b_buffer_1 * 2, v - b_size - b_buffer_2 + v_offs, ), button_type='square', size=(b_size, b_size), label='', autoselect=True, on_activate_call=bui.WeakCall(self._rewind_replay), ) bui.textwidget( parent=self._root_widget, draw_controller=btn, # text='<<', text=bui.charstr(bui.SpecialChar.REWIND_BUTTON), position=( h - b_size - b_buffer_1 * 2, v - b_size * 0.5 - b_buffer_2 + 5 * t_scale + v_offs, ), h_align='center', v_align='center', size=(0, 0), scale=2.0 * t_scale, ) btn = bui.buttonwidget( parent=self._root_widget, position=( h + b_size * 0.5 + b_buffer_1 * 2, v - b_size - b_buffer_2 + v_offs, ), button_type='square', size=(b_size, b_size), label='', autoselect=True, on_activate_call=bui.WeakCall(self._forward_replay), ) bui.textwidget( parent=self._root_widget, draw_controller=btn, # text='>>', text=bui.charstr(bui.SpecialChar.FAST_FORWARD_BUTTON), position=( h + b_size + b_buffer_1 * 2, v - b_size * 0.5 - b_buffer_2 + 5 * t_scale + v_offs, ), h_align='center', v_align='center', size=(0, 0), scale=2.0 * t_scale, ) def _rewind_replay(self) -> None: bs.seek_replay(-2 * pow(2, bs.get_replay_speed_exponent())) def _forward_replay(self) -> None: bs.seek_replay(2 * pow(2, bs.get_replay_speed_exponent())) def _refresh_in_game( self, positions: list[tuple[float, float, float]] ) -> tuple[float, float, float]: # pylint: disable=too-many-branches # pylint: disable=too-many-locals # pylint: disable=too-many-statements assert bui.app.classic is not None custom_menu_entries: list[dict[str, Any]] = [] session = bs.get_foreground_host_session() if session is not None: try: custom_menu_entries = session.get_custom_menu_entries() for cme in custom_menu_entries: cme_any: Any = cme # Type check may not hold true. if ( not isinstance(cme_any, dict) or 'label' not in cme or not isinstance(cme['label'], (str, bui.Lstr)) or 'call' not in cme or not callable(cme['call']) ): raise ValueError( 'invalid custom menu entry: ' + str(cme) ) except Exception: custom_menu_entries = [] logging.exception( 'Error getting custom menu entries for %s.', session ) self._width = 250.0 self._height = 250.0 if self._input_player else 180.0 if (self._is_demo or self._is_arcade) and self._input_player: self._height -= 40 # if not self._have_settings_button: self._height -= 50 if self._connected_to_remote_player: # In this case we have a leave *and* a disconnect button. self._height += 50 self._height += 50 * (len(custom_menu_entries)) uiscale = bui.app.ui_v1.uiscale bui.containerwidget( edit=self._root_widget, size=(self._width, self._height), scale=( 2.15 if uiscale is bui.UIScale.SMALL else 1.6 if uiscale is bui.UIScale.MEDIUM else 1.0 ), ) h = 125.0 v = self._height - 80.0 if self._input_player else self._height - 60 h_offset = 0 d_h_offset = 0 v_offset = -50 for _i in range(6 + len(custom_menu_entries)): positions.append((h, v, 1.0)) v += v_offset h += h_offset h_offset += d_h_offset # Player name if applicable. if self._input_player: player_name = self._input_player.getname() h, v, scale = positions[self._p_index] v += 35 bui.textwidget( parent=self._root_widget, position=(h - self._button_width / 2, v), size=(self._button_width, self._button_height), color=(1, 1, 1, 0.5), scale=0.7, h_align='center', text=bui.Lstr(value=player_name), ) else: player_name = '' h, v, scale = positions[self._p_index] self._p_index += 1 btn = bui.buttonwidget( parent=self._root_widget, position=(h - self._button_width / 2, v), size=(self._button_width, self._button_height), scale=scale, label=bui.Lstr(resource=f'{self._r}.resumeText'), autoselect=self._use_autoselect, on_activate_call=self._resume, ) bui.containerwidget(edit=self._root_widget, cancel_button=btn) # Add any custom options defined by the current game. for entry in custom_menu_entries: h, v, scale = positions[self._p_index] self._p_index += 1 # Ask the entry whether we should resume when we call # it (defaults to true). resume = bool(entry.get('resume_on_call', True)) if resume: call = bui.Call(self._resume_and_call, entry['call']) else: call = bui.Call(entry['call'], bui.WeakCall(self._resume)) bui.buttonwidget( parent=self._root_widget, position=(h - self._button_width / 2, v), size=(self._button_width, self._button_height), scale=scale, on_activate_call=call, label=entry['label'], autoselect=self._use_autoselect, ) # Add a 'leave' button if the menu-owner has a player. if (self._input_player or self._connected_to_remote_player) and not ( self._is_demo or self._is_arcade ): h, v, scale = positions[self._p_index] self._p_index += 1 btn = bui.buttonwidget( parent=self._root_widget, position=(h - self._button_width / 2, v), size=(self._button_width, self._button_height), scale=scale, on_activate_call=self._leave, label='', autoselect=self._use_autoselect, ) if ( player_name != '' and player_name[0] != '<' and player_name[-1] != '>' ): txt = bui.Lstr( resource=f'{self._r}.justPlayerText', subs=[('${NAME}', player_name)], ) else: txt = bui.Lstr(value=player_name) bui.textwidget( parent=self._root_widget, position=( h, v + self._button_height * (0.64 if player_name != '' else 0.5), ), size=(0, 0), text=bui.Lstr(resource=f'{self._r}.leaveGameText'), scale=(0.83 if player_name != '' else 1.0), color=(0.75, 1.0, 0.7), h_align='center', v_align='center', draw_controller=btn, maxwidth=self._button_width * 0.9, ) bui.textwidget( parent=self._root_widget, position=(h, v + self._button_height * 0.27), size=(0, 0), text=txt, color=(0.75, 1.0, 0.7), h_align='center', v_align='center', draw_controller=btn, scale=0.45, maxwidth=self._button_width * 0.9, ) return h, v, scale def _change_replay_speed(self, offs: int) -> None: if not self._replay_speed_text: if bui.do_once(): print('_change_replay_speed called without widget') return bs.set_replay_speed_exponent(bs.get_replay_speed_exponent() + offs) actual_speed = pow(2.0, bs.get_replay_speed_exponent()) bui.textwidget( edit=self._replay_speed_text, text=bui.Lstr( resource='watchWindow.playbackSpeedText', subs=[('${SPEED}', str(actual_speed))], ), ) def _pause_or_resume_replay(self) -> None: if bs.is_replay_paused(): bs.resume_replay() bui.buttonwidget( edit=self._pause_resume_button, label=bui.charstr(bui.SpecialChar.PAUSE_BUTTON), ) else: bs.pause_replay() bui.buttonwidget( edit=self._pause_resume_button, label=bui.charstr(bui.SpecialChar.PLAY_BUTTON), ) def _is_benchmark(self) -> bool: session = bs.get_foreground_host_session() return getattr(session, 'benchmark_type', None) == 'cpu' or ( bui.app.classic is not None and bui.app.classic.stress_test_update_timer is not None ) def _confirm_end_game(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.confirm import ConfirmWindow # FIXME: Currently we crash calling this on client-sessions. # Select cancel by default; this occasionally gets called by accident # in a fit of button mashing and this will help reduce damage. ConfirmWindow( bui.Lstr(resource=f'{self._r}.exitToMenuText'), self._end_game, cancel_is_selected=True, ) def _confirm_end_test(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.confirm import ConfirmWindow # Select cancel by default; this occasionally gets called by accident # in a fit of button mashing and this will help reduce damage. ConfirmWindow( bui.Lstr(resource=f'{self._r}.exitToMenuText'), self._end_game, cancel_is_selected=True, ) def _confirm_end_replay(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.confirm import ConfirmWindow # Select cancel by default; this occasionally gets called by accident # in a fit of button mashing and this will help reduce damage. ConfirmWindow( bui.Lstr(resource=f'{self._r}.exitToMenuText'), self._end_game, cancel_is_selected=True, ) def _confirm_leave_party(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.confirm import ConfirmWindow # Select cancel by default; this occasionally gets called by accident # in a fit of button mashing and this will help reduce damage. ConfirmWindow( bui.Lstr(resource=f'{self._r}.leavePartyConfirmText'), self._leave_party, cancel_is_selected=True, ) def _leave_party(self) -> None: bs.disconnect_from_host() def _end_game(self) -> None: assert bui.app.classic is not None # no-op if our underlying widget is dead or on its way out. if not self._root_widget or self._root_widget.transitioning_out: return bui.containerwidget(edit=self._root_widget, transition='out_left') bui.app.classic.return_to_main_menu_session_gracefully(reset_ui=False) def _leave(self) -> None: if self._input_player: self._input_player.remove_from_game() elif self._connected_to_remote_player: if self._input_device: self._input_device.detach_from_player() self._resume() def _resume_and_call(self, call: Callable[[], Any]) -> None: self._resume() call() def _resume(self) -> None: classic = bui.app.classic assert classic is not None classic.resume() bui.app.ui_v1.clear_main_window() # If there's callbacks waiting for us to resume, call them. for call in classic.main_menu_resume_callbacks: try: call() except Exception: logging.exception('Error in classic resume callback.') classic.main_menu_resume_callbacks.clear()
# def __del__(self) -> None: # self._resume() # Docs-generation hack; import some stuff that we likely only forward-declared # in our actual source code so that docs tools can find it. from typing import (Coroutine, Any, Literal, Callable, Generator, Awaitable, Sequence, Self) import asyncio from concurrent.futures import Future from pathlib import Path from enum import Enum