Package horizons :: Package gui :: Package modules :: Module singleplayermenu
[hide private]
[frames] | no frames]

Source Code for Module horizons.gui.modules.singleplayermenu

  1  # ################################################### 
  2  # Copyright (C) 2008-2017 The Unknown Horizons Team 
  3  # team@unknown-horizons.org 
  4  # This file is part of Unknown Horizons. 
  5  # 
  6  # Unknown Horizons is free software; you can redistribute it and/or modify 
  7  # it under the terms of the GNU General Public License as published by 
  8  # the Free Software Foundation; either version 2 of the License, or 
  9  # (at your option) any later version. 
 10  # 
 11  # This program is distributed in the hope that it will be useful, 
 12  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 13  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 14  # GNU General Public License for more details. 
 15  # 
 16  # You should have received a copy of the GNU General Public License 
 17  # along with this program; if not, write to the 
 18  # Free Software Foundation, Inc., 
 19  # 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA 
 20  # ################################################### 
 21   
 22   
 23  import json 
 24  import locale 
 25  import logging 
 26  import os 
 27  import re 
 28  import subprocess 
 29  import sys 
 30  import tempfile 
 31  from typing import List, Optional, Tuple 
 32   
 33  import horizons.globals 
 34  import horizons.main 
 35  from horizons.constants import LANGUAGENAMES, PATHS, VERSION 
 36  from horizons.extscheduler import ExtScheduler 
 37  from horizons.gui.util import load_uh_widget 
 38  from horizons.gui.widgets.minimap import Minimap, iter_minimap_points_colors 
 39  from horizons.gui.windows import Window 
 40  from horizons.i18n import gettext as T 
 41  from horizons.savegamemanager import SavegameManager 
 42  from horizons.scenario import InvalidScenarioFileFormat, ScenarioEventHandler 
 43  from horizons.util.python.callback import Callback 
 44  from horizons.util.random_map import generate_random_map, generate_random_seed 
 45  from horizons.util.shapes import Rect 
 46  from horizons.util.startgameoptions import StartGameOptions 
 47  from horizons.world import load_raw_world  # FIXME placing this import at the end results in a cycle 
 48   
 49  from .aidataselection import AIDataSelection 
 50  from .playerdataselection import PlayerDataSelection 
