# Released under the MIT License. See LICENSE for details.
#
"""Provides a window for browsing and launching game playlists."""
from __future__ import annotations
import logging
import copy
import math
import bascenev1 as bs
import bauiv1 as bui
[docs]
class PlaylistBrowserWindow(bui.Window):
"""Window for starting teams games."""
def __init__(
self,
sessiontype: type[bs.Session],
transition: str | None = 'in_right',
origin_widget: bui.Widget | None = None,
):
# pylint: disable=too-many-statements
# pylint: disable=cyclic-import
from bauiv1lib.playlist import PlaylistTypeVars
# 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
assert bui.app.classic is not None
# Store state for when we exit the next game.
if issubclass(sessiontype, bs.DualTeamSession):
bui.app.ui_v1.set_main_menu_location('Team Game Select')
bui.set_analytics_screen('Teams Window')
elif issubclass(sessiontype, bs.FreeForAllSession):
bui.app.ui_v1.set_main_menu_location('Free-for-All Game Select')
bui.set_analytics_screen('FreeForAll Window')
else:
raise TypeError(f'Invalid sessiontype: {sessiontype}.')
self._pvars = PlaylistTypeVars(sessiontype)
self._sessiontype = sessiontype
self._customize_button: bui.Widget | None = None
self._sub_width: float | None = None
self._sub_height: float | None = None
self._ensure_standard_playlists_exist()
# Get the current selection (if any).
self._selected_playlist = bui.app.config.get(
self._pvars.config_name + ' Playlist Selection'
)
uiscale = bui.app.ui_v1.uiscale
self._width = 1100.0 if uiscale is bui.UIScale.SMALL else 800.0
x_inset = 150 if uiscale is bui.UIScale.SMALL else 0
self._height = (
480
if uiscale is bui.UIScale.SMALL
else 510 if uiscale is bui.UIScale.MEDIUM else 580
)
top_extra = 20 if uiscale is bui.UIScale.SMALL else 0
super().__init__(
root_widget=bui.containerwidget(
size=(self._width, self._height + top_extra),
transition=transition,
toolbar_visibility='menu_full',
scale_origin_stack_offset=scale_origin,
scale=(
1.69
if uiscale is bui.UIScale.SMALL
else 1.05 if uiscale is bui.UIScale.MEDIUM else 0.9
),
stack_offset=(
(0, -26) if uiscale is bui.UIScale.SMALL else (0, 0)
),
)
)
self._back_button: bui.Widget | None = bui.buttonwidget(
parent=self._root_widget,
position=(59 + x_inset, self._height - 70),
size=(120, 60),
scale=1.0,
on_activate_call=self._on_back_press,
autoselect=True,
label=bui.Lstr(resource='backText'),
button_type='back',
)
bui.containerwidget(
edit=self._root_widget, cancel_button=self._back_button
)
txt = self._title_text = bui.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height - 41),
size=(0, 0),
text=self._pvars.window_title_name,
scale=1.3,
res_scale=1.5,
color=bui.app.ui_v1.heading_color,
h_align='center',
v_align='center',
)
if uiscale is bui.UIScale.SMALL and bui.app.ui_v1.use_toolbars:
bui.textwidget(edit=txt, text='')
bui.buttonwidget(
edit=self._back_button,
button_type='backSmall',
size=(60, 54),
position=(59 + x_inset, self._height - 67),
label=bui.charstr(bui.SpecialChar.BACK),
)
if uiscale is bui.UIScale.SMALL and bui.app.ui_v1.use_toolbars:
self._back_button.delete()
self._back_button = None
bui.containerwidget(
edit=self._root_widget, on_cancel_call=self._on_back_press
)
scroll_offs = 33
else:
scroll_offs = 0
self._scroll_width = self._width - (100 + 2 * x_inset)
self._scroll_height = self._height - (
146
if uiscale is bui.UIScale.SMALL and bui.app.ui_v1.use_toolbars
else 136
)
self._scrollwidget = bui.scrollwidget(
parent=self._root_widget,
highlight=False,
size=(self._scroll_width, self._scroll_height),
position=(
(self._width - self._scroll_width) * 0.5,
65 + scroll_offs,
),
)
bui.containerwidget(edit=self._scrollwidget, claims_left_right=True)
self._subcontainer: bui.Widget | None = None
self._config_name_full = self._pvars.config_name + ' Playlists'
self._last_config = None
# Update now and once per second.
# (this should do our initial refresh)
self._update()
self._update_timer = bui.AppTimer(
1.0, bui.WeakCall(self._update), repeat=True
)
def _ensure_standard_playlists_exist(self) -> None:
plus = bui.app.plus
assert plus is not None
# On new installations, go ahead and create a few playlists
# besides the hard-coded default one:
if not plus.get_v1_account_misc_val('madeStandardPlaylists', False):
plus.add_v1_account_transaction(
{
'type': 'ADD_PLAYLIST',
'playlistType': 'Free-for-All',
'playlistName': bui.Lstr(
resource='singleGamePlaylistNameText'
)
.evaluate()
.replace(
'${GAME}',
bui.Lstr(
translate=('gameNames', 'Death Match')
).evaluate(),
),
'playlist': [
{
'type': 'bs_death_match.DeathMatchGame',
'settings': {
'Epic Mode': False,
'Kills to Win Per Player': 10,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Doom Shroom',
},
},
{
'type': 'bs_death_match.DeathMatchGame',
'settings': {
'Epic Mode': False,
'Kills to Win Per Player': 10,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Crag Castle',
},
},
],
}
)
plus.add_v1_account_transaction(
{
'type': 'ADD_PLAYLIST',
'playlistType': 'Team Tournament',
'playlistName': bui.Lstr(
resource='singleGamePlaylistNameText'
)
.evaluate()
.replace(
'${GAME}',
bui.Lstr(
translate=('gameNames', 'Capture the Flag')
).evaluate(),
),
'playlist': [
{
'type': 'bs_capture_the_flag.CTFGame',
'settings': {
'map': 'Bridgit',
'Score to Win': 3,
'Flag Idle Return Time': 30,
'Flag Touch Return Time': 0,
'Respawn Times': 1.0,
'Time Limit': 600,
'Epic Mode': False,
},
},
{
'type': 'bs_capture_the_flag.CTFGame',
'settings': {
'map': 'Roundabout',
'Score to Win': 2,
'Flag Idle Return Time': 30,
'Flag Touch Return Time': 0,
'Respawn Times': 1.0,
'Time Limit': 600,
'Epic Mode': False,
},
},
{
'type': 'bs_capture_the_flag.CTFGame',
'settings': {
'map': 'Tip Top',
'Score to Win': 2,
'Flag Idle Return Time': 30,
'Flag Touch Return Time': 3,
'Respawn Times': 1.0,
'Time Limit': 300,
'Epic Mode': False,
},
},
],
}
)
plus.add_v1_account_transaction(
{
'type': 'ADD_PLAYLIST',
'playlistType': 'Team Tournament',
'playlistName': bui.Lstr(
translate=('playlistNames', 'Just Sports')
).evaluate(),
'playlist': [
{
'type': 'bs_hockey.HockeyGame',
'settings': {
'Time Limit': 0,
'map': 'Hockey Stadium',
'Score to Win': 1,
'Respawn Times': 1.0,
},
},
{
'type': 'bs_football.FootballTeamGame',
'settings': {
'Time Limit': 0,
'map': 'Football Stadium',
'Score to Win': 21,
'Respawn Times': 1.0,
},
},
],
}
)
plus.add_v1_account_transaction(
{
'type': 'ADD_PLAYLIST',
'playlistType': 'Free-for-All',
'playlistName': bui.Lstr(
translate=('playlistNames', 'Just Epic')
).evaluate(),
'playlist': [
{
'type': 'bs_elimination.EliminationGame',
'settings': {
'Time Limit': 120,
'map': 'Tip Top',
'Respawn Times': 1.0,
'Lives Per Player': 1,
'Epic Mode': 1,
},
}
],
}
)
plus.add_v1_account_transaction(
{
'type': 'SET_MISC_VAL',
'name': 'madeStandardPlaylists',
'value': True,
}
)
plus.run_v1_account_transactions()
def _refresh(self) -> None:
# FIXME: Should tidy this up.
# pylint: disable=too-many-statements
# pylint: disable=too-many-branches
# pylint: disable=too-many-locals
# pylint: disable=too-many-nested-blocks
from efro.util import asserttype
from bascenev1 import get_map_class, filter_playlist
if not self._root_widget:
return
if self._subcontainer is not None:
self._save_state()
self._subcontainer.delete()
# Make sure config exists.
if self._config_name_full not in bui.app.config:
bui.app.config[self._config_name_full] = {}
items = list(bui.app.config[self._config_name_full].items())
# Make sure everything is unicode.
items = [
(i[0].decode(), i[1]) if not isinstance(i[0], str) else i
for i in items
]
items.sort(key=lambda x2: asserttype(x2[0], str).lower())
items = [['__default__', None]] + items # default is always first
count = len(items)
columns = 3
rows = int(math.ceil(float(count) / columns))
button_width = 230
button_height = 230
button_buffer_h = -3
button_buffer_v = 0
self._sub_width = self._scroll_width
self._sub_height = (
40.0 + rows * (button_height + 2 * button_buffer_v) + 90
)
assert self._sub_width is not None
assert self._sub_height is not None
self._subcontainer = bui.containerwidget(
parent=self._scrollwidget,
size=(self._sub_width, self._sub_height),
background=False,
)
children = self._subcontainer.get_children()
for child in children:
child.delete()
assert bui.app.classic is not None
bui.textwidget(
parent=self._subcontainer,
text=bui.Lstr(resource='playlistsText'),
position=(40, self._sub_height - 26),
size=(0, 0),
scale=1.0,
maxwidth=400,
color=bui.app.ui_v1.title_color,
h_align='left',
v_align='center',
)
index = 0
appconfig = bui.app.config
mesh_opaque = bui.getmesh('level_select_button_opaque')
mesh_transparent = bui.getmesh('level_select_button_transparent')
mask_tex = bui.gettexture('mapPreviewMask')
h_offs = 225 if count == 1 else 115 if count == 2 else 0
h_offs_bottom = 0
uiscale = bui.app.ui_v1.uiscale
for y in range(rows):
for x in range(columns):
name = items[index][0]
assert name is not None
pos = (
x * (button_width + 2 * button_buffer_h)
+ button_buffer_h
+ 8
+ h_offs,
self._sub_height
- 47
- (y + 1) * (button_height + 2 * button_buffer_v),
)
btn = bui.buttonwidget(
parent=self._subcontainer,
button_type='square',
size=(button_width, button_height),
autoselect=True,
label='',
position=pos,
)
if (
x == 0
and bui.app.ui_v1.use_toolbars
and uiscale is bui.UIScale.SMALL
):
bui.widget(
edit=btn,
left_widget=bui.get_special_widget('back_button'),
)
if (
x == columns - 1
and bui.app.ui_v1.use_toolbars
and uiscale is bui.UIScale.SMALL
):
bui.widget(
edit=btn,
right_widget=bui.get_special_widget('party_button'),
)
bui.buttonwidget(
edit=btn,
on_activate_call=bui.Call(
self._on_playlist_press, btn, name
),
on_select_call=bui.Call(self._on_playlist_select, name),
)
bui.widget(edit=btn, show_buffer_top=50, show_buffer_bottom=50)
if self._selected_playlist == name:
bui.containerwidget(
edit=self._subcontainer,
selected_child=btn,
visible_child=btn,
)
if self._back_button is not None:
if y == 0:
bui.widget(edit=btn, up_widget=self._back_button)
if x == 0:
bui.widget(edit=btn, left_widget=self._back_button)
print_name: str | bui.Lstr | None
if name == '__default__':
print_name = self._pvars.default_list_name
else:
print_name = name
bui.textwidget(
parent=self._subcontainer,
text=print_name,
position=(
pos[0] + button_width * 0.5,
pos[1] + button_height * 0.79,
),
size=(0, 0),
scale=button_width * 0.003,
maxwidth=button_width * 0.7,
draw_controller=btn,
h_align='center',
v_align='center',
)
# Poke into this playlist and see if we can display some of
# its maps.
map_images = []
try:
map_textures = []
map_texture_entries = []
if name == '__default__':
playlist = self._pvars.get_default_list_call()
else:
if (
name
not in appconfig[
self._pvars.config_name + ' Playlists'
]
):
print(
'NOT FOUND ERR',
appconfig[
self._pvars.config_name + ' Playlists'
],
)
playlist = appconfig[
self._pvars.config_name + ' Playlists'
][name]
playlist = filter_playlist(
playlist,
self._sessiontype,
remove_unowned=False,
mark_unowned=True,
name=name,
)
for entry in playlist:
mapname = entry['settings']['map']
maptype: type[bs.Map] | None
try:
maptype = get_map_class(mapname)
except bui.NotFoundError:
maptype = None
if maptype is not None:
tex_name = maptype.get_preview_texture_name()
if tex_name is not None:
map_textures.append(tex_name)
map_texture_entries.append(entry)
if len(map_textures) >= 6:
break
if len(map_textures) > 4:
img_rows = 3
img_columns = 2
scl = 0.33
h_offs_img = 30
v_offs_img = 126
elif len(map_textures) > 2:
img_rows = 2
img_columns = 2
scl = 0.35
h_offs_img = 24
v_offs_img = 110
elif len(map_textures) > 1:
img_rows = 2
img_columns = 1
scl = 0.5
h_offs_img = 47
v_offs_img = 105
else:
img_rows = 1
img_columns = 1
scl = 0.75
h_offs_img = 20
v_offs_img = 65
v = None
for row in range(img_rows):
for col in range(img_columns):
tex_index = row * img_columns + col
if tex_index < len(map_textures):
entry = map_texture_entries[tex_index]
owned = not (
(
'is_unowned_map' in entry
and entry['is_unowned_map']
)
or (
'is_unowned_game' in entry
and entry['is_unowned_game']
)
)
tex_name = map_textures[tex_index]
h = pos[0] + h_offs_img + scl * 250 * col
v = pos[1] + v_offs_img - scl * 130 * row
map_images.append(
bui.imagewidget(
parent=self._subcontainer,
size=(scl * 250.0, scl * 125.0),
position=(h, v),
texture=bui.gettexture(tex_name),
opacity=1.0 if owned else 0.25,
draw_controller=btn,
mesh_opaque=mesh_opaque,
mesh_transparent=mesh_transparent,
mask_texture=mask_tex,
)
)
if not owned:
bui.imagewidget(
parent=self._subcontainer,
size=(scl * 100.0, scl * 100.0),
position=(h + scl * 75, v + scl * 10),
texture=bui.gettexture('lock'),
draw_controller=btn,
)
if v is not None:
v -= scl * 130.0
except Exception:
logging.exception('Error listing playlist maps.')
if not map_images:
bui.textwidget(
parent=self._subcontainer,
text='???',
scale=1.5,
size=(0, 0),
color=(1, 1, 1, 0.5),
h_align='center',
v_align='center',
draw_controller=btn,
position=(
pos[0] + button_width * 0.5,
pos[1] + button_height * 0.5,
),
)
index += 1
if index >= count:
break
if index >= count:
break
self._customize_button = btn = bui.buttonwidget(
parent=self._subcontainer,
size=(100, 30),
position=(34 + h_offs_bottom, 50),
text_scale=0.6,
label=bui.Lstr(resource='customizeText'),
on_activate_call=self._on_customize_press,
color=(0.54, 0.52, 0.67),
textcolor=(0.7, 0.65, 0.7),
autoselect=True,
)
bui.widget(edit=btn, show_buffer_top=22, show_buffer_bottom=28)
self._restore_state()
[docs]
def on_play_options_window_run_game(self) -> None:
"""(internal)"""
if not self._root_widget:
return
bui.containerwidget(edit=self._root_widget, transition='out_left')
def _on_playlist_select(self, playlist_name: str) -> None:
self._selected_playlist = playlist_name
def _update(self) -> None:
# make sure config exists
if self._config_name_full not in bui.app.config:
bui.app.config[self._config_name_full] = {}
cfg = bui.app.config[self._config_name_full]
if cfg != self._last_config:
self._last_config = copy.deepcopy(cfg)
self._refresh()
def _on_playlist_press(
self, button: bui.Widget, playlist_name: str
) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.playoptions import PlayOptionsWindow
# Make sure the target playlist still exists.
exists = (
playlist_name == '__default__'
or playlist_name in bui.app.config.get(self._config_name_full, {})
)
if not exists:
return
self._save_state()
PlayOptionsWindow(
sessiontype=self._sessiontype,
scale_origin=button.get_screen_space_center(),
playlist=playlist_name,
delegate=self,
)
def _on_customize_press(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.playlist.customizebrowser import (
PlaylistCustomizeBrowserWindow,
)
# 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
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
PlaylistCustomizeBrowserWindow(
origin_widget=self._customize_button,
sessiontype=self._sessiontype,
).get_root_widget(),
from_window=self._root_widget,
)
def _on_back_press(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.play import PlayWindow
# 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
# Store our selected playlist if that's changed.
if self._selected_playlist is not None:
prev_sel = bui.app.config.get(
self._pvars.config_name + ' Playlist Selection'
)
if self._selected_playlist != prev_sel:
cfg = bui.app.config
cfg[self._pvars.config_name + ' Playlist Selection'] = (
self._selected_playlist
)
cfg.commit()
self._save_state()
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
PlayWindow(transition='in_left').get_root_widget(),
from_window=self._root_widget,
)
def _save_state(self) -> None:
try:
sel = self._root_widget.get_selected_child()
if sel == self._back_button:
sel_name = 'Back'
elif sel == self._scrollwidget:
assert self._subcontainer is not None
subsel = self._subcontainer.get_selected_child()
if subsel == self._customize_button:
sel_name = 'Customize'
else:
sel_name = 'Scroll'
else:
raise RuntimeError('Unrecognized selected widget.')
assert bui.app.classic is not None
bui.app.ui_v1.window_states[type(self)] = sel_name
except Exception:
logging.exception('Error saving state for %s.', self)
def _restore_state(self) -> None:
try:
assert bui.app.classic is not None
sel_name = bui.app.ui_v1.window_states.get(type(self))
if sel_name == 'Back':
sel = self._back_button
elif sel_name == 'Scroll':
sel = self._scrollwidget
elif sel_name == 'Customize':
sel = self._scrollwidget
bui.containerwidget(
edit=self._subcontainer,
selected_child=self._customize_button,
visible_child=self._customize_button,
)
else:
sel = self._scrollwidget
bui.containerwidget(edit=self._root_widget, selected_child=sel)
except Exception:
logging.exception('Error restoring state for %s.', self)