diff --git a/src/chat/channel.py b/src/chat/channel.py index 1b51d95b3..5becc35bc 100644 --- a/src/chat/channel.py +++ b/src/chat/channel.py @@ -413,7 +413,8 @@ def resize_map_column(self): def add_chatter(self, chatter, join=False): """ - Adds an user to this chat channel, and assigns an appropriate icon depending on friendship and FAF player status + Adds an user to this chat channel, and assigns an appropriate icon + depending on friendship and FAF player status """ if chatter not in self.chatters: item = Chatter(self.nickList, chatter, self, @@ -427,6 +428,9 @@ def add_chatter(self, chatter, join=False): if join and self.chat_widget.client.joinsparts: self.print_action(chatter.name, "joined the channel.", server_action=True) + if chatter.player is not None and chatter.player.currentGame is not None: + self.chat_widget.client.game_announcer.delayed_friend_events(chatter.player) + def remove_chatter(self, chatter, server_action=None): if chatter in self.chatters: self.nickList.removeRow(self.chatters[chatter].row()) 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 5fcb79d86..d0dcefbc9 100644 --- a/src/client/_clientwindow.py +++ b/src/client/_clientwindow.py @@ -272,9 +272,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. @@ -296,22 +293,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): diff --git a/src/client/gameannouncer.py b/src/client/gameannouncer.py index ae35edf1a..b8ed7fa76 100644 --- a/src/client/gameannouncer.py +++ b/src/client/gameannouncer.py @@ -1,61 +1,86 @@ -from PyQt5.QtCore import QTimer -from model.game import GameState - -from fa import maps +from chat.friendtracker import build_friends_tracker, FriendEvents +import time 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 = 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 = [] + self._delayed_event_list = [] + self.delay_friend_events = True - def _is_friend_host(self, game): - return (game.host_player is not None - and self._me.isFriend(game.host_player.id)) + def _friend_event(self, player, event): + if self.delay_friend_events: + self._delayed_event_list.append((player, event)) + else: + self._friend_announce(player, event) - def _announce_hosting(self, game): - if not self._is_friend_host(game) or not self.announce_games: + def delayed_friend_events(self, player): + if not self.delay_friend_events: 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): + if len(self._delayed_event_list) == 0: + self.delay_friend_events = False return - self._announce(game, "hosting") + i = 0 + for event in self._delayed_event_list: + if player in event: + player, event = self._delayed_event_list.pop(i) + self._friend_announce(player, event) + i += 1 - def _announce_replay(self, game): - if not self._is_friend_host(game) or not self.announce_replays: + def _friend_announce(self, player, event): + if player.currentGame is None: + return + game = player.currentGame + if event == FriendEvents.HOSTING_GAME: + if not self.announce_games: # Menu Option Chat + return + if game.featured_mod == "ladder1v1": + activity = "started" + else: + activity = "is hosting" + elif event == FriendEvents.JOINED_GAME: + if not self.announce_games: # Menu Option Chat + return + if game.featured_mod == "ladder1v1": + activity = "started" + else: + activity = "joined" + elif event == FriendEvents.REPLAY_AVAILABLE: + if not self.announce_replays: # Menu Option Chat + return + activity = "is playing live" + else: # that shouldn't happen 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 = "" else: modname = game.featured_mod + " " - msg = fmt.format(activity, modname, url_color, url, game.title, mapname) - self._client.forwardLocalBroadcast(game.host, msg) + if game.featured_mod != "ladder1v1": + player_info = " [{}/{}]".format(game.num_players, game.max_players) + else: + player_info = "" + time_info = "" + if game.has_live_replay: + time_running = time.time() - game.launched_at + if time_running > 6 * 60: # already running games on client start + time_format = '%M:%S' if time_running < 60 * 60 else '%H:%M:%S' + time_info = " runs {}"\ + .format(time.strftime(time_format, time.gmtime(time_running))) + 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, time_info) + self._client.forwardLocalBroadcast(player.login, msg)