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)