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)