Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add all friends to announcements #942

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/chat/channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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())
Expand Down
157 changes: 157 additions & 0 deletions src/chat/friendtracker.py
Original file line number Diff line number Diff line change
@@ -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
19 changes: 1 addition & 18 deletions src/client/_clientwindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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):
Expand Down
103 changes: 64 additions & 39 deletions src/client/gameannouncer.py
Original file line number Diff line number Diff line change
@@ -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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really like this. We make delaying player messages dependent on the chat widget loading and on IRC info about chatters. This will interfere with everything else that uses this announcer, like notification popups. I'd rather see this reverted to the original approach.

What is the exact reason why the chat widget misses some events? Missing chat tabs might be one. If we additionally queue these messages in _chatwidget and send them to crucial channels once they load, this should fix our problem.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, that's what the delay is for, waiting for chatwidget and chatter to be ready/there.
Once that is done, that part doesn't play any role anymore.
The chatters are not there. Once a chatter is added his message can be put out.
And queuing the message somewhere else, doesn't help, because 'once they load' is the unknown. Without the chatter added one will end up with a message without a sender/chatter. (I tried that with my 2nd source hold back, didn't work.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For all I can tell, the message ends up in a print_raw method in channel.py. It should be able to handle a sender that's not on the chatter list, if it doesn't, then it should be fixed.

I still have an issue with this code having to do special exceptions just for chat. The chat widget as a whole should listen to these announcements and print them to channels - and if a channel is not up yet, it should queue them by itself and send them all once it's up.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it can handle a message form chatter that isn't there, but the message will get the noplayer/offline color (my 2nd source hold back from above). Is that what we find acceptable. I don't. Of course, we could 'fake' the color, as we know the source of the messages.
And the gameannouncer doesn't know anything about source of the delayed_friend_events call, it would do that for replays, so no "special exception just for chat".
But - as I'm sure you insist - let's Qt gameannouncer and move the whole delay line to chat...
(but there is no 'once it's up' (maybe you can find one, I couldn't) there is still only 'chatter is there'.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chat widget has an 'add_channel' method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't do it. I checked (my 2nd source hold back from above).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does, but check out line 350 of _chatwidget.py. The tab is added before we hook up the localBroadcast signal. Swapping these would be the quickest fix I imagine, other than the small refactor I described above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would that change? Would it make the chatter add earlier or faster?
Did you test it? Please do.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mentioned before that emitting notifications when the channel was added wouldn't lead to them being displayed. I'm just suggesting this might be a reason.

Anyway, the issue of the link being displayed in a wrong color in the chat is rather a fault of the display code - the print_raw function assumed so far that it will only print messages from chatters, which makes no sense when it comes from an announcement.
I suggest we split it into two functions - actual 'print_raw_message' that takes all formatting info like a color and doesn't care about message source, and the old 'print_raw' that gets color info from the chatter as it used to and uses 'print_raw_message' to print it. We can then use the 'print_raw_message' function to print announcements.

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 <font color='GoldenRod'>hosting</font>"
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 {} {}<a style="color:{}" href="{}">{}</a> (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 = '{} {}<a style="color:{}" href="{}">{}</a> ' \
'(on <font color="GoldenRod">{}</font> {}{})'
msg = fmt.format(activity, modname, url_color, url, game.title,
game.mapdisplayname, player_info, time_info)
self._client.forwardLocalBroadcast(player.login, msg)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For all I can tell, client's localBroadcast signal is only used by the chat widget. If we give this class its own signal that it emits here and pass the class to the chat widget in the constructor, we can get rid of an unnecessary indirection layer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an additional use of localBroadcast emit("Scores", message["text"]), in handle_notice in _clientwindow. (but I can't remember ever seen that message, is it in use, does it exist?)
That the GameAnnouncer can signal itself is a nice idea, but it is not a Qt object, so how signaling?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can add signals to GameAnnouncer exactly like to other objects, we just need to subclass it from QObject.