diff --git a/src/chat/friendtracker.py b/src/chat/friendtracker.py new file mode 100644 index 000000000..858317f0d --- /dev/null +++ b/src/chat/friendtracker.py @@ -0,0 +1,157 @@ +from PyQt5.QtCore import QObject, pyqtSignal +from enum import Enum +from model.game import GameState + +class FriendEvents(Enum): + HOSTING_GAME = 1 + JOINED_GAME = 2 + REPLAY_AVAILABLE = 3 + + +class OnlineFriendsTracker(QObject): + """ + Keeps track of current online friends. Notifies about added or removed + friends, no matter if it happens through (dis)connecting or through + the user adding or removing friends. + """ + friendAdded = pyqtSignal(object) + friendRemoved = pyqtSignal(object) + + def __init__(self, me, playerset): + QObject.__init__(self) + self.friends = set() + self._me = me + self._playerset = playerset + + self._me.relationsUpdated.connect(self._update_friends) + self._playerset.playerAdded.connect(self._add_or_update_player) + self._playerset.playerRemoved.connect(self._remove_player) + + for player in self._playerset: + self._add_or_update_player(player) + + def _is_friend(self, player): + return self._me.isFriend(player.id) + + def _add_friend(self, player): + if player in self.friends: + return + self.friends.add(player) + self.friendAdded.emit(player) + + def _remove_friend(self, player): + if player not in self.friends: + return + self.friends.remove(player) + self.friendRemoved.emit(player) + + def _add_or_update_player(self, player): + if self._is_friend(player): + self._add_friend(player) + else: + self._remove_friend(player) + + def _remove_player(self, player): + self._remove_friend(player) + + def _update_friends(self, player_ids): + for pid in player_ids: + try: + player = self._playerset[pid] + except KeyError: + continue + self._add_or_update_player(player) + + +class FriendEventTracker(QObject): + """ + Tracks and notifies about interesting events of a single friend player. + """ + friendEvent = pyqtSignal(object, object) + + def __init__(self, friend): + QObject.__init__(self) + self._friend = friend + self._friend_game = None + friend.newCurrentGame.connect(self._on_new_friend_game) + self._reconnect_game_signals() + + def _on_new_friend_game(self): + self._reconnect_game_signals() + self._check_game_joining_event() + + def _reconnect_game_signals(self): + old_game = self._friend_game + if old_game is not None: + old_game.liveReplayAvailable.disconnect( + self._check_game_replay_event) + + new_game = self._friend.currentGame + self._friend_game = new_game + if new_game is not None: + new_game.liveReplayAvailable.connect( + self._check_game_replay_event) + + def _check_game_joining_event(self): + if self._friend_game is None: + return + if self._friend_game.state == GameState.OPEN: + if self._friend_game.host == self._friend.login: + self.friendEvent.emit(self._friend, FriendEvents.HOSTING_GAME) + else: + self.friendEvent.emit(self._friend, FriendEvents.JOINED_GAME) + + def _check_game_replay_event(self): + if self._friend_game is None: + return + if not self._friend_game.has_live_replay: + return + self.friendEvent.emit(self._friend, FriendEvents.REPLAY_AVAILABLE) + + def report_all_events(self): + self._check_game_joining_event() + self._check_game_replay_event() + + +class FriendsEventTracker(QObject): + """ + Forwards notifications about all online friend players. + FIXME: we duplicate all friend tracker signals here, is there a more + elegant way? Maybe an enum and a single signal? + """ + friendEvent = pyqtSignal(object, object) + + def __init__(self, online_friend_tracker): + QObject.__init__(self) + self._online_friend_tracker = online_friend_tracker + self._friend_event_trackers = {} + + self._online_friend_tracker.friendAdded.connect(self._add_friend) + self._online_friend_tracker.friendRemoved.connect(self._remove_friend) + + for friend in self._online_friend_tracker.friends: + self._add_friend(friend) + + def _add_friend(self, friend): + tracker = FriendEventTracker(friend) + tracker.friendEvent.connect(self.friendEvent.emit) + self._friend_event_trackers[friend.id] = tracker + + # No risk of reporting an event twice - either it didn't happen yet + # so it won't be reported here, or it happened already so it wasn't + # tracked + tracker.report_all_events() + + def _remove_friend(self, friend): + try: + # Signals get disconnected automatically since tracker is + # no longer referenced. + del self._friend_event_trackers[friend.id] + except KeyError: + pass + + +def build_friends_tracker(me, playerset): + online_friend_tracker = OnlineFriendsTracker(me, playerset) + friends_event_tracker = FriendsEventTracker(online_friend_tracker) + return friends_event_tracker diff --git a/src/client/_clientwindow.py b/src/client/_clientwindow.py index 6492c914c..d1f6a7516 100644 --- a/src/client/_clientwindow.py +++ b/src/client/_clientwindow.py @@ -152,6 +152,7 @@ def __init__(self, *args, **kwargs): self._state = ClientState.NONE self.session = None + self.init_ready = False # This dictates whether we login automatically in the beginning or # after a disconnect. We turn it on if we're sure we have correct @@ -169,7 +170,6 @@ def __init__(self, *args, **kwargs): self.players = Playerset() # Players known to the client self.gameset = Gameset(self.players) - fa.instance.gameset = self.gameset # FIXME # Handy reference to the User object representing the logged-in user. self.me = User(self.players) @@ -286,9 +286,6 @@ def __init__(self, *args, **kwargs): self.player_colors = PlayerColors(self.me) - self.game_announcer = GameAnnouncer(self.gameset, self.me, - self.player_colors, self) - self.power = 0 # current user power self.id = 0 # Initialize the Menu Bar according to settings etc. @@ -310,22 +307,8 @@ def __init__(self, *args, **kwargs): self.modMenu = None self._alias_window = AliasSearchWindow(self) - #self.nFrame = NewsFrame() - #self.whatsNewLayout.addWidget(self.nFrame) - #self.nFrame.collapse() - - #self.nFrame = NewsFrame() - #self.whatsNewLayout.addWidget(self.nFrame) - - #self.nFrame = NewsFrame() - #self.whatsNewLayout.addWidget(self.nFrame) - - - #self.WPApi = WPAPI(self) - #self.WPApi.newsDone.connect(self.on_wpapi_done) - #self.WPApi.download() - #self.controlsContainerLayout.setAlignment(self.pageControlFrame, QtCore.Qt.AlignRight) + self.game_announcer = GameAnnouncer(self.players, self.me, self.player_colors, self) @property def state(self): @@ -1062,6 +1045,11 @@ def _tabChanged(self, tab, curr, prev): def mainTabChanged(self, curr): self._tabChanged(self.mainTabs, curr, self._main_tab) self._main_tab = curr + # the tab change works after client has initialized + if not self.init_ready: + self.init_ready = self._main_tab == 1 # chatTab + if self.init_ready: + self.game_announcer.delayed_friend_events() @QtCore.pyqtSlot(int) def vaultTabChanged(self, curr): diff --git a/src/client/gameannouncer.py b/src/client/gameannouncer.py index ae35edf1a..7fe27c7e8 100644 --- a/src/client/gameannouncer.py +++ b/src/client/gameannouncer.py @@ -1,61 +1,60 @@ -from PyQt5.QtCore import QTimer from model.game import GameState - -from fa import maps +import chat.friendtracker class GameAnnouncer: - ANNOUNCE_DELAY_SECS = 35 - def __init__(self, gameset, me, colors, client): - self._gameset = gameset + def __init__(self, playerset, me, colors, client): self._me = me self._colors = colors self._client = client - self._gameset.newLobby.connect(self._announce_hosting) - self._gameset.newLiveReplay.connect(self._announce_replay) + self._friends_event_tracker = chat.friendtracker.build_friends_tracker(me, playerset) + self._friends_event_tracker.friendEvent.connect(self._friend_event) self.announce_games = True self.announce_replays = True - self._delayed_host_list = [] - - def _is_friend_host(self, game): - return (game.host_player is not None - and self._me.isFriend(game.host_player.id)) - - def _announce_hosting(self, game): - if not self._is_friend_host(game) or not self.announce_games: - return - announce_delay = QTimer() - announce_delay.setSingleShot(True) - announce_delay.setInterval(self.ANNOUNCE_DELAY_SECS * 1000) - announce_delay.timeout.connect(self._delayed_announce_hosting) - announce_delay.start() - self._delayed_host_list.append((announce_delay, game)) - - def _delayed_announce_hosting(self): - timer, game = self._delayed_host_list.pop(0) - - if (not self._is_friend_host(game) or - not self.announce_games or - game.state != GameState.OPEN): - return - self._announce(game, "hosting") - - def _announce_replay(self, game): - if not self._is_friend_host(game) or not self.announce_replays: - return - self._announce(game, "playing live") - - def _announce(self, game, activity): - url = game.url(game.host_player.id).toString() - url_color = self._colors.getColor("url") - mapname = maps.getDisplayName(game.mapname) - fmt = 'is {} {}{} (on {})' - if game.featured_mod == "faf": - modname = "" + self._delayed_event_list = [] + + def _friend_event(self, player): + if self._client.init_ready: + self._friend_announce(player) + else: + self._delayed_event_list.append(player) + + def delayed_friend_events(self): + while len(self._delayed_event_list) > 0: + player = self._delayed_event_list.pop(0) + self._friend_announce(player) + + def _friend_announce(self, player): + if player.currentGame is None: + msg = "has no game to show" else: - modname = game.featured_mod + " " - msg = fmt.format(activity, modname, url_color, url, game.title, mapname) - self._client.forwardLocalBroadcast(game.host, msg) + game = player.currentGame + player_info = "" + if game.state == GameState.OPEN: + if not self.announce_games: # Menu Option Chat + return + if game.featured_mod == "ladder1v1": + activity = "started" + elif player.login == game.host: + activity = "is hosting" + else: + activity = "joined" + player_info = " [{}/{}]".format(game.num_players, game.max_players) + elif game.state == GameState.PLAYING: + if not self.announce_replays: # Menu Option Chat + return + activity = "is playing live" + else: # GameSate.CLOSED + activity = "has left the building" + if game.featured_mod == "faf": + modname = "" + else: + modname = game.featured_mod + " " + url_color = self._colors.getColor("url") + url = game.url(player.id).toString() + fmt = '{} {}{} (on {} {})' + msg = fmt.format(activity, modname, url_color, url, game.title, game.mapdisplayname, player_info) + self._client.forwardLocalBroadcast(player.login, msg)