diff options
author | Gonzalo Odiard <godiard@gmail.com> | 2014-06-05 17:21:22 (GMT) |
---|---|---|
committer | Gonzalo Odiard <godiard@gmail.com> | 2014-06-16 15:26:37 (GMT) |
commit | 0daea191e3580f8760fce96d0ee24c04bf225943 (patch) | |
tree | 079483b578c0d1421ca4d52a5c1f4baa5ec58064 | |
parent | 1519952d1bd9d9f2825db67772c916c11cf590dc (diff) |
Implement collaboration
This implememtation have two pending issues:
* The id on the user is not available, then we add the user pubkey to
the message. This is suboptimal.
* The messages are text, parsed by hand.
-rwxr-xr-x | activity.py | 138 | ||||
-rw-r--r-- | game.py | 200 | ||||
-rw-r--r-- | textchannel.py | 128 |
3 files changed, 310 insertions, 156 deletions
diff --git a/activity.py b/activity.py index f5ccd63..1a358f4 100755 --- a/activity.py +++ b/activity.py @@ -6,69 +6,52 @@ import json from gi.repository import Gtk from sugar3.activity import activity +from sugar3.presence.presenceservice import PresenceService from sugar3.activity.widgets import ActivityToolbarButton from sugar3.activity.widgets import StopButton from sugar3.graphics.toolbarbox import ToolbarBox from sugar3.graphics.toolbutton import ToolButton +from sugar3.graphics.alert import ErrorAlert +from sugar3 import profile from gettext import gettext as _ +from textchannel import TextChannelWrapper import game class MazeActivity(activity.Activity): def __init__(self, handle): - """Set up the HelloWorld activity.""" + """Set up the Maze activity.""" activity.Activity.__init__(self, handle) + self.build_toolbar() - state = json.loads(self.metadata['state']) - self.game = game.MazeGame(state) + state = None + if 'state' in self.metadata: + state = json.loads(self.metadata['state']) + self.game = game.MazeGame(self, state) self.set_canvas(self.game) self.game.show() self.connect("key_press_event", self.game.key_press_cb) - """ - game_name = 'game' - game_title = _('Maze') - game_size = None # Let olpcgames pick a nice size for us + self.pservice = PresenceService() + self.owner = self.pservice.get_owner() - def __init__(self, handle): - super(MazeActivity, self).__init__(handle) - - # This code was copied from olpcgames.activity.PyGameActivity - def shared_cb(*args, **kwargs): - logging.info('shared: %s, %s', args, kwargs) - try: - mesh.activity_shared(self) - except Exception, err: - logging.error('Failure signaling activity sharing' - 'to mesh module: %s', util.get_traceback(err)) - else: - logging.info('mesh activity shared message sent,' - ' trying to grab focus') - try: - self._pgc.grab_focus() - except Exception, err: - logging.warn('Focus failed: %s', err) - else: - logging.info('asserting focus') - assert self._pgc.is_focus(), \ - 'Did not successfully set pygame canvas focus' - logging.info('callback finished') - - def joined_cb(*args, **kwargs): - logging.info('joined: %s, %s', args, kwargs) - mesh.activity_joined(self) - self._pgc.grab_focus() - self.connect('shared', shared_cb) - self.connect('joined', joined_cb) - - if self.get_shared(): - # if set at this point, it means we've already joined (i.e., - # launched from Neighborhood) - joined_cb() - """ + self.text_channel = None + self.my_key = profile.get_pubkey() + self._alert = None + + if self.shared_activity: + # we are joining the activity + self._add_alert(_('Joining a maze'), _('Connecting...')) + self.connect('joined', self._joined_cb) + if self.get_shared(): + # we have already joined + self._joined_cb() + else: + # we are creating the activity + self.connect('shared', self._shared_cb) def build_toolbar(self): """Build our Activity toolbar for the Sugar system.""" @@ -114,6 +97,75 @@ class MazeActivity(activity.Activity): def _harder_button_cb(self, button): self.game.harder() + def _shared_cb(self, activity): + logging.debug('Maze was shared') + self._add_alert(_('Sharing'), _('This maze is shared.')) + self._setup() + + def _joined_cb(self, activity): + """Joined a shared activity.""" + if not self.shared_activity: + return + logging.debug('Joined a shared chat') + for buddy in self.shared_activity.get_joined_buddies(): + self._buddy_already_exists(buddy) + self._setup() + # request maze data + self.broadcast_msg('req_maze') + + def _setup(self): + self.text_channel = TextChannelWrapper( + self.shared_activity.telepathy_text_chan, + self.shared_activity.telepathy_conn, self.pservice) + self.text_channel.set_received_callback(self._received_cb) + self.shared_activity.connect('buddy-joined', self._buddy_joined_cb) + self.shared_activity.connect('buddy-left', self._buddy_left_cb) + + def _received_cb(self, buddy, text): + if buddy == self.owner: + return + self.game.msg_received(buddy, text) + + def _add_alert(self, title, text=None): + self._alert = ErrorAlert() + self._alert.props.title = title + self._alert.props.msg = text + self.add_alert(self._alert) + self._alert.connect('response', self._alert_cancel_cb) + self._alert.show() + + def _alert_cancel_cb(self, alert, response_id): + self.remove_alert(alert) + self._alert = None + + def update_alert(self, title, text=None): + if self._alert is not None: + self._alert.props.title = title + self._alert.props.msg = text + + def _buddy_joined_cb(self, activity, buddy): + """Show a buddy who joined""" + logging.debug('buddy joined') + if buddy == self.owner: + logging.debug('its me, exit!') + return + self.game.buddy_joined(buddy) + + def _buddy_left_cb(self, activity, buddy): + self.game.buddy_left(buddy) + + def _buddy_already_exists(self, buddy): + """Show a buddy already in the chat.""" + if buddy == self.owner: + return + self.game.buddy_joined(buddy) + + def broadcast_msg(self, message): + if self.text_channel: + # FIXME: can't identify the sender at the other end, + # add the pubkey to the text message + self.text_channel.send('%s|%s' % (self.my_key, message)) + def write_file(self, file_path): logging.debug('Saving the state of the game...') data = {'seed': self.game.maze.seed, @@ -52,11 +52,14 @@ class MazeGame(Gtk.DrawingArea): GOAL_COLOR = (0x00, 0xff, 0x00) WIN_COLOR = (0xff, 0xff, 0x00) - def __init__(self, state=None): + def __init__(self, activity, state=None): super(MazeGame, self).__init__() # note what time it was when we first launched self.game_start_time = time.time() + # the activity is used to communicate with other players + self._activity = activity + xoOwner = presenceService.get_owner() # keep a list of all local players self.localplayers = [] @@ -320,17 +323,15 @@ class MazeGame(Gtk.DrawingArea): player.direction = (0, 1) if len(self.remoteplayers) > 0: - mesh.broadcast("move:%s,%d,%d,%d,%d" % - (player.nick, - player.position[0], - player.position[1], - player.direction[0], - player.direction[1])) + self._activity.broadcast_msg( + "move:%s,%d,%d,%d,%d" % + (player.nick, player.position[0], + player.position[1], player.direction[0], + player.direction[1])) self.player_walk(player) def key_press_cb(self, widget, event): key_name = Gdk.keyval_name(event.keyval) - logging.error('key %s presssed', key_name) if key_name in ('plus', 'equal'): self.harder() elif key_name == 'minus': @@ -350,12 +351,10 @@ class MazeGame(Gtk.DrawingArea): player.direction = (1, 0) if len(self.remoteplayers) > 0: - mesh.broadcast("move:%s,%d,%d,%d,%d" % - (player.uid, - player.position[0], - player.position[1], - player.direction[0], - player.direction[1])) + self._activity.broadcast_msg( + "move:%d,%d,%d,%d" % ( + player.position[0], player.position[1], + player.direction[0], player.direction[1])) self.player_walk(player) def player_walk(self, player): @@ -371,7 +370,7 @@ class MazeGame(Gtk.DrawingArea): self.maze.GOAL: self.finish(player) self.queue_draw() - GObject.timeout_add(200, self.player_walk, player) + GObject.timeout_add(100, self.player_walk, player) """ finish_delay = min(2 * len(self.allplayers), 6) if self.finish_time is not None and \ @@ -379,85 +378,61 @@ class MazeGame(Gtk.DrawingArea): self.harder() """ - def processEvent(self, event): - """Process a single pygame event. This includes keystrokes - as well as multiplayer events from the mesh.""" - if event.type == mesh.CONNECT: - logging.debug("Connected to the mesh") - - elif event.type == mesh.PARTICIPANT_ADD: - logging.debug('mesh.PARTICIPANT_ADD') - - def withBuddy(buddy): - if event.handle == mesh.my_handle(): - logging.debug("Me: %s - %s", buddy.props.nick, - buddy.props.color) - # README: this is a workaround to use an unique - # identifier instead the nick of the buddy - # http://dev.laptop.org/ticket/10750 - count = '' - for i, player in enumerate(self.localplayers): - if i > 0: - count = '-%d' % i - player.uid = mesh.my_handle() + count - else: - logging.debug("Join: %s - %s", buddy.props.nick, - buddy.props.color) - player = Player(buddy) - player.uid = event.handle - self.remoteplayers[event.handle] = player - self.allplayers.append(player) - self.allplayers.extend(player.bonusPlayers()) - self.markPointDirty(player.position) - # send a test message to the new player - mesh.broadcast("Welcome %s" % player.nick) - # tell them which maze we are playing, so they can sync up - mesh.send_to(event.handle, "maze:%d,%d,%d,%d" % - (self.game_running_time(), - self.maze.seed, - self.maze.width, self.maze.height)) - for player in self.localplayers: - if not player.hidden: - mesh.send_to(event.handle, - "move:%s,%d,%d,%d,%d" % - (player.uid, - player.position[0], - player.position[1], - player.direction[0], - player.direction[1])) - - mesh.lookup_buddy(event.handle, callback=withBuddy) - elif event.type == mesh.PARTICIPANT_REMOVE: - logging.debug('mesh.PARTICIPANT_REMOVE') - if event.handle in self.remoteplayers: - player = self.remoteplayers[event.handle] - logging.debug("Leave: %s", player.nick) - self.markPointDirty(player.position) - self.allplayers.remove(player) - for bonusplayer in player.bonusPlayers(): - self.markPointDirty(bonusplayer.position) - self.allplayers.remove(bonusplayer) - del self.remoteplayers[event.handle] - elif event.type == mesh.MESSAGE_UNI or \ - event.type == mesh.MESSAGE_MULTI: - logging.debug('mesh.MESSAGE_UNI or mesh.MESSAGE_MULTI') - if event.handle == mesh.my_handle(): - # ignore messages from ourself - pass - elif event.handle in self.remoteplayers: - player = self.remoteplayers[event.handle] - try: - self.handleMessage(player, event.content) - except: - logging.debug("Error handling message: %s\n%s", - event, sys.exc_info()) - else: - logging.debug("Message from unknown buddy?") + def buddy_joined(self, buddy): + if buddy: + logging.debug("Join: %s - %s", buddy.props.nick, + buddy.props.color) + player = Player(buddy) + player.uid = buddy.get_key() + self.remoteplayers[buddy.get_key()] = player + self.allplayers.append(player) + self.allplayers.extend(player.bonusPlayers()) + self.markPointDirty(player.position) + + def _send_maze(self, player): + # tell them which maze we are playing, so they can sync up + self._activity.broadcast_msg( + "maze:%d,%d,%d,%d" % + (self.game_running_time(), self.maze.seed, self.maze.width, + self.maze.height)) + for player in self.localplayers: + if not player.hidden: + self._activity.broadcast_msg( + "move:%d,%d,%d,%d" % + (player.position[0], player.position[1], + player.direction[0], player.direction[1])) + + def buddy_left(self, buddy): + logging.debug('buddy left %s %s', buddy.__class__, dir(buddy)) + if buddy.get_key() in self.remoteplayers: + player = self.remoteplayers[buddy.get_key()] + logging.debug("Leave: %s", player.nick) + self.markPointDirty(player.position) + self.allplayers.remove(player) + for bonusplayer in player.bonusPlayers(): + self.markPointDirty(bonusplayer.position) + self.allplayers.remove(bonusplayer) + del self.remoteplayers[buddy.get_key()] + + def msg_received(self, buddy, message): + logging.debug('msg received %s', message) + key, message = message.split('|') + if message.startswith('maze'): + self.handleMessage(None, message) + return + + if key in self.remoteplayers: + player = self.remoteplayers[key] + try: + self.handleMessage(player, message) + except: + logging.error("Error handling message: %s\n%s", + message, sys.exc_info()) else: - logging.debug('Unknown event: %r', event) + logging.error("Message from unknown buddy %s", key) def handleMessage(self, player, message): - """Handle a message from a player on the mesh. + """Handle a message from a player. We try to be forward compatible with new versions of Maze by allowing messages to have extra stuff at the end and ignoring unrecognized messages. @@ -474,40 +449,36 @@ class MazeGame(Gtk.DrawingArea): players to use that maze. This way new players will join the existing game properly. - move: nick, x, y, dx, dy + move: x, y, dx, dy A player's at x, y is now moving in direction dx, dy finish: nick, elapsed A player has finished the maze """ - logging.debug('mesh message: %s', message) + logging.debug('message: %s', message) # ignore messages from myself if player in self.localplayers: return - if message.startswith("move:"): + if message == "req_maze": + self._send_maze(player) + elif message.startswith("move:"): # a player has moved - uid, x, y, dx, dy = message[5:].split(",")[:5] - - # README: this function (player.bonusPlayer) sometimes - # returns None and the activity doesn't move the players. - # This is because the name sent to the server is the - # child's name but it returns something like this: - # * 6be01ff2bcfaa58eeacc7f10a57b77b65470d413@jabber.sugarlabs.org - # So, we have set remote users with this kind of name but - # we receive the reald child's name in the mesh message - - player = player.bonusPlayer(uid) - player.hidden = False + x, y, dx, dy = message[5:].split(",")[:5] self.markPointDirty(player.position) player.position = (int(x), int(y)) player.direction = (int(dx), int(dy)) self.markPointDirty(player.position) + self.player_walk(player) elif message.startswith("maze:"): # someone has a different maze than us + self._activity.update_alert('Connected', 'Maze shared!') running_time, seed, width, height = map(lambda x: int(x), message[5:].split(",")[:4]) + if self.maze.seed == seed: + logging.debug('Same seed, don\'t reload Maze') + return # is that maze older than the one we're already playing? # note that we use elapsed time instead of absolute time because # people's clocks are often set to something totally wrong @@ -541,9 +512,10 @@ class MazeGame(Gtk.DrawingArea): if len(self.remoteplayers) > 0: # but fudge it a little so that we can be sure they'll use our maze self.game_start_time -= 10 - mesh.broadcast("maze:%d,%d,%d,%d" % - (self.game_running_time(), self.maze.seed, - self.maze.width, self.maze.height)) + self._activity.broadcast_msg( + "maze:%d,%d,%d,%d" % ( + self.game_running_time(), self.maze.seed, + self.maze.width, self.maze.height)) def easier(self): """Make a new maze that is easier than the current one.""" @@ -558,12 +530,14 @@ class MazeGame(Gtk.DrawingArea): if len(self.remoteplayers) > 0: # but fudge it a little so that we can be sure they'll use our maze self.game_start_time -= 10 - mesh.broadcast("maze:%d,%d,%d,%d" % - (self.game_running_time(), self.maze.seed, - self.maze.width, self.maze.height)) + self._activity.broadcast_msg( + "maze:%d,%d,%d,%d" % ( + self.game_running_time(), + self.maze.seed, self.maze.width, self.maze.height)) def finish(self, player): self.finish_time = time.time() player.elapsed = self.finish_time - self.level_start_time if len(self.remoteplayers) > 0: - mesh.broadcast("finish:%s,%.2f" % (player.nick, player.elapsed)) + self._activity.broadcast_msg("finish:%s,%.2f" % (player.nick, + player.elapsed)) diff --git a/textchannel.py b/textchannel.py new file mode 100644 index 0000000..673a1fd --- /dev/null +++ b/textchannel.py @@ -0,0 +1,128 @@ +import logging + +from telepathy.client import Connection +from telepathy.interfaces import ( + CHANNEL_INTERFACE, CHANNEL_INTERFACE_GROUP, + CHANNEL_TYPE_TEXT) +from telepathy.constants import ( + CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES, + CHANNEL_TEXT_MESSAGE_TYPE_NORMAL) + + +class TextChannelWrapper(object): + """Wrap a telepathy Text Channel to make + usage simpler.""" + def __init__(self, text_chan, conn, pservice): + """Connect to the text channel""" + self._activity_cb = None + self._activity_close_cb = None + self._text_chan = text_chan + self._conn = conn + self._pservice = pservice + self._logger = logging.getLogger( + 'minichat-activity.TextChannelWrapper') + self._signal_matches = [] + m = self._text_chan[CHANNEL_INTERFACE].\ + connect_to_signal( + 'Closed', self._closed_cb) + self._signal_matches.append(m) + + def send(self, text): + """Send text over the Telepathy text channel.""" + # XXX Implement CHANNEL_TEXT_MESSAGE_TYPE_ACTION + if self._text_chan is not None: + self._text_chan[CHANNEL_TYPE_TEXT].Send( + CHANNEL_TEXT_MESSAGE_TYPE_NORMAL, text) + + def close(self): + """Close the text channel.""" + self._logger.debug('Closing text channel') + try: + self._text_chan[CHANNEL_INTERFACE].Close() + except: + self._logger.debug('Channel disappeared!') + self._closed_cb() + + def _closed_cb(self): + """Clean up text channel.""" + self._logger.debug('Text channel closed.') + for match in self._signal_matches: + match.remove() + self._signal_matches = [] + self._text_chan = None + if self._activity_close_cb is not None: + self._activity_close_cb() + + def set_received_callback(self, callback): + """Connect the function callback to the signal. + + callback -- callback function taking buddy + and text args + """ + if self._text_chan is None: + return + self._activity_cb = callback + m = self._text_chan[CHANNEL_TYPE_TEXT].\ + connect_to_signal( + 'Received', self._received_cb) + self._signal_matches.append(m) + + def handle_pending_messages(self): + """Get pending messages and show them as + received.""" + for id, timestamp, sender, type, flags, text \ + in self._text_chan[CHANNEL_TYPE_TEXT].ListPendingMessages( + False): + self._received_cb(id, timestamp, sender, type, flags, text) + + def _received_cb(self, id, timestamp, sender, type, flags, text): + """Handle received text from the text channel. + + Converts sender to a Buddy. + Calls self._activity_cb which is a callback + to the activity. + """ + if self._activity_cb: + buddy = self._get_buddy(sender) + self._activity_cb(buddy, text) + self._text_chan[ + CHANNEL_TYPE_TEXT].AcknowledgePendingMessages([id]) + else: + self._logger.debug( + 'Throwing received message on the floor' + ' since there is no callback connected. See ' + 'set_received_callback') + + def set_closed_callback(self, callback): + """Connect a callback for when the text channel + is closed. + + callback -- callback function taking no args + + """ + self._activity_close_cb = callback + + def _get_buddy(self, cs_handle): + """Get a Buddy from a (possibly channel-specific) + handle.""" + # XXX This will be made redundant once Presence + # Service provides buddy resolution + # Get the Telepathy Connection + tp_name, tp_path = \ + self._pservice.get_preferred_connection() + conn = Connection(tp_name, tp_path) + group = self._text_chan[CHANNEL_INTERFACE_GROUP] + my_csh = group.GetSelfHandle() + if my_csh == cs_handle: + handle = conn.GetSelfHandle() + elif group.GetGroupFlags() & \ + CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES: + handle = group.GetHandleOwners([cs_handle])[0] + else: + handle = cs_handle + + # XXX: deal with failure to get the handle owner + assert handle != 0 + + return self._pservice.get_buddy_by_telepathy_handle( + tp_name, tp_path, handle) |