# -*- test-case-name: mv3d.test.ui.test_playerclient -*- # Copyright (C) 2006-2012 Mortal Coil Games # See LICENSE for details. """ Class for a Player client interface Handles controlling the remote PC, taking player input and converting it to calls to the PC. Handles connecting to a PC as well. """ import logging from time import time from twisted.internet import defer from twisted.internet.defer import inlineCallbacks, returnValue from twisted.internet.task import coiterate, LoopingCall from twisted.application.service import Service from zope.interface import implements #@UnresolvedImport from mv3d.util.iservice import ISimulationClient, IPlayerClient from mv3d.net.client import ServiceLoc from mv3d.client.ui.irenderer import IUserInterfaceLibrary, Ogre3D from mv3d.util.guide import ChangeNotifier, NotifierProperty from twisted.python.log import deferr try: from mv3d.client.ui.ogre3d import OgreRenderer from mv3d.client.ui.cegui import MessageWindow haveOgre = True except ImportError: raise haveOgre = False try: from mv3d.client.ui.panda import PandaRenderer havePanda = True except ImportError: havePanda = False from mv3d.util.classgen import getClass class RendererIterator(object): """ Just a simple class to iterate the renderer """ def __init__(self, parent): self.parent = parent self.count = 0 self.lastiter = time() def __iter__(self): """ Just return ourselves because we are an iterator """ return self def next(self): """ Implementing the iterator interface """ try: self.parent.renderer.iterate(0.02) except: deferr() class PlayerError(Exception): """ Raised when there's an issue with the player service """ class Loading(ChangeNotifier): """ Loading screen """ progress = NotifierProperty("progress", default=0) def addProgress(self, amount): self.progress += amount class PlayerClient(Service): """ The PlayerClient is what talks to servers. It handles connecting to PCs and such. """ implements(IPlayerClient) statsready = False pc = None pcid = None pcname = None autoConnect = False camera = None renderer = None ui = None physicsUpdateHertz = 3 physicsUpdateCall = None def __init__(self): self.actions = {} self.bindings = {} self.loginservers = [] self.aggregators = [] if haveOgre: self.renderer = OgreRenderer() elif havePanda: self.renderer = PandaRenderer() def configure(self, nm, cf): """ Configure the interface with this config file """ if cf.has_option(nm, "defaultLogin"): deflogin = cf.get(nm, "defaultLogin").split(":") self.login = unicode(deflogin[0]) self.password = unicode(deflogin[1]) if cf.has_option(nm, "defaultPC"): self.pcname = unicode(cf.get(nm, "defaultPC")) loginservers = [s.strip() for s in cf.get(nm, "servers").split(",")] for s in loginservers: self.loginservers.append(ServiceLoc(s)) if cf.has_option(nm, "autoConnect"): self.autoConnect = cf.getboolean(nm, "autoConnect") self.acServer = self.loginservers[int(cf.get(nm, "ac-server"))] if cf.has_option(nm, "renderer"): renderClass = getClass(cf.get(nm, "renderer")) if renderClass != self.renderer.__class__: self.renderer = renderClass() self.ui = self.renderer.getRendererClass(IUserInterfaceLibrary)(self) self.renderer.configure(cf) self.ui.configure(cf) def startService(self): """ Called when this service is started. Initialize the UI and start the renderer iterator. """ self.renderer.initialize(logPath=self.parent.dataRoot) self.parent.callWhenInitialized(self.onPostInitialize) def onPostInitialize(self, _conductor): """ Called when all services are ready. """ self.ui.initialize(self.renderer, self.parent.dataRoot) if self.renderer.ownWindow: coiterate(RendererIterator(self)) self.renderer.addFrameCallback(self.onFrameRendered) def stopService(self): """ Called when the service is terminated. Stop all running calls """ self.ui.finalize() self.renderer.finalize() if self.physicsUpdateCall is not None: self.physicsUpdateCall.stop() self.renderer.remFrameCallback(self.onFrameRendered) def getRenderer(self): """ Simply return our renderer """ return self.renderer def getUI(self): """ Simply return our UI """ return self.ui @inlineCallbacks def getPCs(self): """ This function will return a deferred, that will eventually give you a list of oids that your account can use as Players. """ pserv = yield self.parent.getOneService(self.loginservers) pcids = yield pserv.getPCs() returnValue(pcids) @inlineCallbacks def connectPC(self, oid): """ This function will return a deferred that will return 0 if the client doesn't have access to use oid as a PC. Otherwise, it'll return a PlayerManipulator object for that PC which we will store as self.pc. This should probably not be called when reconnecting or anything like that since it assumes a first time connection. """ if self.pc is not None: raise PlayerError("You can't call connectPC after you've connected") progbar = Loading() window = self.ui.load("templates/guide/client/loading.xml", dataContext=progbar) window.show() try: service = yield self.parent.getOneService(self.loginservers) simsvc = self.parent.getLocalService(ISimulationClient) self.pc, agg = yield service.connectPC(oid, simsvc.getNotifier( service)) self.aggregators.append(agg) self.pcid = oid yield simsvc.addView(service, progbar.addProgress) renderer = self.getRenderer() camera = renderer.camera if camera is not None: camera.target = simsvc.items[self.pcid].body self.physicsUpdateCall = LoopingCall(self.updatePCPhysics) self.physicsUpdateCall.start(1.0 / self.physicsUpdateHertz) finally: window.destroy() returnValue(self.pc) def updatePCPhysics(self): """ Update our PC's physics """ simsvc = self.parent.getLocalService(ISimulationClient) body = simsvc.items[self.pcid].body self.do("updateDynamics", body.getPosition(), body.getRotation(), body.getLinearVelocity(), body.getAngularVelocity()) def do(self, what, *a, **kw): """ As long as we are logged in, request that our avatar performs the specified action. """ if self.pc is None: self.parent.log("%s failed to perform action %s -- you haven't" "logged in yet!" % (self.name, what), logging.ERROR) raise PlayerError("No PC yet!") if self.pc.broker.disconnected: self.parent.log("%s failed to perform action %s -- you are" "not connected." % (self.name, what), logging.ERROR) raise PlayerError("Disconnected") return self.pc.callRemote(what, *a, **kw) def createCharacter(self, realmId): """ Tell the server that we would like to create a new player character """ d = self.parent.getOneService(self.loginservers) d.addCallback(lambda i: i.createCharacter(realmId)) def gotIt(c): d = c.callRemote("getUI") d.addCallback(lambda ui: defer.maybeDeferred( ui.construct, c)) def done(ui): ui.initialize(self) return ui.start() return d.addCallback(done) return d.addCallback(gotIt) def addAction(self, b): """ This adds an action binding or something. """ self.actions[b.getName()] = b if self.bindings.has_key(b.getName()): b.SetBindings(self.bindings[b.getName()]) b.activate() def remAction(self, nm): """ Remove an action that was added with addAction """ self.actions[nm].deactivate() del self.actions[nm] def getActions(self): """ Returns the dict of currently set actions """ return self.actions def getAction(self, nm): """ Returns an action specified by nm """ return self.actions[nm] def addBinding(self, n, b): """ Adds a UI binding """ if self.bindings.has_key(n): self.bindings[n].append(b) else: self.bindings[n] = [b] def getBindingsFor(self, n): """ Returns the bindings for a given action """ return self.bindings.get(n, []) def remBindingsFor(self, n): """ Removes all the bindings for a given action """ del self.bindings[n] def remBinding(self, n, b): """ Remove a specific binding """ self.bindings[n].remove(b) def updateLoginServers(self): """ This will get a server connection and ask it for suggestions of other login servers that we can use.. Update the list kept by the client with this new info. """ d = self.parent.getOneService(self.loginservers) d.addCallback(lambda i: i.getPeerServers()) def gotServers(lst): for s in lst: if not s in self.loginservers: self.loginservers.append(s) return d.addCallback(gotServers) @inlineCallbacks def connectedPC(self, _res=None): """ This is called when we have successfully connected to a PC """ #now get the UI uii = yield self.do("getUI") ui = yield uii.construct() ui.initialize(self.ui) ui.start() def updateStat(self, nr, stat): """ Method to update general statistics for the stats ui window """ if not self.statsready: return stat = str(stat) g = self.ui.getComponent("Status%d" % nr) g.text = stat def disconnected(self): """ Called when our connection to the server has been cut off """ self.parent.log("Connection to server lost.", logging.WARNING) # attempt a reconnect to one of our servers d = self.connectPC(self.pcid) def connected(_r): """ Yipee! We connected! """ self.parent.log("Successfully reconnected to server.", logging.INFO) def notConnected(_e): """ Waah! We can't reconnect! """ m = MessageWindow() m.initialize(self.ui, "You have been disconnected.") d = m.start() def ClickedOk(_r): self.ConnectAndSelectPC() return d.addCallback(ClickedOk) d.addCallback(connected) return d def onFrameRendered(self, deltaTime): """ Called by the renderer when a frame is rendered. """ self.updateStat(3, "Triangles: %d" % self.renderer.getTriangleCount()) self.updateStat(0, "IPS: %.2f FPS %.1f" % (self.parent.ips, self.renderer.getFPS())) self.parent.getLocalService(ISimulationClient).iteratePC(deltaTime) @inlineCallbacks def getAssetServices(self): """ Returns a list of asset services from a conneted server """ s = yield self.parent.getOneService(self.loginservers) svcs = yield s.getAssetServices() returnValue(svcs) def registerStats(self, stats): """ Register available stats with the visualizer """ stats.addStat("fps", 0.1, self.renderer.getFPS) stats.addStat("triangleCount", 0.1, self.renderer.getTriangleCount) @inlineCallbacks def createAccount(self, username, password, email): """ Create a new account with the login service. """ # dig up the login services loginLocs = self.parent.connectionFactories["pb"].loginServices login = yield self.parent.getOneService(loginLocs) yield login.createAccount(username, password, email) def isLocal(self): """ We are local so return true """ return True