Added new files

This commit is contained in:
vortex 2024-02-20 23:04:51 +05:30
parent 867634cc5c
commit 3a407868d4
1775 changed files with 550222 additions and 0 deletions

View file

@ -0,0 +1,64 @@
# Released under the MIT License. See LICENSE for details.
#
"""Playlist ui functionality."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
if TYPE_CHECKING:
pass
# FIXME: Could change this to be a classmethod of session types?
class PlaylistTypeVars:
"""Defines values for a playlist type (config names to use, etc)."""
def __init__(self, sessiontype: type[ba.Session]):
from ba.internal import (
get_default_teams_playlist,
get_default_free_for_all_playlist,
)
self.sessiontype: type[ba.Session]
if issubclass(sessiontype, ba.DualTeamSession):
play_mode_name = ba.Lstr(
resource='playModes.teamsText', fallback_resource='teamsText'
)
self.get_default_list_call = get_default_teams_playlist
self.session_type_name = 'ba.DualTeamSession'
self.config_name = 'Team Tournament'
self.window_title_name = ba.Lstr(
resource='playModes.teamsText', fallback_resource='teamsText'
)
self.sessiontype = ba.DualTeamSession
elif issubclass(sessiontype, ba.FreeForAllSession):
play_mode_name = ba.Lstr(
resource='playModes.freeForAllText',
fallback_resource='freeForAllText',
)
self.get_default_list_call = get_default_free_for_all_playlist
self.session_type_name = 'ba.FreeForAllSession'
self.config_name = 'Free-for-All'
self.window_title_name = ba.Lstr(
resource='playModes.freeForAllText',
fallback_resource='freeForAllText',
)
self.sessiontype = ba.FreeForAllSession
else:
raise RuntimeError(
f'Playlist type vars undefined for sessiontype: {sessiontype}'
)
self.default_list_name = ba.Lstr(
resource='defaultGameListNameText',
subs=[('${PLAYMODE}', play_mode_name)],
)
self.default_new_list_name = ba.Lstr(
resource='defaultNewGameListNameText',
subs=[('${PLAYMODE}', play_mode_name)],
)

View file

@ -0,0 +1,271 @@
# Released under the MIT License. See LICENSE for details.
#
"""Provides a window for selecting a game type to add to a playlist."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
import ba.internal
if TYPE_CHECKING:
from bastd.ui.playlist.editcontroller import PlaylistEditController
class PlaylistAddGameWindow(ba.Window):
"""Window for selecting a game type to add to a playlist."""
def __init__(
self,
editcontroller: PlaylistEditController,
transition: str = 'in_right',
):
self._editcontroller = editcontroller
self._r = 'addGameWindow'
uiscale = ba.app.ui.uiscale
self._width = 750 if uiscale is ba.UIScale.SMALL else 650
x_inset = 50 if uiscale is ba.UIScale.SMALL else 0
self._height = (
346
if uiscale is ba.UIScale.SMALL
else 380
if uiscale is ba.UIScale.MEDIUM
else 440
)
top_extra = 30 if uiscale is ba.UIScale.SMALL else 20
self._scroll_width = 210
super().__init__(
root_widget=ba.containerwidget(
size=(self._width, self._height + top_extra),
transition=transition,
scale=(
2.17
if uiscale is ba.UIScale.SMALL
else 1.5
if uiscale is ba.UIScale.MEDIUM
else 1.0
),
stack_offset=(0, 1) if uiscale is ba.UIScale.SMALL else (0, 0),
)
)
self._back_button = ba.buttonwidget(
parent=self._root_widget,
position=(58 + x_inset, self._height - 53),
size=(165, 70),
scale=0.75,
text_scale=1.2,
label=ba.Lstr(resource='backText'),
autoselect=True,
button_type='back',
on_activate_call=self._back,
)
self._select_button = select_button = ba.buttonwidget(
parent=self._root_widget,
position=(self._width - (172 + x_inset), self._height - 50),
autoselect=True,
size=(160, 60),
scale=0.75,
text_scale=1.2,
label=ba.Lstr(resource='selectText'),
on_activate_call=self._add,
)
if ba.app.ui.use_toolbars:
ba.widget(
edit=select_button,
right_widget=ba.internal.get_special_widget('party_button'),
)
ba.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height - 28),
size=(0, 0),
scale=1.0,
text=ba.Lstr(resource=self._r + '.titleText'),
h_align='center',
color=ba.app.ui.title_color,
maxwidth=250,
v_align='center',
)
v = self._height - 64
self._selected_title_text = ba.textwidget(
parent=self._root_widget,
position=(x_inset + self._scroll_width + 50 + 30, v - 15),
size=(0, 0),
scale=1.0,
color=(0.7, 1.0, 0.7, 1.0),
maxwidth=self._width - self._scroll_width - 150 - x_inset * 2,
h_align='left',
v_align='center',
)
v -= 30
self._selected_description_text = ba.textwidget(
parent=self._root_widget,
position=(x_inset + self._scroll_width + 50 + 30, v),
size=(0, 0),
scale=0.7,
color=(0.5, 0.8, 0.5, 1.0),
maxwidth=self._width - self._scroll_width - 150 - x_inset * 2,
h_align='left',
)
scroll_height = self._height - 100
v = self._height - 60
self._scrollwidget = ba.scrollwidget(
parent=self._root_widget,
position=(x_inset + 61, v - scroll_height),
size=(self._scroll_width, scroll_height),
highlight=False,
)
ba.widget(
edit=self._scrollwidget,
up_widget=self._back_button,
left_widget=self._back_button,
right_widget=select_button,
)
self._column: ba.Widget | None = None
v -= 35
ba.containerwidget(
edit=self._root_widget,
cancel_button=self._back_button,
start_button=select_button,
)
self._selected_game_type: type[ba.GameActivity] | None = None
ba.containerwidget(
edit=self._root_widget, selected_child=self._scrollwidget
)
self._game_types: list[type[ba.GameActivity]] = []
# Get actual games loading in the bg.
ba.app.meta.load_exported_classes(
ba.GameActivity,
self._on_game_types_loaded,
completion_cb_in_bg_thread=True,
)
# Refresh with our initial empty list. We'll refresh again once
# game loading is complete.
self._refresh()
def _on_game_types_loaded(
self, gametypes: list[type[ba.GameActivity]]
) -> None:
from ba.internal import get_unowned_game_types
# We asked for a bg thread completion cb so we can do some
# filtering here in the bg thread where its not gonna cause hitches.
assert not ba.in_logic_thread()
sessiontype = self._editcontroller.get_session_type()
unowned = get_unowned_game_types()
self._game_types = [
gt
for gt in gametypes
if gt not in unowned and gt.supports_session_type(sessiontype)
]
# Sort in the current language.
self._game_types.sort(key=lambda g: g.get_display_string().evaluate())
# Tell ourself to refresh back in the logic thread.
ba.pushcall(self._refresh, from_other_thread=True)
def _refresh(self, select_get_more_games_button: bool = False) -> None:
# from ba.internal import get_game_types
if self._column is not None:
self._column.delete()
self._column = ba.columnwidget(
parent=self._scrollwidget, border=2, margin=0
)
for i, gametype in enumerate(self._game_types):
def _doit() -> None:
if self._select_button:
ba.timer(
0.1,
self._select_button.activate,
timetype=ba.TimeType.REAL,
)
txt = ba.textwidget(
parent=self._column,
position=(0, 0),
size=(self._width - 88, 24),
text=gametype.get_display_string(),
h_align='left',
v_align='center',
color=(0.8, 0.8, 0.8, 1.0),
maxwidth=self._scroll_width * 0.8,
on_select_call=ba.Call(self._set_selected_game_type, gametype),
always_highlight=True,
selectable=True,
on_activate_call=_doit,
)
if i == 0:
ba.widget(edit=txt, up_widget=self._back_button)
self._get_more_games_button = ba.buttonwidget(
parent=self._column,
autoselect=True,
label=ba.Lstr(resource=self._r + '.getMoreGamesText'),
color=(0.54, 0.52, 0.67),
textcolor=(0.7, 0.65, 0.7),
on_activate_call=self._on_get_more_games_press,
size=(178, 50),
)
if select_get_more_games_button:
ba.containerwidget(
edit=self._column,
selected_child=self._get_more_games_button,
visible_child=self._get_more_games_button,
)
def _on_get_more_games_press(self) -> None:
from bastd.ui.account import show_sign_in_prompt
from bastd.ui.store.browser import StoreBrowserWindow
if ba.internal.get_v1_account_state() != 'signed_in':
show_sign_in_prompt()
return
StoreBrowserWindow(
modal=True,
show_tab=StoreBrowserWindow.TabID.MINIGAMES,
on_close_call=self._on_store_close,
origin_widget=self._get_more_games_button,
)
def _on_store_close(self) -> None:
self._refresh(select_get_more_games_button=True)
def _add(self) -> None:
ba.internal.lock_all_input() # Make sure no more commands happen.
ba.timer(0.1, ba.internal.unlock_all_input, timetype=ba.TimeType.REAL)
assert self._selected_game_type is not None
self._editcontroller.add_game_type_selected(self._selected_game_type)
def _set_selected_game_type(self, gametype: type[ba.GameActivity]) -> None:
self._selected_game_type = gametype
ba.textwidget(
edit=self._selected_title_text, text=gametype.get_display_string()
)
ba.textwidget(
edit=self._selected_description_text,
text=gametype.get_description_display_string(
self._editcontroller.get_session_type()
),
)
def _back(self) -> None:
self._editcontroller.add_game_cancelled()

View file

