Source code for bauiv1lib.party

# Released under the MIT License. See LICENSE for details.
#
"""Provides party related UI."""

from __future__ import annotations

import math
import logging
from typing import TYPE_CHECKING, cast

import bauiv1 as bui
import bascenev1 as bs
from bauiv1lib.popup import PopupMenuWindow

if TYPE_CHECKING:
    from typing import Sequence, Any

    from bauiv1lib.popup import PopupWindow


[docs] class PartyWindow(bui.Window): """Party list/chat window.""" def __del__(self) -> None: bui.set_party_window_open(False) def __init__(self, origin: Sequence[float] = (0, 0)): bui.set_party_window_open(True) self._r = 'partyWindow' self._popup_type: str | None = None self._popup_party_member_client_id: int | None = None self._popup_party_member_is_host: bool | None = None self._width = 500 assert bui.app.classic is not None uiscale = bui.app.ui_v1.uiscale self._height = ( 365 if uiscale is bui.UIScale.SMALL else 480 if uiscale is bui.UIScale.MEDIUM else 600 ) self._display_old_msgs = True super().__init__( root_widget=bui.containerwidget( size=(self._width, self._height), transition='in_scale', color=(0.40, 0.55, 0.20), parent=bui.get_special_widget('overlay_stack'), on_outside_click_call=self.close_with_sound, scale_origin_stack_offset=origin, scale=( 1.6 if uiscale is bui.UIScale.SMALL else 1.3 if uiscale is bui.UIScale.MEDIUM else 0.9 ), stack_offset=( (200, -10) if uiscale is bui.UIScale.SMALL else ( (260, 0) if uiscale is bui.UIScale.MEDIUM else (370, 60) ) ), ) ) self._cancel_button = bui.buttonwidget( parent=self._root_widget, scale=0.7, position=(30, self._height - 47), size=(50, 50), label='', on_activate_call=self.close, autoselect=True, color=(0.45, 0.63, 0.15), icon=bui.gettexture('crossOut'), iconscale=1.2, ) bui.containerwidget( edit=self._root_widget, cancel_button=self._cancel_button ) self._menu_button = bui.buttonwidget( parent=self._root_widget, scale=0.7, position=(self._width - 60, self._height - 47), size=(50, 50), label='...', autoselect=True, button_type='square', on_activate_call=bui.WeakCall(self._on_menu_button_press), color=(0.55, 0.73, 0.25), iconscale=1.2, ) info = bs.get_connection_to_host_info_2() if info is not None and info.name != '': title = bui.Lstr(value=info.name) else: title = bui.Lstr(resource=f'{self._r}.titleText') self._title_text = bui.textwidget( parent=self._root_widget, scale=0.9, color=(0.5, 0.7, 0.5), text=title, size=(0, 0), position=(self._width * 0.5, self._height - 29), maxwidth=self._width * 0.7, h_align='center', v_align='center', ) self._empty_str = bui.textwidget( parent=self._root_widget, scale=0.75, size=(0, 0), position=(self._width * 0.5, self._height - 65), maxwidth=self._width * 0.85, h_align='center', v_align='center', ) self._scroll_width = self._width - 50 self._scrollwidget = bui.scrollwidget( parent=self._root_widget, size=(self._scroll_width, self._height - 200), position=(30, 80), color=(0.4, 0.6, 0.3), ) self._columnwidget = bui.columnwidget( parent=self._scrollwidget, border=2, left_border=-200, margin=0 ) bui.widget(edit=self._menu_button, down_widget=self._columnwidget) self._muted_text = bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height * 0.5), size=(0, 0), h_align='center', v_align='center', text=bui.Lstr(resource='chatMutedText'), ) self._chat_texts: list[bui.Widget] = [] self._text_field = txt = bui.textwidget( parent=self._root_widget, editable=True, size=(530, 40), position=(44, 39), text='', maxwidth=494, shadow=0.3, flatness=1.0, description=bui.Lstr(resource=f'{self._r}.chatMessageText'), autoselect=True, v_align='center', corner_scale=0.7, ) bui.widget( edit=self._scrollwidget, autoselect=True, left_widget=self._cancel_button, up_widget=self._cancel_button, down_widget=self._text_field, ) bui.widget( edit=self._columnwidget, autoselect=True, up_widget=self._cancel_button, down_widget=self._text_field, ) bui.containerwidget(edit=self._root_widget, selected_child=txt) btn = bui.buttonwidget( parent=self._root_widget, size=(50, 35), label=bui.Lstr(resource=f'{self._r}.sendText'), button_type='square', autoselect=True, position=(self._width - 70, 35), on_activate_call=self._send_chat_message, ) bui.textwidget(edit=txt, on_return_press_call=btn.activate) self._name_widgets: list[bui.Widget] = [] self._roster: list[dict[str, Any]] | None = None self._update_timer = bui.AppTimer( 1.0, bui.WeakCall(self._update), repeat=True ) self._update()
[docs] def on_chat_message(self, msg: str) -> None: """Called when a new chat message comes through.""" if not bui.app.config.resolve('Chat Muted'): self._add_msg(msg)
def _add_msg(self, msg: str) -> None: txt = bui.textwidget( parent=self._columnwidget, h_align='left', v_align='center', scale=0.55, size=(900, 13), text=msg, autoselect=True, maxwidth=self._scroll_width * 0.94, shadow=0.3, flatness=1.0, on_activate_call=bui.Call(self._copy_msg, msg), selectable=True, ) self._chat_texts.append(txt) while len(self._chat_texts) > 40: self._chat_texts.pop(0).delete() bui.containerwidget(edit=self._columnwidget, visible_child=txt) def _copy_msg(self, msg: str) -> None: if bui.clipboard_is_supported(): bui.clipboard_set_text(msg) bui.screenmessage( bui.Lstr(resource='copyConfirmText'), color=(0, 1, 0) ) def _on_menu_button_press(self) -> None: is_muted = bui.app.config.resolve('Chat Muted') assert bui.app.classic is not None uiscale = bui.app.ui_v1.uiscale choices: list[str] = ['unmute' if is_muted else 'mute'] choices_display: list[bui.Lstr] = [ bui.Lstr(resource='chatUnMuteText' if is_muted else 'chatMuteText') ] # Allow the 'Add to Favorites' option only if we're actually # connected to a party and if it doesn't seem to be a private # party (those are dynamically assigned addresses and ports so # it makes no sense to save them). server_info = bs.get_connection_to_host_info_2() if server_info is not None and not server_info.name.startswith( 'Private Party ' ): choices.append('add_to_favorites') choices_display.append(bui.Lstr(resource='addToFavoritesText')) PopupMenuWindow( position=self._menu_button.get_screen_space_center(), scale=( 2.3 if uiscale is bui.UIScale.SMALL else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23 ), choices=choices, choices_display=choices_display, current_choice='unmute' if is_muted else 'mute', delegate=self, ) self._popup_type = 'menu' def _update(self) -> None: # pylint: disable=too-many-locals # pylint: disable=too-many-branches # pylint: disable=too-many-statements # pylint: disable=too-many-nested-blocks # update muted state if bui.app.config.resolve('Chat Muted'): bui.textwidget(edit=self._muted_text, color=(1, 1, 1, 0.3)) # clear any chat texts we're showing if self._chat_texts: while self._chat_texts: first = self._chat_texts.pop() first.delete() else: bui.textwidget(edit=self._muted_text, color=(1, 1, 1, 0.0)) # add all existing messages if chat is not muted if self._display_old_msgs: msgs = bs.get_chat_messages() for msg in msgs: self._add_msg(msg) self._display_old_msgs = False # update roster section roster = bs.get_game_roster() if roster != self._roster: self._roster = roster # clear out old for widget in self._name_widgets: widget.delete() self._name_widgets = [] if not self._roster: top_section_height = 60 bui.textwidget( edit=self._empty_str, text=bui.Lstr(resource=f'{self._r}.emptyText'), ) bui.scrollwidget( edit=self._scrollwidget, size=( self._width - 50, self._height - top_section_height - 110, ), position=(30, 80), ) else: columns = ( 1 if len(self._roster) == 1 else 2 if len(self._roster) == 2 else 3 ) rows = int(math.ceil(float(len(self._roster)) / columns)) c_width = (self._width * 0.9) / max(3, columns) c_width_total = c_width * columns c_height = 24 c_height_total = c_height * rows for y in range(rows): for x in range(columns): index = y * columns + x if index < len(self._roster): t_scale = 0.65 pos = ( self._width * 0.53 - c_width_total * 0.5 + c_width * x - 23, self._height - 65 - c_height * y - 15, ) # if there are players present for this client, use # their names as a display string instead of the # client spec-string try: if self._roster[index]['players']: # if there's just one, use the full name; # otherwise combine short names if len(self._roster[index]['players']) == 1: p_str = self._roster[index]['players'][ 0 ]['name_full'] else: p_str = '/'.join( [ entry['name'] for entry in self._roster[ index ]['players'] ] ) if len(p_str) > 25: p_str = p_str[:25] + '...' else: p_str = self._roster[index][ 'display_string' ] except Exception: logging.exception( 'Error calcing client name str.' ) p_str = '???' widget = bui.textwidget( parent=self._root_widget, position=(pos[0], pos[1]), scale=t_scale, size=(c_width * 0.85, 30), maxwidth=c_width * 0.85, color=(1, 1, 1) if index == 0 else (1, 1, 1), selectable=True, autoselect=True, click_activate=True, text=bui.Lstr(value=p_str), h_align='left', v_align='center', ) self._name_widgets.append(widget) # in newer versions client_id will be present and # we can use that to determine who the host is. # in older versions we assume the first client is # host if self._roster[index]['client_id'] is not None: is_host = self._roster[index]['client_id'] == -1 else: is_host = index == 0 # FIXME: Should pass client_id to these sort of # calls; not spec-string (perhaps should wait till # client_id is more readily available though). bui.textwidget( edit=widget, on_activate_call=bui.Call( self._on_party_member_press, self._roster[index]['client_id'], is_host, widget, ), ) pos = ( self._width * 0.53 - c_width_total * 0.5 + c_width * x, self._height - 65 - c_height * y, ) # Make the assumption that the first roster # entry is the server. # FIXME: Shouldn't do this. if is_host: twd = min( c_width * 0.85, bui.get_string_width( p_str, suppress_warning=True ) * t_scale, ) self._name_widgets.append( bui.textwidget( parent=self._root_widget, position=( pos[0] + twd + 1, pos[1] - 0.5, ), size=(0, 0), h_align='left', v_align='center', maxwidth=c_width * 0.96 - twd, color=(0.1, 1, 0.1, 0.5), text=bui.Lstr( resource=f'{self._r}.hostText' ), scale=0.4, shadow=0.1, flatness=1.0, ) ) bui.textwidget(edit=self._empty_str, text='') bui.scrollwidget( edit=self._scrollwidget, size=( self._width - 50, max(100, self._height - 139 - c_height_total), ), position=(30, 80), )
[docs] def popup_menu_selected_choice( self, popup_window: PopupMenuWindow, choice: str ) -> None: """Called when a choice is selected in the popup.""" del popup_window # unused if self._popup_type == 'partyMemberPress': if self._popup_party_member_is_host: bui.getsound('error').play() bui.screenmessage( bui.Lstr(resource='internal.cantKickHostError'), color=(1, 0, 0), ) else: assert self._popup_party_member_client_id is not None # Ban for 5 minutes. result = bs.disconnect_client( self._popup_party_member_client_id, ban_time=5 * 60 ) if not result: bui.getsound('error').play() bui.screenmessage( bui.Lstr(resource='getTicketsWindow.unavailableText'), color=(1, 0, 0), ) elif self._popup_type == 'menu': if choice in ('mute', 'unmute'): cfg = bui.app.config cfg['Chat Muted'] = choice == 'mute' cfg.apply_and_commit() self._display_old_msgs = True self._update() if choice == 'add_to_favorites': info = bs.get_connection_to_host_info_2() if info is not None: self._add_to_favorites( name=info.name, address=info.address, port_num=info.port, ) else: # We should not allow the user to see this option # if they aren't in a server; this is our bad. bui.screenmessage( bui.Lstr(resource='errorText'), color=(1, 0, 0) ) bui.getsound('error').play() else: print(f'unhandled popup type: {self._popup_type}')
def _add_to_favorites( self, name: str, address: str | None, port_num: int | None ) -> None: addr = address if addr == '': bui.screenmessage( bui.Lstr(resource='internal.invalidAddressErrorText'), color=(1, 0, 0), ) bui.getsound('error').play() return port = port_num if port_num is not None else -1 if port > 65535 or port < 0: bui.screenmessage( bui.Lstr(resource='internal.invalidPortErrorText'), color=(1, 0, 0), ) bui.getsound('error').play() return # Avoid empty names. if not name: name = f'{addr}@{port}' config = bui.app.config if addr: if not isinstance(config.get('Saved Servers'), dict): config['Saved Servers'] = {} config['Saved Servers'][f'{addr}@{port}'] = { 'addr': addr, 'port': port, 'name': name, } config.commit() bui.getsound('gunCocking').play() bui.screenmessage( bui.Lstr( resource='addedToFavoritesText', subs=[('${NAME}', name)] ), color=(0, 1, 0), ) else: bui.screenmessage( bui.Lstr(resource='internal.invalidAddressErrorText'), color=(1, 0, 0), ) bui.getsound('error').play()
[docs] def popup_menu_closing(self, popup_window: PopupWindow) -> None: """Called when the popup is closing."""
def _on_party_member_press( self, client_id: int, is_host: bool, widget: bui.Widget ) -> None: # if we're the host, pop up 'kick' options for all non-host members if bs.get_foreground_host_session() is not None: kick_str = bui.Lstr(resource='kickText') else: # kick-votes appeared in build 14248 info = bs.get_connection_to_host_info_2() if info is None or info.build_number < 14248: return kick_str = bui.Lstr(resource='kickVoteText') assert bui.app.classic is not None uiscale = bui.app.ui_v1.uiscale PopupMenuWindow( position=widget.get_screen_space_center(), scale=( 2.3 if uiscale is bui.UIScale.SMALL else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23 ), choices=['kick'], choices_display=[kick_str], current_choice='kick', delegate=self, ) self._popup_type = 'partyMemberPress' self._popup_party_member_client_id = client_id self._popup_party_member_is_host = is_host def _send_chat_message(self) -> None: text = cast(str, bui.textwidget(query=self._text_field)).strip() if text != '': bs.chatmessage(text) bui.textwidget(edit=self._text_field, text='')
[docs] def close(self) -> None: """Close the window.""" # 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_scale')
[docs] def close_with_sound(self) -> None: """Close the window and make a lovely sound.""" # 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.getsound('swish').play() self.close()