Package horizons :: Package network :: Module networkinterface
[hide private]
[frames] | no frames]

Source Code for Module horizons.network.networkinterface

  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 logging 
 23  import uuid 
 24   
 25  import horizons.globals 
 26  from horizons import network 
 27  from horizons.constants import LANGUAGENAMES, NETWORK, VERSION 
 28  from horizons.extscheduler import ExtScheduler 
 29  from horizons.i18n import gettext as T 
 30  from horizons.messaging.simplemessagebus import SimpleMessageBus 
 31  from horizons.network import CommandError, FatalError, NetworkException, packets 
 32  from horizons.network.common import ErrorType, Game 
 33  from horizons.network.connection import Connection 
 34  from horizons.util.color import Color 
 35  from horizons.util.difficultysettings import DifficultySettings 
 36  from horizons.util.python import parse_port 
 37  from horizons.util.python.singleton import ManualConstructionSingleton 
38 39 40 -class ClientMode:
41 Server = 0 42 Game = 1
43
44 45 -class ClientData:
46 - def __init__(self):
47 self.name = horizons.globals.fife.get_uh_setting("Nickname") 48 self.color = horizons.globals.fife.get_uh_setting("ColorID") 49 self.version = VERSION.RELEASE_VERSION 50 51 try: 52 self.id = uuid.UUID(horizons.globals.fife.get_uh_setting("ClientID")).hex 53 except (ValueError, TypeError): 54 # We need a new client id 55 client_id = uuid.uuid4() 56 horizons.globals.fife.set_uh_setting("ClientID", client_id) 57 horizons.globals.fife.save_settings() 58 self.id = client_id.hex
59
60 61 -class NetworkInterface(object, metaclass=ManualConstructionSingleton):
62 """Interface for low level networking""" 63 64 log = logging.getLogger("network") 65 66 PING_INTERVAL = 0.5 # ping interval in seconds 67
68 - def __init__(self):
69 self._mode = None 70 self.sid = None 71 self.capabilities = None 72 self._game = None 73 74 message_types = ('lobbygame_chat', 'lobbygame_join', 'lobbygame_leave', 75 'lobbygame_terminate', 'lobbygame_toggleready', 76 'lobbygame_changename', 'lobbygame_kick', 77 'lobbygame_changecolor', 'lobbygame_state', 78 'lobbygame_starts', 'game_starts', 79 'game_details_changed', 'game_prepare', 'error') 80 81 self._messagebus = SimpleMessageBus(message_types) 82 self.subscribe = self._messagebus.subscribe 83 self.unsubscribe = self._messagebus.unsubscribe 84 self.broadcast = self._messagebus.broadcast 85 self.discard = self._messagebus.discard 86 87 # create a game_details_changed callback 88 for t in ('lobbygame_join', 'lobbygame_leave', 'lobbygame_changename', 89 'lobbygame_changecolor', 'lobbygame_toggleready'): 90 self.subscribe(t, lambda *a, **b: self.broadcast("game_details_changed")) 91 92 self.subscribe("lobbygame_starts", self._on_lobbygame_starts) 93 self.subscribe('lobbygame_changename', self._on_change_name) 94 self.subscribe('lobbygame_changecolor', self._on_change_color) 95 96 self.received_packets = [] 97 98 ExtScheduler().add_new_object(self.ping, self, self.PING_INTERVAL, -1) 99 100 self._client_data = ClientData() 101 self._setup_client()
102 103 # Connection 104
105 - def _setup_client(self):
106 """Initialize connection object. Does not connect to the server.""" 107 server_address = [NETWORK.SERVER_ADDRESS, NETWORK.SERVER_PORT] 108 client_address = None 109 client_port = parse_port(horizons.globals.fife.get_uh_setting("NetworkPort")) 110 111 if NETWORK.CLIENT_ADDRESS is not None and client_port > 0: 112 client_address = [NETWORK.CLIENT_ADDRESS, client_port] 113 try: 114 self._connection = Connection(self.process_async_packet, server_address, client_address) 115 except NetworkException as e: 116 raise RuntimeError(e)
117 118 @property
119 - def is_connected(self):
120 return self._connection.is_connected
121
122 - def connect(self):
123 """ 124 @throws: NetworkError 125 """ 126 try: 127 self._connection.connect() 128 129 # wait for session id 130 packet = self._connection.receive_packet(packets.server.cmd_session) 131 132 self.sid = packet[1].sid 133 self.capabilities = packet[1].capabilities 134 self._mode = ClientMode.Server 135 self.log.debug("[CONNECT] done (session=%s)", self.sid) 136 self._set_client_language() 137 except NetworkException as e: 138 self.disconnect() 139 raise e
140
141 - def disconnect(self):
142 self._mode = None 143 self._connection.disconnect()
144
145 - def ping(self):
146 """calls _connection.ping until all packets are received""" 147 if self.is_connected: 148 try: 149 while self._connection.ping(): # ping receives packets 150 pass 151 except NetworkException as e: 152 self._handle_exception(e)
153
154 - def network_data_changed(self):
155 """Call in case constants like client address or client port changed. 156 157 @throws RuntimeError in case of invalid data or an NetworkException forwarded from connect 158 """ 159 if self.is_connected: 160 self.disconnect() 161 self._setup_client()
162
163 - def _set_client_language(self):
164 lang = LANGUAGENAMES.get_by_value(horizons.globals.fife.get_uh_setting("Language")) 165 if lang: 166 return self.set_props({'lang': lang})
167
168 - def send_packet(self, packet, *args, **kwargs):
169 """ 170 """ 171 if self._mode is ClientMode.Game: 172 packet = packets.client.game_data(packet) 173 packet.sid = self.sid 174 175 self._connection.send_packet(packet)
176 177 # Game related 178 179 @property
180 - def is_joined(self):
181 return self._game is not None
182
183 - def game2mpgame(self, game):
184 return MPGame(game, self)
185
186 - def get_game(self):
187 game = self._game 188 if game is None: 189 return None 190 return self.game2mpgame(game)
191
192 - def set_props(self, props):
193 try: 194 self.log.debug("[SETPROPS]") 195 self._assert_connection() 196 self.send_packet(packets.client.cmd_sessionprops(props)) 197 self._connection.receive_packet(packets.cmd_ok) 198 except NetworkException as e: 199 self._handle_exception(e) 200 return False 201 return True
202
203 - def creategame(self, mapname, maxplayers, gamename, maphash="", password=""):
204 self.log.debug("[CREATEGAME] %s(h=%s), %s, %s", mapname, maphash, maxplayers, gamename) 205 try: 206 self._assert_connection() 207 self.log.debug("[CREATE] mapname=%s maxplayers=%d", mapname, maxplayers) 208 self.send_packet(packets.client.cmd_creategame( 209 clientver=self._client_data.version, 210 clientid=self._client_data.id, 211 playername=self._client_data.name, 212 playercolor=self._client_data.color, 213 gamename=gamename, 214 mapname=mapname, 215 maxplayers=maxplayers, 216 maphash=maphash, 217 password=password 218 )) 219 packet = self._connection.receive_packet(packets.server.data_gamestate) 220 game = self._game = packet[1].game 221 except NetworkException as e: 222 self._handle_exception(e) 223 return None 224 return self.game2mpgame(game)
225
226 - def joingame(self, uuid, password="", fetch=False):
227 """Join a game with a certain uuid""" 228 i = 2 229 try: 230 while i < 10: # FIXME: try 10 different names and colors 231 try: 232 self._joingame(uuid, password, fetch) 233 return True 234 except CommandError as e: 235 self.log.debug("NetworkInterface: failed to join") 236 e = str(e) 237 if 'name' in e: 238 self.change_name(self._client_data.name + str(i), save=False) 239 elif 'color' in e: 240 self.change_color(self._client_data.color + i, save=False) 241 else: 242 raise 243 i += 1 244 self._joingame(uuid, password, fetch) 245 except NetworkException as e: 246 self._handle_exception(e) 247 return False
248
249 - def _joingame(self, uuid, password="", fetch=False):
250 self._assert_connection() 251 self.log.debug("[JOIN] %s", uuid) 252 self.send_packet(packets.client.cmd_joingame( 253 uuid=uuid, 254 clientver=self._client_data.version, 255 clientid=self._client_data.id, 256 playername=self._client_data.name, 257 playercolor=self._client_data.color, 258 password=password, 259 fetch=fetch 260 )) 261 packet = self._connection.receive_packet(packets.server.data_gamestate) 262 self._game = packet[1].game 263 return self._game
264
265 - def leavegame(self):
266 try: 267 self._assert_connection() 268 self._assert_lobby() 269 self.log.debug("[LEAVE]") 270 self.send_packet(packets.client.cmd_leavegame()) 271 self._connection.receive_packet(packets.cmd_ok) 272 self._game = None 273 except NetworkException as e: 274 fatal = self._handle_exception(e) 275 if fatal: 276 return False 277 return True
278
279 - def chat(self, message):
280 try: 281 self._assert_connection() 282 self._assert_lobby() 283 self.log.debug("[CHAT] %s", message) 284 self.send_packet(packets.client.cmd_chatmsg(message)) 285 except NetworkException as e: 286 self._handle_exception(e) 287 return False 288 return True
289
290 - def get_active_games(self):
291 """Returns a list of active games or None on fatal error""" 292 ret_mp_games = [] 293 try: 294 self._assert_connection() 295 self.log.debug("[LIST]") 296 version = self._client_data.version 297 self.send_packet(packets.client.cmd_listgames(version)) 298 packet = self._connection.receive_packet(packets.server.data_gameslist) 299 games = packet[1].games 300 except NetworkException as e: 301 fatal = self._handle_exception(e) 302 return [] if not fatal else None 303 for game in games: 304 ret_mp_games.append(self.game2mpgame(game)) 305 self.log.debug("NetworkInterface: found active game %s", game.mapname) 306 return ret_mp_games
307
308 - def toggle_ready(self):
309 self.log.debug("[TOGGLEREADY]") 310 self.send_packet(packets.client.cmd_toggleready())
311
312 - def kick(self, player_sid):
313 self.log.debug("[KICK]") 314 self.send_packet(packets.client.cmd_kickplayer(player_sid))
315 316 # Client 317
318 - def get_client_name(self):
319 return self._client_data.name
320
321 - def get_client_color(self):
322 return self._client_data.color
323
324 - def get_clientversion(self):
325 return self._client_data.version
326
327 - def change_name(self, new_name, save=True):
328 if save: 329 horizons.globals.fife.set_uh_setting("Nickname", new_name) 330 horizons.globals.fife.save_settings() 331 332 try: 333 if self._client_data.name == new_name: 334 return True 335 self.log.debug("[CHANGENAME] %s", new_name) 336 if self._mode is None or self._game is None: 337 self._client_data.name = new_name 338 return 339 self.send_packet(packets.client.cmd_changename(new_name)) 340 except NetworkException as e: 341 self._handle_exception(e)
342
343 - def _on_change_name(self, game, plold, plnew, myself):
344 self.log.debug("[ONCHANGENAME] %s -> %s", plold.name, plnew.name) 345 if myself: 346 self._client_data.name = plnew.name
347
348 - def change_color(self, new_color, save=True):
349 new_color %= len(set(Color.get_defaults())) 350 351 if save: 352 horizons.globals.fife.set_uh_setting("ColorID", new_color) 353 horizons.globals.fife.save_settings() 354 355 try: 356 if self._client_data.color == new_color: 357 return 358 self.log.debug("[CHANGECOLOR] %s", new_color) 359 if self._mode is None or self._game is None: 360 self._client_data.color = new_color 361 return 362 self.send_packet(packets.client.cmd_changecolor(new_color)) 363 except NetworkException as e: 364 self._handle_exception(e)
365
366 - def _on_change_color(self, game, plold, plnew, myself):
367 self.log.debug("[ONCHANGECOLOR] %s: %s -> %s", plnew.name, plold.color, plnew.color) 368 if myself: 369 self._client_data.color = plnew.color
370 371 # Helper functions, event callbacks, packet handling 372
373 - def receive_all(self):
374 """ 375 Returns list of all packets, that have arrived until now (since the last call) 376 @return: list of packets 377 """ 378 try: 379 while self._connection.ping(): # ping receives packets 380 pass 381 except NetworkException as e: 382 self.log.debug("ping in receive_all failed: %s ", str(e)) 383 self._handle_exception(e) 384 raise CommandError(e) 385 ret_list = self.received_packets 386 self.received_packets = [] 387 return ret_list
388
389 - def _handle_exception(self, e):
390 try: 391 raise e 392 except FatalError as e: 393 self.broadcast("error", e, fatal=True) 394 self.disconnect() 395 return True 396 except CommandError as e: 397 if e.cmd_type == ErrorType.TerminateGame: 398 self._game = None 399 self.broadcast("error", e, fatal=False) 400 return False 401 except NetworkException as e: 402 self.broadcast("error", e, fatal=False) 403 return False
404
405 - def process_async_packet(self, packet):
406 """ 407 return True if packet was processed successfully 408 return False if packet should be queue 409 """ 410 if packet is None: 411 return True 412 if isinstance(packet[1], packets.server.cmd_chatmsg): 413 # ignore packet if we are not a game lobby 414 if self._game is None: 415 return True 416 self.broadcast("lobbygame_chat", self._game, packet[1].playername, packet[1].chatmsg) 417 elif isinstance(packet[1], packets.server.data_gamestate): 418 # ignore packet if we are not a game lobby 419 if self._game is None: 420 return True 421 self.broadcast("lobbygame_state", self._game, packet[1].game) 422 423 oldplayers = list(self._game.players) 424 self._game = packet[1].game 425 426 # calculate changeset 427 for pnew in self._game.players: 428 found = None 429 for pold in oldplayers: 430 if pnew.sid == pold.sid: 431 found = pold 432 myself = (pnew.sid == self.sid) 433 if pnew.name != pold.name: 434 self.broadcast("lobbygame_changename", self._game, pold, pnew, myself) 435 if pnew.color != pold.color: 436 self.broadcast("lobbygame_changecolor", self._game, pold, pnew, myself) 437 if pnew.ready != pold.ready: 438 self.broadcast("lobbygame_toggleready", self._game, pold, pnew, myself) 439 break 440 if found is None: 441 self.broadcast("lobbygame_join", self._game, pnew) 442 else: 443 oldplayers.remove(found) 444 for pold in oldplayers: 445 self.broadcast("lobbygame_leave", self._game, pold) 446 return True 447 elif isinstance(packet[1], packets.server.cmd_preparegame): 448 # ignore packet if we are not a game lobby 449 if self._game is None: 450 return True 451 self._on_game_prepare() 452 elif isinstance(packet[1], packets.server.cmd_startgame): 453 # ignore packet if we are not a game lobby 454 if self._game is None: 455 return True 456 self._on_game_start() 457 elif isinstance(packet[1], packets.client.game_data): 458 self.log.debug("[GAMEDATA] from %s", packet[0].address) 459 self._on_game_data(packet[1].data) 460 elif isinstance(packet[1], packets.server.cmd_kickplayer): 461 player = packet[1].player 462 game = self._game 463 myself = (player.sid == self.sid) 464 if myself: 465 # this will destroy self._game 466 self._assert_connection() 467 self._assert_lobby() 468 self.log.debug("[LEAVE]") 469 self._game = None 470 self.broadcast("lobbygame_kick", game, player, myself) 471 472 return False
473
474 - def _on_game_prepare(self):
475 self.log.debug("[GAMEPREPARE]") 476 self._game.state = Game.State.Prepare 477 self.broadcast("lobbygame_starts", self._game) 478 self.send_packet(packets.client.cmd_preparedgame())
479
480 - def _on_game_start(self):
481 self.log.debug("[GAMESTART]") 482 self._game.state = Game.State.Running 483 self._mode = ClientMode.Game 484 self.broadcast("game_starts", self._game)
485
486 - def _on_lobbygame_starts(self, game):
487 game = self.get_game() 488 self.broadcast("game_prepare", game)
489
490 - def _on_game_data(self, data):
491 self.received_packets.append(data)
492
493 - def _assert_connection(self):
494 if self._mode is None: 495 raise network.NotConnected() 496 if self._mode is not ClientMode.Server: 497 raise network.NotInServerMode("We are not in server mode")
498
499 - def _assert_lobby(self):
500 if self._game is None: 501 raise network.NotInGameLobby("We are not in a game lobby")
502
503 504 -class MPGame:
505 - def __init__(self, game, netif):
506 self.uuid = game.uuid 507 self.creator = game.creator 508 self.map_name = game.mapname 509 self.map_hash = game.maphash 510 self.player_limit = game.maxplayers 511 self.player_count = game.playercnt 512 self.players = game.players 513 self.version = game.clientversion 514 self.name = game.name 515 self.password = game.password 516 self.netif = netif
517 518 @property
519 - def is_savegame(self):
520 return bool(self.map_hash)
521 522 @property
523 - def has_password(self):
524 return self.password
525
526 - def get_player_list(self):
527 ret_players = [] 528 for index, player in enumerate(self.players, start=1): 529 # TODO: add support for selecting difficulty levels to the GUI 530 status = T('Ready') if player.ready else T('Not Ready') 531 ret_players.append({ 532 'id': index, 533 'sid': player.sid, 534 'name': player.name, 535 'color': Color.get(player.color), 536 'clientid': player.clientid, 537 'local': self.netif.get_client_name() == player.name, 538 'ai': False, 539 'difficulty': DifficultySettings.DEFAULT_LEVEL, 540 'status': status 541 }) 542 return ret_players
543
544 - def __str__(self):
545 return "{} ({:d}/{:d})".format(self.map_name, self.player_count, self.player_limit)
546