# -*- test-case-name: mv3d.test.test_playerservice -*- # Copyright (C) 2006-2012 Mortal Coil Games # See LICENSE for details. """ Class for player server interface This interface should be in charge of handling everything that a player could ever want. Clients will only have access to this server interface. """ from twisted.internet import defer, reactor from twisted.internet.defer import inlineCallbacks, returnValue from twisted.internet.task import LoopingCall from twisted.spread import pb from twisted.application.service import Service from twisted.python.log import deferr from zope.interface import implements #@UnresolvedImport from mv3d.net.client import ServiceLoc from mv3d.net.pb import Manipulator, CacheableError from mv3d.net.security import requirePermissions, Securable from mv3d.util.classgen import ClassGenerator from mv3d.util.math3d import Vector, Quaternion from mv3d.util.iservice import ( IService, IAssetClient, ISimulationClient, ) from mv3d.util.modifier import IModifiable, Modifiable from mv3d.util.conductor import parseInterfaceConfig, parsePermissionConfig from mv3d.phys.ibody import IGateway from mv3d.server.model.view import IViewable from mv3d.server.model.area import IArea from mv3d.server.service import ( checkServicePermissions, viewed, viewedWithClient, ) from mv3d.phys.body import StringFlinger from mv3d.util.guide import ChangeNotifier, NotifierProperty import mv3d import os from mv3d.util.profiler import timed class PlayerError(Exception): """ Raised when there's a problem with a player's request """ class PlayerManipulator(Manipulator): """ This class is basically the default interface that clients use to control their characters. It attaches to the character by the parent variable, and the clients get a remote reference to it. """ def view_Walk(self, _, direction): """ Causes the player to walk forward or back """ if direction == -1: d = 1 else: d = -1 self.parent.walk((0, 0, d)) def view_Strafe(self, _, direction): """ Move left or right """ if direction == -1: d = 1 else: d = -1 self.parent.walk((d, 0, 0)) def view_Turn(self, _, direction): """ Turn the player around """ if direction == 0: #print "Stopping turning" self.parent.stopTurning() return d = 0 if direction == -1: d = 1 else: d = -1 self.parent.turn((0, d, 0)) def view_StopTurning(self, _): """ Stop the turning """ self.parent.turn((0, 0, 0)) def view_Jump(self, _): """ Causes the player to jump in the air """ def jump(): self.parent.jump() self.parent.getBody().getVisualObjects()[0].stopAnimation("jump") self.parent.getBody().getVisualObjects()[0].startAnimation("idle", True) reactor.callLater(0.3, jump) #@UndefinedVariable self.parent.getBody().getVisualObjects()[0].stopAnimation("idle") self.parent.getBody().getVisualObjects()[0].startAnimation("jump") def view_Wave(self, _): """ Useful for waving at other players. """ self.parent.getBody().getVisualObjects()[0].stopAnimation("idle") self.parent.getBody().getVisualObjects()[0].startAnimation("wave") def stopAnim(): self.parent.getBody().getVisualObjects()[0].stopAnimation("wave") self.parent.getBody().getVisualObjects()[0].startAnimation("idle", True) reactor.callLater(2, stopAnim) #@UndefinedVariable def view_Say(self, _, text): """ Called to broadcast a chat message """ #print "Got says!" self.parent.parent.parent.broadcastEvent( category="chat", subject=self.parent, verb="says", object="'%s'" % text) def view_Kick(self, _): """ Kick! """ p1 = Vector(self.parent.getPosition()) + Vector(0, -5, 0) p2 = p1 + (Quaternion(self.parent.getRotation()).rotate( Vector(0, 0, -3))) p3 = p1 + (Quaternion(self.parent.getRotation()).rotate( Vector(0, 0, -18))) d = self.parent.getItem(self.parent.listContainedBy()[0]) # print "Kicking!", tuple(p1), tuple(p2), tuple(p3) def gotArea(area): hit = None hitdepth = 1000 hitloc = None for sp in [area.getDisabledSpace(), area.getEnabledSpace()]: r = sp.castRay(p2, p3) if r.mindepth < hitdepth: hitdepth = r.mindepth hit = r.mingeom hitloc = r.minpos if hit is not None: body = hit.getBody() if body is None and hasattr(hit, "body"): body = hit.body # print "Hit something", hit, body if body is not None: # print "Kicking it" body.enable() body.addForceAtPos((Quaternion(self.parent.getRotation() ).rotate(Vector(0, 200000, -500000))), hitloc) d.addCallback(gotArea) self.parent.getBody().getVisualObjects()[0].stopAnimation("idle") self.parent.getBody().getVisualObjects()[0].startAnimation("kick") def stopAnim(): self.parent.getBody().getVisualObjects()[0].stopAnimation("kick") self.parent.getBody().getVisualObjects()[0].startAnimation("idle", True) reactor.callLater(0.7, stopAnim) #@UndefinedVariable return d def view_getUI(self, _): """ Returns the class responsible for the client side of the player's ui """ return ClassGenerator(classname="PlayerMovement", modulename="mv3d.client.ui.player") @inlineCallbacks def view_startEditor(self, client): """ Start the in game editor by returning a list of top level modifiers for each service that supports it and allows modification by the client. """ svcs = dict() for svc in self.parent.parent.parent.services: if IModifiable.providedBy(svc): #@UndefinedVariable mods = yield svc.getModifiers("ige") svcs[svc.name] = {} for mod in mods: for perm in mod.getMinimumPermissions(): if svc.checkPermissions(client, perm): svcs[svc.name][mod.name] = mod break returnValue(svcs) def view_updateDynamics(self, _client, pos, rot, lvel, avel): """ Tell the server what your position rotation lvel and avel are. """ # # Uncomment to enable server side validation. # if Vector(Vector(pos) - self.parent.body.getPosition()).length() > 10: # print "Too far!", pos, Vector(Vector(pos) - self.parent.body.getPosition()).length() # return # if not "admin" in client.getGroups(): # return body = self.parent.body if body.priority != 1: if (hasattr(body.getManipulator(), "broker") and body.getManipulator().broker.disconnected): raise CacheableError("Disconnected") return body.getManipulator().callRemote( "manipulate", "updateDynamics", pos, rot, lvel, avel) body.updateDynamics(pos, rot, lvel, avel) # if hasattr(body, "updateOtherClients"): # body.updateOtherClients("updateDynamics", pos, rot, lvel, avel) # else: # body.updateSlaves("updateDynamics", pos, rot, lvel, avel) class PlayerView(object): """ PlayerView contains info on what a specific player can see. It provides a way to limit the player's vision into the world as well as facilitating notifications of items that enter and leave the player's view. """ range = 4000 timeBetweenUpdates = 1 updateCall = None def __init__(self, avatar, clientPerspective, notifier=None): self.avatar = avatar self.parent = self.avatar.parent self.clientPerspective = clientPerspective self.notifier = notifier self.inView = {} self.areaOffsets = {} self.checked = [] self.aggregator = StringFlinger(ignoreList=[self.avatar.getID()]) self.aggregator.grantPermission("read", "all") self.aggregator.grantPermission("reference", "all") def startUpdating(self): """ Begins the process of continuous updates of this view """ self.updateCall = LoopingCall(self.update) self.updateCall.start(self.timeBetweenUpdates) def stopUpdating(self): """ Stop the update loop """ self.updateCall.stop() def giveUpdate(self, body, pos, rot, lvel, avel): """ An update has come in for the aggregator, but we need to make sure it is an object in the view! """ if self.inView.has_key(body.objectid): self.aggregator.giveUpdate(body, pos, rot, lvel, avel) def removeBody(self, body): """ Send this to our aggregator """ self.aggregator.removeBody(body) @timed @inlineCallbacks def update(self): """ Perform an update of the objects in the view, but also check to make sure that the notifier is still connected. """ self.checked = [] self.areaOffsets = {} try: if self.notifier is not None and self.notifier.broker.disconnected: self.stopUpdating() psvc = self.parent.parent.getLocalService(IPlayerService) psvc.removeView(self) else: for containerid in self.avatar.listContainedBy(): container = self.parent.pools[containerid] if IArea.providedBy(container): #@UndefinedVariable if not self.areaOffsets.has_key(containerid): self.areaOffsets[containerid] = 0 yield self.searchArea(container) for item in self.inView.keys(): if not item in self.checked: self.removeObject(item) except Exception, exc: deferr(exc, "Received while updating a view.") @timed def removeObject(self, oid): """ If oid is in the list of items that are within the player's view, then it will be removed. Otherwise, do nothing. Returns True if the item was actually removed. """ if self.inView.has_key(oid): del self.inView[oid] if self.notifier is not None: d = self.notifier.callRemote("leftView", oid) # this solution here is a hack. for some reason, in tests, # the try/except wrapper around update catches errors here, but # in the real world, it doesn't. so instead, we'll ignore them # at this point. (while logging of course) def gotError(e): deferr(e, "Received while removing an object from a client") return True d.addCallback(lambda _: True) d.addErrback(gotError) return d return defer.succeed(True) return defer.succeed(False) @timed def addObject(self, oid, distance): """ If oid is not in the list of items that are within the player's view, then it will be added. Otherwise, do nothing. Returns True if the item was actaully added. """ if not self.inView.has_key(oid): # print "add object", oid self.inView[oid] = distance if self.notifier is not None: d = self.notifier.callRemote("enteredView", oid) # this solution here is a hack. for some reason, in tests, # the try/except wrapper around update catches errros here, but # in the real world, it doesn't. so instead, we'll ignore them # at this point. (while logging of course) def gotError(e): deferr(e, "Received while adding an object to a client") return True d.addErrback(gotError) return d.addCallback(lambda _: True) return defer.succeed(True) self.inView[oid] = distance return defer.succeed(False) @timed @inlineCallbacks def checkObject(self, oid, observer=None): """ Check if the object specified by oid should in fact be in the view. If so, then add it to the list if it isn't already there. If not, remove it from the list if it is in it. Any changes made will cause a notification to be made. """ self.checked.append(oid) if observer is None: observer = self.avatar item = self.parent.items[oid] distance = item.getDistanceFromObserver(observer) for containerid in item.listContainedBy(): if containerid in self.areaOffsets.keys(): distance += self.areaOffsets[containerid] break else: yield self.removeObject(oid) returnValue(False) if distance > self.range: yield self.removeObject(oid) returnValue(False) yield self.addObject(oid, distance) returnValue(True) @inlineCallbacks def checkGateway(self, offset, gateway, observer): """ Determine if this gateway is within the view range. If so, then scan the area beyond. """ distance = offset + gateway.getDistanceFromObserver(observer) runIt = False if not gateway.destinationID in self.areaOffsets.keys(): if distance < self.range: runIt = True elif self.areaOffsets[gateway.destinationID] > distance: runIt = True if runIt: destarea = yield gateway.getDestination() self.areaOffsets[gateway.destinationID] = ( offset + gateway.getDistanceFromObserver(observer)) yield self.searchArea(destarea, gateway) @timed @inlineCallbacks def searchArea(self, area, observer=None): """ Go through all the objects in the area and check if they are in view. Search for areas connected to the area which are close enough to be within range @TODO: This is fairly messed up for translating gateways or instances where there are multiple gateways to the same area. """ if observer is None: observer = self.avatar offset = self.areaOffsets[area.getID()] for itemid in area.listContents(): item = self.parent.items[itemid] if IViewable.providedBy(item): #@UndefinedVariable yield self.checkObject(itemid, observer) if (hasattr(item, "body") and IGateway.providedBy(item.body) and item.body.canSeeThrough): yield self.checkGateway(offset, item.body, observer) def getSortedOIDs(self): """ Returns oids for items in the view that are sorted by distance """ sortedList = sorted(zip(self.inView.values(), self.inView.keys())) return [item[1] for item in sortedList] class PlayerServiceView(pb.Viewable): """ A service that allows players to log in and interact """ def __init__(self, service): self.service = service def getProtocol(self): """ Return what protocol we implement """ return "pb" @checkServicePermissions("read") @viewedWithClient def view_getPCs(self, client): """ Return the list of PC oids to the player """ @checkServicePermissions("read") @viewedWithClient def view_getPCInfo(self, client, pc): """ Return info about a player's PC to the player """ @checkServicePermissions("read") @viewedWithClient def view_connectPC(self, client, pcid): """ Connect the player to this pc (o==object id) During this process, we will create a PlayerView and add it to our list of players. Then return a PlayerManipulator type object and a PlayerView """ @checkServicePermissions("read") @viewedWithClient def view_getPlayerView(self, client): """ Returns the player view associated with this connection. If there is none, then return 0 (meaning you probably haven't called ConnectPC yet) """ @checkServicePermissions("read") @viewed def view_createCharacter(self, client, realm=0): """ Create a new character """ @checkServicePermissions("read") @viewed def view_getPeerServers(self, client): """ Just hand out our list of peers. Nothing fancy. Clients will use it to build their list of MV3D servers. """ @checkServicePermissions("read") @viewed def view_getAssetServices(self, client): """ Retrieve a list of asset services that are available """ @checkServicePermissions("read") @viewedWithClient def view_countObjectsInView(self, client): """ Returns the number of objects in the client's view """ @checkServicePermissions("read") @viewedWithClient def view_getObjectIDsInView(self, client, offset=0, limit=None): """ Returns a number of object ids that are in the client's view. The list can be limited using the offset and limit. If limit is None then all objects starting with offset will be returned. The objects are sorted by distance, so object at offset 0 is the closest to the client (this generally would be the client's avatar) """ @checkServicePermissions("read") @viewedWithClient def view_viewObjects(self, client, oids): """ Returns the cacheable object views for the given oids. They are first checked to be within the objects that can be viewed by the client. """ @checkServicePermissions("read") @viewed def view_getStatistics(self, client): """ Retrieves the statistics from the realm server """ @checkServicePermissions("read") @viewed def view_getConfig(self, client): """ Returns a dict with the config of this player service. """ @checkServicePermissions("modify") @viewed def view_setPeers(self, client, peers): """ Sets the peers for this service """ @checkServicePermissions("modify") @viewed def view_setSimServices(self, client, services): """ Sets the list of sim services to use """ @checkServicePermissions("modify") @viewed def view_setPublicAssetServices(self, client, services): """ Sets the list of asset services to use """ class IPlayerService(IService): """ A full player service interface """ def getPlayers(): #@NoSelf """ Returns the players connected to this service """ def createCharacter(realmId): #@NoSelf """ returns the instance for name's character generator """ def getPCs(con): #@NoSelf """ Return the list of PC oids to the player """ def getPCInfo(con, pc): #@NoSelf """ Get info on a given pc """ def connectPC(con, pcid): #@NoSelf """ Connect the player to this pc During this process, we will create a PlayerView and add it to our list of players. Then return a PlayerManipulator type object and a PlayerView """ def getPlayerView(con): #@NoSelf """ Returns the player view associated with this connection. If there is none, then return 0 (meaning you probably haven't called ConnectPC yet) """ def getPeerServers(): #@NoSelf """ Just hand out our list of peers. Nothing fancy. Clients will use it to build their list of MV3D servers. """ def countObjectsInView(client): #@NoSelf """ Returns the number of objects within the client's view range """ def getObjectIDsInView(client, offset=0, limit=None): #@NoSelf """ Returns a number of object ids that are in the client's view. The list can be limited using the offset and limit. If limit is None then all objects starting with offset will be returned. The objects are sorted by distance, so object at offset 0 is the closest to the client (this generally would be the client's avatar) """ def viewObjects(client, oids): #@NoSelf """ Returns the cacheable object views for the given oids. They are first checked to be within the objects that can be viewed by the client. """ class PlayerService(Service, Securable, Modifiable): """ Handles the server side of players. """ implements(IPlayerService) classModifiers = dict( guide=[ClassGenerator("mv3d.server.player.PlayerConfig")]) def __init__(self): Securable.__init__(self) Modifiable.__init__(self) self.players = {} self.peers = [] # A list of peer servers to hand out to clients self.interfaces = {} self.simulators = [] self.publicAssetServices = [] self.config = {} def configure(self, nm, cf): """ Configure this interface """ self.config.update(dict(cf.items(nm))) if cf.has_option(nm, "grantPermissions"): parsePermissionConfig(cf.get(nm, "grantPermissions"), self, True) if cf.has_option(nm, "denyPermissions"): parsePermissionConfig(cf.get(nm, "grantPermissions"), self, False) if cf.has_option(nm, "interfaces"): parseInterfaceConfig(cf.get(nm, "interfaces"), self) if cf.has_option(nm, "simulators"): sims = cf.get(nm, "simulators").split(",") for sim in sims: self.simulators.append(ServiceLoc(sim.strip())) if cf.has_option(nm, "peers"): peers = cf.get(nm, "peers").split(",") for peer in peers: self.peers.append(ServiceLoc(peer.strip())) if cf.has_option(nm, "publicAssetServices"): pas = [s.strip() for s in cf.get(nm, "publicAssetServices").split(",")] for ps in pas: self.publicAssetServices.append(ServiceLoc(ps)) def stopService(self): """ Make sure to stop all the views """ Service.stopService(self) for view in self.players.values(): view.stopUpdating() def getPlayers(self): """ Returns the dict of players that are currently connected to this service. """ return self.players def getPCs(self, con): """ Return the list of PC oids to the player """ return con.accountInfo["pcs"] @inlineCallbacks def getPCInfo(self, con, pc): """ Get info on a given pc """ if not pc in con.accountInfo["pcs"]: raise PlayerError("This account does not have access to that PC") svc = yield self.parent.getOneService(self.simulators) dfrds = [svc.getItem(pc), svc.getRealmInfo(pc[0])] pcobj, realminfo = yield defer.gatherResults(dfrds) assetIDs = [] [assetIDs.extend(vob.getAssets()) for vob in pcobj.body.vobs] if (hasattr(pcobj.body, "representationID") and pcobj.body.representationID): assetIDs.append(pcobj.body.representationID) dfrds = [] for a in pcobj.listContainedBy(): dfrds.append(svc.getItem(a)) areas = yield defer.gatherResults(dfrds) an = " and ".join([a.getName() for a in areas]) returnValue((pcobj.getName(), an, realminfo, assetIDs)) @inlineCallbacks def connectPC(self, client, pcid, notifier): """ Connect the player to this pc. During this process, we will create a PlayerView and add it to our list of players. Then return a PlayerManipulator type object and a PlayerView """ if not pcid in client.accountInfo["pcs"]: raise PlayerError("This account does not have access to that PC") if self.players.has_key(client): raise PlayerError("This client is already connected to a PC") sim = yield self.parent.getOneService(self.simulators) item = yield sim.getItem(pcid) yield item.activate() client.pcid = pcid manip = item.getManipulator("Player Manipulator") view = PlayerView(item, client) self.parent.log("%s becomes %s" % (client.getName(), item.getName())) yield view.update() view.notifier = notifier self.players[client] = view yield client.readyClass([ClassGenerator( "mv3d.client.view.player.PlayerViewClient", sourceclass="mv3d.server.player.PlayerView"), ClassGenerator("mv3d.phys.body.StringCatcher", sourceclass="mv3d.phys.body.StringFlinger")]) view.startUpdating() client.bodydynamicsaggregator = view client.parent.parent.addIterator(view.aggregator.iterate) returnValue((manip, view.aggregator)) def getPlayerView(self, con): """ Returns the player view associated with this connection. If there is none, then return 0 (meaning you probably haven't called ConnectPC yet) """ return self.players[con] @inlineCallbacks def createCharacter(self, realmId): """ Create a new character """ # pylint: disable-msg=E1101 sim = self.parent.getLocalService(ISimulationClient) # pylint: enable-msg=E1101 realm = yield sim.getRealm(realmId) returnValue(realm.getCharacterGenerator()) def getPeerServers(self): """ Just hand out our list of peers. Nothing fancy. Clients will use it to build their list of MV3D servers. """ return self.peers def countObjectsInView(self, client): """ Returns the number of objects within the client's view range """ return len(self.players[client].inView) def getObjectIDsInView(self, client, offset=0, limit=None): """ Returns a number of object ids that are in the client's view. The list can be limited using the offset and limit. If limit is None then all objects starting with offset will be returned. The objects are sorted by distance, so object at offset 0 is the closest to the client (this generally would be the client's avatar) """ return self.players[client].getSortedOIDs()[offset:offset + limit] @inlineCallbacks def viewObjects(self, client, oids): """ Returns the cacheable object views for the given oids. They are first checked to be within the objects that can be viewed by the client. """ views = [] deps = [] for oid in oids: if not self.players[client].inView.has_key(oid): raise KeyError("Item %r is not in view." % (oid,)) item = yield self.parent.getLocalService( ISimulationClient).getItem(oid) view = item.getView() yield item.updateView() view.grantPermission("reference", client.getName()) view.grantPermission("read", client.getName()) if item == self.players[client].avatar: view.setOwner(client.accountInfo) view.body.setOwner(client.accountInfo) classgens = view.getClassGenerator() # HACK -- we should be more consistent if isinstance(classgens, list): deps.extend(classgens) else: deps.append(classgens) deps.extend(view.getClassDependencies()) for area in view.areas.values(): deps.append(area.getClassGenerator()) deps.extend(area.getClassDependencies()) deps.append(area.realm.getClassGenerator()) deps.extend(area.realm.getClassDependencies()) views.append(view) yield client.readyClass(deps) returnValue(views) def removeView(self, view): """ Just simply search our views and remove the specified one """ index = self.players.values().index(view) key = self.players.keys()[index] if view.avatar is not None: view.avatar.deactivate() view.avatar.getView().setOwner(None) view.avatar.body.setOwner(None) self.parent.broadcastEvent(category="server", subject=self.players[key].avatar, verb="leaves", object="the game") del self.players[key] @requirePermissions("reference") def getInterface(self, _client, protocol): """ Hand out public interfaces """ return self.interfaces[protocol] def getAssetServices(self): """ Returns a list of AssetServices for clients to use """ if len(self.publicAssetServices): return self.publicAssetServices # in this case, return our own asset service a = self.parent.getLocalService(IAssetClient) return [a.publicLocation] def getStatistics(self): """ Return a dict with some helpful stats """ return dict(playerCount=len(self.players), simCount=len(self.simulators)) def getPlayerCount(self): """ Return the number of players """ return len(self.players) def registerStats(self, stats): """ Register available stats with the visualizer """ stats.addStat("playerCount", 1, self.getPlayerCount) def getConfig(self): """ Returns a dict with the config of this player service. """ return self.config def setPeers(self, peers): """ Sets the peers for this service """ self.peers = [ServiceLoc(svc) for svc in peers] def setSimServices(self, services): """ Sets the list of sim services to use """ self.simulators = [ServiceLoc(svc) for svc in services] def setPublicAssetServices(self, services): """ Sets the list of asset services to use """ self.publicAssetServices = [ServiceLoc(svc) for svc in services] def isLocal(self): """ We are local so return true """ return True class PlayerConfig(ChangeNotifier): """ Guide interface for changing player properties """ peers = NotifierProperty("_peers", "") simServices = NotifierProperty("_simServices}", "self/Sim") publicAssetServices = NotifierProperty("_publicAssetServices", "self/Asset") window = None def __init__(self, player): self.player = player self.getConfig() self.doneDeferred = defer.Deferred() @inlineCallbacks def getConfig(self): """ Called on startup to load everything """ config = yield self.player.getConfig() self.peers = ", ".join([str(svc) for svc in config.get( "peers", [])]) self.simServices = ", ".join([str(svc) for svc in config.get( "simServices", [])]) self.publicAssetServices = ", ".join([str(svc) for svc in config.get( "publicAssetServices", [])]) try: from mv3d.tools.guidewx import WxParser parser = WxParser() fname = os.path.abspath(os.path.join(os.path.dirname(mv3d.__file__), "..", "templates", "guide", "playerconfig.xml")) self.window = parser.load(fname, self) except: deferr() self.window.show() def cancel(self, _obj, _property): """ Called when the user hits the cancel button """ if self.doneDeferred is not None: self.doneDeferred.callback(False) self.doneDeferred = None self.window.destroy() @inlineCallbacks def submit(self, _obj, _property): """ Called when the user hits the submit button. """ if self.doneDeferred is not None: peers = self.peers.split(",") simServices = self.simServices.split(",") publicAssetServices = self.publicAssetServices.split(",") yield self.player.setPeers(peers) yield self.player.setSimServices(simServices) yield self.player.setPublicAssetServices(publicAssetServices) self.doneDeferred.callback(True) self.doneDeferred = None self.window.destroy()