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

Source Code for Module horizons.gui.modules.multiplayermenu

  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  import hashlib 
 23  import textwrap 
 24   
 25  from fife.extensions.pychan.widgets import HBox, Label, TextField 
 26   
 27  import horizons.main 
 28  from horizons.component.ambientsoundcomponent import AmbientSoundComponent 
 29  from horizons.constants import MULTIPLAYER 
 30  from horizons.extscheduler import ExtScheduler 
 31  from horizons.gui.util import load_uh_widget 
 32  from horizons.gui.widgets.icongroup import hr as HRule 
 33  from horizons.gui.widgets.imagebutton import CancelButton, OkButton 
 34  from horizons.gui.widgets.minimap import Minimap 
 35  from horizons.gui.windows import Popup, Window 
 36  from horizons.i18n import gettext as T 
 37  from horizons.network import enet 
 38  from horizons.network.common import ErrorType 
 39  from horizons.network.networkinterface import NetworkInterface 
 40  from horizons.savegamemanager import SavegameManager 
 41  from horizons.util.color import Color 
 42  from horizons.util.python.callback import Callback 
 43  from horizons.world import load_raw_world 
 44   
 45  from .playerdataselection import PlayerDataSelection 
 46   
 47   
48 -class MultiplayerMenu(Window):
49
50 - def __init__(self, mainmenu, windows):
51 super().__init__(windows) 52 self._mainmenu = mainmenu 53 self._gui = load_uh_widget('multiplayermenu.xml') 54 self._gui.mapEvents({ 55 'cancel': self._windows.close, 56 'join': self._join_game, 57 'create': self._create_game, 58 'refresh': Callback(self._refresh, play_sound=True) 59 }) 60 61 self._gui.findChild(name='gamelist').capture(self._update_game_details) 62 self._playerdata = PlayerDataSelection() 63 self._gui.findChild(name="playerdataselectioncontainer").addChild(self._playerdata.get_widget()) 64 65 # to track if the menu window is opened or not. 66 self._is_open = False
67
68 - def hide(self):
69 # Save the player-data on hide so that other menus gets updated data 70 self._playerdata.save_settings() 71 self._gui.hide() 72 ExtScheduler().rem_all_classinst_calls(self)
73
74 - def show(self):
75 if not self._check_connection(): 76 return 77 78 if not self._refresh(): 79 self._windows.close() 80 return 81 82 if not self._is_open: 83 self._is_open = True 84 # subscribe "error" when this menu window is firstly opened 85 # only unsubscribe if this menu window is closed 86 NetworkInterface().subscribe("error", self._on_error) 87 88 # get updated player data 89 self._playerdata.update_data() 90 91 self._gui.show() 92 93 # TODO: Remove once loading a game is implemented again 94 self._gui.findChild(name='load').parent.hide() 95 96 ExtScheduler().add_new_object(self._refresh, self, run_in=5, loops=-1)
97
98 - def close(self):
99 # if the window is not open (due to connection errors), just do nothing 100 if not self._is_open: 101 return 102 103 self.hide() 104 105 NetworkInterface().unsubscribe("error", self._on_error) 106 self._is_open = False 107 108 # the window is also closed when a game starts, don't disconnect in that case 109 if NetworkInterface().is_connected and not NetworkInterface().is_joined: 110 NetworkInterface().disconnect()
111
112 - def on_return(self):
113 self._join_game()
114
115 - def _check_connection(self):
116 """ 117 Check if all dependencies for multiplayer games are met and we can connect to 118 the master server. If any dependency is not met, the window is closed. 119 """ 120 # It is important to close this window before showing the error popup. 121 # Otherwise closing the popup will trigger `show` again, a new attempt 122 # to connect is made, which ends up in an endless loop. 123 124 if enet is None: 125 self._windows.close() 126 headline = T("Unable to find pyenet") 127 descr = T('The multiplayer feature requires the library "pyenet", ' 128 "which could not be found on your system.") 129 advice = T("For instructions on installing pyenet see:" 130 "https://github.com/unknown-horizons/unknown-horizons/wiki/Installing-PyEnet") 131 self._windows.open_error_popup(headline, descr, advice) 132 return False 133 134 if NetworkInterface() is None: 135 try: 136 NetworkInterface.create_instance() 137 except RuntimeError as e: 138 self._windows.close() 139 headline = T("Failed to initialize networking.") 140 descr = T("Network features could not be initialized with the current configuration.") 141 advice = T("Check the settings you specified in the network section.") 142 self._windows.open_error_popup(headline, descr, advice, str(e)) 143 return False 144 145 if not NetworkInterface().is_connected: 146 try: 147 NetworkInterface().connect() 148 except TypeError as terr: 149 self._windows.close() 150 headline = T("Pyenet type error") 151 descr = T("You are probably using an incompatible pyenet installation") 152 advice = T("For instructions on properly installing pyenet see: " 153 "https://github.com/unknown-horizons/unknown-horizons/wiki/Installing-PyEnet") 154 self._windows.open_error_popup(headline, descr, advice, str(terr)) 155 except Exception as err: 156 self._windows.close() 157 headline = T("Fatal Network Error") 158 descr = T("Could not connect to master server.") 159 advice = T("Please check your Internet connection. If it is fine, " 160 "it means our master server is temporarily down.") 161 self._windows.open_error_popup(headline, descr, advice, str(err)) 162 return False 163 164 if NetworkInterface().is_joined: 165 if not NetworkInterface().leavegame(): 166 self._windows.close() 167 return False 168 169 return True
170
171 - def _on_error(self, exception, fatal=True):
172 """Error callback""" 173 if not fatal: 174 self._windows.open_popup(T("Error"), str(exception)) 175 else: 176 self._windows.open_popup(T("Fatal Network Error"), 177 T("Something went wrong with the network:") + '\n' + 178 str(exception)) 179 # FIXME: this shouldn't be necessary, the main menu window is still somewhere 180 # in the stack and we just need to get rid of all MP related windows 181 self._mainmenu.show_main()
182
183 - def _display_game_name(self, game):
184 same_version = game.version == NetworkInterface().get_clientversion() 185 template = "{password}{gamename}: {name} ({players}, {limit}){version}" 186 return template.format( 187 password="(Password!) " if game.has_password else "", 188 name=game.map_name, 189 gamename=game.name, 190 players=game.player_count, 191 limit=game.player_limit, 192 version=" " + T("Version differs!") if not same_version else "")
193
194 - def _refresh(self, play_sound=False):
195 """Refresh list of games. 196 197 @param play_sound: whether to play the refresh sound 198 @return bool, whether refresh worked 199 """ 200 if play_sound: 201 AmbientSoundComponent.play_special('refresh') 202 203 self._games = NetworkInterface().get_active_games() 204 if self._games is None: 205 return False 206 207 gamelist = [self._display_game_name(g) for g in self._games] 208 self._gui.distributeInitialData({'gamelist': gamelist}) 209 self._gui.distributeData({'gamelist': 0}) 210 self._update_game_details() 211 return True
212
213 - def _update_game_details(self):
214 """Set map name and other misc data in a widget.""" 215 try: 216 index = self._gui.collectData('gamelist') 217 game = self._games[index] 218 except IndexError: 219 return 220 221 self._gui.findChild(name="game_map").text = T("Map: {map_name}").format(map_name=game.map_name) 222 self._gui.findChild(name="game_name").text = T("Name: {game_name}").format(game_name=game.name) 223 self._gui.findChild(name="game_creator").text = T("Creator: {game_creator}").format(game_creator=game.creator) 224 self._gui.findChild(name="game_playersnum").text = T("Players: {player_amount}/{player_limit}").format( 225 player_amount=game.player_count, 226 player_limit=game.player_limit) 227 228 vbox_inner = self._gui.findChild(name="game_info") 229 vbox_inner.adaptLayout()
230
231 - def _join_game(self):
232 """Joins a multiplayer game. Displays lobby for that specific game""" 233 try: 234 index = self._gui.collectData('gamelist') 235 game = self._games[index] 236 except IndexError: 237 return 238 239 if game.uuid == -1: # -1 signals no game 240 AmbientSoundComponent.play_special('error') 241 return 242 243 if game.version != NetworkInterface().get_clientversion(): 244 self._windows.open_popup(T("Wrong version"), 245 T("The game's version differs from your version. " 246 "Every player in a multiplayer game must use the same version. " 247 "This can be fixed by every player updating to the latest version. " 248 "Game version: {game_version} Your version: {own_version}").format( 249 game_version=game.version, 250 own_version=NetworkInterface().get_clientversion())) 251 return 252 253 NetworkInterface().change_name(self._playerdata.get_player_name()) 254 NetworkInterface().change_color(self._playerdata.get_player_color().id) 255 256 if game.password: 257 # ask the player for the password 258 popup = PasswordInput(self._windows) 259 password = self._windows.open(popup) 260 if password is None: 261 return 262 password = hashlib.sha1(password.encode(encoding='utf-8')).hexdigest() 263 success = NetworkInterface().joingame(game.uuid, password) 264 if not success: 265 return 266 elif not NetworkInterface().joingame(game.uuid, ''): 267 return 268 269 window = GameLobby(self._windows) 270 self._windows.open(window)
271
272 - def _create_game(self):
273 NetworkInterface().change_name(self._playerdata.get_player_name()) 274 NetworkInterface().change_color(self._playerdata.get_player_color().id) 275 self._windows.open(CreateGame(self._windows))
276 277
278 -class PasswordInput(Popup):
279 """Popup where players enter a password to join multiplayer games.""" 280 focus = 'password' 281
282 - def __init__(self, windows):
283 title = T('Password of the game') 284 text = T('Enter password:') 285 super().__init__(windows, title, text, show_cancel_button=True)
286
287 - def prepare(self, **kwargs):
288 super().prepare(**kwargs) 289 pw = TextField(name='password', fixed_size=(320, 20)) 290 box = self._gui.findChild(name='message_box') 291 box.addChild(pw)
292
293 - def act(self, send_password):
294 if not send_password: 295 return 296 return self._gui.collectData("password")
297 298
299 -class CreateGame(Window):
300 """Interface for creating a multiplayer game""" 301
302 - def __init__(self, windows):
303 super().__init__(windows) 304 305 self._gui = load_uh_widget('multiplayer_creategame.xml') 306 self._gui.mapEvents({ 307 'cancel': self._windows.close, 308 'create': self.act, 309 }) 310 311 self._files = [] 312 self._maps_display = [] 313 self._map_preview = None
314
315 - def hide(self):
316 self._gui.hide()
317
318 - def show(self):
319 self._files, self._maps_display = SavegameManager.get_maps() 320 321 self._gui.distributeInitialData({ 322 'maplist': self._maps_display, 323 'playerlimit': list(range(2, MULTIPLAYER.MAX_PLAYER_COUNT)) 324 }) 325 326 if self._maps_display: # select first entry 327 self._gui.distributeData({ 328 'maplist': 0, 329 'playerlimit': 0 330 }) 331 self._update_infos() 332 333 self._gui.findChild(name="maplist").mapEvents({ 334 'maplist/action': self._update_infos 335 }) 336 337 gamenametextfield = self._gui.findChild(name='gamename') 338 def gamename_clicked(): 339 if gamenametextfield.text == 'Unnamed Game': 340 gamenametextfield.text = ""
341 gamenametextfield.capture(gamename_clicked, event_name='mouseClicked') 342 self._gui.show()
343
344 - def act(self):
345 mapindex = self._gui.collectData('maplist') 346 mapname = self._maps_display[mapindex] 347 maxplayers = self._gui.collectData('playerlimit') + 2 # 1 is the first entry 348 gamename = self._gui.collectData('gamename') 349 password = self._gui.collectData('password') 350 maphash = "" 351 352 password = hashlib.sha1(password.encode(encoding='utf-8')).hexdigest() if password != "" else "" 353 game = NetworkInterface().creategame(mapname, maxplayers, gamename, maphash, password) 354 if game: 355 # FIXME When canceling the lobby, I'd like the player to return to the main mp 356 # menu, and not see the 'create game' again. We need to close this window, however, 357 # this will trigger the display of the main gui, which will part the game in 358 # `MultiplayerMenu._check_connection` 359 #self._windows.close() 360 window = GameLobby(self._windows) 361 self._windows.open(window)
362
363 - def _update_infos(self):
364 index = self._gui.collectData('maplist') 365 mapfile = self._files[index] 366 number_of_players = SavegameManager.get_recommended_number_of_players(mapfile) 367 368 lbl = self._gui.findChild(name="recommended_number_of_players_lbl") 369 lbl.text = T("Recommended number of players: {number}").format(number=number_of_players) 370 371 self._update_map_preview(mapfile)
372
373 - def _update_map_preview(self, map_file):
374 if self._map_preview: 375 self._map_preview.end() 376 377 world = load_raw_world(map_file) 378 self._map_preview = Minimap( 379 self._gui.findChild(name='map_preview_minimap'), 380 session=None, 381 view=None, 382 world=world, 383 targetrenderer=horizons.globals.fife.targetrenderer, 384 imagemanager=horizons.globals.fife.imagemanager, 385 cam_border=False, 386 use_rotation=False, 387 tooltip=None, 388 on_click=None, 389 preview=True) 390 391 self._map_preview.draw()
392 393
394 -class GameLobby(Window):
395 """Chat with other players, change name, wait for the game to begin.""" 396
397 - def __init__(self, windows):
398 super().__init__(windows) 399 400 self._gui = load_uh_widget('multiplayer_gamelobby.xml') 401 402 self._gui.mapEvents({ 403 'cancel': self._cancel, 404 'ready_btn': self._on_ready_button_pressed, 405 }) 406 407 NetworkInterface().subscribe("game_prepare", self._prepare_game)
408
409 - def _on_ready_button_pressed(self):
410 ready_button = self._gui.findChild(name="ready_btn") 411 ready_button.toggle() 412 ready_label = self._gui.findChild(name="ready_lbl") 413 if ready_button.is_active: 414 ready_label.text = T("Ready") + ":" 415 else: 416 ready_label.text = T("Not ready") + ":" 417 ready_label.adaptLayout() 418 NetworkInterface().toggle_ready()
419
420 - def hide(self):
421 self._gui.hide()
422
423 - def _cancel(self):
424 """When the lobby is cancelled, close the window and leave the game. 425 426 We can't do this in `close`, because the window will be closed when a game starts 427 as well, and we don't want to leave the game then. 428 """ 429 self._windows.close() 430 NetworkInterface().leavegame()
431
432 - def on_escape(self):
433 self._cancel()
434
435 - def close(self):
436 self.hide() 437 438 NetworkInterface().unsubscribe("lobbygame_chat", self._on_chat_message) 439 NetworkInterface().unsubscribe("lobbygame_join", self._on_player_joined) 440 NetworkInterface().unsubscribe("lobbygame_leave", self._on_player_left) 441 NetworkInterface().unsubscribe("lobbygame_kick", self._on_player_kicked) 442 NetworkInterface().unsubscribe("lobbygame_changename", self._on_player_changed_name) 443 NetworkInterface().unsubscribe("lobbygame_changecolor", self._on_player_changed_color) 444 NetworkInterface().unsubscribe("lobbygame_toggleready", self._on_player_toggled_ready) 445 NetworkInterface().unsubscribe("game_details_changed", self._update_game_details) 446 NetworkInterface().unsubscribe("game_prepare", self._prepare_game) 447 NetworkInterface().unsubscribe("error", self._on_error)
448
449 - def open(self):
450 textfield = self._gui.findChild(name="chatTextField") 451 textfield.capture(self._send_chat_message) 452 welcome_string = T("Enter your message") 453 def chatfield_clicked(): 454 if textfield.text == welcome_string: 455 textfield.text = ""
456 textfield.text = welcome_string 457 textfield.capture(chatfield_clicked, event_name="mouseClicked") 458 459 self._update_game_details() 460 461 NetworkInterface().subscribe("lobbygame_chat", self._on_chat_message) 462 NetworkInterface().subscribe("lobbygame_join", self._on_player_joined) 463 NetworkInterface().subscribe("lobbygame_leave", self._on_player_left) 464 NetworkInterface().subscribe("lobbygame_kick", self._on_player_kicked) 465 NetworkInterface().subscribe("lobbygame_changename", self._on_player_changed_name) 466 NetworkInterface().subscribe("lobbygame_changecolor", self._on_player_changed_color) 467 NetworkInterface().subscribe("lobbygame_toggleready", self._on_player_toggled_ready) 468 NetworkInterface().subscribe("game_details_changed", self._update_game_details) 469 NetworkInterface().subscribe("error", self._on_error) 470 471 self.show()
472
473 - def show(self):
474 self._gui.show()
475
476 - def _on_error(self, error, fatal=True):
477 if error.cmd_type == ErrorType.TerminateGame: 478 # We can't use `_cancel` here, since that calls `leavegame`, which isn't 479 # possible since the game was terminated already. 480 self._windows.close()
481
482 - def _prepare_game(self, game):
483 horizons.main.prepare_multiplayer(game)
484
485 - def _update_game_details(self):
486 """Set map name and other misc data""" 487 game = NetworkInterface().get_game() 488 489 self._gui.findChild(name="game_map").text = T("Map: {map_name}").format(map_name=game.map_name) 490 self._gui.findChild(name="game_name").text = T("Name: {game_name}").format(game_name=game.name) 491 self._gui.findChild(name="game_creator").text = T("Creator: {game_creator}").format(game_creator=game.creator) 492 self._gui.findChild(name="game_playersnum").text = T("Players: {player_amount}/{player_limit}").format( 493 player_amount=game.player_count, 494 player_limit=game.player_limit) 495 496 self._update_players_box(game) 497 self._gui.findChild(name="game_info").adaptLayout()
498
499 - def _update_players_box(self, game):
500 """Updates player list.""" 501 players_vbox = self._gui.findChild(name="players_vbox") 502 players_vbox.removeAllChildren() 503 504 hr = HRule() 505 players_vbox.addChild(hr) 506 507 def _add_player_line(player): 508 name = player['name'] 509 pname = Label(name="pname_{}".format(name)) 510 pname.helptext = T("Click here to change your name and/or color") 511 pname.text = name 512 pname.min_size = pname.max_size = (130, 15) 513 514 if name == NetworkInterface().get_client_name(): 515 pname.capture(Callback(self._show_change_player_details_popup, game)) 516 517 pcolor = Label(name="pcolor_{}".format(name), text=" ") 518 pcolor.helptext = T("Click here to change your name and/or color") 519 pcolor.background_color = player['color'] 520 pcolor.min_size = pcolor.max_size = (15, 15) 521 522 if name == NetworkInterface().get_client_name(): 523 pcolor.capture(Callback(self._show_change_player_details_popup, game)) 524 525 pstatus = Label(name="pstatus_{}".format(name)) 526 pstatus.text = "\t\t\t" + player['status'] 527 pstatus.min_size = pstatus.max_size = (120, 15) 528 529 picon = HRule(name="picon_{}".format(name)) 530 531 hbox = HBox() 532 hbox.addChildren(pname, pcolor, pstatus) 533 534 if NetworkInterface().get_client_name() == game.creator and name != game.creator: 535 pkick = CancelButton(name="pkick_{}".format(name)) 536 pkick.helptext = T("Kick {player}").format(player=name) 537 pkick.capture(Callback(NetworkInterface().kick, player['sid'])) 538 pkick.path = "images/buttons/delete_small" 539 pkick.min_size = pkick.max_size = (20, 15) 540 hbox.addChild(pkick) 541 542 players_vbox.addChildren(hbox, picon)
543 544 for player in game.get_player_list(): 545 _add_player_line(player) 546 547 players_vbox.adaptLayout() 548
549 - def _show_change_player_details_popup(self, game):
550 """Shows a dialog where the player can change its name and/or color""" 551 552 assigned = [p["color"] for p in NetworkInterface().get_game().get_player_list() 553 if p["name"] != NetworkInterface().get_client_name()] 554 unused_colors = set(Color.get_defaults()) - set(assigned) 555 556 playerdata = PlayerDataSelection(color_palette=unused_colors) 557 playerdata.set_player_name(NetworkInterface().get_client_name()) 558 playerdata.set_color(NetworkInterface().get_client_color()) 559 560 dialog = load_uh_widget('set_player_details.xml') 561 dialog.findChild(name="playerdataselectioncontainer").addChild(playerdata.get_widget()) 562 563 def _change_playerdata(): 564 NetworkInterface().change_name(playerdata.get_player_name()) 565 NetworkInterface().change_color(playerdata.get_player_color().id) 566 dialog.hide() 567 self._update_game_details()
568 569 def _cancel(): 570 dialog.hide() 571 572 dialog.mapEvents({ 573 OkButton.DEFAULT_NAME: _change_playerdata, 574 CancelButton.DEFAULT_NAME: _cancel 575 }) 576 577 dialog.show() 578 579 # Functions for handling events on the left side (chat) 580
581 - def _send_chat_message(self):
582 """Sends a chat message. Called when user presses enter in the input field""" 583 msg = self._gui.findChild(name="chatTextField").text 584 if msg: 585 self._gui.findChild(name="chatTextField").text = "" 586 NetworkInterface().chat(msg)
587
588 - def _print_event(self, msg, wrap="*"):
589 line_max_length = 40 590 if wrap: 591 msg = "{} {} {}".format(wrap, msg, wrap) 592 593 lines = textwrap.wrap(msg, line_max_length) 594 595 chatbox = self._gui.findChild(name="chatbox") 596 chatbox.items = chatbox.items + lines 597 chatbox.selected = len(chatbox.items) - 1
598
599 - def _on_chat_message(self, game, player, msg):
600 self._print_event(player + ": " + msg, wrap="")
601
602 - def _on_player_joined(self, game, player):
603 self._print_event(T("{player} has joined the game").format(player=player.name))
604
605 - def _on_player_left(self, game, player):
606 self._print_event(T("{player} has left the game").format(player=player.name))
607
608 - def _on_player_toggled_ready(self, game, plold, plnew, myself):
609 self._update_players_box(NetworkInterface().get_game()) 610 if myself: 611 if plnew.ready: 612 self._print_event(T("You are now ready")) 613 else: 614 self._print_event(T("You are not ready anymore")) 615 else: 616 if plnew.ready: 617 self._print_event(T("{player} is now ready").format(player=plnew.name)) 618 else: 619 self._print_event(T("{player} not ready anymore").format(player=plnew.name))
620
621 - def _on_player_changed_name(self, game, plold, plnew, myself):
622 if myself: 623 self._print_event(T("You are now known as {new_name}").format(new_name=plnew.name)) 624 else: 625 self._print_event(T("{player} is now known as {new_name}").format(player=plold.name, new_name=plnew.name))
626
627 - def _on_player_changed_color(self, game, plold, plnew, myself):
628 if myself: 629 self._print_event(T("You changed your color")) 630 else: 631 self._print_event(T("{player} changed their color").format(player=plnew.name))
632
633 - def _on_player_kicked(self, game, player, myself):
634 if myself: 635 self._windows.open_popup(T("Kicked"), T("You have been kicked from the game by creator")) 636 self._windows.close() 637 else: 638 self._print_event(T("{player} got kicked by creator").format(player=player.name))
639