
import datetime
import ast

import tornado.gen
import bson
from bson.objectid import ObjectId
import motor

import twcommon.misc
import twcommon.localize
from twcommon import wcproto
from twcommon.excepts import MessageException, ErrorMessageException

class Command:
    # As commands are defined with the @command decorator, they are stuffed
    # in this dict.
    all_commands = {}

    def __init__(self, name, func, isserver=False, restrict=None, noneedmongo=False, preconnection=False, doeswrite=False):
        self.name = name
        self.func = tornado.gen.coroutine(func)
        # isserver could be merged into restrict='server', since restrict
        # only applies to player commands.
        self.isserver = isserver
        self.restrict = restrict
        self.noneedmongo = noneedmongo
        self.preconnection = preconnection
        self.doeswrite = doeswrite
        
    def __repr__(self):
        return '<Command "%s">' % (self.name,)


def command(name, **kwargs):
    """Decorator for command functions.
    """
    def wrap(func):
        cmd = Command(name, func, **kwargs)
        if name in Command.all_commands:
            raise Exception('Command name defined twice: "%s"', name)
        Command.all_commands[name] = cmd
        return cmd
    return wrap

def define_commands():
    """
    Define all the commands which will be used by the server. Return them
    in a dict.

    Note that the last argument will be a IOStream for server commands,
    but a PlayerConnection for player commands. Never the twain shall
    meet.

    These functions wind up as entries in the Command.all_commands dict.
    The arguments to @command wind up as properties of the Command object
    that wraps the function. Oh, and the function is always a
    tornado.gen.coroutine -- you don't need to declare that.
    """

    @command('shutdownprocess', isserver=True, noneedmongo=True)
    def cmd_shutdownprocess(app, task, cmd, stream):
        """Shut down the process. We do this from a command, so that we
        can say for sure that no other command is in flight.
        This will be the last command to execute.
        """
        restartreason = getattr(cmd, 'restarting', False)
        if restartreason:
            val = 'Server broadcast: Server is restarting!'
        else:
            val = 'Server broadcast: Server is shutting down!'
        for stream in app.webconns.all():
            stream.write(wcproto.message(0, {'cmd':'messageall', 'text':val}))
        # Bump the lastactive timestamp, if possible.
        try:
            yield motor.Op(app.mongodb.config.update,
                           {'key':'lastactive'},
                           {'key':'lastactive', 'val':task.starttime}, upsert=True)
        except:
            pass
        # Tell the app to shut down.
        app.shutdown(restartreason)
        # At this point ioloop is still running, but the command queue
        # is frozen. A sys.exit will be along shortly.

    @command('dbconnected', isserver=True, doeswrite=True)
    def cmd_dbconnected(app, task, cmd, stream):
        # We've connected (or reconnected) to mongodb. Re-synchronize any
        # data that we had cached from there.
        
        # First, grab and then update the lastactive value.
        lastactive = None
        lastactivediff = None
        try:
            res = yield motor.Op(app.mongodb.config.find_one,
                                 {'key':'lastactive'})
            if res:
                lastactive = res['val']
                lastactivediff = task.starttime - lastactive
        except:
            pass
        task.log.info('server last known active: %s (%s ago)', lastactive, lastactivediff)
        yield motor.Op(app.mongodb.config.update,
                       {'key':'lastactive'},
                       {'key':'lastactive', 'val':task.starttime}, upsert=True)
        
        # Load up the localization data.
        try:
            task.app.localize = yield twcommon.localize.load_localization(task.app)
        except Exception as ex:
            task.log.warning('Caught exception (loading localization data): %s', ex, exc_info=app.debugstacktraces)

        # Awaken any inhabited instances. If an instance is marked awake
        # but uninhabited, put it to sleep.
        # Go through the list of players who are in the world.
        inhabset = set()
        cursor = app.mongodb.playstate.find({'iid':{'$ne':None}},
                                            {'_id':1, 'iid':1})
        while (yield cursor.fetch_next):
            playstate = cursor.next_object()
            iid = playstate['iid']
            if iid:
                inhabset.add(iid)
        # cursor autoclose
        # Go through the list of apparently-awake instances.
        awakeset = set()
        cursor = app.mongodb.instances.find({'lastawake':True},
                                            {'_id':1})
        while (yield cursor.fetch_next):
            instance = cursor.next_object()
            iid = instance['_id']
            if iid:
                awakeset.add(iid)
        # cursor autoclose
        # In an ideal world, inhabset is a subset of awakeset. But we'll
        # be careful.
        iidls = list(inhabset.union(awakeset))
        iidls.sort()  # Just for consistency
        for iid in iidls:
            if iid not in inhabset:
                # Instance should be asleep. We don't call the hook, just
                # set lastawake to the lastactive time.
                task.log.warning('Instance %s found awake, but with no players! Marking it asleep.', iid)
                yield motor.Op(app.mongodb.instances.update,
                               {'_id':iid},
                               {'$set':{'lastawake':lastactive}})
                continue
            # Instance should be awake. Call the hook and set lastawake true.
            # The hook's _slept argument will be lastactive.
            awakening = app.ipool.notify_instance(iid)
            if awakening:
                app.log.info('Awakening instance %s (slept roughly %s)', iid, lastactive)
                yield motor.Op(app.mongodb.instances.update,
                               {'_id':iid},
                               {'$set':{'lastawake':True}})
                instance = yield motor.Op(app.mongodb.instances.find_one,
                                          {'_id':iid})
                loctx = two.task.LocContext(None, wid=instance['wid'], scid=instance['scid'], iid=iid)
                task.resetticks()
                yield two.execute.try_hook(task, 'on_wake', loctx, 'awakening instance',
                                           lambda:{ '_slept':lastactive })

    @command('checkuninhabited', isserver=True, doeswrite=True)
    def cmd_checkuninhabited(app, task, cmd, stream):
        # Go through all the awake instances. Those that are still
        # inhabited, bump their timers. Those that have not been inhabited
        # for a while, put to sleep.
        # But first, bump the lastactive timestamp.
        yield motor.Op(app.mongodb.config.update,
                       {'key':'lastactive'},
                       {'key':'lastactive', 'val':task.starttime}, upsert=True)
        # Go through the list of players who are in the world.
        iidset = set()
        cursor = app.mongodb.playstate.find({'iid':{'$ne':None}},
                                            {'_id':1, 'iid':1})
        while (yield cursor.fetch_next):
            playstate = cursor.next_object()
            iid = playstate['iid']
            if iid:
                iidset.add(iid)
        # cursor autoclose
        iidls = list(iidset)
        for iid in iidls:
            instance = app.ipool.get(iid)
            # These instances should always be in the pool, but we'll
            # do a safety check anyway.
            if instance:
                instance.lastinhabited = task.starttime
        tooold = task.starttime - app.ipool.UNINHABITED_LIMIT
        for instance in app.ipool.all():
            iid = instance.iid
            if instance.lastinhabited < tooold:
                app.log.info('Sleeping instance %s', iid)
                yield motor.Op(app.mongodb.instances.update,
                               {'_id':iid},
                               {'$set':{'lastawake':task.starttime}})
                instance = yield motor.Op(app.mongodb.instances.find_one,
                                          {'_id':iid})
                loctx = two.task.LocContext(None, wid=instance['wid'], scid=instance['scid'], iid=iid)
                task.resetticks()
                yield two.execute.try_hook(task, 'on_sleep', loctx, 'sleeping instance')
                app.ipool.remove_instance(iid)
    
    @command('sleepinstance', isserver=True)
    def cmd_sleepinstance(app, task, cmd, stream):
        inst = app.ipool.get(cmd.iid)
        if not inst:
            task.log.warning('sleepinstance: instance is not awake (%s)', cmd.iid)
            return
        cursor = app.mongodb.playstate.find({'iid':cmd.iid},
                                            {'_id':1})
        res = yield motor.Op(cursor.count)
        # cursor autoclose
        if res:
            task.log.warning('sleepinstance: unable to sleep instance because %d players are present', res)
            return
        # Mark the instance as not having been touched recently. Then
        # schedule an emergency checkuninhabited event, which should result
        # in the instance being put to sleep.
        inst.ancientify()
        app.queue_command({'cmd':'checkuninhabited'})
        
    @command('connect', isserver=True, noneedmongo=True)
    def cmd_connect(app, task, cmd, stream):
        assert stream is not None, 'Tweb connect command from no stream.'
        stream.write(wcproto.message(0, {'cmd':'connectok'}))

        # Accept any connections that tweb is holding.
        for connobj in cmd.connections:
            if not app.mongodb:
                # Reject the players.
                stream.write(wcproto.message(0, {'cmd':'playernotok', 'connid':connobj.connid, 'text':'The database is not available.'}))
                continue
            conn = app.playconns.add(connobj.connid, connobj.uid, connobj.email, stream)
            stream.write(wcproto.message(0, {'cmd':'playerok', 'connid':conn.connid}))
            app.queue_command({'cmd':'connrefreshall', 'connid':conn.connid})
            app.log.info('Player %s has reconnected (uid %s)', conn.email, conn.uid)
            # But don't queue a portin command, because people are no more
            # likely to be in the void than usual.
        
        # Broadcast a message to the returned players.
        val = 'Server broadcast: Server has restarted!'
        stream.write(wcproto.message(0, {'cmd':'messageall', 'text':val}))

    @command('disconnect', isserver=True, noneedmongo=True)
    def cmd_disconnect(app, task, cmd, stream):
        for (connid, conn) in app.playconns.as_dict().items():
            if conn.twwcid == cmd.twwcid:
                try:
                    app.playconns.remove(connid)
                except:
                    pass
        app.log.warning('Tweb has disconnected; now %d connections remain', len(app.playconns.as_dict()))

    @command('checkdisconnected', isserver=True, doeswrite=True)
    def cmd_checkdisconnected(app, task, cmd, stream):
        # Construct a list of players who are in the world, but
        # disconnected. (But disconnected more than a minute ago.)
        ls = []
        inworld = 0
        recentcount = 0
        limit = datetime.timedelta(minutes=1)
        cursor = app.mongodb.playstate.find({'iid':{'$ne':None}},
                                            {'_id':1})
        while (yield cursor.fetch_next):
            playstate = cursor.next_object()
            conncount = app.playconns.count_for_uid(playstate['_id'])
            inworld += 1
            if not conncount:
                discontime = app.playconns.disconnected_time_uid(playstate['_id'])
                if discontime is None or discontime > limit:
                    ls.append(playstate['_id'])
                else:
                    recentcount += 1
        # cursor autoclose

        if inworld or recentcount or ls:
            app.log.info('checkdisconnected: %d players in world, %d recently disconnected, %d really disconnected', inworld, recentcount, len(ls))
        
        for uid in ls:
            app.playconns.clear_disconnected_time_uid(uid)
            app.queue_command({'cmd':'tovoid', 'uid':uid, 'portin':False})

        # Second task: construct a list of guest accounts which are marked
        # in-use, but are disconnected. Kill their sessions and give them
        # the special cleanup flag (guestsession=True).
        ls = []
        cursor = app.mongodb.players.find({'guest':True, 'guestsession':{'$ne':None}},
                                          {'name':1, 'guestsession':1})
        while (yield cursor.fetch_next):
            player = cursor.next_object()
            if player['guestsession'] is True:
                continue  # already being cleaned up
            conncount = app.playconns.count_for_uid(player['_id'])
            if not conncount:
                ls.append(player)
        # cursor autoclose
        ### Keep a two-strikes list here too?
        for player in ls:
            app.log.info('checkdisconnected: guest %s will be disconnected from session %s', player['name'], player['guestsession'].decode())
            yield motor.Op(app.mongodb.sessions.remove,
                           {'sid':player['guestsession']})
            yield motor.Op(app.mongodb.players.update,
                           {'_id':player['_id']},
                           {'$set':{'guestsession':True}})

        # Third task: launch cleanup on guest accounts. Some might be
        # left over from previous attempts, so we construct a new list.
        ls = []
        cursor = app.mongodb.players.find({'guest':True, 'guestsession':True},
                                          {'name':1})
        while (yield cursor.fetch_next):
            player = cursor.next_object()
            ls.append(player)
        # cursor autoclose
        for player in ls:
            app.queue_command({'cmd':'cleanupguest', 'uid':player['_id']})

    @command('cleanupguest', isserver=True, doeswrite=True)
    def cmd_cleanupguest(app, task, cmd, stream):
        # Clean up a guest's instances and personal data.
        # This is messy because it may have to put instances to sleep,
        # which is a command in its own right.
        player = yield motor.Op(app.mongodb.players.find_one,
                                {'_id':cmd.uid},
                                {'name':1, 'guest':1, 'guestsession':1,
                                 'scid':1, 'plistid':1})
        if not player or not player.get('guest') or player.get('guestsession') is not True:
            return
        playstate = yield motor.Op(app.mongodb.playstate.find_one,
                                   {'_id':player['_id']},
                                   {'iid':1})
        if playstate['iid']:
            app.log.warning('cleanupguest: tried to clean guest %s, but they were in instance %s', player['name'], playstate['iid'])
            return
        
        app.log.info('cleanupguest: cleaning guest %s', player['name'])

        asleepls = []
        awakels = []
        cursor = app.mongodb.instances.find({'scid':player['scid']})
        while (yield cursor.fetch_next):
            instance = cursor.next_object()
            if instance.get('lastawake', None) is True:
                awakels.append(instance)
            else:
                asleepls.append(instance)
        # cursor autoclose

        app.log.info('cleanupguest: %d instances awake, %d asleep', len(awakels), len(asleepls))

        moretodo = False

        for instance in awakels:
            # This works the same as the bootinstance command.
            cursor = app.mongodb.playstate.find({'iid':instance['_id']},
                                                {'_id':1})
            while (yield cursor.fetch_next):
                ply = cursor.next_object()
                app.queue_command({'cmd':'tovoid', 'uid':ply['_id'], 'portin':True})
            # cursor autoclose
            app.queue_command({'cmd':'sleepinstance', 'iid':instance['_id']})
            moretodo = True

        # Any already-asleep instances can be wiped.
        for instance in asleepls:
            app.log.info('cleanupguest: wiping instance %s', instance['_id'])
            yield motor.Op(app.mongodb.instanceprop.remove,
                           {'iid':instance['_id']})
            yield motor.Op(app.mongodb.iplayerprop.remove,
                           {'iid':instance['_id']})
            yield motor.Op(app.mongodb.portals.remove,
                           {'iid':instance['_id']})
            yield motor.Op(app.mongodb.instances.remove,
                           {'_id':instance['_id']})

        if moretodo:
            # Some instances are still being put to sleep, so we can't
            # finish cleaning up.
            return

        app.log.info('cleanupguest: finishing up guest %s', player['name'])
        yield motor.Op(app.mongodb.iplayerprop.remove,
                       {'uid':player['_id']})
        yield motor.Op(app.mongodb.playprefs.remove,
                       {'uid':player['_id']})
        yield motor.Op(app.mongodb.portals.remove,
                       {'plistid': player['plistid']})
        # The player's starting portal will be created in create_session_guest.
        # Don't worry about playstate; we've already established that
        # the player is out-of-world.
        
        yield motor.Op(app.mongodb.players.update,
                       {'_id':player['_id']},
                       {'$set':{'guestsession':None}})
        # Done!

    @command('tovoid', isserver=True, doeswrite=True)
    def cmd_tovoid(app, task, cmd, stream):
        oldloctx = yield task.get_loctx(cmd.uid)
        yield two.execute.try_hook(task, 'on_leave', oldloctx, 'leaving loc, tovoid',
                                   lambda:{
                '_from':two.execute.LocationProxy(oldloctx.locid) if oldloctx.locid else None,
                '_to':None })
                
        # If portto is None, we'll wind up porting to the player's panic
        # location.
        portto = getattr(cmd, 'portto', None)
        
        task.write_event(cmd.uid, app.localize('action.portout')) # 'The world fades away.'
        others = yield task.find_locale_players(uid=cmd.uid, notself=True)
        if others:
            res = yield motor.Op(app.mongodb.players.find_one,
                                 {'_id':cmd.uid},
                                 {'name':1})
            playername = res['name']
            task.write_event(others, app.localize('action.oportout') % (playername,)) # '%s disappears.'
        # Move the player to the void.
        yield motor.Op(app.mongodb.playstate.update,
                       {'_id':cmd.uid},
                       {'$set':{'focus':None, 'iid':None, 'locid':None,
                                'portto':portto,
                                'lastlocid': None,
                                'lastmoved':task.starttime }})
        task.set_dirty(cmd.uid, DIRTY_FOCUS | DIRTY_LOCALE | DIRTY_WORLD | DIRTY_POPULACE | DIRTY_TOOL)
        task.set_data_change( ('playstate', cmd.uid, 'iid') )
        task.set_data_change( ('playstate', cmd.uid, 'locid') )
        if oldloctx.iid:
            task.set_data_change( ('populace', oldloctx.iid, oldloctx.locid) )
        task.clear_loctx(cmd.uid)
        if cmd.portin:
            app.schedule_command({'cmd':'portin', 'uid':cmd.uid}, 1.5)
        
    @command('logplayerconntable', isserver=True, noneedmongo=True)
    def cmd_logplayerconntable(app, task, cmd, stream):
        app.playconns.dumplog()
        
    @command('holler', isserver=True)
    def cmd_holler(app, task, cmd, stream):
        val = 'Admin broadcast: ' + cmd.text
        for stream in app.webconns.all():
            stream.write(wcproto.message(0, {'cmd':'messageall', 'text':val}))
        
    @command('timerevent', isserver=True, doeswrite=True)
    def cmd_timerevent(app, task, cmd, stream):
        iid = cmd.iid
        instance = app.ipool.get(iid)
        if not instance:
            raise ErrorMessageException('instance is not awake')
        instance = yield motor.Op(app.mongodb.instances.find_one,
                                  {'_id':iid})
        loctx = two.task.LocContext(None, wid=instance['wid'], scid=instance['scid'], iid=iid)
        func = cmd.func
        locals = None
        if isinstance(func, two.symbols.ScriptCallable):
            # This is a cheesy way to handle callables. But it works.
            locals = { '_timerarg': func }
            func = { 'type':'code', 'text':'_timerarg()' }
            functype = EVALTYPE_RAW
        elif func and twcommon.misc.is_typed_dict(func, 'code'):
            functype = EVALTYPE_RAW
        else:
            func = str(func)
            functype = EVALTYPE_CODE
        ctx = two.evalctx.EvalPropContext(task, loctx=loctx, level=LEVEL_EXECUTE)
        try:
            yield ctx.eval(func, evaltype=functype, locals=locals)
        except Exception as ex:
            task.log.warning('Caught exception (timer event): %s', ex, exc_info=app.debugstacktraces)
        
    @command('connrefreshall', isserver=True, doeswrite=True)
    def cmd_connrefreshall(app, task, cmd, stream):
        # Refresh one connection (not all the player's connections!)
        conn = app.playconns.get(cmd.connid)
        if not conn:
            return
        task.set_dirty(conn, DIRTY_ALL)
        app.queue_command({'cmd':'connupdateplist', 'connid':cmd.connid})
        app.queue_command({'cmd':'connupdatescopes', 'connid':cmd.connid})
        ### probably queue a connupdatefriends, too
    
    @command('connupdateplist', isserver=True)
    def cmd_connupdateplist(app, task, cmd, stream):
        # Re-send the player's portlist to one connection.
        conn = app.playconns.get(cmd.connid)
        if not conn:
            return
        player = yield motor.Op(app.mongodb.players.find_one,
                                {'_id':conn.uid},
                                {'plistid':1})
        if not player:
            return
        playstate = yield motor.Op(app.mongodb.playstate.find_one,
                                   {'_id':conn.uid},
                                   {'iid':1})
        if not playstate:
            return
        plistid = player['plistid']
        iid = playstate['iid']
        cursor = app.mongodb.portals.find({'plistid':plistid, 'iid':None})
        ls = []
        while (yield cursor.fetch_next):
            portal = cursor.next_object()
            ls.append(portal)
        # cursor autoclose
        map = {}
        for portal in ls:
            desc = yield two.execute.portal_description(app, portal, conn.uid, uidiid=iid, location=True, short=True)
            if desc:
                strid = str(portal['_id'])
                desc['portid'] = strid
                desc['listpos'] = portal.get('listpos', 0.0)
                map[strid] = desc
        conn.write({'cmd':'updateplist', 'clear': True, 'map':map})

    @command('connupdatescopes', isserver=True)
    def cmd_connupdatescopes(app, task, cmd, stream):
        # Re-send the player's available scope list to one connection.
        conn = app.playconns.get(cmd.connid)
        if not conn:
            return
        player = yield motor.Op(app.mongodb.players.find_one,
                                {'_id':conn.uid},
                                {'plistid':1, 'scid':1})
        if not player:
            return

        map = {}
        config = yield motor.Op(app.mongodb.config.find_one,
                                {'key':'globalscopeid'})
        scope = yield two.execute.scope_description(app, config['val'], conn.uid)
        if scope:
            map[scope['id']] = scope

        scope = yield two.execute.scope_description(app, player['scid'], conn.uid)
        if scope:
            map[scope['id']] = scope
            
        ### And any personal scopes you have access to
        ### And any group scopes you have access to

        conn.write({'cmd':'updatescopes', 'clear':True, 'map':map})

    @command('buildcopyportal', isserver=True, doeswrite=True)
    def cmd_buildcopyportal(app, task, cmd, stream):
        uid = ObjectId(cmd.uid)
        wid = ObjectId(cmd.wid)
        locid = ObjectId(cmd.locid)
        
        world = yield motor.Op(app.mongodb.worlds.find_one,
                                {'_id':wid})
        if not world:
            raise ErrorMessageException('buildcopyportal: no such world: %s' % (wid,))
        if world['creator'] != uid:
            raise ErrorMessageException('buildcopyportal: world not owned by player: %s' % (wid,))

        loc = yield motor.Op(app.mongodb.locations.find_one,
                             {'_id':locid})
        if not loc:
            raise ErrorMessageException('buildcopyportal: no such location: %s' % (locid,))

        player = yield motor.Op(app.mongodb.players.find_one,
                                {'_id':uid},
                                {'scid':1, 'plistid':1})
        plistid = player['plistid']
        
        # This command comes from the build interface; the player is creating
        # a new link to his own world. We go with a global-scope link,
        # unless the world is personal-only.
        if world['instancing'] != 'solo':
            config = yield motor.Op(app.mongodb.config.find_one,
                                    {'key':'globalscopeid'})
            scid = config['val']
        else:
            scid = player['scid']

        portid = yield two.execute.create_portal_for_player(app, uid, plistid, wid, scid, locid)
        app.log.info('Build portal created: %s', portid)
        
    @command('externalcopyportal', isserver=True, doeswrite=True)
    def cmd_externalcopyportal(app, task, cmd, stream):
        uid = ObjectId(cmd.uid)
        portid = ObjectId(cmd.portid)
        
        portal = yield motor.Op(app.mongodb.portals.find_one,
                                {'_id':portid})
        wid = portal['wid']
        locid = portal['locid']
        
        world = yield motor.Op(app.mongodb.worlds.find_one,
                               {'_id':wid})
        if not world:
            raise ErrorMessageException('externalcopyportal: no such world: %s' % (wid,))

        scid = yield two.execute.portal_resolve_scope(app, portal, uid, None, world)
        
        player = yield motor.Op(app.mongodb.players.find_one,
                                {'_id':uid},
                                {'plistid':1})
        plistid = player['plistid']

        # Create a portal (or return the existing one, if already present)
        # The "silent" argument just means that an existing duplicate will
        # be returned without complaint.
        newportid = yield two.execute.create_portal_for_player(app, uid, plistid, wid, scid, locid, silent=True)
        app.log.info('URL-based portal: %s', newportid)

        # Check that the new portal exists *and* is in the right place.
        newportal = yield motor.Op(app.mongodb.portals.find_one,
                                   {'_id':newportid,
                                    'iid':None, 'plistid':plistid})
        if not newportal:
            raise ErrorMessageException('externalcopyportal: new portal was not correct: %s' % (newportid,))
        
        if cmd.focus:
            # Focus the new portal, as if by the plistselect command.
            msg = app.localize('message.desc_own_portlist')
            focusobj = ['portlist', plistid, False, msg, False, newportid]
            yield motor.Op(app.mongodb.playstate.update,
                           {'_id':uid},
                           {'$set':{'focus':focusobj}})
            task.set_dirty(uid, DIRTY_FOCUS)
        
    @command('notifydatachange', isserver=True, doeswrite=True)
    def cmd_notifydatachange(app, task, cmd, stream):
        ls = cmd.change
        # We may need to handle other data-key formats eventually. But
        # right now, it's all [db, wid, locid/uid, key] where db is
        # 'worldprop' or 'wplayerprop' and the id values may be None
        # or ObjectId.
        ### All of this prop-access stuff will need to go through the 
        ### propcache, when the propcache has a lifespan.
        if type(ls[1]) is str:
            ls[1] = ObjectId(ls[1])
        if type(ls[2]) is str:
            ls[2] = ObjectId(ls[2])
        key = tuple(ls)
        app.log.info('Build change notification: %s', key)
        task.set_data_change(key)
        
    @command('playeropen', noneedmongo=True, preconnection=True)
    def cmd_playeropen(app, task, cmd, conn):
        assert conn is None, 'playeropen command with connection not None'
        connid = cmd._connid
        
        if not app.mongodb:
            # Reject the players anyhow.
            try:
                cmd._stream.write(wcproto.message(0, {'cmd':'playernotok', 'connid':connid, 'text':'The database is not available.'}))
            except:
                pass
            return
            
        conn = app.playconns.add(connid, cmd.uid, cmd.email, cmd._stream)
        cmd._stream.write(wcproto.message(0, {'cmd':'playerok', 'connid':connid}))
        app.queue_command({'cmd':'connrefreshall', 'connid':connid})
        app.log.info('Player %s has connected (uid %s)', conn.email, conn.uid)
        # If the player is in the void, put them somewhere.
        app.queue_command({'cmd':'portin', 'uid':conn.uid})

    @command('playerclose')
    def cmd_playerclose(app, task, cmd, conn):
        app.log.info('Player %s has disconnected (uid %s)', conn.email, conn.uid)
        try:
            app.playconns.remove(conn.connid)
        except Exception as ex:
            app.log.error('Failed to remove on playerclose %d: %s', conn.connid, ex)
    
    @command('portin', isserver=True, doeswrite=True)
    def cmd_portin(app, task, cmd, stream):
        # When a player is in the void, this command should come along
        # shortly thereafter and send them to a destination.
        player = yield motor.Op(app.mongodb.players.find_one,
                                {'_id':cmd.uid},
                                {'name':1, 'scid':1, 'plistid':1})
        playstate = yield motor.Op(app.mongodb.playstate.find_one,
                                   {'_id':cmd.uid})
        if not player or not playstate:
            raise ErrorMessageException('Portin: no such player: %s' % (cmd.uid,))
        playername = player['name']
        if playstate.get('iid', None) and playstate.get('locid', None):
            app.log.info('Player %s is already in the world', playername)
            return
        # Figure out what destination was set. If none, default to the
        # player's chosen panic location. If none, try the start world.
        newloc = playstate.get('portto', None)
        if newloc:
            newwid = newloc['wid']
            newscid = newloc['scid']
            newlocid = newloc['locid']
        else:
            # Look through the player's list and find the preferred entry.
            plistid = player['plistid']
            res = yield motor.Op(app.mongodb.portals.find_one,
                                 {'plistid':plistid, 'iid':None, 'preferred':True})
            if res:
                newwid = res['wid']
                newscid = res['scid']
                newlocid = res['locid']
            else:
                # Last hope: the start world.
                res = yield motor.Op(app.mongodb.config.find_one,
                                     {'key':'startworldloc'})
                lockey = res['val']
                res = yield motor.Op(app.mongodb.config.find_one,
                                     {'key':'startworldid'})
                newwid = res['val']
                newscid = player['scid']
                res = yield motor.Op(app.mongodb.locations.find_one,
                                     {'wid':newwid, 'key':lockey})
                newlocid = res['_id']
        app.log.debug('Player portin to %s, %s, %s', newwid, newscid, newlocid)
        
        instance = yield motor.Op(app.mongodb.instances.find_one,
                                  {'wid':newwid, 'scid':newscid})
        if instance:
            minaccess = instance.get('minaccess', ACC_VISITOR)
        else:
            minaccess = ACC_VISITOR
        if False: ### check minaccess against scope access!
            task.write_event(cmd.uid, app.localize('message.instance_no_access')) # 'You do not have access to this instance.'
            return

        # This is the one and only spot in the server code where a player
        # *enters* a new instance. It's also one of the two places where
        # instances are created.
        
        if instance:
            newiid = instance['_id']
        else:
            newiid = yield motor.Op(app.mongodb.instances.insert,
                                    {'wid':newwid, 'scid':newscid})
            app.log.info('Created instance %s (world %s, scope %s)', newiid, newwid, newscid)
            loctx = two.task.LocContext(None, wid=newwid, scid=newscid, iid=newiid)
            task.resetticks()
            yield two.execute.try_hook(task, 'on_init', loctx, 'initing instance')

        awakening = app.ipool.notify_instance(newiid)
        if awakening:
            app.log.info('Awakening instance %s', newiid)
            lastawake = None
            res = yield motor.Op(app.mongodb.instances.find_one,
                                 {'_id':newiid})
            if res:
                lastawake = res.get('lastawake', None)
                if isinstance(lastawake, bool):
                    task.log.warning('Instance lastawake was %s at awake (entry)!', lastawake)
                    lastawake = task.starttime
            yield motor.Op(app.mongodb.instances.update,
                           {'_id':newiid},
                           {'$set':{'lastawake':True}})
            loctx = two.task.LocContext(None, wid=newwid, scid=newscid, iid=newiid)
            task.resetticks()
            yield two.execute.try_hook(task, 'on_wake', loctx, 'awakening instance',
                                       lambda:{ '_slept':lastawake })

        # Focus special case: if we were focussed on a personal portal
        # in the void, retain that. This supports the use of an external
        # portal while you're not logged in.
        newfocus = None
        oldfocus = playstate.get('focus', None)
        if isinstance(oldfocus, list) and len(oldfocus) >= 6 and oldfocus[0] == 'portlist':
            if oldfocus[1] == player['plistid']:
                task.log.info('Retaining personal portal focus from void')
                newfocus = oldfocus

        yield motor.Op(app.mongodb.playstate.update,
                       {'_id':cmd.uid},
                       {'$set':{'iid':newiid,
                                'locid':newlocid,
                                'focus':newfocus,
                                'lastmoved': task.starttime,
                                'lastlocid': None,
                                'portto':None }})
        task.set_dirty(cmd.uid, DIRTY_FOCUS | DIRTY_LOCALE | DIRTY_WORLD | DIRTY_POPULACE | DIRTY_TOOL)
        task.set_data_change( ('playstate', cmd.uid, 'iid') )
        task.set_data_change( ('playstate', cmd.uid, 'locid') )
        task.set_data_change( ('populace', newiid, newlocid) )
        task.clear_loctx(cmd.uid)

        others = yield task.find_locale_players(uid=cmd.uid, notself=True)
        if others:
            task.write_event(others, app.localize('action.oportin') % (playername,)) # '%s appears.'
        task.write_event(cmd.uid, app.localize('action.portin')) # 'You are somewhere new.'

        newloctx = yield task.get_loctx(cmd.uid)
        yield two.execute.try_hook(task, 'on_enter', newloctx, 'entering loc, linkin',
                                   lambda:{
                '_from':None,
                '_to':two.execute.LocationProxy(newlocid)})
        
    @command('uiprefs')
    def cmd_uiprefs(app, task, cmd, conn):
        # Could we handle this in tweb? I guess, if we cared.
        # Note that this command isn't marked writable, because it only
        # writes to an obscure collection that never affects anybody's
        # display.
        for (key, val) in cmd.map.__dict__.items():
            res = yield motor.Op(app.mongodb.playprefs.update,
                                 {'uid':conn.uid, 'key':key},
                                 {'uid':conn.uid, 'key':key, 'val':val},
                                 upsert=True)

    @command('meta')
    def cmd_meta(app, task, cmd, conn):
        ls = cmd.text.split()
        if not ls:
            raise MessageException('You must supply a command after the slash. Try \u201C/help\u201D.')
        key = ls[0]
        newcmd = Command.all_commands.get('meta_'+key)
        if not newcmd:
            raise MessageException('Command \u201C/%s\u201D not understood. Try \u201C/help\u201D.' % (key,))
        app.queue_command({'cmd':newcmd.name, 'args':ls[1:]}, connid=conn.connid)

    @command('meta_help')
    def cmd_meta_help(app, task, cmd, conn):
        conn.write({'cmd':'message', 'text':'Quick help:'})
        conn.write({'cmd':'message', 'text':'Type to speak out loud (to nearby players). A message that begins with a colon (\u201C:dances\u201D) will appear as a pose (\u201CBelford dances\u201D). \u201C/me dances\u201D does the same.'})
        conn.write({'cmd':'message', 'text':'Other commands:'})
        conn.write({'cmd':'message', 'text':'/shout text: Send message to all players in the same world (instance) as you.'})
        conn.write({'cmd':'message', 'text':'/refresh: Reload the current location. (Or you can use your browser\'s refresh button.)'})
        conn.write({'cmd':'message', 'text':'/panic: Jump to your selected panic location. \xA0 /panicstart: Jump back to the start world.'})
        conn.write({'cmd':'message', 'text':'/playstate: Display your identity and location, with database IDs.'})
        return

    @command('meta_refresh')
    def cmd_meta_refresh(app, task, cmd, conn):
        conn.write({'cmd':'message', 'text':'Refreshing display...'})
        app.queue_command({'cmd':'connrefreshall', 'connid':conn.connid})

    @command('meta_playstate', restrict='debug')
    def cmd_meta_playstate(app, task, cmd, conn):
        loctx = yield task.get_loctx(conn.uid)

        player = yield motor.Op(app.mongodb.players.find_one,
                                {'_id':conn.uid})
        
        msg = 'You are "%s" <%s> (%s).' % (player['name'], player['email'], conn.uid,)
        conn.write({'cmd':'message', 'text':msg})

        if loctx.wid:
            world = yield motor.Op(app.mongodb.worlds.find_one,
                                   {'_id':loctx.wid})
            name = '(none)'
            if world:
                name = world.get('name', '???')
            msg = 'World: "%s" (%s).' % (name, loctx.wid)
            conn.write({'cmd':'message', 'text':msg})
        if loctx.iid:
            msg = 'Instance: (%s).' % (loctx.iid,)
            conn.write({'cmd':'message', 'text':msg})
        if loctx.scid:
            scope = yield motor.Op(app.mongodb.scopes.find_one,
                                   {'_id':loctx.scid})
            if scope:
                msg = 'Scope: %s (%s).' % (scope['type'], loctx.scid)
                conn.write({'cmd':'message', 'text':msg})
        if loctx.locid:
            loc = yield motor.Op(app.mongodb.locations.find_one,
                                 {'_id':loctx.locid})
            if loc:
                msg = 'Location: "%s" (%s).' % (loc['name'], loctx.locid)
                conn.write({'cmd':'message', 'text':msg})
        
    @command('meta_me')
    def cmd_me(app, task, cmd, conn):
        res = yield motor.Op(app.mongodb.players.find_one,
                             {'_id':conn.uid},
                             {'name':1})
        playername = res['name']
        if not cmd.args:
            raise MessageException('No pose given.')
        text = ' '.join(cmd.args)
        val = '%s %s' % (playername, text,)
        everyone = yield task.find_locale_players()
        task.write_event(everyone, val)

    @command('meta_shout')
    def cmd_shout(app, task, cmd, conn):
        res = yield motor.Op(app.mongodb.players.find_one,
                             {'_id':conn.uid},
                             {'name':1})
        playername = res['name']
        if not cmd.args:
            raise MessageException('No message given.')
        text = ' '.join(cmd.args)
        oval = 'Realm message from %s: %s' % (playername, text,)
        loctx = yield task.get_loctx(conn.uid)
        others = yield task.find_location_players(loctx.iid, None)
        for obj in others:
            subls = app.playconns.get_for_uid(obj)
            if subls:
                for conn in subls:
                    conn.write({'cmd':'message', 'text':oval})

    @command('meta_scopeaccess', restrict='debug')
    def cmd_meta_scopeaccess(app, task, cmd, conn):
        loctx = yield task.get_loctx(conn.uid)
        level = yield two.execute.scope_access_level(app, conn.uid, loctx.wid, loctx.scid)
        val = 'Access level to current scope: %s' % (level,)
        conn.write({'cmd':'message', 'text':val})        

    @command('meta_actionmaps', restrict='debug')
    def cmd_meta_actionmaps(app, task, cmd, conn):
        val = 'Locale action map: %s' % (conn.localeactions,)
        conn.write({'cmd':'message', 'text':val})
        if conn.populaceactions:
            val = 'Populace action map: %s' % (conn.populaceactions,)
            conn.write({'cmd':'message', 'text':val})
        if conn.focusactions:
            val = 'Focus action map: %s' % (conn.focusactions,)
            conn.write({'cmd':'message', 'text':val})
        if conn.toolactions:
            val = 'Tool action map: %s' % (conn.toolactions,)
            conn.write({'cmd':'message', 'text':val})

    @command('meta_dependencies', restrict='debug')
    def cmd_meta_dependencies(app, task, cmd, conn):
        def keyfunc(tup):
            return [ tup[-1] ] + tup[ 0 : -1 ]
        def func(label, depset):
            if not depset:
                return
            conn.write({'cmd':'message', 'text':label+' dependency set:'})
            ls = [ [ str(val) for val in tup ] for tup in depset ]
            ls.sort(key=keyfunc)
            for tup in ls:
                conn.write({'cmd':'message', 'text':'- ' + str(tup)})
        func('Locale', conn.localedependencies)
        func('Populace', conn.populacedependencies)
        func('Focus', conn.focusdependencies)
        func('Tool', conn.tooldependencies)
        
    @command('meta_showipool', restrict='debug')
    def cmd_meta_showipool(app, task, cmd, conn):
        ls = app.ipool.all()
        instls = ', '.join([ str(val.iid) for val in ls ])
        raise MessageException('Instance pool has %d awake instances: %s' % (len(ls), instls))

    @command('meta_panic')
    def cmd_meta_panic(app, task, cmd, conn):
        app.queue_command({'cmd':'tovoid', 'uid':conn.uid, 'portin':True})

    @command('meta_panicstart')
    def cmd_meta_panicstart(app, task, cmd, conn):
        app.queue_command({'cmd':'portstart'}, connid=task.connid, twwcid=task.twwcid)

    @command('meta_bootinstance', restrict='creator')
    def cmd_meta_bootinstance(app, task, cmd, conn):
        # Get a list of all players in this instance.
        loctx = yield task.get_loctx(conn.uid)
        if not loctx.iid:
            raise MessageException('You are not in an instance.')
        cursor = app.mongodb.playstate.find({'iid':loctx.iid},
                                            {'_id':1})
        while (yield cursor.fetch_next):
            player = cursor.next_object()
            app.queue_command({'cmd':'tovoid', 'uid':player['_id'], 'portin':True})
        # cursor autoclose
        app.queue_command({'cmd':'sleepinstance', 'iid':loctx.iid})
        
    @command('meta_getprop', restrict='creator')
    def cmd_meta_getprop(app, task, cmd, conn):
        if len(cmd.args) != 1:
            raise MessageException('Usage: /getprop key')
        origkey = cmd.args[0]
        key = origkey
        playstate = yield motor.Op(app.mongodb.playstate.find_one,
                                   {'_id':conn.uid},
                                   {'iid':1, 'locid':1})
        iid = playstate['iid']
        if not iid:
            # In the void, there should be no actions.
            raise ErrorMessageException('You are between worlds.')
        ### All of this prop-access stuff will need to go through the 
        ### propcache, when the propcache has a lifespan.
        instance = yield motor.Op(app.mongodb.instances.find_one,
                                  {'_id':iid})
        wid = instance['wid']
        locid = playstate['locid']
        if '.' in key:
            lockey, dummy, key = key.partition('.')
            if not lockey:
                locid = None
            elif lockey == '@':
                locid = '@'
            else:
                location = yield motor.Op(app.mongodb.locations.find_one,
                                          {'wid':wid, 'key':lockey},
                                          {'_id':1})
                if not location:
                    raise ErrorMessageException('No such location: %s' % (lockey,))
                locid = location['_id']
        if locid == '@':
            res = yield motor.Op(app.mongodb.iplayerprop.find_one,
                             {'iid':iid, 'uid':conn.uid, 'key':key})
            if res:
                raise MessageException('Player instance property: %s = %s' % (key, repr(res['val'])))
            res = yield motor.Op(app.mongodb.wplayerprop.find_one,
                                 {'wid':wid, 'uid':conn.uid, 'key':key})
            if res:
                raise MessageException('Player world property: %s = %s' % (key, repr(res['val'])))
            raise MessageException('Player instance/world property not set: %s' % (key,))
        res = yield motor.Op(app.mongodb.instanceprop.find_one,
                             {'iid':iid, 'locid':locid, 'key':key})
        if res:
            raise MessageException('Instance property: %s = %s' % (origkey, repr(res['val'])))
        res = yield motor.Op(app.mongodb.worldprop.find_one,
                                 {'wid':wid, 'locid':locid, 'key':key})
        if res:
            raise MessageException('World property: %s = %s' % (origkey, repr(res['val'])))
        raise MessageException('Instance/world property not set: %s' % (origkey,))

    @command('meta_delprop', restrict='creator', doeswrite=True)
    def cmd_meta_delprop(app, task, cmd, conn):
        if len(cmd.args) != 1:
            raise MessageException('Usage: /delprop key')
        origkey = cmd.args[0]
        key = origkey
        playstate = yield motor.Op(app.mongodb.playstate.find_one,
                                   {'_id':conn.uid},
                                   {'iid':1, 'locid':1})
        iid = playstate['iid']
        if not iid:
            # In the void, there should be no actions.
            raise ErrorMessageException('You are between worlds.')
        instance = yield motor.Op(app.mongodb.instances.find_one,
                                  {'_id':iid})
        wid = instance['wid']
        locid = playstate['locid']
        if '.' in key:
            lockey, dummy, key = key.partition('.')
            if not lockey:
                locid = None
            elif lockey == '@':
                locid = '@'
            else:
                location = yield motor.Op(app.mongodb.locations.find_one,
                                          {'wid':wid, 'key':lockey},
                                          {'_id':1})
                if not location:
                    raise ErrorMessageException('No such location: %s' % (lockey,))
                locid = location['_id']
        if locid == '@':
            res = yield motor.Op(app.mongodb.iplayerprop.find_one,
                             {'iid':iid, 'uid':conn.uid, 'key':key})
            if not res:
                raise MessageException('Player instance property not set: %s' % (key,))
            yield motor.Op(app.mongodb.iplayerprop.remove,
                       {'iid':iid, 'uid':conn.uid, 'key':key})
            task.set_data_change( ('iplayerprop', iid, conn.uid, key) )
            raise MessageException('Player instance property deleted: %s' % (key,))
        res = yield motor.Op(app.mongodb.instanceprop.find_one,
                             {'iid':iid, 'locid':locid, 'key':key})
        if not res:
            raise MessageException('Instance property not set: %s' % (origkey,))
        yield motor.Op(app.mongodb.instanceprop.remove,
                       {'iid':iid, 'locid':locid, 'key':key})
        task.set_data_change( ('instanceprop', iid, locid, key) )
        raise MessageException('Instance property deleted: %s' % (origkey,))
                
    @command('meta_setprop', restrict='creator', doeswrite=True)
    def cmd_meta_setprop(app, task, cmd, conn):
        if len(cmd.args) == 0:
            raise MessageException('Usage: /setprop key val')
        origkey = cmd.args[0]
        key = origkey
        newval = ' '.join(cmd.args[1:])
        try:
            newval = ast.literal_eval(newval)
            # Make sure the new value is a db-writable object.
            two.propcache.checkwritable(newval)
        except Exception as ex:
            raise ErrorMessageException('Invalid property value: %s (%s)' % (newval, ex))
        playstate = yield motor.Op(app.mongodb.playstate.find_one,
                                   {'_id':conn.uid},
                                   {'iid':1, 'locid':1})
        iid = playstate['iid']
        if not iid:
            # In the void, there should be no actions.
            raise ErrorMessageException('You are between worlds.')
        instance = yield motor.Op(app.mongodb.instances.find_one,
                                  {'_id':iid})
        wid = instance['wid']
        locid = playstate['locid']
        if '.' in key:
            lockey, dummy, key = key.partition('.')
            if not lockey:
                locid = None
            elif lockey == '@':
                locid = '@'
            else:
                location = yield motor.Op(app.mongodb.locations.find_one,
                                          {'wid':wid, 'key':lockey},
                                          {'_id':1})
                if not location:
                    raise ErrorMessageException('No such location: %s' % (lockey,))
                locid = location['_id']
        if not key.isidentifier():
            ### Permits Unicode identifiers, but whatever
            raise ErrorMessageException('Symbol assignment to invalid key: %s' % (key,))
        if locid == '@':
            yield motor.Op(app.mongodb.iplayerprop.update,
                       {'iid':iid, 'uid':conn.uid, 'key':key},
                       {'iid':iid, 'uid':conn.uid, 'key':key, 'val':newval},
                       upsert=True)
            task.set_data_change( ('iplayerprop', iid, conn.uid, key) )
            raise MessageException('Player instance property set: %s = %s' % (key, repr(newval)))            
        yield motor.Op(app.mongodb.instanceprop.update,
                       {'iid':iid, 'locid':locid, 'key':key},
                       {'iid':iid, 'locid':locid, 'key':key, 'val':newval},
                       upsert=True)
        task.set_data_change( ('instanceprop', iid, locid, key) )
        raise MessageException('Instance property set: %s = %s' % (origkey, repr(newval)))
                
    @command('meta_move', restrict='creator', doeswrite=True)
    def cmd_meta_move(app, task, cmd, conn):
        if len(cmd.args) != 1:
            raise MessageException('Usage: /move location-key')
        lockey = cmd.args[0]

        loctx = yield task.get_loctx(conn.uid)
        location = yield motor.Op(app.mongodb.locations.find_one,
                                  {'wid':loctx.wid, 'key':lockey},
                                  {'_id':1})
        if not location:
            raise ErrorMessageException('No such location: %s' % (lockey,))
        ctx = two.evalctx.EvalPropContext(task, loctx=loctx, level=LEVEL_EXECUTE)

        yield ctx.perform_move(location['_id'], 'Your location changes.', False, None, False, None, False)
        
    @command('meta_eval', restrict='creator', doeswrite=True)
    def cmd_meta_eval(app, task, cmd, conn):
        if len(cmd.args) == 0:
            raise MessageException('Usage: /eval code')
        code = ' '.join(cmd.args)
        loctx = yield task.get_loctx(conn.uid)
        ctx = two.evalctx.EvalPropContext(task, loctx=loctx, level=LEVEL_EXECUTE)
        try:
            newval = yield ctx.eval(code, evaltype=EVALTYPE_CODE, locals=conn.debuglocals)
            if ctx.accum:
                printed = ''.join([str(val) for val in ctx.accum])
                conn.write({'cmd':'event', 'text':'Eval printed: %s' % (printed,)})
            if newval is None:
                conn.write({'cmd':'event', 'text':'Eval done.'})
            else:
                conn.write({'cmd':'event', 'text':'Eval result: %s' % (repr(newval),)})
        except Exception as ex:
            task.log.warning('Eval failed: %s', ex, exc_info=app.debugstacktraces)
            exmsg = '%s: %s' % (ex.__class__.__name__, ex,)
            conn.write({'cmd':'event', 'text':'Eval raised exception: %s' % (exmsg,)})
        
    @command('meta_holler', restrict='admin')
    def cmd_meta_holler(app, task, cmd, conn):
        val = 'Admin broadcast: ' + (' '.join(cmd.args))
        for stream in app.webconns.all():
            stream.write(wcproto.message(0, {'cmd':'messageall', 'text':val}))

    @command('meta_shutdown', restrict='admin')
    def cmd_meta_shutdown(app, task, cmd, conn):
        app.queue_command({'cmd':'shutdownprocess'})
        
    @command('meta_debugstacktraces', restrict='admin')
    def cmd_meta_debugstacktraces(app, task, cmd, conn):
        app.debugstacktraces = not app.debugstacktraces
        raise MessageException('debugstacktraces now %s' % (app.debugstacktraces,))
        
    @command('portstart', doeswrite=True)
    def cmd_portstart(app, task, cmd, conn):
        # Fling the player back to the start world. (Not necessarily the
        # same as a panic or initial login!)
        player = yield motor.Op(app.mongodb.players.find_one,
                                {'_id':conn.uid},
                                {'scid':1})
        res = yield motor.Op(app.mongodb.config.find_one,
                             {'key':'startworldloc'})
        lockey = res['val']
        res = yield motor.Op(app.mongodb.config.find_one,
                             {'key':'startworldid'})
        newwid = res['val']
        newscid = player['scid']
        res = yield motor.Op(app.mongodb.locations.find_one,
                             {'wid':newwid, 'key':lockey})
        newlocid = res['_id']
        
        app.queue_command({'cmd':'tovoid', 'uid':conn.uid, 'portin':True,
                           'portto':{'wid':newwid, 'scid':newscid, 'locid':newlocid}})
        
    @command('plistselect', doeswrite=True)
    def cmd_plistselect(app, task, cmd, conn):
        # Called when the player selects a booklet link.
        player = yield motor.Op(app.mongodb.players.find_one,
                                {'_id':conn.uid},
                                {'plistid':1})
        plistid = player['plistid']
        portal = yield motor.Op(app.mongodb.portals.find_one,
                                {'_id':ObjectId(cmd.portid), 'plistid':plistid, 'iid':None})
        if not portal:
            raise ErrorMessageException('No such portal in your collection.')
        msg = app.localize('message.desc_own_portlist')
        focusobj = ['portlist', plistid, False, msg, False, portal['_id']]
        yield motor.Op(app.mongodb.playstate.update,
                       {'_id':conn.uid},
                       {'$set':{'focus':focusobj}})
        task.set_dirty(conn.uid, DIRTY_FOCUS)

        
    @command('setpreferredportal')
    def cmd_setpreferredportal(app, task, cmd, conn):
        # This updates the database, but not in a way that notifies anybody.
        player = yield motor.Op(app.mongodb.players.find_one,
                                {'_id':conn.uid},
                                {'plistid':1})
        portal = yield motor.Op(app.mongodb.portals.find_one,
                                {'_id':ObjectId(cmd.portid), 'plistid':player['plistid'], 'iid':None})
        if not portal:
            raise ErrorMessageException('No such portal in your collection.')
        # Remove all preferred flags for this player
        yield motor.Op(app.mongodb.portals.update,
                       {'plistid':player['plistid'], 'iid':None, 'preferred':True},
                       {'$unset': {'preferred':1}},
                       multi=True)
        # And set the new one
        yield motor.Op(app.mongodb.portals.update,
                       {'_id':portal['_id']},
                       {'$set': {'preferred':True}})
        desc = yield two.execute.portal_description(app, portal, conn.uid, location=True)
        raise MessageException(app.localize('message.panic_portal_set') % (desc['world'], desc['location'])) # 'Panic portal set to %s, %s.'
        
    @command('deleteownportal', doeswrite=True)
    def cmd_deleteownportal(app, task, cmd, conn):
        player = yield motor.Op(app.mongodb.players.find_one,
                                {'_id':conn.uid},
                                {'plistid':1})
        portal = yield motor.Op(app.mongodb.portals.find_one,
                                {'_id':ObjectId(cmd.portid), 'plistid':player['plistid'], 'iid':None})
        if not portal:
            raise ErrorMessageException('No such portal in your collection.')
        yield motor.Op(app.mongodb.portals.remove,
                       {'_id':portal['_id']})
        map = { str(portal['_id']):False }
        conn.write({'cmd':'updateplist', 'map':map})
        conn.write({'cmd':'message', 'text':app.localize('message.delete_own_portal_ok')}) # 'You remove the portal from your collection.'
        yield motor.Op(app.mongodb.playstate.update,
                       {'_id':conn.uid},
                       {'$set':{'focus':None}})
        task.set_dirty(conn.uid, DIRTY_FOCUS)

    @command('selfdesc', doeswrite=True)
    def cmd_selfdesc(app, task, cmd, conn):
        if getattr(cmd, 'pronoun', None):
            if cmd.pronoun not in ("he", "she", "it", "they", "name"):
                raise ErrorMessageException('Invalid pronoun: %s' % (cmd.pronoun,))
            yield motor.Op(app.mongodb.players.update,
                           {'_id':conn.uid},
                           {'$set': {'pronoun':cmd.pronoun}})
            task.set_data_change( ('players', conn.uid, 'pronoun') )
        if getattr(cmd, 'desc', None):
            val = str(cmd.desc)
            if len(val) > twcommon.misc.MAX_DESCLINE_LENGTH:
                val = val[0:twcommon.misc.MAX_DESCLINE_LENGTH]
            yield motor.Op(app.mongodb.players.update,
                           {'_id':conn.uid},
                           {'$set': {'desc':val}})
            task.set_data_change( ('players', conn.uid, 'desc') )
        
    @command('say')
    def cmd_say(app, task, cmd, conn):
        res = yield motor.Op(app.mongodb.players.find_one,
                             {'_id':conn.uid},
                             {'name':1})
        playername = res['name']
        if cmd.text.endswith('?'):
            (say, says) = ('ask', 'asks')
        elif cmd.text.endswith('!'):
            (say, says) = ('exclaim', 'exclaims')
        else:
            (say, says) = ('say', 'says')
        val = 'You %s, \u201C%s\u201D' % (say, cmd.text,)
        task.write_event(conn.uid, val)
        others = yield task.find_locale_players(notself=True)
        if others:
            oval = '%s %s, \u201C%s\u201D' % (playername, says, cmd.text,)
            task.write_event(others, oval)

    @command('pose')
    def cmd_pose(app, task, cmd, conn):
        res = yield motor.Op(app.mongodb.players.find_one,
                             {'_id':conn.uid},
                             {'name':1})
        playername = res['name']
        val = '%s %s' % (playername, cmd.text,)
        everyone = yield task.find_locale_players()
        task.write_event(everyone, val)

    @command('action', doeswrite=True)
    def cmd_action(app, task, cmd, conn):
        # First check that the action is one currently visible to the player.
        action = conn.localeactions.get(cmd.action)
        if action is None:
            action = conn.focusactions.get(cmd.action)
        if action is None:
            action = conn.populaceactions.get(cmd.action)
        if action is None:
            action = conn.toolactions.get(cmd.action)
        if action is None:
            # Drop this silently; it's probably the result of net lag.
            task.log.warning('Action not available: %s', cmd.action)
            return
        res = yield two.execute.perform_action(task, cmd, conn, action)
        
    @command('dropfocus', doeswrite=True)
    def cmd_dropfocus(app, task, cmd, conn):
        playstate = yield motor.Op(app.mongodb.playstate.find_one,
                                   {'_id':conn.uid},
                                   {'focus':1})
        yield motor.Op(app.mongodb.playstate.update,
                       {'_id':conn.uid},
                       {'$set':{'focus':None}})
        task.set_dirty(conn.uid, DIRTY_FOCUS)
        
    return Command.all_commands

# Late imports, to avoid circularity
import two.execute
import two.evalctx
import two.task
import two.symbols
from two.evalctx import LEVEL_EXECUTE
from two.evalctx import EVALTYPE_RAW, EVALTYPE_CODE
from two.task import DIRTY_ALL, DIRTY_WORLD, DIRTY_LOCALE, DIRTY_POPULACE, DIRTY_FOCUS, DIRTY_TOOL
from twcommon.access import ACC_VISITOR
