|
| 1 | +""" |
| 2 | +Custom playlist creation and management system for Dojo. |
| 3 | +
|
| 4 | +This module provides functionality for users to create, edit, and save |
| 5 | +custom playlists that persist between sessions. |
| 6 | +""" |
| 7 | + |
| 8 | +import json |
| 9 | +import os |
| 10 | +from typing import List, Dict, Any, Optional, Tuple |
| 11 | +from playlist import Playlist, ScenarioConfig, PlaylistSettings, PlayerRole |
| 12 | +from scenario import OffensiveMode, DefensiveMode |
| 13 | +from menu import MenuRenderer, UIElement |
| 14 | +from pydantic import BaseModel, Field, ValidationError |
| 15 | +from custom_scenario import CustomScenario, get_custom_scenarios |
| 16 | + |
| 17 | +class CustomPlaylistManager: |
| 18 | + def __init__(self, renderer, main_menu_renderer): |
| 19 | + self.renderer = renderer |
| 20 | + self.main_menu_renderer = main_menu_renderer |
| 21 | + # Current playlist being created/edited |
| 22 | + self.current_playlist_name = "" |
| 23 | + self.current_scenarios = [] |
| 24 | + self.current_custom_scenarios = [] |
| 25 | + self.current_boost_range = [12, 100] # Default boost range |
| 26 | + self.current_timeout = 7.0 |
| 27 | + self.current_rule_zero = False |
| 28 | + |
| 29 | + def load_custom_playlists(self): |
| 30 | + """Load custom playlists from disk and return a list of all custom playlists""" |
| 31 | + custom_playlists = {} |
| 32 | + for file in os.listdir(_get_custom_playlists_path()): |
| 33 | + if file.endswith(".json"): |
| 34 | + with open(os.path.join(_get_custom_playlists_path(), file), "r") as f: |
| 35 | + custom_playlists[file.replace(".json", "")] = Playlist.model_validate_json(f.read()) |
| 36 | + return custom_playlists |
| 37 | + |
| 38 | + def create_playlist_creation_menu(self): |
| 39 | + """Create the main playlist creation menu""" |
| 40 | + menu = MenuRenderer(self.renderer, columns=1, render_function=self._render_playlist_details) |
| 41 | + menu.add_element(UIElement("Create Custom Playlist", header=True)) |
| 42 | + menu.add_element(UIElement("Set Playlist Name", submenu=self._create_name_input_menu(), display_value_function=self.get_current_playlist_name)) |
| 43 | + menu.add_element(UIElement("Add PresetScenarios", submenu=self._create_scenario_selection_menu())) |
| 44 | + menu.add_element(UIElement("Add Custom Scenario", submenu=self._create_custom_scenario_selection_menu())) |
| 45 | + menu.add_element(UIElement("Set Boost Range", submenu=self._create_boost_range_menu(), display_value_function=self.get_current_playlist_boost_range)) |
| 46 | + menu.add_element(UIElement("Set Timeout", submenu=self._create_timeout_menu(), display_value_function=self.get_current_playlist_timeout)) |
| 47 | + menu.add_element(UIElement("Toggle Rule Zero", function=self._toggle_rule_zero, display_value_function=self.get_current_playlist_rule_zero)) |
| 48 | + menu.add_element(UIElement("Save Playlist", function=self._save_current_playlist)) |
| 49 | + menu.add_element(UIElement("Cancel", function=self._cancel_playlist_creation)) |
| 50 | + return menu |
| 51 | + |
| 52 | + ### Element value retrieval functions |
| 53 | + def get_current_playlist_name(self): |
| 54 | + return self.current_playlist_name |
| 55 | + |
| 56 | + |
| 57 | + def get_current_playlist_boost_range(self): |
| 58 | + return self.current_boost_range |
| 59 | + |
| 60 | + def get_current_playlist_timeout(self): |
| 61 | + return self.current_timeout |
| 62 | + |
| 63 | + def get_current_playlist_rule_zero(self): |
| 64 | + return self.current_rule_zero |
| 65 | + |
| 66 | + def _render_playlist_details(self): |
| 67 | + # Create a playlist out of current settings |
| 68 | + playlist = Playlist( |
| 69 | + name=self.current_playlist_name, |
| 70 | + description=f"Custom playlist with {len(self.current_scenarios)} scenarios", |
| 71 | + scenarios=self.current_scenarios.copy(), |
| 72 | + custom_scenarios=self.current_custom_scenarios.copy(), |
| 73 | + settings=PlaylistSettings(timeout=self.current_timeout, shuffle=True, boost_range=self.current_boost_range, rule_zero=self.current_rule_zero) |
| 74 | + ) |
| 75 | + playlist.render_details(self.renderer) |
| 76 | + |
| 77 | + def _create_name_input_menu(self): |
| 78 | + """Create menu for setting playlist name""" |
| 79 | + menu = MenuRenderer(self.renderer, columns=1, text_input=True, text_input_callback=self._set_playlist_name) |
| 80 | + return menu |
| 81 | + |
| 82 | + def _create_scenario_selection_menu(self): |
| 83 | + """Create menu for selecting scenarios to add""" |
| 84 | + menu = MenuRenderer(self.renderer, columns=3, show_selections=True, render_function=self._render_playlist_details) |
| 85 | + |
| 86 | + # Column 1: Offensive modes |
| 87 | + menu.add_element(UIElement("Offensive Mode", header=True), column=0) |
| 88 | + for mode in OffensiveMode: |
| 89 | + menu.add_element(UIElement( |
| 90 | + mode.name.replace('_', ' ').title(), |
| 91 | + function=self._set_temp_offensive_mode, |
| 92 | + function_args=mode, |
| 93 | + chooseable=True, |
| 94 | + ), column=0) |
| 95 | + |
| 96 | + # Column 2: Defensive modes |
| 97 | + menu.add_element(UIElement("Defensive Mode", header=True), column=1) |
| 98 | + for mode in DefensiveMode: |
| 99 | + menu.add_element(UIElement( |
| 100 | + mode.name.replace('_', ' ').title(), |
| 101 | + function=self._set_temp_defensive_mode, |
| 102 | + function_args=mode, |
| 103 | + chooseable=True, |
| 104 | + ), column=1) |
| 105 | + |
| 106 | + # Column 3: Player role and actions |
| 107 | + menu.add_element(UIElement("Player Role", header=True), column=2) |
| 108 | + menu.add_element(UIElement("Offense", function=self._set_temp_player_role, function_args=PlayerRole.OFFENSE, chooseable=True), column=2) |
| 109 | + menu.add_element(UIElement("Defense", function=self._set_temp_player_role, function_args=PlayerRole.DEFENSE, chooseable=True), column=2) |
| 110 | + menu.add_element(UIElement("", header=True), column=2) # Spacer |
| 111 | + menu.add_element(UIElement("Add Scenario", function=self._add_current_scenario), column=2) |
| 112 | + |
| 113 | + |
| 114 | + return menu |
| 115 | + |
| 116 | + def _create_custom_scenario_selection_menu(self): |
| 117 | + """Create menu for selecting custom scenarios to add""" |
| 118 | + menu = MenuRenderer(self.renderer, columns=2, show_selections=True, render_function=self._render_playlist_details) |
| 119 | + custom_scenarios = get_custom_scenarios() |
| 120 | + |
| 121 | + # Column 1: Custom scenarios |
| 122 | + for scenario_name in custom_scenarios: |
| 123 | + menu.add_element(UIElement(scenario_name, function=self._add_custom_scenario, function_args=scenario_name)) |
| 124 | + |
| 125 | + # Column 2: Player role and actions |
| 126 | + return menu |
| 127 | + |
| 128 | + def _create_boost_range_menu(self): |
| 129 | + """Create menu for setting boost range""" |
| 130 | + menu = MenuRenderer(self.renderer, columns=2, render_function=self._render_playlist_details) |
| 131 | + |
| 132 | + # Column 1: Min boost |
| 133 | + menu.add_element(UIElement("Min Boost", header=True), column=0) |
| 134 | + for boost in [0, 12, 20, 30, 40, 50, 60, 70]: |
| 135 | + menu.add_element(UIElement( |
| 136 | + str(boost), |
| 137 | + function=self._set_min_boost, |
| 138 | + function_args=boost |
| 139 | + ), column=0) |
| 140 | + |
| 141 | + # Column 2: Max boost |
| 142 | + menu.add_element(UIElement("Max Boost", header=True), column=1) |
| 143 | + for boost in [50, 60, 70, 80, 90, 100]: |
| 144 | + menu.add_element(UIElement( |
| 145 | + str(boost), |
| 146 | + function=self._set_max_boost, |
| 147 | + function_args=boost |
| 148 | + ), column=1) |
| 149 | + |
| 150 | + return menu |
| 151 | + |
| 152 | + def _create_timeout_menu(self): |
| 153 | + """Create menu for setting timeout""" |
| 154 | + menu = MenuRenderer(self.renderer, columns=1, render_function=self._render_playlist_details) |
| 155 | + menu.add_element(UIElement("Set Timeout (seconds)", header=True)) |
| 156 | + |
| 157 | + for timeout in [5.0, 7.0, 10.0, 15.0, 20.0, 30.0]: |
| 158 | + menu.add_element(UIElement( |
| 159 | + f"{timeout}s", |
| 160 | + function=self._set_timeout, |
| 161 | + function_args=timeout |
| 162 | + )) |
| 163 | + |
| 164 | + return menu |
| 165 | + |
| 166 | + # Temporary variables for scenario creation |
| 167 | + temp_offensive_mode = None |
| 168 | + temp_defensive_mode = None |
| 169 | + temp_player_role = None |
| 170 | + |
| 171 | + def _set_playlist_name(self, name): |
| 172 | + self.current_playlist_name = name |
| 173 | + print(f"Playlist name set to: {name}") |
| 174 | + |
| 175 | + def _set_temp_offensive_mode(self, mode): |
| 176 | + self.temp_offensive_mode = mode |
| 177 | + print(f"Selected offensive mode: {mode.name}") |
| 178 | + |
| 179 | + def _set_temp_defensive_mode(self, mode): |
| 180 | + self.temp_defensive_mode = mode |
| 181 | + print(f"Selected defensive mode: {mode.name}") |
| 182 | + |
| 183 | + def _set_temp_player_role(self, role): |
| 184 | + self.temp_player_role = role |
| 185 | + print(f"Selected player role: {role.name}") |
| 186 | + |
| 187 | + def _add_current_scenario(self): |
| 188 | + """Add the currently selected scenario configuration""" |
| 189 | + if self.temp_offensive_mode and self.temp_defensive_mode and self.temp_player_role: |
| 190 | + scenario = ScenarioConfig( |
| 191 | + offensive_mode=self.temp_offensive_mode, |
| 192 | + defensive_mode=self.temp_defensive_mode, |
| 193 | + player_role=self.temp_player_role |
| 194 | + ) |
| 195 | + self.current_scenarios.append(scenario) |
| 196 | + print(f"Added scenario: {self.temp_offensive_mode.name} vs {self.temp_defensive_mode.name} ({self.temp_player_role.name})") |
| 197 | + |
| 198 | + # Reset temp variables |
| 199 | + self.temp_offensive_mode = None |
| 200 | + self.temp_defensive_mode = None |
| 201 | + self.temp_player_role = None |
| 202 | + |
| 203 | + # Exit the submenu |
| 204 | + if self.main_menu_renderer: |
| 205 | + self.main_menu_renderer.handle_back_key() |
| 206 | + else: |
| 207 | + print("Please select offensive mode, defensive mode, and player role first") |
| 208 | + |
| 209 | + |
| 210 | + |
| 211 | + def _set_min_boost(self, boost): |
| 212 | + """Set minimum boost value""" |
| 213 | + self.current_boost_range = (boost, max(boost + 10, self.current_boost_range[1])) |
| 214 | + print(f"Set boost range: {self.current_boost_range}") |
| 215 | + |
| 216 | + def _set_max_boost(self, boost): |
| 217 | + """Set maximum boost value""" |
| 218 | + self.current_boost_range = (min(boost - 10, self.current_boost_range[0]), boost) |
| 219 | + print(f"Set boost range: {self.current_boost_range}") |
| 220 | + |
| 221 | + def _set_timeout(self, timeout): |
| 222 | + """Set scenario timeout""" |
| 223 | + self.current_timeout = timeout |
| 224 | + print(f"Set timeout: {timeout}s") |
| 225 | + |
| 226 | + def _toggle_rule_zero(self): |
| 227 | + """Toggle rule zero setting""" |
| 228 | + self.current_rule_zero = not self.current_rule_zero |
| 229 | + print(f"Rule zero: {'ON' if self.current_rule_zero else 'OFF'}") |
| 230 | + |
| 231 | + |
| 232 | + def _save_current_playlist(self): |
| 233 | + """Save the currently configured playlist to file, and register it in the playlist registry""" |
| 234 | + if not self.current_playlist_name: |
| 235 | + print("Please set a playlist name first") |
| 236 | + return |
| 237 | + |
| 238 | + # Register the playlist in the playlist registry |
| 239 | + # and save it to file |
| 240 | + playlist = Playlist( |
| 241 | + name=self.current_playlist_name, |
| 242 | + description=f"Custom playlist with {len(self.current_scenarios)} scenarios", |
| 243 | + scenarios=self.current_scenarios.copy(), |
| 244 | + custom_scenarios=self.current_custom_scenarios.copy(), |
| 245 | + settings=PlaylistSettings(timeout=self.current_timeout, shuffle=True, boost_range=self.current_boost_range, rule_zero=self.current_rule_zero) |
| 246 | + ) |
| 247 | + |
| 248 | + with open(os.path.join(_get_custom_playlists_path(), f"{self.current_playlist_name}.json"), "w") as f: |
| 249 | + f.write(playlist.model_dump_json()) |
| 250 | + |
| 251 | + def _cancel_playlist_creation(self): |
| 252 | + """Cancel playlist creation and reset""" |
| 253 | + self._reset_current_playlist() |
| 254 | + print("Playlist creation cancelled") |
| 255 | + |
| 256 | + def _reset_current_playlist(self): |
| 257 | + """Reset current playlist creation data""" |
| 258 | + self.current_playlist_name = "" |
| 259 | + self.current_scenarios = [] |
| 260 | + self.current_custom_scenarios = [] |
| 261 | + self.current_boost_range = [12, 100] |
| 262 | + self.current_timeout = 7.0 |
| 263 | + self.current_rule_zero = False |
| 264 | + self.temp_offensive_mode = None |
| 265 | + self.temp_defensive_mode = None |
| 266 | + self.temp_player_role = None |
| 267 | + |
| 268 | + def _add_custom_scenario(self, scenario_name): |
| 269 | + """Add a custom scenario""" |
| 270 | + self.current_custom_scenarios.append(CustomScenario.load(scenario_name)) |
| 271 | + print(f"Added custom scenario: {scenario_name}") |
| 272 | + |
| 273 | + def get_custom_playlists(self): |
| 274 | + """Get all custom playlists""" |
| 275 | + # Load all custom playlists from disk |
| 276 | + custom_playlists = {} |
| 277 | + for file in os.listdir(_get_custom_playlists_path()): |
| 278 | + if file.endswith(".json"): |
| 279 | + with open(os.path.join(_get_custom_playlists_path(), file), "r") as f: |
| 280 | + custom_playlists[file.replace(".json", "")] = Playlist.model_validate_json(f.read()) |
| 281 | + return custom_playlists |
| 282 | + |
| 283 | + |
| 284 | +def _get_custom_playlists_path(): |
| 285 | + appdata_path = os.path.expandvars("%APPDATA%") |
| 286 | + if not os.path.exists(os.path.join(appdata_path, "RLBot", "Dojo", "Playlists")): |
| 287 | + os.makedirs(os.path.join(appdata_path, "RLBot", "Dojo", "Playlists")) |
| 288 | + return os.path.join(appdata_path, "RLBot", "Dojo", "Playlists") |
0 commit comments