@ -0,0 +1,761 @@
# Released under the MIT License. See LICENSE for details.
#
"""Provides a window for browsing and launching game playlists."""
from __future__ import annotations
import copy
import math
from typing import TYPE_CHECKING
import ba
import ba.internal
if TYPE_CHECKING:
pass
class PlaylistBrowserWindow(ba.Window):
"""Window for starting teams games."""
def __init__(
self,
sessiontype: type[ba.Session],
transition: str | None = 'in_right',
origin_widget: ba.Widget | None = None,
):
# pylint: disable=too-many-statements
# pylint: disable=cyclic-import
from bastd.ui.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
# Store state for when we exit the next game.
if issubclass(sessiontype, ba.DualTeamSession):
ba.app.ui.set_main_menu_location('Team Game Select')
ba.set_analytics_screen('Teams Window')
elif issubclass(sessiontype, ba.FreeForAllSession):
ba.app.ui.set_main_menu_location('Free-for-All Game Select')
ba.set_analytics_screen('FreeForAll Window')
else:
raise TypeError(f'Invalid sessiontype: {sessiontype}.')
self._pvars = PlaylistTypeVars(sessiontype)
self._sessiontype = sessiontype
self._customize_button: ba.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 = ba.app.config.get(
self._pvars.config_name + ' Playlist Selection'
)
uiscale = ba.app.ui.uiscale
self._width = 900.0 if uiscale is ba.UIScale.SMALL else 800.0
x_inset = 50 if uiscale is ba.UIScale.SMALL else 0
self._height = (
480
if uiscale is ba.UIScale.SMALL
else 510
if uiscale is ba.UIScale.MEDIUM
else 580
)
top_extra = 20 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_full',
scale_origin_stack_offset=scale_origin,
scale=(
1.69
if uiscale is ba.UIScale.SMALL
else 1.05
if uiscale is ba.UIScale.MEDIUM
else 0.9
),
stack_offset=(0, -26)
if uiscale is ba.UIScale.SMALL
else (0, 0),
)
)
self._back_button: ba.Widget | None = ba.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=ba.Lstr(resource='backText'),
button_type='back',
)
ba.containerwidget(
edit=self._root_widget, cancel_button=self._back_button
)
txt = self._title_text = ba.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=ba.app.ui.heading_color,
h_align='center',
v_align='center',
)
if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars:
ba.textwidget(edit=txt, text='')
ba.buttonwidget(
edit=self._back_button,
button_type='backSmall',
size=(60, 54),
position=(59 + x_inset, self._height - 67),
label=ba.charstr(ba.SpecialChar.BACK),
)
if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars:
self._back_button.delete()
self._back_button = None
ba.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 ba.UIScale.SMALL and ba.app.ui.use_toolbars
else 136
)
self._scrollwidget = ba.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,
),
)
ba.containerwidget(edit=self._scrollwidget, claims_left_right=True)
self._subcontainer: ba.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 = ba.Timer(
1.0,
ba.WeakCall(self._update),
timetype=ba.TimeType.REAL,
repeat=True,
)
def _ensure_standard_playlists_exist(self) -> None:
# On new installations, go ahead and create a few playlists
# besides the hard-coded default one:
if not ba.internal.get_v1_account_misc_val(
'madeStandardPlaylists', False
):
ba.internal.add_transaction(
{
'type': 'ADD_PLAYLIST',
'playlistType': 'Free-for-All',
'playlistName': ba.Lstr(
resource='singleGamePlaylistNameText'
)
.evaluate()
.replace(
'${GAME}',
ba.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',
},
},
],
}
)
ba.internal.add_transaction(
{
'type': 'ADD_PLAYLIST',
'playlistType': 'Team Tournament',
'playlistName': ba.Lstr(
resource='singleGamePlaylistNameText'
)
.evaluate()
.replace(
'${GAME}',
ba.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,
},
},
],
}
)
ba.internal.add_transaction(
{
'type': 'ADD_PLAYLIST',
'playlistType': 'Team Tournament',
'playlistName': ba.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,
},
},
],
}
)
ba.internal.add_transaction(
{
'type': 'ADD_PLAYLIST',
'playlistType': 'Free-for-All',
'playlistName': ba.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,
},
}
],
}
)
ba.internal.add_transaction(
{
'type': 'SET_MISC_VAL',
'name': 'madeStandardPlaylists',
'value': True,
}
)
ba.internal.run_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 ba.internal 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 ba.app.config:
ba.app.config[self._config_name_full] = {}
items = list(ba.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 = ba.containerwidget(
parent=self._scrollwidget,
size=(self._sub_width, self._sub_height),
background=False,
)
children = self._subcontainer.get_children()
for child in children:
child.delete()
ba.textwidget(
parent=self._subcontainer,
text=ba.Lstr(resource='playlistsText'),
position=(40, self._sub_height - 26),
size=(0, 0),
scale=1.0,
maxwidth=400,
color=ba.app.ui.title_color,
h_align='left',
v_align='center',
)
index = 0
appconfig = ba.app.config
model_opaque = ba.getmodel('level_select_button_opaque')
model_transparent = ba.getmodel('level_select_button_transparent')
mask_tex = ba.gettexture('mapPreviewMask')
h_offs = 225 if count == 1 else 115 if count == 2 else 0
h_offs_bottom = 0
uiscale = ba.app.ui.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 = ba.buttonwidget(
parent=self._subcontainer,
button_type='square',
size=(button_width, button_height),
autoselect=True,
label='',
position=pos,
)
if (
x == 0
and ba.app.ui.use_toolbars
and uiscale is ba.UIScale.SMALL
):
ba.widget(
edit=btn,
left_widget=ba.internal.get_special_widget(
'back_button'
),
)
if (
x == columns - 1
and ba.app.ui.use_toolbars
and uiscale is ba.UIScale.SMALL
):
ba.widget(
edit=btn,
right_widget=ba.internal.get_special_widget(
'party_button'
),
)
ba.buttonwidget(
edit=btn,
on_activate_call=ba.Call(
self._on_playlist_press, btn, name
),
on_select_call=ba.Call(self._on_playlist_select, name),
)
ba.widget(edit=btn, show_buffer_top=50, show_buffer_bottom=50)
if self._selected_playlist == name:
ba.containerwidget(
edit=self._subcontainer,
selected_child=btn,
visible_child=btn,
)
if self._back_button is not None:
if y == 0:
ba.widget(edit=btn, up_widget=self._back_button)
if x == 0:
ba.widget(edit=btn, left_widget=self._back_button)
print_name: str | ba.Lstr | None
if name == '__default__':
print_name = self._pvars.default_list_name
else:
print_name = name
ba.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[ba.Map] | None
try:
maptype = get_map_class(mapname)
except ba.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(
ba.imagewidget(
parent=self._subcontainer,
size=(scl * 250.0, scl * 125.0),
position=(h, v),
texture=ba.gettexture(tex_name),
opacity=1.0 if owned else 0.25,
draw_controller=btn,
model_opaque=model_opaque,
model_transparent=model_transparent,
mask_texture=mask_tex,
)
)
if not owned:
ba.imagewidget(
parent=self._subcontainer,
size=(scl * 100.0, scl * 100.0),
position=(h + scl * 75, v + scl * 10),
texture=ba.gettexture('lock'),
draw_controller=btn,
)
if v is not None:
v -= scl * 130.0
except Exception:
ba.print_exception('Error listing playlist maps.')
if not map_images:
ba.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 = ba.buttonwidget(
parent=self._subcontainer,
size=(100, 30),
position=(34 + h_offs_bottom, 50),
text_scale=0.6,
label=ba.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,
)
ba.widget(edit=btn, show_buffer_top=22, show_buffer_bottom=28)
self._restore_state()
def on_play_options_window_run_game(self) -> None:
"""(internal)"""
if not self._root_widget:
return
ba.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 ba.app.config:
ba.app.config[self._config_name_full] = {}
cfg = ba.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: ba.Widget, playlist_name: str) -> None:
# pylint: disable=cyclic-import
from bastd.ui.playoptions import PlayOptionsWindow
# Make sure the target playlist still exists.
exists = (
playlist_name == '__default__'
or playlist_name in ba.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 bastd.ui.playlist.customizebrowser import (
PlaylistCustomizeBrowserWindow,
)
self._save_state()
ba.containerwidget(edit=self._root_widget, transition='out_left')
ba.app.ui.set_main_menu_window(
PlaylistCustomizeBrowserWindow(
origin_widget=self._customize_button,
sessiontype=self._sessiontype,
).get_root_widget()
)
def _on_back_press(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.play import PlayWindow
# Store our selected playlist if that's changed.
if self._selected_playlist is not None:
prev_sel = ba.app.config.get(
self._pvars.config_name + ' Playlist Selection'
)
if self._selected_playlist != prev_sel:
cfg = ba.app.config
cfg[
self._pvars.config_name + ' Playlist Selection'
] = self._selected_playlist
cfg.commit()
self._save_state()
ba.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
ba.app.ui.set_main_menu_window(
PlayWindow(transition='in_left').get_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 Exception('unrecognized selected widget')
ba.app.ui.window_states[type(self)] = sel_name
except Exception:
ba.print_exception(f'Error saving state for {self}.')
def _restore_state(self) -> None:
try:
sel_name = ba.app.ui.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
ba.containerwidget(
edit=self._subcontainer,
selected_child=self._customize_button,
visible_child=self._customize_button,
)
else:
sel = self._scrollwidget
ba.containerwidget(edit=self._root_widget, selected_child=sel)
except Exception:
ba.print_exception(f'Error restoring state for {self}.')

View file

@ -0,0 +1,715 @@
# Released under the MIT License. See LICENSE for details.
#
"""Provides UI for viewing/creating/editing playlists."""
from __future__ import annotations
import copy
import time
from typing import TYPE_CHECKING
import ba
import ba.internal
if TYPE_CHECKING:
from typing import Any
class PlaylistCustomizeBrowserWindow(ba.Window):
"""Window for viewing a playlist."""
def __init__(
self,
sessiontype: type[ba.Session],
transition: str = 'in_right',
select_playlist: str | None = None,
origin_widget: ba.Widget | None = None,
):
# Yes this needs tidying.
# pylint: disable=too-many-locals
# pylint: disable=too-many-statements
# pylint: disable=cyclic-import
from bastd.ui import playlist
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
self._sessiontype = sessiontype
self._pvars = playlist.PlaylistTypeVars(sessiontype)
self._max_playlists = 30
self._r = 'gameListWindow'
uiscale = ba.app.ui.uiscale
self._width = 750.0 if uiscale is ba.UIScale.SMALL else 650.0
x_inset = 50.0 if uiscale is ba.UIScale.SMALL else 0.0
self._height = (
380.0
if uiscale is ba.UIScale.SMALL
else 420.0
if uiscale is ba.UIScale.MEDIUM
else 500.0
)
top_extra = 20.0 if uiscale is ba.UIScale.SMALL else 0.0
super().__init__(
root_widget=ba.containerwidget(
size=(self._width, self._height + top_extra),
transition=transition,
scale_origin_stack_offset=scale_origin,
scale=(
2.05
if uiscale is ba.UIScale.SMALL
else 1.5
if uiscale is ba.UIScale.MEDIUM
else 1.0
),
stack_offset=(0, -10)
if uiscale is ba.UIScale.SMALL
else (0, 0),
)
)
self._back_button = back_button = btn = ba.buttonwidget(
parent=self._root_widget,
position=(43 + x_inset, self._height - 60),
size=(160, 68),
scale=0.77,
autoselect=True,
text_scale=1.3,
label=ba.Lstr(resource='backText'),
button_type='back',
)
ba.textwidget(
parent=self._root_widget,
position=(0, self._height - 47),
size=(self._width, 25),
text=ba.Lstr(
resource=self._r + '.titleText',
subs=[('${TYPE}', self._pvars.window_title_name)],
),
color=ba.app.ui.heading_color,
maxwidth=290,
h_align='center',
v_align='center',
)
ba.buttonwidget(
edit=btn,
button_type='backSmall',
size=(60, 60),
label=ba.charstr(ba.SpecialChar.BACK),
)
v = self._height - 59.0
h = 41 + x_inset
b_color = (0.6, 0.53, 0.63)
b_textcolor = (0.75, 0.7, 0.8)
self._lock_images: list[ba.Widget] = []
lock_tex = ba.gettexture('lock')
scl = (
1.1
if uiscale is ba.UIScale.SMALL
else 1.27
if uiscale is ba.UIScale.MEDIUM
else 1.57
)
scl *= 0.63
v -= 65.0 * scl
new_button = btn = ba.buttonwidget(
parent=self._root_widget,
position=(h, v),
size=(90, 58.0 * scl),
on_activate_call=self._new_playlist,
color=b_color,
autoselect=True,
button_type='square',
textcolor=b_textcolor,
text_scale=0.7,
label=ba.Lstr(
resource='newText', fallback_resource=self._r + '.newText'
),
)
self._lock_images.append(
ba.imagewidget(
parent=self._root_widget,
size=(30, 30),
draw_controller=btn,
position=(h - 10, v + 58.0 * scl - 28),
texture=lock_tex,
)
)
v -= 65.0 * scl
self._edit_button = edit_button = btn = ba.buttonwidget(
parent=self._root_widget,
position=(h, v),
size=(90, 58.0 * scl),
on_activate_call=self._edit_playlist,
color=b_color,
autoselect=True,
textcolor=b_textcolor,
button_type='square',
text_scale=0.7,
label=ba.Lstr(
resource='editText', fallback_resource=self._r + '.editText'
),
)
self._lock_images.append(
ba.imagewidget(
parent=self._root_widget,
size=(30, 30),
draw_controller=btn,
position=(h - 10, v + 58.0 * scl - 28),
texture=lock_tex,
)
)
v -= 65.0 * scl
duplicate_button = btn = ba.buttonwidget(
parent=self._root_widget,
position=(h, v),
size=(90, 58.0 * scl),
on_activate_call=self._duplicate_playlist,
color=b_color,
autoselect=True,
textcolor=b_textcolor,
button_type='square',
text_scale=0.7,
label=ba.Lstr(
resource='duplicateText',
fallback_resource=self._r + '.duplicateText',
),
)
self._lock_images.append(
ba.imagewidget(
parent=self._root_widget,
size=(30, 30),
draw_controller=btn,
position=(h - 10, v + 58.0 * scl - 28),
texture=lock_tex,
)
)
v -= 65.0 * scl
delete_button = btn = ba.buttonwidget(
parent=self._root_widget,
position=(h, v),
size=(90, 58.0 * scl),
on_activate_call=self._delete_playlist,
color=b_color,
autoselect=True,
textcolor=b_textcolor,
button_type='square',
text_scale=0.7,
label=ba.Lstr(
resource='deleteText', fallback_resource=self._r + '.deleteText'
),
)
self._lock_images.append(
ba.imagewidget(
parent=self._root_widget,
size=(30, 30),
draw_controller=btn,
position=(h - 10, v + 58.0 * scl - 28),
texture=lock_tex,
)
)
v -= 65.0 * scl
self._import_button = ba.buttonwidget(
parent=self._root_widget,
position=(h, v),
size=(90, 58.0 * scl),
on_activate_call=self._import_playlist,
color=b_color,
autoselect=True,
textcolor=b_textcolor,
button_type='square',
text_scale=0.7,
label=ba.Lstr(resource='importText'),
)
v -= 65.0 * scl
btn = ba.buttonwidget(
parent=self._root_widget,
position=(h, v),
size=(90, 58.0 * scl),
on_activate_call=self._share_playlist,
color=b_color,
autoselect=True,
textcolor=b_textcolor,
button_type='square',
text_scale=0.7,
label=ba.Lstr(resource='shareText'),
)
self._lock_images.append(
ba.imagewidget(
parent=self._root_widget,
size=(30, 30),
draw_controller=btn,
position=(h - 10, v + 58.0 * scl - 28),
texture=lock_tex,
)
)
v = self._height - 75
self._scroll_height = self._height - 119
scrollwidget = ba.scrollwidget(
parent=self._root_widget,
position=(140 + x_inset, v - self._scroll_height),
size=(self._width - (180 + 2 * x_inset), self._scroll_height + 10),
highlight=False,
)
ba.widget(edit=back_button, right_widget=scrollwidget)
self._columnwidget = ba.columnwidget(
parent=scrollwidget, border=2, margin=0
)
h = 145
self._do_randomize_val = ba.app.config.get(
self._pvars.config_name + ' Playlist Randomize', 0
)
h += 210
for btn in [new_button, delete_button, edit_button, duplicate_button]:
ba.widget(edit=btn, right_widget=scrollwidget)
ba.widget(
edit=scrollwidget,
left_widget=new_button,
right_widget=ba.internal.get_special_widget('party_button')
if ba.app.ui.use_toolbars
else None,
)
# make sure config exists
self._config_name_full = self._pvars.config_name + ' Playlists'
if self._config_name_full not in ba.app.config:
ba.app.config[self._config_name_full] = {}
self._selected_playlist_name: str | None = None
self._selected_playlist_index: int | None = None
self._playlist_widgets: list[ba.Widget] = []
self._refresh(select_playlist=select_playlist)
ba.buttonwidget(edit=back_button, on_activate_call=self._back)
ba.containerwidget(edit=self._root_widget, cancel_button=back_button)
ba.containerwidget(edit=self._root_widget, selected_child=scrollwidget)
# Keep our lock images up to date/etc.
self._update_timer = ba.Timer(
1.0,
ba.WeakCall(self._update),
timetype=ba.TimeType.REAL,
repeat=True,
)
self._update()
def _update(self) -> None:
have = ba.app.accounts_v1.have_pro_options()
for lock in self._lock_images:
ba.imagewidget(edit=lock, opacity=0.0 if have else 1.0)
def _back(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.playlist import browser
if self._selected_playlist_name is not None:
cfg = ba.app.config
cfg[
self._pvars.config_name + ' Playlist Selection'
] = self._selected_playlist_name
cfg.commit()
ba.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
ba.app.ui.set_main_menu_window(
browser.PlaylistBrowserWindow(
transition='in_left', sessiontype=self._sessiontype
).get_root_widget()
)
def _select(self, name: str, index: int) -> None:
self._selected_playlist_name = name
self._selected_playlist_index = index
def _run_selected_playlist(self) -> None:
# pylint: disable=cyclic-import
ba.internal.unlock_all_input()
try:
ba.internal.new_host_session(self._sessiontype)
except Exception:
from bastd import mainmenu
ba.print_exception(f'Error running session {self._sessiontype}.')
# Drop back into a main menu session.
ba.internal.new_host_session(mainmenu.MainMenuSession)
def _choose_playlist(self) -> None:
if self._selected_playlist_name is None:
return
self._save_playlist_selection()
ba.containerwidget(edit=self._root_widget, transition='out_left')
ba.internal.fade_screen(False, endcall=self._run_selected_playlist)
ba.internal.lock_all_input()
def _refresh(self, select_playlist: str | None = None) -> None:
from efro.util import asserttype
old_selection = self._selected_playlist_name
# If there was no prev selection, look in prefs.
if old_selection is None:
old_selection = ba.app.config.get(
self._pvars.config_name + ' Playlist Selection'
)
old_selection_index = self._selected_playlist_index
# Delete old.
while self._playlist_widgets:
self._playlist_widgets.pop().delete()
items = list(ba.app.config[self._config_name_full].items())
# Make sure everything is unicode now.
items = [
(i[0].decode(), i[1]) if not isinstance(i[0], str) else i
for i in items
]
items.sort(key=lambda x: asserttype(x[0], str).lower())
items = [['__default__', None]] + items # Default is always first.
index = 0
for pname, _ in items:
assert pname is not None
txtw = ba.textwidget(
parent=self._columnwidget,
size=(self._width - 40, 30),
maxwidth=self._width - 110,
text=self._get_playlist_display_name(pname),
h_align='left',
v_align='center',
color=(0.6, 0.6, 0.7, 1.0)
if pname == '__default__'
else (0.85, 0.85, 0.85, 1),
always_highlight=True,
on_select_call=ba.Call(self._select, pname, index),
on_activate_call=ba.Call(self._edit_button.activate),
selectable=True,
)
ba.widget(edit=txtw, show_buffer_top=50, show_buffer_bottom=50)
# Hitting up from top widget should jump to 'back'
if index == 0:
ba.widget(edit=txtw, up_widget=self._back_button)
self._playlist_widgets.append(txtw)
# Select this one if the user requested it.
if select_playlist is not None:
if pname == select_playlist:
ba.columnwidget(
edit=self._columnwidget,
selected_child=txtw,
visible_child=txtw,
)
else:
# Select this one if it was previously selected.
# Go by index if there's one.
if old_selection_index is not None:
if index == old_selection_index:
ba.columnwidget(
edit=self._columnwidget,
selected_child=txtw,
visible_child=txtw,
)
else: # Otherwise look by name.
if pname == old_selection:
ba.columnwidget(
edit=self._columnwidget,
selected_child=txtw,
visible_child=txtw,
)
index += 1
def _save_playlist_selection(self) -> None:
# Store the selected playlist in prefs.
# This serves dual purposes of letting us re-select it next time
# if we want and also lets us pass it to the game (since we reset
# the whole python environment that's not actually easy).
cfg = ba.app.config
cfg[
self._pvars.config_name + ' Playlist Selection'
] = self._selected_playlist_name
cfg[
self._pvars.config_name + ' Playlist Randomize'
] = self._do_randomize_val
cfg.commit()
def _new_playlist(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.playlist.editcontroller import PlaylistEditController
from bastd.ui.purchase import PurchaseWindow
if not ba.app.accounts_v1.have_pro_options():
PurchaseWindow(items=['pro'])
return
# Clamp at our max playlist number.
if len(ba.app.config[self._config_name_full]) > self._max_playlists:
ba.screenmessage(
ba.Lstr(
translate=(
'serverResponses',
'Max number of playlists reached.',
)
),
color=(1, 0, 0),
)
ba.playsound(ba.getsound('error'))
return
# In case they cancel so we can return to this state.
self._save_playlist_selection()
# Kick off the edit UI.
PlaylistEditController(sessiontype=self._sessiontype)
ba.containerwidget(edit=self._root_widget, transition='out_left')
def _edit_playlist(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.playlist.editcontroller import PlaylistEditController
from bastd.ui.purchase import PurchaseWindow
if not ba.app.accounts_v1.have_pro_options():
PurchaseWindow(items=['pro'])
return
if self._selected_playlist_name is None:
return
if self._selected_playlist_name == '__default__':
ba.playsound(ba.getsound('error'))
ba.screenmessage(ba.Lstr(resource=self._r + '.cantEditDefaultText'))
return
self._save_playlist_selection()
PlaylistEditController(
existing_playlist_name=self._selected_playlist_name,
sessiontype=self._sessiontype,
)
ba.containerwidget(edit=self._root_widget, transition='out_left')
def _do_delete_playlist(self) -> None:
ba.internal.add_transaction(
{
'type': 'REMOVE_PLAYLIST',
'playlistType': self._pvars.config_name,
'playlistName': self._selected_playlist_name,
}
)
ba.internal.run_transactions()
ba.playsound(ba.getsound('shieldDown'))
# (we don't use len()-1 here because the default list adds one)
assert self._selected_playlist_index is not None
if self._selected_playlist_index > len(
ba.app.config[self._pvars.config_name + ' Playlists']
):
self._selected_playlist_index = len(
ba.app.config[self._pvars.config_name + ' Playlists']
)
self._refresh()
def _import_playlist(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.playlist import share
# Gotta be signed in for this to work.
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
share.SharePlaylistImportWindow(
origin_widget=self._import_button,
on_success_callback=ba.WeakCall(self._on_playlist_import_success),
)
def _on_playlist_import_success(self) -> None:
self._refresh()
def _on_share_playlist_response(self, name: str, response: Any) -> None:
# pylint: disable=cyclic-import
from bastd.ui.playlist import share
if response is None:
ba.screenmessage(
ba.Lstr(resource='internal.unavailableNoConnectionText'),
color=(1, 0, 0),
)
ba.playsound(ba.getsound('error'))
return
share.SharePlaylistResultsWindow(name, response)
def _share_playlist(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.purchase import PurchaseWindow
if not ba.app.accounts_v1.have_pro_options():
PurchaseWindow(items=['pro'])
return
# Gotta be signed in for this to work.
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
if self._selected_playlist_name == '__default__':
ba.playsound(ba.getsound('error'))
ba.screenmessage(
ba.Lstr(resource=self._r + '.cantShareDefaultText'),
color=(1, 0, 0),
)
return
if self._selected_playlist_name is None:
return
ba.internal.add_transaction(
{
'type': 'SHARE_PLAYLIST',
'expire_time': time.time() + 5,
'playlistType': self._pvars.config_name,
'playlistName': self._selected_playlist_name,
},
callback=ba.WeakCall(
self._on_share_playlist_response, self._selected_playlist_name
),
)
ba.internal.run_transactions()
ba.screenmessage(ba.Lstr(resource='sharingText'))
def _delete_playlist(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.purchase import PurchaseWindow
from bastd.ui.confirm import ConfirmWindow
if not ba.app.accounts_v1.have_pro_options():
PurchaseWindow(items=['pro'])
return
if self._selected_playlist_name is None:
return
if self._selected_playlist_name == '__default__':
ba.playsound(ba.getsound('error'))
ba.screenmessage(
ba.Lstr(resource=self._r + '.cantDeleteDefaultText')
)
else:
ConfirmWindow(
ba.Lstr(
resource=self._r + '.deleteConfirmText',
subs=[('${LIST}', self._selected_playlist_name)],
),
self._do_delete_playlist,
450,
150,
)
def _get_playlist_display_name(self, playlist: str) -> ba.Lstr:
if playlist == '__default__':
return self._pvars.default_list_name
return (
playlist
if isinstance(playlist, ba.Lstr)
else ba.Lstr(value=playlist)
)
def _duplicate_playlist(self) -> None:
# pylint: disable=too-many-branches
# pylint: disable=cyclic-import
from bastd.ui.purchase import PurchaseWindow
if not ba.app.accounts_v1.have_pro_options():
PurchaseWindow(items=['pro'])
return
if self._selected_playlist_name is None:
return
plst: list[dict[str, Any]] | None
if self._selected_playlist_name == '__default__':
plst = self._pvars.get_default_list_call()
else:
plst = ba.app.config[self._config_name_full].get(
self._selected_playlist_name
)
if plst is None:
ba.playsound(ba.getsound('error'))
return
# clamp at our max playlist number
if len(ba.app.config[self._config_name_full]) > self._max_playlists:
ba.screenmessage(
ba.Lstr(
translate=(
'serverResponses',
'Max number of playlists reached.',
)
),
color=(1, 0, 0),
)
ba.playsound(ba.getsound('error'))
return
copy_text = ba.Lstr(resource='copyOfText').evaluate()
# get just 'Copy' or whatnot
copy_word = copy_text.replace('${NAME}', '').strip()
# find a valid dup name that doesn't exist
test_index = 1
base_name = self._get_playlist_display_name(
self._selected_playlist_name
).evaluate()
# If it looks like a copy, strip digits and spaces off the end.
if copy_word in base_name:
while base_name[-1].isdigit() or base_name[-1] == ' ':
base_name = base_name[:-1]
while True:
if copy_word in base_name:
test_name = base_name
else:
test_name = copy_text.replace('${NAME}', base_name)
if test_index > 1:
test_name += ' ' + str(test_index)
if test_name not in ba.app.config[self._config_name_full]:
break
test_index += 1
ba.internal.add_transaction(
{
'type': 'ADD_PLAYLIST',
'playlistType': self._pvars.config_name,
'playlistName': test_name,
'playlist': copy.deepcopy(plst),
}
)
ba.internal.run_transactions()
ba.playsound(ba.getsound('gunCocking'))
self._refresh(select_playlist=test_name)

View file

@ -0,0 +1,467 @@
# Released under the MIT License. See LICENSE for details.
#
"""Provides a window for editing individual game playlists."""
from __future__ import annotations
from typing import TYPE_CHECKING, cast
import ba
import ba.internal
if TYPE_CHECKING:
from bastd.ui.playlist.editcontroller import PlaylistEditController
class PlaylistEditWindow(ba.Window):
"""Window for editing an individual game playlist."""
def __init__(
self,
editcontroller: PlaylistEditController,
transition: str = 'in_right',
):
# pylint: disable=too-many-statements
# pylint: disable=too-many-locals
prev_selection: str | None
self._editcontroller = editcontroller
self._r = 'editGameListWindow'
prev_selection = self._editcontroller.get_edit_ui_selection()
uiscale = ba.app.ui.uiscale
self._width = 770 if uiscale is ba.UIScale.SMALL else 670
x_inset = 50 if uiscale is ba.UIScale.SMALL else 0
self._height = (
400
if uiscale is ba.UIScale.SMALL
else 470
if uiscale is ba.UIScale.MEDIUM
else 540
)
top_extra = 20 if uiscale is ba.UIScale.SMALL else 0
super().__init__(
root_widget=ba.containerwidget(
size=(self._width, self._height + top_extra),
transition=transition,
scale=(
2.0
if uiscale is ba.UIScale.SMALL
else 1.3
if uiscale is ba.UIScale.MEDIUM
else 1.0
),
stack_offset=(0, -16)
if uiscale is ba.UIScale.SMALL
else (0, 0),
)
)
cancel_button = ba.buttonwidget(
parent=self._root_widget,
position=(35 + x_inset, self._height - 60),
scale=0.8,
size=(175, 60),
autoselect=True,
label=ba.Lstr(resource='cancelText'),
text_scale=1.2,
)
save_button = btn = ba.buttonwidget(
parent=self._root_widget,
position=(self._width - (195 + x_inset), self._height - 60),
scale=0.8,
size=(190, 60),
autoselect=True,
left_widget=cancel_button,
label=ba.Lstr(resource='saveText'),
text_scale=1.2,
)
if ba.app.ui.use_toolbars:
ba.widget(
edit=btn,
right_widget=ba.internal.get_special_widget('party_button'),
)
ba.widget(
edit=cancel_button,
left_widget=cancel_button,
right_widget=save_button,
)
ba.textwidget(
parent=self._root_widget,
position=(-10, self._height - 50),
size=(self._width, 25),
text=ba.Lstr(resource=self._r + '.titleText'),
color=ba.app.ui.title_color,
scale=1.05,
h_align='center',
v_align='center',
maxwidth=270,
)
v = self._height - 115.0
self._scroll_width = self._width - (205 + 2 * x_inset)
ba.textwidget(
parent=self._root_widget,
text=ba.Lstr(resource=self._r + '.listNameText'),
position=(196 + x_inset, v + 31),
maxwidth=150,
color=(0.8, 0.8, 0.8, 0.5),
size=(0, 0),
scale=0.75,
h_align='right',
v_align='center',
)
self._text_field = ba.textwidget(
parent=self._root_widget,
position=(210 + x_inset, v + 7),
size=(self._scroll_width - 53, 43),
text=self._editcontroller.getname(),
h_align='left',
v_align='center',
max_chars=40,
autoselect=True,
color=(0.9, 0.9, 0.9, 1.0),
description=ba.Lstr(resource=self._r + '.listNameText'),
editable=True,
padding=4,
on_return_press_call=self._save_press_with_sound,
)
ba.widget(edit=cancel_button, down_widget=self._text_field)
self._list_widgets: list[ba.Widget] = []
h = 40 + x_inset
v = self._height - 172.0
b_color = (0.6, 0.53, 0.63)
b_textcolor = (0.75, 0.7, 0.8)
v -= 2.0
v += 63
scl = (
1.03
if uiscale is ba.UIScale.SMALL
else 1.36
if uiscale is ba.UIScale.MEDIUM
else 1.74
)
v -= 63.0 * scl
add_game_button = ba.buttonwidget(
parent=self._root_widget,
position=(h, v),
size=(110, 61.0 * scl),
on_activate_call=self._add,
on_select_call=ba.Call(self._set_ui_selection, 'add_button'),
autoselect=True,
button_type='square',
color=b_color,
textcolor=b_textcolor,
text_scale=0.8,
label=ba.Lstr(resource=self._r + '.addGameText'),
)
ba.widget(edit=add_game_button, up_widget=self._text_field)
v -= 63.0 * scl
self._edit_button = edit_game_button = ba.buttonwidget(
parent=self._root_widget,
position=(h, v),
size=(110, 61.0 * scl),
on_activate_call=self._edit,
on_select_call=ba.Call(self._set_ui_selection, 'editButton'),
autoselect=True,
button_type='square',
color=b_color,
textcolor=b_textcolor,
text_scale=0.8,
label=ba.Lstr(resource=self._r + '.editGameText'),
)
v -= 63.0 * scl
remove_game_button = ba.buttonwidget(
parent=self._root_widget,
position=(h, v),
size=(110, 61.0 * scl),
text_scale=0.8,
on_activate_call=self._remove,
autoselect=True,
button_type='square',
color=b_color,
textcolor=b_textcolor,
label=ba.Lstr(resource=self._r + '.removeGameText'),
)
v -= 40
h += 9
ba.buttonwidget(
parent=self._root_widget,
position=(h, v),
size=(42, 35),
on_activate_call=self._move_up,
label=ba.charstr(ba.SpecialChar.UP_ARROW),
button_type='square',
color=b_color,
textcolor=b_textcolor,
autoselect=True,
repeat=True,
)
h += 52
ba.buttonwidget(
parent=self._root_widget,
position=(h, v),
size=(42, 35),
on_activate_call=self._move_down,
autoselect=True,
button_type='square',
color=b_color,
textcolor=b_textcolor,
label=ba.charstr(ba.SpecialChar.DOWN_ARROW),
repeat=True,
)
v = self._height - 100
scroll_height = self._height - 155
scrollwidget = ba.scrollwidget(
parent=self._root_widget,
position=(160 + x_inset, v - scroll_height),
highlight=False,
on_select_call=ba.Call(self._set_ui_selection, 'gameList'),
size=(self._scroll_width, (scroll_height - 15)),
)
ba.widget(
edit=scrollwidget,
left_widget=add_game_button,
right_widget=scrollwidget,
)
self._columnwidget = ba.columnwidget(
parent=scrollwidget, border=2, margin=0
)
ba.widget(edit=self._columnwidget, up_widget=self._text_field)
for button in [add_game_button, edit_game_button, remove_game_button]:
ba.widget(
edit=button, left_widget=button, right_widget=scrollwidget
)
self._refresh()
ba.buttonwidget(edit=cancel_button, on_activate_call=self._cancel)
ba.containerwidget(
edit=self._root_widget,
cancel_button=cancel_button,
selected_child=scrollwidget,
)
ba.buttonwidget(edit=save_button, on_activate_call=self._save_press)
ba.containerwidget(edit=self._root_widget, start_button=save_button)
if prev_selection == 'add_button':
ba.containerwidget(
edit=self._root_widget, selected_child=add_game_button
)
elif prev_selection == 'editButton':
ba.containerwidget(
edit=self._root_widget, selected_child=edit_game_button
)
elif prev_selection == 'gameList':
ba.containerwidget(
edit=self._root_widget, selected_child=scrollwidget
)
def _set_ui_selection(self, selection: str) -> None:
self._editcontroller.set_edit_ui_selection(selection)
def _cancel(self) -> None:
from bastd.ui.playlist.customizebrowser import (
PlaylistCustomizeBrowserWindow,
)
ba.playsound(ba.getsound('powerdown01'))
ba.containerwidget(edit=self._root_widget, transition='out_right')
ba.app.ui.set_main_menu_window(
PlaylistCustomizeBrowserWindow(
transition='in_left',
sessiontype=self._editcontroller.get_session_type(),
select_playlist=(
self._editcontroller.get_existing_playlist_name()
),
).get_root_widget()
)
def _add(self) -> None:
# Store list name then tell the session to perform an add.
self._editcontroller.setname(
cast(str, ba.textwidget(query=self._text_field))
)
self._editcontroller.add_game_pressed()
def _edit(self) -> None:
# Store list name then tell the session to perform an add.
self._editcontroller.setname(
cast(str, ba.textwidget(query=self._text_field))
)
self._editcontroller.edit_game_pressed()
def _save_press(self) -> None:
from bastd.ui.playlist.customizebrowser import (
PlaylistCustomizeBrowserWindow,
)
new_name = cast(str, ba.textwidget(query=self._text_field))
if (
new_name != self._editcontroller.get_existing_playlist_name()
and new_name
in ba.app.config[
self._editcontroller.get_config_name() + ' Playlists'
]
):
ba.screenmessage(
ba.Lstr(resource=self._r + '.cantSaveAlreadyExistsText')
)
ba.playsound(ba.getsound('error'))
return
if not new_name:
ba.playsound(ba.getsound('error'))
return
if not self._editcontroller.get_playlist():
ba.screenmessage(
ba.Lstr(resource=self._r + '.cantSaveEmptyListText')
)
ba.playsound(ba.getsound('error'))
return
# We couldn't actually replace the default list anyway, but disallow
# using its exact name to avoid confusion.
if new_name == self._editcontroller.get_default_list_name().evaluate():
ba.screenmessage(
ba.Lstr(resource=self._r + '.cantOverwriteDefaultText')
)
ba.playsound(ba.getsound('error'))
return
# If we had an old one, delete it.
if self._editcontroller.get_existing_playlist_name() is not None:
ba.internal.add_transaction(
{
'type': 'REMOVE_PLAYLIST',
'playlistType': self._editcontroller.get_config_name(),
'playlistName': (
self._editcontroller.get_existing_playlist_name()
),
}
)
ba.internal.add_transaction(
{
'type': 'ADD_PLAYLIST',
'playlistType': self._editcontroller.get_config_name(),
'playlistName': new_name,
'playlist': self._editcontroller.get_playlist(),
}
)
ba.internal.run_transactions()
ba.containerwidget(edit=self._root_widget, transition='out_right')
ba.playsound(ba.getsound('gunCocking'))
ba.app.ui.set_main_menu_window(
PlaylistCustomizeBrowserWindow(
transition='in_left',
sessiontype=self._editcontroller.get_session_type(),
select_playlist=new_name,
).get_root_widget()
)
def _save_press_with_sound(self) -> None:
ba.playsound(ba.getsound('swish'))
self._save_press()
def _select(self, index: int) -> None:
self._editcontroller.set_selected_index(index)
def _refresh(self) -> None:
from ba.internal import getclass
# Need to grab this here as rebuilding the list will
# change it otherwise.
old_selection_index = self._editcontroller.get_selected_index()
while self._list_widgets:
self._list_widgets.pop().delete()
for index, pentry in enumerate(self._editcontroller.get_playlist()):
try:
cls = getclass(pentry['type'], subclassof=ba.GameActivity)
desc = cls.get_settings_display_string(pentry)
except Exception:
ba.print_exception()
desc = "(invalid: '" + pentry['type'] + "')"
txtw = ba.textwidget(
parent=self._columnwidget,
size=(self._width - 80, 30),
on_select_call=ba.Call(self._select, index),
always_highlight=True,
color=(0.8, 0.8, 0.8, 1.0),
padding=0,
maxwidth=self._scroll_width * 0.93,
text=desc,
on_activate_call=self._edit_button.activate,
v_align='center',
selectable=True,
)
ba.widget(edit=txtw, show_buffer_top=50, show_buffer_bottom=50)
# Wanna be able to jump up to the text field from the top one.
if index == 0:
ba.widget(edit=txtw, up_widget=self._text_field)
self._list_widgets.append(txtw)
if old_selection_index == index:
ba.columnwidget(
edit=self._columnwidget,
selected_child=txtw,
visible_child=txtw,
)
def _move_down(self) -> None:
playlist = self._editcontroller.get_playlist()
index = self._editcontroller.get_selected_index()
if index >= len(playlist) - 1:
return
tmp = playlist[index]
playlist[index] = playlist[index + 1]
playlist[index + 1] = tmp
index += 1
self._editcontroller.set_playlist(playlist)
self._editcontroller.set_selected_index(index)
self._refresh()
def _move_up(self) -> None:
playlist = self._editcontroller.get_playlist()
index = self._editcontroller.get_selected_index()
if index < 1:
return
tmp = playlist[index]
playlist[index] = playlist[index - 1]
playlist[index - 1] = tmp
index -= 1
self._editcontroller.set_playlist(playlist)
self._editcontroller.set_selected_index(index)
self._refresh()
def _remove(self) -> None:
playlist = self._editcontroller.get_playlist()
index = self._editcontroller.get_selected_index()
if not playlist:
return
del playlist[index]
if index >= len(playlist):
index = len(playlist) - 1
self._editcontroller.set_playlist(playlist)
self._editcontroller.set_selected_index(index)
ba.playsound(ba.getsound('shieldDown'))
self._refresh()

View file

@ -0,0 +1,236 @@
# Released under the MIT License. See LICENSE for details.
#
"""Defines a controller for wrangling playlist edit UIs."""
from __future__ import annotations
import copy
from typing import TYPE_CHECKING
import ba
if TYPE_CHECKING:
from typing import Any
class PlaylistEditController:
"""Coordinates various UIs involved in playlist editing."""
def __init__(
self,
sessiontype: type[ba.Session],
existing_playlist_name: str | None = None,
transition: str = 'in_right',
playlist: list[dict[str, Any]] | None = None,
playlist_name: str | None = None,
):
from ba.internal import preload_map_preview_media, filter_playlist
from bastd.ui.playlist import PlaylistTypeVars
from bastd.ui.playlist.edit import PlaylistEditWindow
appconfig = ba.app.config
# Since we may be showing our map list momentarily,
# lets go ahead and preload all map preview textures.
preload_map_preview_media()
self._sessiontype = sessiontype
self._editing_game = False
self._editing_game_type: type[ba.GameActivity] | None = None
self._pvars = PlaylistTypeVars(sessiontype)
self._existing_playlist_name = existing_playlist_name
self._config_name_full = self._pvars.config_name + ' Playlists'
# Make sure config exists.
if self._config_name_full not in appconfig:
appconfig[self._config_name_full] = {}
self._selected_index = 0
if existing_playlist_name:
self._name = existing_playlist_name
# Filter out invalid games.
self._playlist = filter_playlist(
appconfig[self._pvars.config_name + ' Playlists'][
existing_playlist_name
],
sessiontype=sessiontype,
remove_unowned=False,
name=existing_playlist_name,
)
self._edit_ui_selection = None
else:
if playlist is not None:
self._playlist = playlist
else:
self._playlist = []
if playlist_name is not None:
self._name = playlist_name
else:
# Find a good unused name.
i = 1
while True:
self._name = (
self._pvars.default_new_list_name.evaluate()
+ ((' ' + str(i)) if i > 1 else '')
)
if (
self._name
not in appconfig[self._pvars.config_name + ' Playlists']
):
break
i += 1
# Also we want it to start with 'add' highlighted since its empty
# and that's all they can do.
self._edit_ui_selection = 'add_button'
ba.app.ui.set_main_menu_window(
PlaylistEditWindow(
editcontroller=self, transition=transition
).get_root_widget()
)
def get_config_name(self) -> str:
"""(internal)"""
return self._pvars.config_name
def get_existing_playlist_name(self) -> str | None:
"""(internal)"""
return self._existing_playlist_name
def get_edit_ui_selection(self) -> str | None:
"""(internal)"""
return self._edit_ui_selection
def set_edit_ui_selection(self, selection: str) -> None:
"""(internal)"""
self._edit_ui_selection = selection
def getname(self) -> str:
"""(internal)"""
return self._name
def setname(self, name: str) -> None:
"""(internal)"""
self._name = name
def get_playlist(self) -> list[dict[str, Any]]:
"""Return the current state of the edited playlist."""
return copy.deepcopy(self._playlist)
def set_playlist(self, playlist: list[dict[str, Any]]) -> None:
"""Set the playlist contents."""
self._playlist = copy.deepcopy(playlist)
def get_session_type(self) -> type[ba.Session]:
"""Return the ba.Session type for this edit-session."""
return self._sessiontype
def get_selected_index(self) -> int:
"""Return the index of the selected playlist."""
return self._selected_index
def get_default_list_name(self) -> ba.Lstr:
"""(internal)"""
return self._pvars.default_list_name
def set_selected_index(self, index: int) -> None:
"""Sets the selected playlist index."""
self._selected_index = index
def add_game_pressed(self) -> None:
"""(internal)"""
from bastd.ui.playlist.addgame import PlaylistAddGameWindow
ba.app.ui.clear_main_menu_window(transition='out_left')
ba.app.ui.set_main_menu_window(
PlaylistAddGameWindow(editcontroller=self).get_root_widget()
)
def edit_game_pressed(self) -> None:
"""Should be called by supplemental UIs when a game is to be edited."""
from ba.internal import getclass
if not self._playlist:
return
self._show_edit_ui(
gametype=getclass(
self._playlist[self._selected_index]['type'],
subclassof=ba.GameActivity,
),
settings=self._playlist[self._selected_index],
)
def add_game_cancelled(self) -> None:
"""(internal)"""
from bastd.ui.playlist.edit import PlaylistEditWindow
ba.app.ui.clear_main_menu_window(transition='out_right')
ba.app.ui.set_main_menu_window(
PlaylistEditWindow(
editcontroller=self, transition='in_left'
).get_root_widget()
)
def _show_edit_ui(
self, gametype: type[ba.GameActivity], settings: dict[str, Any] | None
) -> None:
self._editing_game = settings is not None
self._editing_game_type = gametype
assert self._sessiontype is not None
gametype.create_settings_ui(
self._sessiontype, copy.deepcopy(settings), self._edit_game_done
)
def add_game_type_selected(self, gametype: type[ba.GameActivity]) -> None:
"""(internal)"""
self._show_edit_ui(gametype=gametype, settings=None)
def _edit_game_done(self, config: dict[str, Any] | None) -> None:
from bastd.ui.playlist.edit import PlaylistEditWindow
from bastd.ui.playlist.addgame import PlaylistAddGameWindow
from ba.internal import get_type_name
if config is None:
# If we were editing, go back to our list.
if self._editing_game:
ba.playsound(ba.getsound('powerdown01'))
ba.app.ui.clear_main_menu_window(transition='out_right')
ba.app.ui.set_main_menu_window(
PlaylistEditWindow(
editcontroller=self, transition='in_left'
).get_root_widget()
)
# Otherwise we were adding; go back to the add type choice list.
else:
ba.app.ui.clear_main_menu_window(transition='out_right')
ba.app.ui.set_main_menu_window(
PlaylistAddGameWindow(
editcontroller=self, transition='in_left'
).get_root_widget()
)
else:
# Make sure type is in there.
assert self._editing_game_type is not None
config['type'] = get_type_name(self._editing_game_type)
if self._editing_game:
self._playlist[self._selected_index] = copy.deepcopy(config)
else:
# Add a new entry to the playlist.
insert_index = min(
len(self._playlist), self._selected_index + 1
)
self._playlist.insert(insert_index, copy.deepcopy(config))
self._selected_index = insert_index
ba.playsound(ba.getsound('gunCocking'))
ba.app.ui.clear_main_menu_window(transition='out_right')
ba.app.ui.set_main_menu_window(
PlaylistEditWindow(
editcontroller=self, transition='in_left'
).get_root_widget()
)

View file

@ -0,0 +1,595 @@
# Released under the MIT License. See LICENSE for details.
#
"""Provides UI for editing a game config."""
from __future__ import annotations
import copy
import random
from typing import TYPE_CHECKING, cast
import ba
import ba.internal
if TYPE_CHECKING:
from typing import Any, Callable
class PlaylistEditGameWindow(ba.Window):
"""Window for editing a game config."""
def __init__(
self,
gametype: type[ba.GameActivity],
sessiontype: type[ba.Session],
config: dict[str, Any] | None,
completion_call: Callable[[dict[str, Any] | None], Any],
default_selection: str | None = None,
transition: str = 'in_right',
edit_info: dict[str, Any] | None = None,
):
# pylint: disable=too-many-branches
# pylint: disable=too-many-statements
# pylint: disable=too-many-locals
from ba.internal import (
get_unowned_maps,
get_filtered_map_name,
get_map_class,
get_map_display_string,
)
self._gametype = gametype
self._sessiontype = sessiontype
# If we're within an editing session we get passed edit_info
# (returning from map selection window, etc).
if edit_info is not None:
self._edit_info = edit_info
# ..otherwise determine whether we're adding or editing a game based
# on whether an existing config was passed to us.
else:
if config is None:
self._edit_info = {'editType': 'add'}
else:
self._edit_info = {'editType': 'edit'}
self._r = 'gameSettingsWindow'
valid_maps = gametype.get_supported_maps(sessiontype)
if not valid_maps:
ba.screenmessage(ba.Lstr(resource='noValidMapsErrorText'))
raise Exception('No valid maps')
self._settings_defs = gametype.get_available_settings(sessiontype)
self._completion_call = completion_call
# To start with, pick a random map out of the ones we own.
unowned_maps = get_unowned_maps()
valid_maps_owned = [m for m in valid_maps if m not in unowned_maps]
if valid_maps_owned:
self._map = valid_maps[random.randrange(len(valid_maps_owned))]
# Hmmm.. we own none of these maps.. just pick a random un-owned one
# I guess.. should this ever happen?
else:
self._map = valid_maps[random.randrange(len(valid_maps))]
is_add = self._edit_info['editType'] == 'add'
# If there's a valid map name in the existing config, use that.
try:
if (
config is not None
and 'settings' in config
and 'map' in config['settings']
):
filtered_map_name = get_filtered_map_name(
config['settings']['map']
)
if filtered_map_name in valid_maps:
self._map = filtered_map_name
except Exception:
ba.print_exception('Error getting map for editor.')
if config is not None and 'settings' in config:
self._settings = config['settings']
else:
self._settings = {}
self._choice_selections: dict[str, int] = {}
uiscale = ba.app.ui.uiscale
width = 720 if uiscale is ba.UIScale.SMALL else 620
x_inset = 50 if uiscale is ba.UIScale.SMALL else 0
height = (
365
if uiscale is ba.UIScale.SMALL
else 460
if uiscale is ba.UIScale.MEDIUM
else 550
)
spacing = 52
y_extra = 15
y_extra2 = 21
map_tex_name = get_map_class(self._map).get_preview_texture_name()
if map_tex_name is None:
raise Exception('no map preview tex found for' + self._map)
map_tex = ba.gettexture(map_tex_name)
top_extra = 20 if uiscale is ba.UIScale.SMALL else 0
super().__init__(
root_widget=ba.containerwidget(
size=(width, height + top_extra),
transition=transition,
scale=(
2.19
if uiscale is ba.UIScale.SMALL
else 1.35
if uiscale is ba.UIScale.MEDIUM
else 1.0
),
stack_offset=(0, -17)
if uiscale is ba.UIScale.SMALL
else (0, 0),
)
)
btn = ba.buttonwidget(
parent=self._root_widget,
position=(45 + x_inset, height - 82 + y_extra2),
size=(180, 70) if is_add else (180, 65),
label=ba.Lstr(resource='backText')
if is_add
else ba.Lstr(resource='cancelText'),
button_type='back' if is_add else None,
autoselect=True,
scale=0.75,
text_scale=1.3,
on_activate_call=ba.Call(self._cancel),
)
ba.containerwidget(edit=self._root_widget, cancel_button=btn)
add_button = ba.buttonwidget(
parent=self._root_widget,
position=(width - (193 + x_inset), height - 82 + y_extra2),
size=(200, 65),
scale=0.75,
text_scale=1.3,
label=ba.Lstr(resource=self._r + '.addGameText')
if is_add
else ba.Lstr(resource='doneText'),
)
if ba.app.ui.use_toolbars:
pbtn = ba.internal.get_special_widget('party_button')
ba.widget(edit=add_button, right_widget=pbtn, up_widget=pbtn)
ba.textwidget(
parent=self._root_widget,
position=(-8, height - 70 + y_extra2),
size=(width, 25),
text=gametype.get_display_string(),
color=ba.app.ui.title_color,
maxwidth=235,
scale=1.1,
h_align='center',
v_align='center',
)
map_height = 100
scroll_height = map_height + 10 # map select and margin
# Calc our total height we'll need
scroll_height += spacing * len(self._settings_defs)
scroll_width = width - (86 + 2 * x_inset)
self._scrollwidget = ba.scrollwidget(
parent=self._root_widget,
position=(44 + x_inset, 35 + y_extra),
size=(scroll_width, height - 116),
highlight=False,
claims_left_right=True,
claims_tab=True,
selection_loops_to_parent=True,
)
self._subcontainer = ba.containerwidget(
parent=self._scrollwidget,
size=(scroll_width, scroll_height),
background=False,
claims_left_right=True,
claims_tab=True,
selection_loops_to_parent=True,
)
v = scroll_height - 5
h = -40
# Keep track of all the selectable widgets we make so we can wire
# them up conveniently.
widget_column: list[list[ba.Widget]] = []
# Map select button.
ba.textwidget(
parent=self._subcontainer,
position=(h + 49, v - 63),
size=(100, 30),
maxwidth=110,
text=ba.Lstr(resource='mapText'),
h_align='left',
color=(0.8, 0.8, 0.8, 1.0),
v_align='center',
)
ba.imagewidget(
parent=self._subcontainer,
size=(256 * 0.7, 125 * 0.7),
position=(h + 261 - 128 + 128.0 * 0.56, v - 90),
texture=map_tex,
model_opaque=ba.getmodel('level_select_button_opaque'),
model_transparent=ba.getmodel('level_select_button_transparent'),
mask_texture=ba.gettexture('mapPreviewMask'),
)
map_button = btn = ba.buttonwidget(
parent=self._subcontainer,
size=(140, 60),
position=(h + 448, v - 72),
on_activate_call=ba.Call(self._select_map),
scale=0.7,
label=ba.Lstr(resource='mapSelectText'),
)
widget_column.append([btn])
ba.textwidget(
parent=self._subcontainer,
position=(h + 363 - 123, v - 114),
size=(100, 30),
flatness=1.0,
shadow=1.0,
scale=0.55,
maxwidth=256 * 0.7 * 0.8,
text=get_map_display_string(self._map),
h_align='center',
color=(0.6, 1.0, 0.6, 1.0),
v_align='center',
)
v -= map_height
for setting in self._settings_defs:
value = setting.default
value_type = type(value)
# Now, if there's an existing value for it in the config,
# override with that.
try:
if (
config is not None
and 'settings' in config
and setting.name in config['settings']
):
value = value_type(config['settings'][setting.name])
except Exception:
ba.print_exception()
# Shove the starting value in there to start.
self._settings[setting.name] = value
name_translated = self._get_localized_setting_name(setting.name)
mw1 = 280
mw2 = 70
# Handle types with choices specially:
if isinstance(setting, ba.ChoiceSetting):
for choice in setting.choices:
if len(choice) != 2:
raise ValueError(
"Expected 2-member tuples for 'choices'; got: "
+ repr(choice)
)
if not isinstance(choice[0], str):
raise TypeError(
'First value for choice tuple must be a str; got: '
+ repr(choice)
)
if not isinstance(choice[1], value_type):
raise TypeError(
'Choice type does not match default value; choice:'
+ repr(choice)
+ '; setting:'
+ repr(setting)
)
if value_type not in (int, float):
raise TypeError(
'Choice type setting must have int or float default; '
'got: ' + repr(setting)
)
# Start at the choice corresponding to the default if possible.
self._choice_selections[setting.name] = 0
for index, choice in enumerate(setting.choices):
if choice[1] == value:
self._choice_selections[setting.name] = index
break
v -= spacing
ba.textwidget(
parent=self._subcontainer,
position=(h + 50, v),
size=(100, 30),
maxwidth=mw1,
text=name_translated,
h_align='left',
color=(0.8, 0.8, 0.8, 1.0),
v_align='center',
)
txt = ba.textwidget(
parent=self._subcontainer,
position=(h + 509 - 95, v),
size=(0, 28),
text=self._get_localized_setting_name(
setting.choices[self._choice_selections[setting.name]][
0
]
),
editable=False,
color=(0.6, 1.0, 0.6, 1.0),
maxwidth=mw2,
h_align='right',
v_align='center',
padding=2,
)
btn1 = ba.buttonwidget(
parent=self._subcontainer,
position=(h + 509 - 50 - 1, v),
size=(28, 28),
label='<',
autoselect=True,
on_activate_call=ba.Call(
self._choice_inc, setting.name, txt, setting, -1
),
repeat=True,
)
btn2 = ba.buttonwidget(
parent=self._subcontainer,
position=(h + 509 + 5, v),
size=(28, 28),
label='>',
autoselect=True,
on_activate_call=ba.Call(
self._choice_inc, setting.name, txt, setting, 1
),
repeat=True,
)
widget_column.append([btn1, btn2])
elif isinstance(setting, (ba.IntSetting, ba.FloatSetting)):
v -= spacing
min_value = setting.min_value
max_value = setting.max_value
increment = setting.increment
ba.textwidget(
parent=self._subcontainer,
position=(h + 50, v),
size=(100, 30),
text=name_translated,
h_align='left',
color=(0.8, 0.8, 0.8, 1.0),
v_align='center',
maxwidth=mw1,
)
txt = ba.textwidget(
parent=self._subcontainer,
position=(h + 509 - 95, v),
size=(0, 28),
text=str(value),
editable=False,
color=(0.6, 1.0, 0.6, 1.0),
maxwidth=mw2,
h_align='right',
v_align='center',
padding=2,
)
btn1 = ba.buttonwidget(
parent=self._subcontainer,
position=(h + 509 - 50 - 1, v),
size=(28, 28),
label='-',
autoselect=True,
on_activate_call=ba.Call(
self._inc,
txt,
min_value,
max_value,
-increment,
value_type,
setting.name,
),
repeat=True,
)
btn2 = ba.buttonwidget(
parent=self._subcontainer,
position=(h + 509 + 5, v),
size=(28, 28),
label='+',
autoselect=True,
on_activate_call=ba.Call(
self._inc,
txt,
min_value,
max_value,
increment,
value_type,
setting.name,
),
repeat=True,
)
widget_column.append([btn1, btn2])
elif value_type == bool:
v -= spacing
ba.textwidget(
parent=self._subcontainer,
position=(h + 50, v),
size=(100, 30),
text=name_translated,
h_align='left',
color=(0.8, 0.8, 0.8, 1.0),
v_align='center',
maxwidth=mw1,
)
txt = ba.textwidget(
parent=self._subcontainer,
position=(h + 509 - 95, v),
size=(0, 28),
text=ba.Lstr(resource='onText')
if value
else ba.Lstr(resource='offText'),
editable=False,
color=(0.6, 1.0, 0.6, 1.0),
maxwidth=mw2,
h_align='right',
v_align='center',
padding=2,
)
cbw = ba.checkboxwidget(
parent=self._subcontainer,
text='',
position=(h + 505 - 50 - 5, v - 2),
size=(200, 30),
autoselect=True,
textcolor=(0.8, 0.8, 0.8),
value=value,
on_value_change_call=ba.Call(
self._check_value_change, setting.name, txt
),
)
widget_column.append([cbw])
else:
raise Exception()
# Ok now wire up the column.
try:
prev_widgets: list[ba.Widget] | None = None
for cwdg in widget_column:
if prev_widgets is not None:
# Wire our rightmost to their rightmost.
ba.widget(edit=prev_widgets[-1], down_widget=cwdg[-1])
ba.widget(cwdg[-1], up_widget=prev_widgets[-1])
# Wire our leftmost to their leftmost.
ba.widget(edit=prev_widgets[0], down_widget=cwdg[0])
ba.widget(cwdg[0], up_widget=prev_widgets[0])
prev_widgets = cwdg
except Exception:
ba.print_exception(
'Error wiring up game-settings-select widget column.'
)
ba.buttonwidget(edit=add_button, on_activate_call=ba.Call(self._add))
ba.containerwidget(
edit=self._root_widget,
selected_child=add_button,
start_button=add_button,
)
if default_selection == 'map':
ba.containerwidget(
edit=self._root_widget, selected_child=self._scrollwidget
)
ba.containerwidget(
edit=self._subcontainer, selected_child=map_button
)
def _get_localized_setting_name(self, name: str) -> ba.Lstr:
return ba.Lstr(translate=('settingNames', name))
def _select_map(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.playlist.mapselect import PlaylistMapSelectWindow
# Replace ourself with the map-select UI.
ba.containerwidget(edit=self._root_widget, transition='out_left')
ba.app.ui.set_main_menu_window(
PlaylistMapSelectWindow(
self._gametype,
self._sessiontype,
copy.deepcopy(self._getconfig()),
self._edit_info,
self._completion_call,
).get_root_widget()
)
def _choice_inc(
self,
setting_name: str,
widget: ba.Widget,
setting: ba.ChoiceSetting,
increment: int,
) -> None:
choices = setting.choices
if increment > 0:
self._choice_selections[setting_name] = min(
len(choices) - 1, self._choice_selections[setting_name] + 1
)
else:
self._choice_selections[setting_name] = max(
0, self._choice_selections[setting_name] - 1
)
ba.textwidget(
edit=widget,
text=self._get_localized_setting_name(
choices[self._choice_selections[setting_name]][0]
),
)
self._settings[setting_name] = choices[
self._choice_selections[setting_name]
][1]
def _cancel(self) -> None:
self._completion_call(None)
def _check_value_change(
self, setting_name: str, widget: ba.Widget, value: int
) -> None:
ba.textwidget(
edit=widget,
text=ba.Lstr(resource='onText')
if value
else ba.Lstr(resource='offText'),
)
self._settings[setting_name] = value
def _getconfig(self) -> dict[str, Any]:
settings = copy.deepcopy(self._settings)
settings['map'] = self._map
return {'settings': settings}
def _add(self) -> None:
self._completion_call(copy.deepcopy(self._getconfig()))
def _inc(
self,
ctrl: ba.Widget,
min_val: int | float,
max_val: int | float,
increment: int | float,
setting_type: type,
setting_name: str,
) -> None:
if setting_type == float:
val = float(cast(str, ba.textwidget(query=ctrl)))
else:
val = int(cast(str, ba.textwidget(query=ctrl)))
val += increment
val = max(min_val, min(val, max_val))
if setting_type == float:
ba.textwidget(edit=ctrl, text=str(round(val, 2)))
elif setting_type == int:
ba.textwidget(edit=ctrl, text=str(int(val)))
else:
raise TypeError('invalid vartype: ' + str(setting_type))
self._settings[setting_name] = val

View file

@ -0,0 +1,308 @@
# Released under the MIT License. See LICENSE for details.
#
"""Provides UI for selecting maps in playlists."""
from __future__ import annotations
import math
from typing import TYPE_CHECKING
import ba
import ba.internal
if TYPE_CHECKING:
from typing import Any, Callable
class PlaylistMapSelectWindow(ba.Window):
"""Window to select a map."""
def __init__(
self,
gametype: type[ba.GameActivity],
sessiontype: type[ba.Session],
config: dict[str, Any],
edit_info: dict[str, Any],
completion_call: Callable[[dict[str, Any] | None], Any],
transition: str = 'in_right',
):
from ba.internal import get_filtered_map_name
self._gametype = gametype
self._sessiontype = sessiontype
self._config = config
self._completion_call = completion_call
self._edit_info = edit_info
self._maps: list[tuple[str, ba.Texture]] = []
try:
self._previous_map = get_filtered_map_name(
config['settings']['map']
)
except Exception:
self._previous_map = ''
uiscale = ba.app.ui.uiscale
width = 715 if uiscale is ba.UIScale.SMALL else 615
x_inset = 50 if uiscale is ba.UIScale.SMALL else 0
height = (
400
if uiscale is ba.UIScale.SMALL
else 480
if uiscale is ba.UIScale.MEDIUM
else 600
)
top_extra = 20 if uiscale is ba.UIScale.SMALL else 0
super().__init__(
root_widget=ba.containerwidget(
size=(width, height + top_extra),
transition=transition,
scale=(
2.17
if uiscale is ba.UIScale.SMALL
else 1.3
if uiscale is ba.UIScale.MEDIUM
else 1.0
),
stack_offset=(0, -27)
if uiscale is ba.UIScale.SMALL
else (0, 0),
)
)
self._cancel_button = btn = ba.buttonwidget(
parent=self._root_widget,
position=(38 + x_inset, height - 67),
size=(140, 50),
scale=0.9,
text_scale=1.0,
autoselect=True,
label=ba.Lstr(resource='cancelText'),
on_activate_call=self._cancel,
)
ba.containerwidget(edit=self._root_widget, cancel_button=btn)
ba.textwidget(
parent=self._root_widget,
position=(width * 0.5, height - 46),
size=(0, 0),
maxwidth=260,
scale=1.1,
text=ba.Lstr(
resource='mapSelectTitleText',
subs=[('${GAME}', self._gametype.get_display_string())],
),
color=ba.app.ui.title_color,
h_align='center',
v_align='center',
)
v = height - 70
self._scroll_width = width - (80 + 2 * x_inset)
self._scroll_height = height - 140
self._scrollwidget = ba.scrollwidget(
parent=self._root_widget,
position=(40 + x_inset, v - self._scroll_height),
size=(self._scroll_width, self._scroll_height),
)
ba.containerwidget(
edit=self._root_widget, selected_child=self._scrollwidget
)
ba.containerwidget(edit=self._scrollwidget, claims_left_right=True)
self._subcontainer: ba.Widget | None = None
self._refresh()
def _refresh(self, select_get_more_maps_button: bool = False) -> None:
# pylint: disable=too-many-statements
# pylint: disable=too-many-branches
# pylint: disable=too-many-locals
from ba.internal import (
get_unowned_maps,
get_map_class,
get_map_display_string,
)
# Kill old.
if self._subcontainer is not None:
self._subcontainer.delete()
model_opaque = ba.getmodel('level_select_button_opaque')
model_transparent = ba.getmodel('level_select_button_transparent')
self._maps = []
map_list = self._gametype.get_supported_maps(self._sessiontype)
map_list_sorted = list(map_list)
map_list_sorted.sort()
unowned_maps = get_unowned_maps()
for mapname in map_list_sorted:
# Disallow ones we don't own.
if mapname in unowned_maps:
continue
map_tex_name = get_map_class(mapname).get_preview_texture_name()
if map_tex_name is not None:
try:
map_tex = ba.gettexture(map_tex_name)
self._maps.append((mapname, map_tex))
except Exception:
print(f'Invalid map preview texture: "{map_tex_name}".')
else:
print('Error: no map preview texture for map:', mapname)
count = len(self._maps)
columns = 2
rows = int(math.ceil(float(count) / columns))
button_width = 220
button_height = button_width * 0.5
button_buffer_h = 16
button_buffer_v = 19
self._sub_width = self._scroll_width * 0.95
self._sub_height = (
5 + rows * (button_height + 2 * button_buffer_v) + 100
)
self._subcontainer = ba.containerwidget(
parent=self._scrollwidget,
size=(self._sub_width, self._sub_height),
background=False,
)
index = 0
mask_texture = ba.gettexture('mapPreviewMask')
h_offs = 130 if len(self._maps) == 1 else 0
for y in range(rows):
for x in range(columns):
pos = (
x * (button_width + 2 * button_buffer_h)
+ button_buffer_h
+ h_offs,
self._sub_height
- (y + 1) * (button_height + 2 * button_buffer_v)
+ 12,
)
btn = ba.buttonwidget(
parent=self._subcontainer,
button_type='square',
size=(button_width, button_height),
autoselect=True,
texture=self._maps[index][1],
mask_texture=mask_texture,
model_opaque=model_opaque,
model_transparent=model_transparent,
label='',
color=(1, 1, 1),
on_activate_call=ba.Call(
self._select_with_delay, self._maps[index][0]
),
position=pos,
)
if x == 0:
ba.widget(edit=btn, left_widget=self._cancel_button)
if y == 0:
ba.widget(edit=btn, up_widget=self._cancel_button)
if x == columns - 1 and ba.app.ui.use_toolbars:
ba.widget(
edit=btn,
right_widget=ba.internal.get_special_widget(
'party_button'
),
)
ba.widget(edit=btn, show_buffer_top=60, show_buffer_bottom=60)
if self._maps[index][0] == self._previous_map:
ba.containerwidget(
edit=self._subcontainer,
selected_child=btn,
visible_child=btn,
)
name = get_map_display_string(self._maps[index][0])
ba.textwidget(
parent=self._subcontainer,
text=name,
position=(pos[0] + button_width * 0.5, pos[1] - 12),
size=(0, 0),
scale=0.5,
maxwidth=button_width,
draw_controller=btn,
h_align='center',
v_align='center',
color=(0.8, 0.8, 0.8, 0.8),
)
index += 1
if index >= count:
break
if index >= count:
break
self._get_more_maps_button = btn = ba.buttonwidget(
parent=self._subcontainer,
size=(self._sub_width * 0.8, 60),
position=(self._sub_width * 0.1, 30),
label=ba.Lstr(resource='mapSelectGetMoreMapsText'),
on_activate_call=self._on_store_press,
color=(0.6, 0.53, 0.63),
textcolor=(0.75, 0.7, 0.8),
autoselect=True,
)
ba.widget(edit=btn, show_buffer_top=30, show_buffer_bottom=30)
if select_get_more_maps_button:
ba.containerwidget(
edit=self._subcontainer, selected_child=btn, visible_child=btn
)
def _on_store_press(self) -> None:
from bastd.ui import account
from bastd.ui.store.browser import StoreBrowserWindow
if ba.internal.get_v1_account_state() != 'signed_in':
account.show_sign_in_prompt()
return
StoreBrowserWindow(
modal=True,
show_tab=StoreBrowserWindow.TabID.MAPS,
on_close_call=self._on_store_close,
origin_widget=self._get_more_maps_button,
)
def _on_store_close(self) -> None:
self._refresh(select_get_more_maps_button=True)
def _select(self, map_name: str) -> None:
from bastd.ui.playlist.editgame import PlaylistEditGameWindow
self._config['settings']['map'] = map_name
ba.containerwidget(edit=self._root_widget, transition='out_right')
ba.app.ui.set_main_menu_window(
PlaylistEditGameWindow(
self._gametype,
self._sessiontype,
self._config,
self._completion_call,
default_selection='map',
transition='in_left',
edit_info=self._edit_info,
).get_root_widget()
)
def _select_with_delay(self, map_name: str) -> None:
ba.internal.lock_all_input()
ba.timer(0.1, ba.internal.unlock_all_input, timetype=ba.TimeType.REAL)
ba.timer(
0.1, ba.WeakCall(self._select, map_name), timetype=ba.TimeType.REAL
)
def _cancel(self) -> None:
from bastd.ui.playlist.editgame import PlaylistEditGameWindow
ba.containerwidget(edit=self._root_widget, transition='out_right')
ba.app.ui.set_main_menu_window(
PlaylistEditGameWindow(
self._gametype,
self._sessiontype,
self._config,
self._completion_call,
default_selection='map',
transition='in_left',
edit_info=self._edit_info,
).get_root_widget()
)

View file

@ -0,0 +1,159 @@
# Released under the MIT License. See LICENSE for details.
#
"""UI functionality for importing shared playlists."""
from __future__ import annotations
import time
from typing import TYPE_CHECKING
import ba
import ba.internal
from bastd.ui import promocode
if TYPE_CHECKING:
from typing import Any, Callable
class SharePlaylistImportWindow(promocode.PromoCodeWindow):
"""Window for importing a shared playlist."""
def __init__(
self,
origin_widget: ba.Widget | None = None,
on_success_callback: Callable[[], Any] | None = None,
):
promocode.PromoCodeWindow.__init__(
self, modal=True, origin_widget=origin_widget
)
self._on_success_callback = on_success_callback
def _on_import_response(self, response: dict[str, Any] | None) -> None:
if response is None:
ba.screenmessage(ba.Lstr(resource='errorText'), color=(1, 0, 0))
ba.playsound(ba.getsound('error'))
return
if response['playlistType'] == 'Team Tournament':
playlist_type_name = ba.Lstr(resource='playModes.teamsText')
elif response['playlistType'] == 'Free-for-All':
playlist_type_name = ba.Lstr(resource='playModes.freeForAllText')
else:
playlist_type_name = ba.Lstr(value=response['playlistType'])
ba.screenmessage(
ba.Lstr(
resource='importPlaylistSuccessText',
subs=[
('${TYPE}', playlist_type_name),
('${NAME}', response['playlistName']),
],
),
color=(0, 1, 0),
)
ba.playsound(ba.getsound('gunCocking'))
if self._on_success_callback is not None:
self._on_success_callback()
ba.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
def _do_enter(self) -> None:
ba.internal.add_transaction(
{
'type': 'IMPORT_PLAYLIST',
'expire_time': time.time() + 5,
'code': ba.textwidget(query=self._text_field),
},
callback=ba.WeakCall(self._on_import_response),
)
ba.internal.run_transactions()
ba.screenmessage(ba.Lstr(resource='importingText'))
class SharePlaylistResultsWindow(ba.Window):
"""Window for sharing playlists."""
def __init__(
self, name: str, data: str, origin: tuple[float, float] = (0.0, 0.0)
):
del origin # unused arg
self._width = 450
self._height = 300
uiscale = ba.app.ui.uiscale
super().__init__(
root_widget=ba.containerwidget(
size=(self._width, self._height),
color=(0.45, 0.63, 0.15),
transition='in_scale',
scale=(
1.8
if uiscale is ba.UIScale.SMALL
else 1.35
if uiscale is ba.UIScale.MEDIUM
else 1.0
),
)
)
ba.playsound(ba.getsound('cashRegister'))
ba.playsound(ba.getsound('swish'))
self._cancel_button = ba.buttonwidget(
parent=self._root_widget,
scale=0.7,
position=(40, self._height - 40),
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
)
ba.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height * 0.745),
size=(0, 0),
color=ba.app.ui.infotextcolor,
scale=1.0,
flatness=1.0,
h_align='center',
v_align='center',
text=ba.Lstr(
resource='exportSuccessText', subs=[('${NAME}', name)]
),
maxwidth=self._width * 0.85,
)
ba.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height * 0.645),
size=(0, 0),
color=ba.app.ui.infotextcolor,
scale=0.6,
flatness=1.0,
h_align='center',
v_align='center',
text=ba.Lstr(resource='importPlaylistCodeInstructionsText'),
maxwidth=self._width * 0.85,
)
ba.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height * 0.4),
size=(0, 0),
color=(1.0, 3.0, 1.0),
scale=2.3,
h_align='center',
v_align='center',
text=data,
maxwidth=self._width * 0.85,
)
def close(self) -> None:
"""Close the window."""
ba.containerwidget(edit=self._root_widget, transition='out_scale')