-
Notifications
You must be signed in to change notification settings - Fork 88
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
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
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: | ||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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'.)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.