51 52 53 -class SingleplayerMenu(Window):
54
55 - def __init__(self, windows):
56 super().__init__(windows) 57 58 self._mode = None 59 60 self._gui = load_uh_widget('singleplayermenu.xml') 61 self._gui.mapEvents({ 62 'cancel': self._windows.close, 63 'okay': self.act, 64 'scenario': Callback(self._select_mode, 'scenario'), 65 'random': Callback(self._select_mode, 'random'), 66 'free_maps': Callback(self._select_mode, 'free_maps') 67 }) 68 69 self._playerdata = PlayerDataSelection() 70 self._aidata = AIDataSelection() 71 self._gui.findChild(name="playerdataselectioncontainer").addChild(self._playerdata.get_widget()) 72 self._gui.findChild(name="aidataselectioncontainer").addChild(self._aidata.get_widget())
73
74 - def hide(self):
75 # Save the player-data on hide so that other menus gets updated data 76 self._playerdata.save_settings() 77 self._gui.hide()
78
79 - def show(self):
80 self._playerdata.update_data() 81 self._gui.findChild(name='scenario').marked = True 82 self._select_mode('scenario')
83
84 - def on_return(self):
85 self.act()
86
87 - def _select_mode(self, mode):
88 self._gui.hide() 89 90 modes = { 91 'random': RandomMapWidget, 92 'free_maps': FreeMapsWidget, 93 'scenario': ScenarioMapWidget, 94 } 95 96 # remove old widget 97 if self._mode: 98 self._mode.end() 99 self._gui.findChild(name="right_side_box").removeChild(self._mode.get_widget()) 100 101 self._mode = modes[mode](self._windows, self, self._aidata) 102 self._mode.show() 103 104 self._gui.findChild(name="right_side_box").addChild(self._mode.get_widget()) 105 self._gui.show()
106
107 - def act(self):
108 """Start the game. Called when OK button is pressed.""" 109 player_color = self._playerdata.get_player_color() 110 player_name = self._playerdata.get_player_name() 111 112 if not player_name: 113 self._windows.open_popup(T("Invalid player name"), T("You entered an invalid playername.")) 114 return 115 116 horizons.globals.fife.set_uh_setting("Nickname", player_name) 117 118 self._windows.close() 119 self._mode.act(player_name, player_color)
120
121 122 -class GameSettingsWidget:
123 """Toggle trader/pirates/disasters and change resource density.""" 124
125 - def __init__(self):
126 self._gui = load_uh_widget('game_settings.xml')
127
128 - def get_widget(self):
129 return self._gui
130
131 - def show(self):
132 # make click on labels change the respective checkboxes 133 checkboxes = [('free_trader', 'MapSettingsFreeTraderEnabled'), 134 ('pirates', 'MapSettingsPirateEnabled'), 135 ('disasters', 'MapSettingsDisastersEnabled')] 136 137 for (setting, setting_save_name) in checkboxes: 138 139 def on_box_toggle(setting, setting_save_name): 140 """Called whenever the checkbox is toggled""" 141 box = self._gui.findChild(name=setting) 142 horizons.globals.fife.set_uh_setting(setting_save_name, box.marked) 143 horizons.globals.fife.save_settings()
144 145 def toggle(setting, setting_save_name): 146 """Called by the label to toggle the checkbox""" 147 box = self._gui.findChild(name=setting) 148 box.marked = not box.marked
149 150 self._gui.findChild(name=setting).capture(Callback(on_box_toggle, setting, setting_save_name)) 151 self._gui.findChild(name=setting).marked = horizons.globals.fife.get_uh_setting(setting_save_name) 152 self._gui.findChild(name='lbl_' + setting).capture(Callback(toggle, setting, setting_save_name)) 153 154 resource_density_slider = self._gui.findChild(name='resource_density_slider') 155 156 def on_resource_density_slider_change(): 157 self._gui.findChild(name='resource_density_lbl').text = T('Resource density:') + ' ' + \ 158 str(resource_density_slider.value) + 'x' 159 horizons.globals.fife.set_uh_setting("MapResourceDensity", resource_density_slider.value) 160 horizons.globals.fife.save_settings() 161 162 resource_density_slider.capture(on_resource_density_slider_change) 163 resource_density_slider.value = horizons.globals.fife.get_uh_setting("MapResourceDensity") 164 165 on_resource_density_slider_change() 166 167 @property
168 - def natural_resource_multiplier(self):
169 return self._gui.findChild(name='resource_density_slider').value
170 171 @property
172 - def free_trader(self):
173 return self._gui.findChild(name='free_trader').marked
174 175 @property
176 - def pirates(self):
177 return self._gui.findChild(name='pirates').marked
178 179 @property
180 - def disasters(self):
181 return self._gui.findChild(name='disasters').marked
182
183 184 -class RandomMapWidget:
185 """Create a random map, influence map generation with multiple sliders.""" 186
187 - def __init__(self, windows, singleplayer_menu, aidata):
188 self._windows = windows 189 self._singleplayer_menu = singleplayer_menu 190 self._aidata = aidata 191 192 self._gui = load_uh_widget('sp_random.xml') 193 self._map_parameters = {} # stores the current values from the sliders 194 self._game_settings = GameSettingsWidget() 195 196 # Map preview 197 self._last_map_parameters = None 198 self._preview_process = None 199 self._preview_output = None 200 self._map_preview = None
201
202 - def end(self):
203 if self._preview_process: 204 self._preview_process.kill() 205 self._preview_process = None 206 ExtScheduler().rem_all_classinst_calls(self)
207
208 - def get_widget(self):
209 return self._gui
210
211 - def act(self, player_name, player_color):
212 self.end() 213 214 map_file = generate_random_map(*self._get_map_parameters()) 215 216 options = StartGameOptions.create_start_map(map_file) 217 options.set_human_data(player_name, player_color) 218 options.ai_players = self._aidata.get_ai_players() 219 options.trader_enabled = self._game_settings.free_trader 220 options.pirate_enabled = self._game_settings.pirates 221 options.disasters_enabled = self._game_settings.disasters 222 options.natural_resource_multiplier = self._game_settings.natural_resource_multiplier 223 horizons.main.start_singleplayer(options)
224
225 - def show(self):
226 seed_string_field = self._gui.findChild(name='seed_string_field') 227 seed_string_field.capture(self._on_random_parameter_changed) 228 seed_string_field.text = generate_random_seed(seed_string_field.text) 229 230 parameters = ( 231 ('map_size', T('Map size:'), 'RandomMapSize'), 232 ('water_percent', T('Water:'), 'RandomMapWaterPercent'), 233 ('max_island_size', T('Max island size:'), 'RandomMapMaxIslandSize'), 234 ('preferred_island_size', T('Preferred island size:'), 'RandomMapPreferredIslandSize'), 235 ('island_size_deviation', T('Island size deviation:'), 'RandomMapIslandSizeDeviation'), 236 ) 237 238 for param, __, setting_name in parameters: 239 self._map_parameters[param] = int(horizons.globals.fife.get_uh_setting(setting_name)) 240 241 def make_on_change(param, text, setting_name): 242 # When a slider is changed, update the value displayed in the label, save the value 243 # in the settings and store the value in self._map_parameters 244 def on_change(): 245 slider = self._gui.findChild(name=param + '_slider') 246 self._gui.findChild(name=param + '_lbl').text = text + ' ' + str(int(slider.value)) 247 horizons.globals.fife.set_uh_setting(setting_name, slider.value) 248 horizons.globals.fife.save_settings() 249 self._map_parameters[param] = int(slider.value) 250 self._on_random_parameter_changed()
251 return on_change
252 253 for param, text, setting_name in parameters: 254 slider = self._gui.findChild(name=param + '_slider') 255 on_change = make_on_change(param, text, setting_name) 256 slider.capture(on_change) 257 slider.value = horizons.globals.fife.get_uh_setting(setting_name) 258 on_change() 259 260 self._gui.findChild(name='game_settings_box').addChild(self._game_settings.get_widget()) 261 self._game_settings.show() 262 self._aidata.show() 263
264 - def _get_map_parameters(self):
265 return ( 266 self._gui.findChild(name='seed_string_field').text, 267 self._map_parameters['map_size'], 268 self._map_parameters['water_percent'], 269 self._map_parameters['max_island_size'], 270 self._map_parameters['preferred_island_size'], 271 self._map_parameters['island_size_deviation'] 272 )
273
274 - def _on_random_parameter_changed(self):
275 self._update_map_preview()
276 277 # Map preview 278
279 - def _on_preview_click(self, event, drag):
280 seed_string_field = self._gui.findChild(name='seed_string_field') 281 seed_string_field.text = generate_random_seed(seed_string_field.text) 282 self._on_random_parameter_changed()
283
284 - def _update_map_preview(self):
285 """Start a new process to generate a map preview.""" 286 current_parameters = self._get_map_parameters() 287 if self._last_map_parameters == current_parameters: 288 # nothing changed, don't generate a new preview 289 return 290 291 self._last_map_parameters = current_parameters 292 293 if self._preview_process: 294 self._preview_process.kill() # process exists, therefore up is scheduled already 295 296 # launch process in background to calculate minimap data 297 minimap_icon = self._gui.findChild(name='map_preview_minimap') 298 params = json.dumps(((minimap_icon.width, minimap_icon.height), current_parameters)) 299 300 args = [sys.executable, sys.argv[0], "--generate-minimap", params] 301 # We're running UH in a new process, make sure fife is setup correctly 302 if horizons.main.command_line_arguments.fife_path: 303 args.extend(["--fife-path", horizons.main.command_line_arguments.fife_path]) 304 305 handle, self._preview_output = tempfile.mkstemp() 306 os.close(handle) 307 self._preview_process = subprocess.Popen(args=args, stdout=open(self._preview_output, "w")) 308 self._set_map_preview_status("Generating preview…") 309 310 ExtScheduler().add_new_object(self._poll_preview_process, self, 0.5)
311
312 - def _poll_preview_process(self):
313 """This will be called regularly to see if the process ended. 314 315 If the process has not yet finished, schedule a new callback to this function. 316 Otherwise use the data to update the minimap. 317 """ 318 if not self._preview_process: 319 return 320 321 self._preview_process.poll() 322 323 if self._preview_process.returncode is None: # not finished 324 ExtScheduler().add_new_object(self._poll_preview_process, self, 0.1) 325 return 326 elif self._preview_process.returncode != 0: 327 self._preview_process = None 328 self._set_map_preview_status("An unknown error occurred while generating the map preview") 329 return 330 331 with open(self._preview_output, 'r') as f: 332 data = f.read() 333 # Sometimes the subprocess outputs more then the minimap data, e.g. debug 334 # information. Since we just read from its stdout, parse out the data that 335 # is relevant to us. 336 data = re.findall(r'^DATA (\[\[.*\]\]) ENDDATA$', data, re.MULTILINE)[0] 337 data = json.loads(data) 338 339 os.unlink(self._preview_output) 340 self._preview_process = None 341 342 if self._map_preview: 343 self._map_preview.end() 344 345 self._map_preview = Minimap( 346 self._gui.findChild(name='map_preview_minimap'), 347 session=None, 348 view=None, 349 world=None, 350 targetrenderer=horizons.globals.fife.targetrenderer, 351 imagemanager=horizons.globals.fife.imagemanager, 352 cam_border=False, 353 use_rotation=False, 354 tooltip=T("Click to generate a different random map"), 355 on_click=self._on_preview_click, 356 preview=True) 357 358 self._map_preview.draw_data(data) 359 self._set_map_preview_status("")
360
361 - def _set_map_preview_status(self, text):
362 self._gui.findChild(name="map_preview_status_label").text = text
363
364 365 -class FreeMapsWidget:
366 """Start a game by selecting an existing map.""" 367
368 - def __init__(self, windows, singleplayer_menu, aidata):
369 self._windows = windows 370 self._singleplayer_menu = singleplayer_menu 371 self._aidata = aidata 372 373 self._gui = load_uh_widget('sp_free_maps.xml') 374 self._game_settings = GameSettingsWidget() 375 376 self._map_preview = None
377
378 - def end(self):
379 pass
380
381 - def get_widget(self):
382 return self._gui
383
384 - def act(self, player_name, player_color):
385 map_file = self._get_selected_map() 386 387 options = StartGameOptions.create_start_map(map_file) 388 options.set_human_data(player_name, player_color) 389 options.ai_players = self._aidata.get_ai_players() 390 options.trader_enabled = self._game_settings.free_trader 391 options.pirate_enabled = self._game_settings.pirates 392 options.disasters_enabled = self._game_settings.disasters 393 options.natural_resource_multiplier = self._game_settings.natural_resource_multiplier 394 horizons.main.start_singleplayer(options)
395
396 - def show(self):
397 self._files, maps_display = SavegameManager.get_maps() 398 399 self._gui.distributeInitialData({'maplist': maps_display}) 400 self._gui.mapEvents({ 401 'maplist/action': self._update_map_infos, 402 }) 403 if maps_display: # select first entry 404 self._gui.distributeData({'maplist': 0}) 405 self._update_map_infos() 406 407 self._gui.findChild(name='game_settings_box').addChild(self._game_settings.get_widget()) 408 self._game_settings.show() 409 self._aidata.show()
410
411 - def _update_map_infos(self):
412 map_file = self._get_selected_map() 413 414 number_of_players = SavegameManager.get_recommended_number_of_players(map_file) 415 lbl = self._gui.findChild(name="recommended_number_of_players_lbl") 416 lbl.text = T("Recommended number of players: {number}").format(number=number_of_players) 417 418 self._update_map_preview(map_file)
419
420 - def _get_selected_map(self):
421 selection_index = self._gui.collectData('maplist') 422 assert selection_index != -1 423 424 return self._files[self._gui.collectData('maplist')]
425
426 - def _update_map_preview(self, map_file):
427 if self._map_preview: 428 self._map_preview.end() 429 430 world = load_raw_world(map_file) 431 self._map_preview = Minimap( 432 self._gui.findChild(name='map_preview_minimap'), 433 session=None, 434 view=None, 435 world=world, 436 targetrenderer=horizons.globals.fife.targetrenderer, 437 imagemanager=horizons.globals.fife.imagemanager, 438 cam_border=False, 439 use_rotation=False, 440 tooltip=None, 441 on_click=None, 442 preview=True) 443 444 self._map_preview.draw()
445
446 447 -class ScenarioMapWidget:
448 """Start a scenario (with a specific language).""" 449
450 - def __init__(self, windows, singleplayer_menu, aidata):
451 self._windows = windows 452 self._singleplayer_menu = singleplayer_menu 453 self._aidata = aidata 454 self._scenarios = {} 455 456 self._language_fallback_active = False 457 458 self._gui = load_uh_widget('sp_scenario.xml')
459
460 - def end(self):
461 pass
462
463 - def get_widget(self):
464 return self._gui
465
466 - def act(self, player_name, player_color):
467 map_file = self._get_selected_map() 468 469 try: 470 options = StartGameOptions.create_start_scenario(map_file) 471 options.set_human_data(player_name, player_color) 472 horizons.main.start_singleplayer(options) 473 except InvalidScenarioFileFormat as e: 474 self._show_invalid_scenario_file_popup(e)
475
476 - def show(self):
477 self._aidata.hide() 478 479 self._scenarios = SavegameManager.get_available_scenarios() 480 481 # get the map files and their display names. display tutorials on top. 482 self.maps_display = list(self._scenarios.keys()) 483 if not self.maps_display: 484 return 485 486 prefer_tutorial = lambda x: ('tutorial' not in x, x) 487 self.maps_display.sort(key=prefer_tutorial) 488 489 self._gui.distributeInitialData({'maplist': self.maps_display}) 490 self._gui.distributeData({'maplist': 0}) 491 self._gui.mapEvents({ 492 'maplist/action': self._on_map_change, 493 'uni_langlist/action': self._update_infos, 494 }) 495 self._on_map_change()
496
497 - def _show_invalid_scenario_file_popup(self, exception):
498 """Shows a popup complaining about invalid scenario file. 499 500 @param exception: Something that str() will convert to an error message 501 """ 502 logging.getLogger('gui.windows').error("Error: %s", exception) 503 self._windows.open_error_popup( 504 T("Invalid scenario file"), 505 description=T("The selected file is not a valid scenario file."), 506 details=T("Error message:") + ' ' + str(str(exception)), 507 advice=T("Please report this to the author."))
508
509 - def _on_map_change(self):
510 # type: () -> None 511 lang_list = self._gui.findChild(name="uni_langlist") 512 selected_language = lang_list.selected_item 513 514 if (selected_language is None 515 or self._language_fallback_active): 516 # Either no language is selected (this happens initially), or the previous 517 # map needed a fallback language: we want to choose a more appropriate 518 # one for the new map. 519 selected_language = LANGUAGENAMES[horizons.globals.fife.get_locale()] 520 self._language_fallback_active = False 521 522 self._update_infos(selected_language=selected_language)
523
524 - def _update_infos(self, selected_language=None):
525 # type: (Optional[str]) -> None 526 """ 527 Check if selected language is available or pick a fallback language. Fill in infos 528 of selected scenario. 529 """ 530 scenario_idx = self._gui.findChild(name="maplist").selected_item 531 scenario = self._scenarios[scenario_idx] 532 533 lang_list = self._gui.findChild(name="uni_langlist") 534 selected_language = selected_language if selected_language is not None else lang_list.selected_item 535 536 available_languages = self.get_available_languages(scenario) 537 if selected_language not in available_languages: 538 selected_language = LANGUAGENAMES[self.guess_suitable_default_locale(available_languages)] 539 self._language_fallback_active = True 540 else: 541 self._language_fallback_active = False 542 543 lang_list.items = available_languages 544 lang_list.selected = available_languages.index(selected_language) 545 546 selected_language_code = LANGUAGENAMES.get_by_value(selected_language) 547 translated_scenario = self.find_map_filename(scenario, selected_language_code) 548 if translated_scenario is None: 549 return 550 551 self._update_scenario_translation_infos(translated_scenario)
552 553 @staticmethod
554 - def guess_suitable_default_locale(available_languages):
555 # type: (List[str]) -> Optional[str] 556 """Attempts to guess a reasonable localized scenario to preselect in SP menu. 557 558 If no filename was found so far for our scenario: 559 1. Try harder to find locale of user 560 2. Try to find a file for the system locale 561 3. Fall back to English 562 """ 563 try: 564 default_locale, default_encoding = locale.getdefaultlocale() 565 except ValueError: # OS X sometimes returns 'UTF-8' as locale, which is a ValueError 566 default_locale = 'en' 567 568 possibilities = [ 569 default_locale, 570 default_locale.split('@')[0], 571 default_locale.split('_')[0], 572 'en', 573 ] 574 for lang in possibilities: 575 if LANGUAGENAMES[lang] in available_languages: 576 return lang
577
578 - def _update_scenario_translation_infos(self, scenario):
579 """Fill in translation infos of selected scenario to translation label.""" 580 try: 581 metadata = ScenarioEventHandler.get_metadata_from_file(scenario) 582 except InvalidScenarioFileFormat as e: 583 self._show_invalid_scenario_file_popup(e) 584 return 585 586 translation_status = metadata.get('translation_status', '') 587 lbl = self._gui.findChild(name="translation_status") 588 lbl.text = translation_status 589 590 lbl = self._gui.findChild(name="uni_map_difficulty") 591 lbl.text = T("Difficulty: {difficulty}").format(difficulty=metadata['difficulty']) 592 593 lbl = self._gui.findChild(name="uni_map_author") 594 lbl.text = T("Author: {author}").format(author=metadata['author']) 595 596 lbl = self._gui.findChild(name="uni_map_desc") 597 lbl.text = T("Description: {desc}").format(desc=metadata['description'])
598 599 @staticmethod
600 - def find_map_filename(scenario, target_locale):
601 # type: (List[Tuple[str, str]], str) -> Optional[str] 602 """Finds the given map's filename with its locale.""" 603 for language, mapfile in scenario: 604 if language == target_locale and os.path.exists(mapfile): 605 return mapfile
606 607 @staticmethod
608 - def get_available_languages(scenario):
609 # type: (List[Tuple[str, str]]) -> List[str] 610 scenario_langs = {language for language, filename in scenario} 611 return [LANGUAGENAMES[l] for l in sorted(scenario_langs)]
612
613 - def _get_selected_map(self):
614 selection_index = self._gui.collectData('maplist') 615 assert selection_index != -1 616 scenario = self._scenarios[self.maps_display[selection_index]] 617 language_index = self._gui.collectData('uni_langlist') 618 return scenario[language_index][1]
619
620 621 -def generate_random_minimap(size, parameters):
622 """Called as subprocess, calculates minimap data and passes it via string via stdout""" 623 # called as standalone basically, so init everything we need 624 from horizons.entities import Entities 625 from horizons.main import _create_main_db 626 627 if not VERSION.IS_DEV_VERSION: 628 # Hack enable atlases. 629 # Usually the minimap generator uses single tile files, but in release 630 # mode these are not available. Therefor we have to hackenable atlases 631 # for the minimap generation in this case. This forces the game to use 632 # the correct imageloader 633 # In normal dev mode + enabled atlases we ignore this and just continue 634 # to use single tile files instead of atlases for the minimap generation. 635 # These are always available in dev checkouts 636 PATHS.DB_FILES = PATHS.DB_FILES + (PATHS.ATLAS_DB_PATH, ) 637 638 db = _create_main_db() 639 horizons.globals.db = db 640 horizons.globals.fife.init_animation_loader(not VERSION.IS_DEV_VERSION) 641 Entities.load_grounds(db, load_now=False) # create all references 642 643 map_file = generate_random_map(*parameters) 644 world = load_raw_world(map_file) 645 location = Rect.init_from_topleft_and_size_tuples((0, 0), size) 646 647 # communicate via stdout. Sometimes the process seems to print more information, therefore 648 # we add markers around our data so it's easier for the caller to get to the data. 649 args = (location, world, Minimap.COLORS['island'], Minimap.COLORS['water']) 650 data = [(x, y, r, g, b) for (x, y), (r, g, b) in iter_minimap_points_colors(*args)] 651 print('DATA', json.dumps(data), 'ENDDATA')
652