bastd.ui.playlist.editgame
Provides UI for editing a game config.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Provides UI for editing a game config.""" 4 5from __future__ import annotations 6 7import copy 8import random 9from typing import TYPE_CHECKING, cast 10 11import _ba 12import ba 13 14if TYPE_CHECKING: 15 from typing import Any, Callable 16 17 18class PlaylistEditGameWindow(ba.Window): 19 """Window for editing a game config.""" 20 21 def __init__(self, 22 gametype: type[ba.GameActivity], 23 sessiontype: type[ba.Session], 24 config: dict[str, Any] | None, 25 completion_call: Callable[[dict[str, Any] | None], Any], 26 default_selection: str | None = None, 27 transition: str = 'in_right', 28 edit_info: dict[str, Any] | None = None): 29 # pylint: disable=too-many-branches 30 # pylint: disable=too-many-statements 31 # pylint: disable=too-many-locals 32 from ba.internal import (get_unowned_maps, get_filtered_map_name, 33 get_map_class, get_map_display_string) 34 self._gametype = gametype 35 self._sessiontype = sessiontype 36 37 # If we're within an editing session we get passed edit_info 38 # (returning from map selection window, etc). 39 if edit_info is not None: 40 self._edit_info = edit_info 41 42 # ..otherwise determine whether we're adding or editing a game based 43 # on whether an existing config was passed to us. 44 else: 45 if config is None: 46 self._edit_info = {'editType': 'add'} 47 else: 48 self._edit_info = {'editType': 'edit'} 49 50 self._r = 'gameSettingsWindow' 51 52 valid_maps = gametype.get_supported_maps(sessiontype) 53 if not valid_maps: 54 ba.screenmessage(ba.Lstr(resource='noValidMapsErrorText')) 55 raise Exception('No valid maps') 56 57 self._settings_defs = gametype.get_available_settings(sessiontype) 58 self._completion_call = completion_call 59 60 # To start with, pick a random map out of the ones we own. 61 unowned_maps = get_unowned_maps() 62 valid_maps_owned = [m for m in valid_maps if m not in unowned_maps] 63 if valid_maps_owned: 64 self._map = valid_maps[random.randrange(len(valid_maps_owned))] 65 66 # Hmmm.. we own none of these maps.. just pick a random un-owned one 67 # I guess.. should this ever happen? 68 else: 69 self._map = valid_maps[random.randrange(len(valid_maps))] 70 71 is_add = (self._edit_info['editType'] == 'add') 72 73 # If there's a valid map name in the existing config, use that. 74 try: 75 if (config is not None and 'settings' in config 76 and 'map' in config['settings']): 77 filtered_map_name = get_filtered_map_name( 78 config['settings']['map']) 79 if filtered_map_name in valid_maps: 80 self._map = filtered_map_name 81 except Exception: 82 ba.print_exception('Error getting map for editor.') 83 84 if config is not None and 'settings' in config: 85 self._settings = config['settings'] 86 else: 87 self._settings = {} 88 89 self._choice_selections: dict[str, int] = {} 90 91 uiscale = ba.app.ui.uiscale 92 width = 720 if uiscale is ba.UIScale.SMALL else 620 93 x_inset = 50 if uiscale is ba.UIScale.SMALL else 0 94 height = (365 if uiscale is ba.UIScale.SMALL else 95 460 if uiscale is ba.UIScale.MEDIUM else 550) 96 spacing = 52 97 y_extra = 15 98 y_extra2 = 21 99 100 map_tex_name = (get_map_class(self._map).get_preview_texture_name()) 101 if map_tex_name is None: 102 raise Exception('no map preview tex found for' + self._map) 103 map_tex = ba.gettexture(map_tex_name) 104 105 top_extra = 20 if uiscale is ba.UIScale.SMALL else 0 106 super().__init__(root_widget=ba.containerwidget( 107 size=(width, height + top_extra), 108 transition=transition, 109 scale=(2.19 if uiscale is ba.UIScale.SMALL else 110 1.35 if uiscale is ba.UIScale.MEDIUM else 1.0), 111 stack_offset=(0, -17) if uiscale is ba.UIScale.SMALL else (0, 0))) 112 113 btn = ba.buttonwidget( 114 parent=self._root_widget, 115 position=(45 + x_inset, height - 82 + y_extra2), 116 size=(180, 70) if is_add else (180, 65), 117 label=ba.Lstr(resource='backText') if is_add else ba.Lstr( 118 resource='cancelText'), 119 button_type='back' if is_add else None, 120 autoselect=True, 121 scale=0.75, 122 text_scale=1.3, 123 on_activate_call=ba.Call(self._cancel)) 124 ba.containerwidget(edit=self._root_widget, cancel_button=btn) 125 126 add_button = ba.buttonwidget( 127 parent=self._root_widget, 128 position=(width - (193 + x_inset), height - 82 + y_extra2), 129 size=(200, 65), 130 scale=0.75, 131 text_scale=1.3, 132 label=ba.Lstr(resource=self._r + 133 '.addGameText') if is_add else ba.Lstr( 134 resource='doneText')) 135 136 if ba.app.ui.use_toolbars: 137 pbtn = _ba.get_special_widget('party_button') 138 ba.widget(edit=add_button, right_widget=pbtn, up_widget=pbtn) 139 140 ba.textwidget(parent=self._root_widget, 141 position=(-8, height - 70 + y_extra2), 142 size=(width, 25), 143 text=gametype.get_display_string(), 144 color=ba.app.ui.title_color, 145 maxwidth=235, 146 scale=1.1, 147 h_align='center', 148 v_align='center') 149 150 map_height = 100 151 152 scroll_height = map_height + 10 # map select and margin 153 154 # Calc our total height we'll need 155 scroll_height += spacing * len(self._settings_defs) 156 157 scroll_width = width - (86 + 2 * x_inset) 158 self._scrollwidget = ba.scrollwidget(parent=self._root_widget, 159 position=(44 + x_inset, 160 35 + y_extra), 161 size=(scroll_width, height - 116), 162 highlight=False, 163 claims_left_right=True, 164 claims_tab=True, 165 selection_loops_to_parent=True) 166 self._subcontainer = ba.containerwidget(parent=self._scrollwidget, 167 size=(scroll_width, 168 scroll_height), 169 background=False, 170 claims_left_right=True, 171 claims_tab=True, 172 selection_loops_to_parent=True) 173 174 v = scroll_height - 5 175 h = -40 176 177 # Keep track of all the selectable widgets we make so we can wire 178 # them up conveniently. 179 widget_column: list[list[ba.Widget]] = [] 180 181 # Map select button. 182 ba.textwidget(parent=self._subcontainer, 183 position=(h + 49, v - 63), 184 size=(100, 30), 185 maxwidth=110, 186 text=ba.Lstr(resource='mapText'), 187 h_align='left', 188 color=(0.8, 0.8, 0.8, 1.0), 189 v_align='center') 190 191 ba.imagewidget( 192 parent=self._subcontainer, 193 size=(256 * 0.7, 125 * 0.7), 194 position=(h + 261 - 128 + 128.0 * 0.56, v - 90), 195 texture=map_tex, 196 model_opaque=ba.getmodel('level_select_button_opaque'), 197 model_transparent=ba.getmodel('level_select_button_transparent'), 198 mask_texture=ba.gettexture('mapPreviewMask')) 199 map_button = btn = ba.buttonwidget( 200 parent=self._subcontainer, 201 size=(140, 60), 202 position=(h + 448, v - 72), 203 on_activate_call=ba.Call(self._select_map), 204 scale=0.7, 205 label=ba.Lstr(resource='mapSelectText')) 206 widget_column.append([btn]) 207 208 ba.textwidget(parent=self._subcontainer, 209 position=(h + 363 - 123, v - 114), 210 size=(100, 30), 211 flatness=1.0, 212 shadow=1.0, 213 scale=0.55, 214 maxwidth=256 * 0.7 * 0.8, 215 text=get_map_display_string(self._map), 216 h_align='center', 217 color=(0.6, 1.0, 0.6, 1.0), 218 v_align='center') 219 v -= map_height 220 221 for setting in self._settings_defs: 222 value = setting.default 223 value_type = type(value) 224 225 # Now, if there's an existing value for it in the config, 226 # override with that. 227 try: 228 if (config is not None and 'settings' in config 229 and setting.name in config['settings']): 230 value = value_type(config['settings'][setting.name]) 231 except Exception: 232 ba.print_exception() 233 234 # Shove the starting value in there to start. 235 self._settings[setting.name] = value 236 237 name_translated = self._get_localized_setting_name(setting.name) 238 239 mw1 = 280 240 mw2 = 70 241 242 # Handle types with choices specially: 243 if isinstance(setting, ba.ChoiceSetting): 244 for choice in setting.choices: 245 if len(choice) != 2: 246 raise ValueError( 247 "Expected 2-member tuples for 'choices'; got: " + 248 repr(choice)) 249 if not isinstance(choice[0], str): 250 raise TypeError( 251 'First value for choice tuple must be a str; got: ' 252 + repr(choice)) 253 if not isinstance(choice[1], value_type): 254 raise TypeError( 255 'Choice type does not match default value; choice:' 256 + repr(choice) + '; setting:' + repr(setting)) 257 if value_type not in (int, float): 258 raise TypeError( 259 'Choice type setting must have int or float default; ' 260 'got: ' + repr(setting)) 261 262 # Start at the choice corresponding to the default if possible. 263 self._choice_selections[setting.name] = 0 264 for index, choice in enumerate(setting.choices): 265 if choice[1] == value: 266 self._choice_selections[setting.name] = index 267 break 268 269 v -= spacing 270 ba.textwidget(parent=self._subcontainer, 271 position=(h + 50, v), 272 size=(100, 30), 273 maxwidth=mw1, 274 text=name_translated, 275 h_align='left', 276 color=(0.8, 0.8, 0.8, 1.0), 277 v_align='center') 278 txt = ba.textwidget( 279 parent=self._subcontainer, 280 position=(h + 509 - 95, v), 281 size=(0, 28), 282 text=self._get_localized_setting_name(setting.choices[ 283 self._choice_selections[setting.name]][0]), 284 editable=False, 285 color=(0.6, 1.0, 0.6, 1.0), 286 maxwidth=mw2, 287 h_align='right', 288 v_align='center', 289 padding=2) 290 btn1 = ba.buttonwidget(parent=self._subcontainer, 291 position=(h + 509 - 50 - 1, v), 292 size=(28, 28), 293 label='<', 294 autoselect=True, 295 on_activate_call=ba.Call( 296 self._choice_inc, setting.name, txt, 297 setting, -1), 298 repeat=True) 299 btn2 = ba.buttonwidget(parent=self._subcontainer, 300 position=(h + 509 + 5, v), 301 size=(28, 28), 302 label='>', 303 autoselect=True, 304 on_activate_call=ba.Call( 305 self._choice_inc, setting.name, txt, 306 setting, 1), 307 repeat=True) 308 widget_column.append([btn1, btn2]) 309 310 elif isinstance(setting, (ba.IntSetting, ba.FloatSetting)): 311 v -= spacing 312 min_value = setting.min_value 313 max_value = setting.max_value 314 increment = setting.increment 315 ba.textwidget(parent=self._subcontainer, 316 position=(h + 50, v), 317 size=(100, 30), 318 text=name_translated, 319 h_align='left', 320 color=(0.8, 0.8, 0.8, 1.0), 321 v_align='center', 322 maxwidth=mw1) 323 txt = ba.textwidget(parent=self._subcontainer, 324 position=(h + 509 - 95, v), 325 size=(0, 28), 326 text=str(value), 327 editable=False, 328 color=(0.6, 1.0, 0.6, 1.0), 329 maxwidth=mw2, 330 h_align='right', 331 v_align='center', 332 padding=2) 333 btn1 = ba.buttonwidget(parent=self._subcontainer, 334 position=(h + 509 - 50 - 1, v), 335 size=(28, 28), 336 label='-', 337 autoselect=True, 338 on_activate_call=ba.Call( 339 self._inc, txt, min_value, 340 max_value, -increment, value_type, 341 setting.name), 342 repeat=True) 343 btn2 = ba.buttonwidget(parent=self._subcontainer, 344 position=(h + 509 + 5, v), 345 size=(28, 28), 346 label='+', 347 autoselect=True, 348 on_activate_call=ba.Call( 349 self._inc, txt, min_value, 350 max_value, increment, value_type, 351 setting.name), 352 repeat=True) 353 widget_column.append([btn1, btn2]) 354 355 elif value_type == bool: 356 v -= spacing 357 ba.textwidget(parent=self._subcontainer, 358 position=(h + 50, v), 359 size=(100, 30), 360 text=name_translated, 361 h_align='left', 362 color=(0.8, 0.8, 0.8, 1.0), 363 v_align='center', 364 maxwidth=mw1) 365 txt = ba.textwidget( 366 parent=self._subcontainer, 367 position=(h + 509 - 95, v), 368 size=(0, 28), 369 text=ba.Lstr(resource='onText') if value else ba.Lstr( 370 resource='offText'), 371 editable=False, 372 color=(0.6, 1.0, 0.6, 1.0), 373 maxwidth=mw2, 374 h_align='right', 375 v_align='center', 376 padding=2) 377 cbw = ba.checkboxwidget(parent=self._subcontainer, 378 text='', 379 position=(h + 505 - 50 - 5, v - 2), 380 size=(200, 30), 381 autoselect=True, 382 textcolor=(0.8, 0.8, 0.8), 383 value=value, 384 on_value_change_call=ba.Call( 385 self._check_value_change, 386 setting.name, txt)) 387 widget_column.append([cbw]) 388 389 else: 390 raise Exception() 391 392 # Ok now wire up the column. 393 try: 394 # pylint: disable=unsubscriptable-object 395 prev_widgets: list[ba.Widget] | None = None 396 for cwdg in widget_column: 397 if prev_widgets is not None: 398 # Wire our rightmost to their rightmost. 399 ba.widget(edit=prev_widgets[-1], down_widget=cwdg[-1]) 400 ba.widget(cwdg[-1], up_widget=prev_widgets[-1]) 401 402 # Wire our leftmost to their leftmost. 403 ba.widget(edit=prev_widgets[0], down_widget=cwdg[0]) 404 ba.widget(cwdg[0], up_widget=prev_widgets[0]) 405 prev_widgets = cwdg 406 except Exception: 407 ba.print_exception( 408 'Error wiring up game-settings-select widget column.') 409 410 ba.buttonwidget(edit=add_button, on_activate_call=ba.Call(self._add)) 411 ba.containerwidget(edit=self._root_widget, 412 selected_child=add_button, 413 start_button=add_button) 414 415 if default_selection == 'map': 416 ba.containerwidget(edit=self._root_widget, 417 selected_child=self._scrollwidget) 418 ba.containerwidget(edit=self._subcontainer, 419 selected_child=map_button) 420 421 def _get_localized_setting_name(self, name: str) -> ba.Lstr: 422 return ba.Lstr(translate=('settingNames', name)) 423 424 def _select_map(self) -> None: 425 # pylint: disable=cyclic-import 426 from bastd.ui.playlist.mapselect import PlaylistMapSelectWindow 427 428 # Replace ourself with the map-select UI. 429 ba.containerwidget(edit=self._root_widget, transition='out_left') 430 ba.app.ui.set_main_menu_window( 431 PlaylistMapSelectWindow(self._gametype, self._sessiontype, 432 copy.deepcopy(self._getconfig()), 433 self._edit_info, 434 self._completion_call).get_root_widget()) 435 436 def _choice_inc(self, setting_name: str, widget: ba.Widget, 437 setting: ba.ChoiceSetting, increment: int) -> None: 438 choices = setting.choices 439 if increment > 0: 440 self._choice_selections[setting_name] = min( 441 len(choices) - 1, self._choice_selections[setting_name] + 1) 442 else: 443 self._choice_selections[setting_name] = max( 444 0, self._choice_selections[setting_name] - 1) 445 ba.textwidget(edit=widget, 446 text=self._get_localized_setting_name( 447 choices[self._choice_selections[setting_name]][0])) 448 self._settings[setting_name] = choices[ 449 self._choice_selections[setting_name]][1] 450 451 def _cancel(self) -> None: 452 self._completion_call(None) 453 454 def _check_value_change(self, setting_name: str, widget: ba.Widget, 455 value: int) -> None: 456 ba.textwidget(edit=widget, 457 text=ba.Lstr(resource='onText') if value else ba.Lstr( 458 resource='offText')) 459 self._settings[setting_name] = value 460 461 def _getconfig(self) -> dict[str, Any]: 462 settings = copy.deepcopy(self._settings) 463 settings['map'] = self._map 464 return {'settings': settings} 465 466 def _add(self) -> None: 467 self._completion_call(copy.deepcopy(self._getconfig())) 468 469 def _inc(self, ctrl: ba.Widget, min_val: int | float, max_val: int | float, 470 increment: int | float, setting_type: type, 471 setting_name: str) -> None: 472 if setting_type == float: 473 val = float(cast(str, ba.textwidget(query=ctrl))) 474 else: 475 val = int(cast(str, ba.textwidget(query=ctrl))) 476 val += increment 477 val = max(min_val, min(val, max_val)) 478 if setting_type == float: 479 ba.textwidget(edit=ctrl, text=str(round(val, 2))) 480 elif setting_type == int: 481 ba.textwidget(edit=ctrl, text=str(int(val))) 482 else: 483 raise TypeError('invalid vartype: ' + str(setting_type)) 484 self._settings[setting_name] = val
class
PlaylistEditGameWindow(ba.ui.Window):
19class PlaylistEditGameWindow(ba.Window): 20 """Window for editing a game config.""" 21 22 def __init__(self, 23 gametype: type[ba.GameActivity], 24 sessiontype: type[ba.Session], 25 config: dict[str, Any] | None, 26 completion_call: Callable[[dict[str, Any] | None], Any], 27 default_selection: str | None = None, 28 transition: str = 'in_right', 29 edit_info: dict[str, Any] | None = None): 30 # pylint: disable=too-many-branches 31 # pylint: disable=too-many-statements 32 # pylint: disable=too-many-locals 33 from ba.internal import (get_unowned_maps, get_filtered_map_name, 34 get_map_class, get_map_display_string) 35 self._gametype = gametype 36 self._sessiontype = sessiontype 37 38 # If we're within an editing session we get passed edit_info 39 # (returning from map selection window, etc). 40 if edit_info is not None: 41 self._edit_info = edit_info 42 43 # ..otherwise determine whether we're adding or editing a game based 44 # on whether an existing config was passed to us. 45 else: 46 if config is None: 47 self._edit_info = {'editType': 'add'} 48 else: 49 self._edit_info = {'editType': 'edit'} 50 51 self._r = 'gameSettingsWindow' 52 53 valid_maps = gametype.get_supported_maps(sessiontype) 54 if not valid_maps: 55 ba.screenmessage(ba.Lstr(resource='noValidMapsErrorText')) 56 raise Exception('No valid maps') 57 58 self._settings_defs = gametype.get_available_settings(sessiontype) 59 self._completion_call = completion_call 60 61 # To start with, pick a random map out of the ones we own. 62 unowned_maps = get_unowned_maps() 63 valid_maps_owned = [m for m in valid_maps if m not in unowned_maps] 64 if valid_maps_owned: 65 self._map = valid_maps[random.randrange(len(valid_maps_owned))] 66 67 # Hmmm.. we own none of these maps.. just pick a random un-owned one 68 # I guess.. should this ever happen? 69 else: 70 self._map = valid_maps[random.randrange(len(valid_maps))] 71 72 is_add = (self._edit_info['editType'] == 'add') 73 74 # If there's a valid map name in the existing config, use that. 75 try: 76 if (config is not None and 'settings' in config 77 and 'map' in config['settings']): 78 filtered_map_name = get_filtered_map_name( 79 config['settings']['map']) 80 if filtered_map_name in valid_maps: 81 self._map = filtered_map_name 82 except Exception: 83 ba.print_exception('Error getting map for editor.') 84 85 if config is not None and 'settings' in config: 86 self._settings = config['settings'] 87 else: 88 self._settings = {} 89 90 self._choice_selections: dict[str, int] = {} 91 92 uiscale = ba.app.ui.uiscale 93 width = 720 if uiscale is ba.UIScale.SMALL else 620 94 x_inset = 50 if uiscale is ba.UIScale.SMALL else 0 95 height = (365 if uiscale is ba.UIScale.SMALL else 96 460 if uiscale is ba.UIScale.MEDIUM else 550) 97 spacing = 52 98 y_extra = 15 99 y_extra2 = 21 100 101 map_tex_name = (get_map_class(self._map).get_preview_texture_name()) 102 if map_tex_name is None: 103 raise Exception('no map preview tex found for' + self._map) 104 map_tex = ba.gettexture(map_tex_name) 105 106 top_extra = 20 if uiscale is ba.UIScale.SMALL else 0 107 super().__init__(root_widget=ba.containerwidget( 108 size=(width, height + top_extra), 109 transition=transition, 110 scale=(2.19 if uiscale is ba.UIScale.SMALL else 111 1.35 if uiscale is ba.UIScale.MEDIUM else 1.0), 112 stack_offset=(0, -17) if uiscale is ba.UIScale.SMALL else (0, 0))) 113 114 btn = ba.buttonwidget( 115 parent=self._root_widget, 116 position=(45 + x_inset, height - 82 + y_extra2), 117 size=(180, 70) if is_add else (180, 65), 118 label=ba.Lstr(resource='backText') if is_add else ba.Lstr( 119 resource='cancelText'), 120 button_type='back' if is_add else None, 121 autoselect=True, 122 scale=0.75, 123 text_scale=1.3, 124 on_activate_call=ba.Call(self._cancel)) 125 ba.containerwidget(edit=self._root_widget, cancel_button=btn) 126 127 add_button = ba.buttonwidget( 128 parent=self._root_widget, 129 position=(width - (193 + x_inset), height - 82 + y_extra2), 130 size=(200, 65), 131 scale=0.75, 132 text_scale=1.3, 133 label=ba.Lstr(resource=self._r + 134 '.addGameText') if is_add else ba.Lstr( 135 resource='doneText')) 136 137 if ba.app.ui.use_toolbars: 138 pbtn = _ba.get_special_widget('party_button') 139 ba.widget(edit=add_button, right_widget=pbtn, up_widget=pbtn) 140 141 ba.textwidget(parent=self._root_widget, 142 position=(-8, height - 70 + y_extra2), 143 size=(width, 25), 144 text=gametype.get_display_string(), 145 color=ba.app.ui.title_color, 146 maxwidth=235, 147 scale=1.1, 148 h_align='center', 149 v_align='center') 150 151 map_height = 100 152 153 scroll_height = map_height + 10 # map select and margin 154 155 # Calc our total height we'll need 156 scroll_height += spacing * len(self._settings_defs) 157 158 scroll_width = width - (86 + 2 * x_inset) 159 self._scrollwidget = ba.scrollwidget(parent=self._root_widget, 160 position=(44 + x_inset, 161 35 + y_extra), 162 size=(scroll_width, height - 116), 163 highlight=False, 164 claims_left_right=True, 165 claims_tab=True, 166 selection_loops_to_parent=True) 167 self._subcontainer = ba.containerwidget(parent=self._scrollwidget, 168 size=(scroll_width, 169 scroll_height), 170 background=False, 171 claims_left_right=True, 172 claims_tab=True, 173 selection_loops_to_parent=True) 174 175 v = scroll_height - 5 176 h = -40 177 178 # Keep track of all the selectable widgets we make so we can wire 179 # them up conveniently. 180 widget_column: list[list[ba.Widget]] = [] 181 182 # Map select button. 183 ba.textwidget(parent=self._subcontainer, 184 position=(h + 49, v - 63), 185 size=(100, 30), 186 maxwidth=110, 187 text=ba.Lstr(resource='mapText'), 188 h_align='left', 189 color=(0.8, 0.8, 0.8, 1.0), 190 v_align='center') 191 192 ba.imagewidget( 193 parent=self._subcontainer, 194 size=(256 * 0.7, 125 * 0.7), 195 position=(h + 261 - 128 + 128.0 * 0.56, v - 90), 196 texture=map_tex, 197 model_opaque=ba.getmodel('level_select_button_opaque'), 198 model_transparent=ba.getmodel('level_select_button_transparent'), 199 mask_texture=ba.gettexture('mapPreviewMask')) 200 map_button = btn = ba.buttonwidget( 201 parent=self._subcontainer, 202 size=(140, 60), 203 position=(h + 448, v - 72), 204 on_activate_call=ba.Call(self._select_map), 205 scale=0.7, 206 label=ba.Lstr(resource='mapSelectText')) 207 widget_column.append([btn]) 208 209 ba.textwidget(parent=self._subcontainer, 210 position=(h + 363 - 123, v - 114), 211 size=(100, 30), 212 flatness=1.0, 213 shadow=1.0, 214 scale=0.55, 215 maxwidth=256 * 0.7 * 0.8, 216 text=get_map_display_string(self._map), 217 h_align='center', 218 color=(0.6, 1.0, 0.6, 1.0), 219 v_align='center') 220 v -= map_height 221 222 for setting in self._settings_defs: 223 value = setting.default 224 value_type = type(value) 225 226 # Now, if there's an existing value for it in the config, 227 # override with that. 228 try: 229 if (config is not None and 'settings' in config 230 and setting.name in config['settings']): 231 value = value_type(config['settings'][setting.name]) 232 except Exception: 233 ba.print_exception() 234 235 # Shove the starting value in there to start. 236 self._settings[setting.name] = value 237 238 name_translated = self._get_localized_setting_name(setting.name) 239 240 mw1 = 280 241 mw2 = 70 242 243 # Handle types with choices specially: 244 if isinstance(setting, ba.ChoiceSetting): 245 for choice in setting.choices: 246 if len(choice) != 2: 247 raise ValueError( 248 "Expected 2-member tuples for 'choices'; got: " + 249 repr(choice)) 250 if not isinstance(choice[0], str): 251 raise TypeError( 252 'First value for choice tuple must be a str; got: ' 253 + repr(choice)) 254 if not isinstance(choice[1], value_type): 255 raise TypeError( 256 'Choice type does not match default value; choice:' 257 + repr(choice) + '; setting:' + repr(setting)) 258 if value_type not in (int, float): 259 raise TypeError( 260 'Choice type setting must have int or float default; ' 261 'got: ' + repr(setting)) 262 263 # Start at the choice corresponding to the default if possible. 264 self._choice_selections[setting.name] = 0 265 for index, choice in enumerate(setting.choices): 266 if choice[1] == value: 267 self._choice_selections[setting.name] = index 268 break 269 270 v -= spacing 271 ba.textwidget(parent=self._subcontainer, 272 position=(h + 50, v), 273 size=(100, 30), 274 maxwidth=mw1, 275 text=name_translated, 276 h_align='left', 277 color=(0.8, 0.8, 0.8, 1.0), 278 v_align='center') 279 txt = ba.textwidget( 280 parent=self._subcontainer, 281 position=(h + 509 - 95, v), 282 size=(0, 28), 283 text=self._get_localized_setting_name(setting.choices[ 284 self._choice_selections[setting.name]][0]), 285 editable=False, 286 color=(0.6, 1.0, 0.6, 1.0), 287 maxwidth=mw2, 288 h_align='right', 289 v_align='center', 290 padding=2) 291 btn1 = ba.buttonwidget(parent=self._subcontainer, 292 position=(h + 509 - 50 - 1, v), 293 size=(28, 28), 294 label='<', 295 autoselect=True, 296 on_activate_call=ba.Call( 297 self._choice_inc, setting.name, txt, 298 setting, -1), 299 repeat=True) 300 btn2 = ba.buttonwidget(parent=self._subcontainer, 301 position=(h + 509 + 5, v), 302 size=(28, 28), 303 label='>', 304 autoselect=True, 305 on_activate_call=ba.Call( 306 self._choice_inc, setting.name, txt, 307 setting, 1), 308 repeat=True) 309 widget_column.append([btn1, btn2]) 310 311 elif isinstance(setting, (ba.IntSetting, ba.FloatSetting)): 312 v -= spacing 313 min_value = setting.min_value 314 max_value = setting.max_value 315 increment = setting.increment 316 ba.textwidget(parent=self._subcontainer, 317 position=(h + 50, v), 318 size=(100, 30), 319 text=name_translated, 320 h_align='left', 321 color=(0.8, 0.8, 0.8, 1.0), 322 v_align='center', 323 maxwidth=mw1) 324 txt = ba.textwidget(parent=self._subcontainer, 325 position=(h + 509 - 95, v), 326 size=(0, 28), 327 text=str(value), 328 editable=False, 329 color=(0.6, 1.0, 0.6, 1.0), 330 maxwidth=mw2, 331 h_align='right', 332 v_align='center', 333 padding=2) 334 btn1 = ba.buttonwidget(parent=self._subcontainer, 335 position=(h + 509 - 50 - 1, v), 336 size=(28, 28), 337 label='-', 338 autoselect=True, 339 on_activate_call=ba.Call( 340 self._inc, txt, min_value, 341 max_value, -increment, value_type, 342 setting.name), 343 repeat=True) 344 btn2 = ba.buttonwidget(parent=self._subcontainer, 345 position=(h + 509 + 5, v), 346 size=(28, 28), 347 label='+', 348 autoselect=True, 349 on_activate_call=ba.Call( 350 self._inc, txt, min_value, 351 max_value, increment, value_type, 352 setting.name), 353 repeat=True) 354 widget_column.append([btn1, btn2]) 355 356 elif value_type == bool: 357 v -= spacing 358 ba.textwidget(parent=self._subcontainer, 359 position=(h + 50, v), 360 size=(100, 30), 361 text=name_translated, 362 h_align='left', 363 color=(0.8, 0.8, 0.8, 1.0), 364 v_align='center', 365 maxwidth=mw1) 366 txt = ba.textwidget( 367 parent=self._subcontainer, 368 position=(h + 509 - 95, v), 369 size=(0, 28), 370 text=ba.Lstr(resource='onText') if value else ba.Lstr( 371 resource='offText'), 372 editable=False, 373 color=(0.6, 1.0, 0.6, 1.0), 374 maxwidth=mw2, 375 h_align='right', 376 v_align='center', 377 padding=2) 378 cbw = ba.checkboxwidget(parent=self._subcontainer, 379 text='', 380 position=(h + 505 - 50 - 5, v - 2), 381 size=(200, 30), 382 autoselect=True, 383 textcolor=(0.8, 0.8, 0.8), 384 value=value, 385 on_value_change_call=ba.Call( 386 self._check_value_change, 387 setting.name, txt)) 388 widget_column.append([cbw]) 389 390 else: 391 raise Exception() 392 393 # Ok now wire up the column. 394 try: 395 # pylint: disable=unsubscriptable-object 396 prev_widgets: list[ba.Widget] | None = None 397 for cwdg in widget_column: 398 if prev_widgets is not None: 399 # Wire our rightmost to their rightmost. 400 ba.widget(edit=prev_widgets[-1], down_widget=cwdg[-1]) 401 ba.widget(cwdg[-1], up_widget=prev_widgets[-1]) 402 403 # Wire our leftmost to their leftmost. 404 ba.widget(edit=prev_widgets[0], down_widget=cwdg[0]) 405 ba.widget(cwdg[0], up_widget=prev_widgets[0]) 406 prev_widgets = cwdg 407 except Exception: 408 ba.print_exception( 409 'Error wiring up game-settings-select widget column.') 410 411 ba.buttonwidget(edit=add_button, on_activate_call=ba.Call(self._add)) 412 ba.containerwidget(edit=self._root_widget, 413 selected_child=add_button, 414 start_button=add_button) 415 416 if default_selection == 'map': 417 ba.containerwidget(edit=self._root_widget, 418 selected_child=self._scrollwidget) 419 ba.containerwidget(edit=self._subcontainer, 420 selected_child=map_button) 421 422 def _get_localized_setting_name(self, name: str) -> ba.Lstr: 423 return ba.Lstr(translate=('settingNames', name)) 424 425 def _select_map(self) -> None: 426 # pylint: disable=cyclic-import 427 from bastd.ui.playlist.mapselect import PlaylistMapSelectWindow 428 429 # Replace ourself with the map-select UI. 430 ba.containerwidget(edit=self._root_widget, transition='out_left') 431 ba.app.ui.set_main_menu_window( 432 PlaylistMapSelectWindow(self._gametype, self._sessiontype, 433 copy.deepcopy(self._getconfig()), 434 self._edit_info, 435 self._completion_call).get_root_widget()) 436 437 def _choice_inc(self, setting_name: str, widget: ba.Widget, 438 setting: ba.ChoiceSetting, increment: int) -> None: 439 choices = setting.choices 440 if increment > 0: 441 self._choice_selections[setting_name] = min( 442 len(choices) - 1, self._choice_selections[setting_name] + 1) 443 else: 444 self._choice_selections[setting_name] = max( 445 0, self._choice_selections[setting_name] - 1) 446 ba.textwidget(edit=widget, 447 text=self._get_localized_setting_name( 448 choices[self._choice_selections[setting_name]][0])) 449 self._settings[setting_name] = choices[ 450 self._choice_selections[setting_name]][1] 451 452 def _cancel(self) -> None: 453 self._completion_call(None) 454 455 def _check_value_change(self, setting_name: str, widget: ba.Widget, 456 value: int) -> None: 457 ba.textwidget(edit=widget, 458 text=ba.Lstr(resource='onText') if value else ba.Lstr( 459 resource='offText')) 460 self._settings[setting_name] = value 461 462 def _getconfig(self) -> dict[str, Any]: 463 settings = copy.deepcopy(self._settings) 464 settings['map'] = self._map 465 return {'settings': settings} 466 467 def _add(self) -> None: 468 self._completion_call(copy.deepcopy(self._getconfig())) 469 470 def _inc(self, ctrl: ba.Widget, min_val: int | float, max_val: int | float, 471 increment: int | float, setting_type: type, 472 setting_name: str) -> None: 473 if setting_type == float: 474 val = float(cast(str, ba.textwidget(query=ctrl))) 475 else: 476 val = int(cast(str, ba.textwidget(query=ctrl))) 477 val += increment 478 val = max(min_val, min(val, max_val)) 479 if setting_type == float: 480 ba.textwidget(edit=ctrl, text=str(round(val, 2))) 481 elif setting_type == int: 482 ba.textwidget(edit=ctrl, text=str(int(val))) 483 else: 484 raise TypeError('invalid vartype: ' + str(setting_type)) 485 self._settings[setting_name] = val
Window for editing a game config.
PlaylistEditGameWindow( gametype: type[ba._gameactivity.GameActivity], sessiontype: type[ba._session.Session], config: dict[str, typing.Any] | None, completion_call: Callable[[dict[str, Any] | None], Any], default_selection: str | None = None, transition: str = 'in_right', edit_info: dict[str, typing.Any] | None = None)
22 def __init__(self, 23 gametype: type[ba.GameActivity], 24 sessiontype: type[ba.Session], 25 config: dict[str, Any] | None, 26 completion_call: Callable[[dict[str, Any] | None], Any], 27 default_selection: str | None = None, 28 transition: str = 'in_right', 29 edit_info: dict[str, Any] | None = None): 30 # pylint: disable=too-many-branches 31 # pylint: disable=too-many-statements 32 # pylint: disable=too-many-locals 33 from ba.internal import (get_unowned_maps, get_filtered_map_name, 34 get_map_class, get_map_display_string) 35 self._gametype = gametype 36 self._sessiontype = sessiontype 37 38 # If we're within an editing session we get passed edit_info 39 # (returning from map selection window, etc). 40 if edit_info is not None: 41 self._edit_info = edit_info 42 43 # ..otherwise determine whether we're adding or editing a game based 44 # on whether an existing config was passed to us. 45 else: 46 if config is None: 47 self._edit_info = {'editType': 'add'} 48 else: 49 self._edit_info = {'editType': 'edit'} 50 51 self._r = 'gameSettingsWindow' 52 53 valid_maps = gametype.get_supported_maps(sessiontype) 54 if not valid_maps: 55 ba.screenmessage(ba.Lstr(resource='noValidMapsErrorText')) 56 raise Exception('No valid maps') 57 58 self._settings_defs = gametype.get_available_settings(sessiontype) 59 self._completion_call = completion_call 60 61 # To start with, pick a random map out of the ones we own. 62 unowned_maps = get_unowned_maps() 63 valid_maps_owned = [m for m in valid_maps if m not in unowned_maps] 64 if valid_maps_owned: 65 self._map = valid_maps[random.randrange(len(valid_maps_owned))] 66 67 # Hmmm.. we own none of these maps.. just pick a random un-owned one 68 # I guess.. should this ever happen? 69 else: 70 self._map = valid_maps[random.randrange(len(valid_maps))] 71 72 is_add = (self._edit_info['editType'] == 'add') 73 74 # If there's a valid map name in the existing config, use that. 75 try: 76 if (config is not None and 'settings' in config 77 and 'map' in config['settings']): 78 filtered_map_name = get_filtered_map_name( 79 config['settings']['map']) 80 if filtered_map_name in valid_maps: 81 self._map = filtered_map_name 82 except Exception: 83 ba.print_exception('Error getting map for editor.') 84 85 if config is not None and 'settings' in config: 86 self._settings = config['settings'] 87 else: 88 self._settings = {} 89 90 self._choice_selections: dict[str, int] = {} 91 92 uiscale = ba.app.ui.uiscale 93 width = 720 if uiscale is ba.UIScale.SMALL else 620 94 x_inset = 50 if uiscale is ba.UIScale.SMALL else 0 95 height = (365 if uiscale is ba.UIScale.SMALL else 96 460 if uiscale is ba.UIScale.MEDIUM else 550) 97 spacing = 52 98 y_extra = 15 99 y_extra2 = 21 100 101 map_tex_name = (get_map_class(self._map).get_preview_texture_name()) 102 if map_tex_name is None: 103 raise Exception('no map preview tex found for' + self._map) 104 map_tex = ba.gettexture(map_tex_name) 105 106 top_extra = 20 if uiscale is ba.UIScale.SMALL else 0 107 super().__init__(root_widget=ba.containerwidget( 108 size=(width, height + top_extra), 109 transition=transition, 110 scale=(2.19 if uiscale is ba.UIScale.SMALL else 111 1.35 if uiscale is ba.UIScale.MEDIUM else 1.0), 112 stack_offset=(0, -17) if uiscale is ba.UIScale.SMALL else (0, 0))) 113 114 btn = ba.buttonwidget( 115 parent=self._root_widget, 116 position=(45 + x_inset, height - 82 + y_extra2), 117 size=(180, 70) if is_add else (180, 65), 118 label=ba.Lstr(resource='backText') if is_add else ba.Lstr( 119 resource='cancelText'), 120 button_type='back' if is_add else None, 121 autoselect=True, 122 scale=0.75, 123 text_scale=1.3, 124 on_activate_call=ba.Call(self._cancel)) 125 ba.containerwidget(edit=self._root_widget, cancel_button=btn) 126 127 add_button = ba.buttonwidget( 128 parent=self._root_widget, 129 position=(width - (193 + x_inset), height - 82 + y_extra2), 130 size=(200, 65), 131 scale=0.75, 132 text_scale=1.3, 133 label=ba.Lstr(resource=self._r + 134 '.addGameText') if is_add else ba.Lstr( 135 resource='doneText')) 136 137 if ba.app.ui.use_toolbars: 138 pbtn = _ba.get_special_widget('party_button') 139 ba.widget(edit=add_button, right_widget=pbtn, up_widget=pbtn) 140 141 ba.textwidget(parent=self._root_widget, 142 position=(-8, height - 70 + y_extra2), 143 size=(width, 25), 144 text=gametype.get_display_string(), 145 color=ba.app.ui.title_color, 146 maxwidth=235, 147 scale=1.1, 148 h_align='center', 149 v_align='center') 150 151 map_height = 100 152 153 scroll_height = map_height + 10 # map select and margin 154 155 # Calc our total height we'll need 156 scroll_height += spacing * len(self._settings_defs) 157 158 scroll_width = width - (86 + 2 * x_inset) 159 self._scrollwidget = ba.scrollwidget(parent=self._root_widget, 160 position=(44 + x_inset, 161 35 + y_extra), 162 size=(scroll_width, height - 116), 163 highlight=False, 164 claims_left_right=True, 165 claims_tab=True, 166 selection_loops_to_parent=True) 167 self._subcontainer = ba.containerwidget(parent=self._scrollwidget, 168 size=(scroll_width, 169 scroll_height), 170 background=False, 171 claims_left_right=True, 172 claims_tab=True, 173 selection_loops_to_parent=True) 174 175 v = scroll_height - 5 176 h = -40 177 178 # Keep track of all the selectable widgets we make so we can wire 179 # them up conveniently. 180 widget_column: list[list[ba.Widget]] = [] 181 182 # Map select button. 183 ba.textwidget(parent=self._subcontainer, 184 position=(h + 49, v - 63), 185 size=(100, 30), 186 maxwidth=110, 187 text=ba.Lstr(resource='mapText'), 188 h_align='left', 189 color=(0.8, 0.8, 0.8, 1.0), 190 v_align='center') 191 192 ba.imagewidget( 193 parent=self._subcontainer, 194 size=(256 * 0.7, 125 * 0.7), 195 position=(h + 261 - 128 + 128.0 * 0.56, v - 90), 196 texture=map_tex, 197 model_opaque=ba.getmodel('level_select_button_opaque'), 198 model_transparent=ba.getmodel('level_select_button_transparent'), 199 mask_texture=ba.gettexture('mapPreviewMask')) 200 map_button = btn = ba.buttonwidget( 201 parent=self._subcontainer, 202 size=(140, 60), 203 position=(h + 448, v - 72), 204 on_activate_call=ba.Call(self._select_map), 205 scale=0.7, 206 label=ba.Lstr(resource='mapSelectText')) 207 widget_column.append([btn]) 208 209 ba.textwidget(parent=self._subcontainer, 210 position=(h + 363 - 123, v - 114), 211 size=(100, 30), 212 flatness=1.0, 213 shadow=1.0, 214 scale=0.55, 215 maxwidth=256 * 0.7 * 0.8, 216 text=get_map_display_string(self._map), 217 h_align='center', 218 color=(0.6, 1.0, 0.6, 1.0), 219 v_align='center') 220 v -= map_height 221 222 for setting in self._settings_defs: 223 value = setting.default 224 value_type = type(value) 225 226 # Now, if there's an existing value for it in the config, 227 # override with that. 228 try: 229 if (config is not None and 'settings' in config 230 and setting.name in config['settings']): 231 value = value_type(config['settings'][setting.name]) 232 except Exception: 233 ba.print_exception() 234 235 # Shove the starting value in there to start. 236 self._settings[setting.name] = value 237 238 name_translated = self._get_localized_setting_name(setting.name) 239 240 mw1 = 280 241 mw2 = 70 242 243 # Handle types with choices specially: 244 if isinstance(setting, ba.ChoiceSetting): 245 for choice in setting.choices: 246 if len(choice) != 2: 247 raise ValueError( 248 "Expected 2-member tuples for 'choices'; got: " + 249 repr(choice)) 250 if not isinstance(choice[0], str): 251 raise TypeError( 252 'First value for choice tuple must be a str; got: ' 253 + repr(choice)) 254 if not isinstance(choice[1], value_type): 255 raise TypeError( 256 'Choice type does not match default value; choice:' 257 + repr(choice) + '; setting:' + repr(setting)) 258 if value_type not in (int, float): 259 raise TypeError( 260 'Choice type setting must have int or float default; ' 261 'got: ' + repr(setting)) 262 263 # Start at the choice corresponding to the default if possible. 264 self._choice_selections[setting.name] = 0 265 for index, choice in enumerate(setting.choices): 266 if choice[1] == value: 267 self._choice_selections[setting.name] = index 268 break 269 270 v -= spacing 271 ba.textwidget(parent=self._subcontainer, 272 position=(h + 50, v), 273 size=(100, 30), 274 maxwidth=mw1, 275 text=name_translated, 276 h_align='left', 277 color=(0.8, 0.8, 0.8, 1.0), 278 v_align='center') 279 txt = ba.textwidget( 280 parent=self._subcontainer, 281 position=(h + 509 - 95, v), 282 size=(0, 28), 283 text=self._get_localized_setting_name(setting.choices[ 284 self._choice_selections[setting.name]][0]), 285 editable=False, 286 color=(0.6, 1.0, 0.6, 1.0), 287 maxwidth=mw2, 288 h_align='right', 289 v_align='center', 290 padding=2) 291 btn1 = ba.buttonwidget(parent=self._subcontainer, 292 position=(h + 509 - 50 - 1, v), 293 size=(28, 28), 294 label='<', 295 autoselect=True, 296 on_activate_call=ba.Call( 297 self._choice_inc, setting.name, txt, 298 setting, -1), 299 repeat=True) 300 btn2 = ba.buttonwidget(parent=self._subcontainer, 301 position=(h + 509 + 5, v), 302 size=(28, 28), 303 label='>', 304 autoselect=True, 305 on_activate_call=ba.Call( 306 self._choice_inc, setting.name, txt, 307 setting, 1), 308 repeat=True) 309 widget_column.append([btn1, btn2]) 310 311 elif isinstance(setting, (ba.IntSetting, ba.FloatSetting)): 312 v -= spacing 313 min_value = setting.min_value 314 max_value = setting.max_value 315 increment = setting.increment 316 ba.textwidget(parent=self._subcontainer, 317 position=(h + 50, v), 318 size=(100, 30), 319 text=name_translated, 320 h_align='left', 321 color=(0.8, 0.8, 0.8, 1.0), 322 v_align='center', 323 maxwidth=mw1) 324 txt = ba.textwidget(parent=self._subcontainer, 325 position=(h + 509 - 95, v), 326 size=(0, 28), 327 text=str(value), 328 editable=False, 329 color=(0.6, 1.0, 0.6, 1.0), 330 maxwidth=mw2, 331 h_align='right', 332 v_align='center', 333 padding=2) 334 btn1 = ba.buttonwidget(parent=self._subcontainer, 335 position=(h + 509 - 50 - 1, v), 336 size=(28, 28), 337 label='-', 338 autoselect=True, 339 on_activate_call=ba.Call( 340 self._inc, txt, min_value, 341 max_value, -increment, value_type, 342 setting.name), 343 repeat=True) 344 btn2 = ba.buttonwidget(parent=self._subcontainer, 345 position=(h + 509 + 5, v), 346 size=(28, 28), 347 label='+', 348 autoselect=True, 349 on_activate_call=ba.Call( 350 self._inc, txt, min_value, 351 max_value, increment, value_type, 352 setting.name), 353 repeat=True) 354 widget_column.append([btn1, btn2]) 355 356 elif value_type == bool: 357 v -= spacing 358 ba.textwidget(parent=self._subcontainer, 359 position=(h + 50, v), 360 size=(100, 30), 361 text=name_translated, 362 h_align='left', 363 color=(0.8, 0.8, 0.8, 1.0), 364 v_align='center', 365 maxwidth=mw1) 366 txt = ba.textwidget( 367 parent=self._subcontainer, 368 position=(h + 509 - 95, v), 369 size=(0, 28), 370 text=ba.Lstr(resource='onText') if value else ba.Lstr( 371 resource='offText'), 372 editable=False, 373 color=(0.6, 1.0, 0.6, 1.0), 374 maxwidth=mw2, 375 h_align='right', 376 v_align='center', 377 padding=2) 378 cbw = ba.checkboxwidget(parent=self._subcontainer, 379 text='', 380 position=(h + 505 - 50 - 5, v - 2), 381 size=(200, 30), 382 autoselect=True, 383 textcolor=(0.8, 0.8, 0.8), 384 value=value, 385 on_value_change_call=ba.Call( 386 self._check_value_change, 387 setting.name, txt)) 388 widget_column.append([cbw]) 389 390 else: 391 raise Exception() 392 393 # Ok now wire up the column. 394 try: 395 # pylint: disable=unsubscriptable-object 396 prev_widgets: list[ba.Widget] | None = None 397 for cwdg in widget_column: 398 if prev_widgets is not None: 399 # Wire our rightmost to their rightmost. 400 ba.widget(edit=prev_widgets[-1], down_widget=cwdg[-1]) 401 ba.widget(cwdg[-1], up_widget=prev_widgets[-1]) 402 403 # Wire our leftmost to their leftmost. 404 ba.widget(edit=prev_widgets[0], down_widget=cwdg[0]) 405 ba.widget(cwdg[0], up_widget=prev_widgets[0]) 406 prev_widgets = cwdg 407 except Exception: 408 ba.print_exception( 409 'Error wiring up game-settings-select widget column.') 410 411 ba.buttonwidget(edit=add_button, on_activate_call=ba.Call(self._add)) 412 ba.containerwidget(edit=self._root_widget, 413 selected_child=add_button, 414 start_button=add_button) 415 416 if default_selection == 'map': 417 ba.containerwidget(edit=self._root_widget, 418 selected_child=self._scrollwidget) 419 ba.containerwidget(edit=self._subcontainer, 420 selected_child=map_button)
Inherited Members
- ba.ui.Window
- get_root_widget