vh-bombsquad-modded-server-.../dist/ba_data/python/bastd/ui/party.py
2024-02-26 00:17:10 +05:30

507 lines
19 KiB
Python

# Released under the MIT License. See LICENSE for details.
#
"""Provides party related UI."""
from __future__ import annotations
import math
from typing import TYPE_CHECKING, cast
import ba
import ba.internal
from bastd.ui import popup
if TYPE_CHECKING:
from typing import Sequence, Any
class PartyWindow(ba.Window):
"""Party list/chat window."""
def __del__(self) -> None:
ba.internal.set_party_window_open(False)
def __init__(self, origin: Sequence[float] = (0, 0)):
ba.internal.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
uiscale = ba.app.ui.uiscale
self._height = (
365
if uiscale is ba.UIScale.SMALL
else 480
if uiscale is ba.UIScale.MEDIUM
else 600
)
super().__init__(
root_widget=ba.containerwidget(
size=(self._width, self._height),
transition='in_scale',
color=(0.40, 0.55, 0.20),
parent=ba.internal.get_special_widget('overlay_stack'),
on_outside_click_call=self.close_with_sound,
scale_origin_stack_offset=origin,
scale=(
2.0
if uiscale is ba.UIScale.SMALL
else 1.35
if uiscale is ba.UIScale.MEDIUM
else 1.0
),
stack_offset=(0, -10)
if uiscale is ba.UIScale.SMALL
else (240, 0)
if uiscale is ba.UIScale.MEDIUM
else (330, 20),
)
)
self._cancel_button = ba.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=ba.gettexture('crossOut'),
iconscale=1.2,
)
ba.containerwidget(
edit=self._root_widget, cancel_button=self._cancel_button
)
self._menu_button = ba.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=ba.WeakCall(self._on_menu_button_press),
color=(0.55, 0.73, 0.25),
iconscale=1.2,
)
info = ba.internal.get_connection_to_host_info()
if info.get('name', '') != '':
title = ba.Lstr(value=info['name'])
else:
title = ba.Lstr(resource=self._r + '.titleText')
self._title_text = ba.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 = ba.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 = ba.scrollwidget(
parent=self._root_widget,
size=(self._scroll_width, self._height - 200),
position=(30, 80),
color=(0.4, 0.6, 0.3),
)
self._columnwidget = ba.columnwidget(
parent=self._scrollwidget, border=2, margin=0
)
ba.widget(edit=self._menu_button, down_widget=self._columnwidget)
self._muted_text = ba.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height * 0.5),
size=(0, 0),
h_align='center',
v_align='center',
text=ba.Lstr(resource='chatMutedText'),
)
self._chat_texts: list[ba.Widget] = []
# add all existing messages if chat is not muted
if not ba.app.config.resolve('Chat Muted'):
msgs = ba.internal.get_chat_messages()
for msg in msgs:
self._add_msg(msg)
self._text_field = txt = ba.textwidget(
parent=self._root_widget,
editable=True,
size=(530, 40),
position=(44, 39),
text='',
maxwidth=494,
shadow=0.3,
flatness=1.0,
description=ba.Lstr(resource=self._r + '.chatMessageText'),
autoselect=True,
v_align='center',
corner_scale=0.7,
)
ba.widget(
edit=self._scrollwidget,
autoselect=True,
left_widget=self._cancel_button,
up_widget=self._cancel_button,
down_widget=self._text_field,
)
ba.widget(
edit=self._columnwidget,
autoselect=True,
up_widget=self._cancel_button,
down_widget=self._text_field,
)
ba.containerwidget(edit=self._root_widget, selected_child=txt)
btn = ba.buttonwidget(
parent=self._root_widget,
size=(50, 35),
label=ba.Lstr(resource=self._r + '.sendText'),
button_type='square',
autoselect=True,
position=(self._width - 70, 35),
on_activate_call=self._send_chat_message,
)
ba.textwidget(edit=txt, on_return_press_call=btn.activate)
self._name_widgets: list[ba.Widget] = []
self._roster: list[dict[str, Any]] | None = None
self._update_timer = ba.Timer(
1.0,
ba.WeakCall(self._update),
repeat=True,
timetype=ba.TimeType.REAL,
)
self._update()
def on_chat_message(self, msg: str) -> None:
"""Called when a new chat message comes through."""
if not ba.app.config.resolve('Chat Muted'):
self._add_msg(msg)
def _add_msg(self, msg: str) -> None:
txt = ba.textwidget(
parent=self._columnwidget,
text=msg,
h_align='left',
v_align='center',
size=(0, 13),
scale=0.55,
maxwidth=self._scroll_width * 0.94,
shadow=0.3,
flatness=1.0,
)
self._chat_texts.append(txt)
while len(self._chat_texts) > 40:
self._chat_texts.pop(0).delete()
ba.containerwidget(edit=self._columnwidget, visible_child=txt)
def _on_menu_button_press(self) -> None:
is_muted = ba.app.config.resolve('Chat Muted')
uiscale = ba.app.ui.uiscale
popup.PopupMenuWindow(
position=self._menu_button.get_screen_space_center(),
scale=(
2.3
if uiscale is ba.UIScale.SMALL
else 1.65
if uiscale is ba.UIScale.MEDIUM
else 1.23
),
choices=['unmute' if is_muted else 'mute'],
choices_display=[
ba.Lstr(
resource='chatUnMuteText' if is_muted else 'chatMuteText'
)
],
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 ba.app.config.resolve('Chat Muted'):
ba.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:
ba.textwidget(edit=self._muted_text, color=(1, 1, 1, 0.0))
# update roster section
roster = ba.internal.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
ba.textwidget(
edit=self._empty_str,
text=ba.Lstr(resource=self._r + '.emptyText'),
)
ba.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:
ba.print_exception(
'Error calcing client name str.'
)
p_str = '???'
widget = ba.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=ba.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).
ba.textwidget(
edit=widget,
on_activate_call=ba.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,
ba.internal.get_string_width(
p_str, suppress_warning=True
)
* t_scale,
)
self._name_widgets.append(
ba.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=ba.Lstr(
resource=self._r + '.hostText'
),
scale=0.4,
shadow=0.1,
flatness=1.0,
)
)
ba.textwidget(edit=self._empty_str, text='')
ba.scrollwidget(
edit=self._scrollwidget,
size=(
self._width - 50,
max(100, self._height - 139 - c_height_total),
),
position=(30, 80),
)
def popup_menu_selected_choice(
self, popup_window: popup.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:
ba.playsound(ba.getsound('error'))
ba.screenmessage(
ba.Lstr(resource='internal.cantKickHostError'),
color=(1, 0, 0),
)
else:
assert self._popup_party_member_client_id is not None
# Ban for 5 minutes.
result = ba.internal.disconnect_client(
self._popup_party_member_client_id, ban_time=5 * 60
)
if not result:
ba.playsound(ba.getsound('error'))
ba.screenmessage(
ba.Lstr(resource='getTicketsWindow.unavailableText'),
color=(1, 0, 0),
)
elif self._popup_type == 'menu':
if choice in ('mute', 'unmute'):
cfg = ba.app.config
cfg['Chat Muted'] = choice == 'mute'
cfg.apply_and_commit()
self._update()
else:
print(f'unhandled popup type: {self._popup_type}')
def popup_menu_closing(self, popup_window: popup.PopupWindow) -> None:
"""Called when the popup is closing."""
def _on_party_member_press(
self, client_id: int, is_host: bool, widget: ba.Widget
) -> None:
# if we're the host, pop up 'kick' options for all non-host members
if ba.internal.get_foreground_host_session() is not None:
kick_str = ba.Lstr(resource='kickText')
else:
# kick-votes appeared in build 14248
if (
ba.internal.get_connection_to_host_info().get('build_number', 0)
< 14248
):
return
kick_str = ba.Lstr(resource='kickVoteText')
uiscale = ba.app.ui.uiscale
popup.PopupMenuWindow(
position=widget.get_screen_space_center(),
scale=(
2.3
if uiscale is ba.UIScale.SMALL
else 1.65
if uiscale is ba.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:
ba.internal.chatmessage(
cast(str, ba.textwidget(query=self._text_field))
)
ba.textwidget(edit=self._text_field, text='')
def close(self) -> None:
"""Close the window."""
ba.containerwidget(edit=self._root_widget, transition='out_scale')
def close_with_sound(self) -> None:
"""Close the window and make a lovely sound."""
ba.playsound(ba.getsound('swish'))
self.close()