Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGonzalo Odiard <godiard@gmail.com>2014-06-05 17:21:22 (GMT)
committer Gonzalo Odiard <godiard@gmail.com>2014-06-16 15:26:37 (GMT)
commit0daea191e3580f8760fce96d0ee24c04bf225943 (patch)
tree079483b578c0d1421ca4d52a5c1f4baa5ec58064
parent1519952d1bd9d9f2825db67772c916c11cf590dc (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-xactivity.py138
-rw-r--r--game.py200
-rw-r--r--textchannel.py128
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,
diff --git a/game.py b/game.py
index 3011721..46d170f 100644
--- a/game.py
+++ b/game.py
@@ -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)