# -*- test-case-name: mv3d.test.test_simclient -*- # Copyright (C) 2006-2012 Mortal Coil Games # See LICENSE for details. """ Class for a simulation client interface Handles a player view along with client side physics, graphics, and sound. """ from twisted.application.service import Service from twisted.internet.defer import inlineCallbacks, DeferredList from twisted.internet.task import coiterate, LoopingCall from twisted.spread import pb from zope.interface import implements from mv3d.util.iservice import (ISimulationClient, IPlayerClient, IIterableService) from twisted.python.log import deferr class ItemIterator: """ Iterates through a list of items """ def __init__(self, parent, items, skip=None): self.skip = skip or [] self.parent = parent self.items = items.__iter__() def next(self): """ implimenting the iter interface """ itm = self.items.next() if itm in self.skip: return try: wasEnabled = self.parent.enabledState.get(itm.getID(), itm.isEnabled()) r = itm.iterate(0.05) self.parent.enabledState[itm.getID()] = itm.isEnabled() if itm.getID() == self.parent.pcid: # print time.time(), itm.isEnabled(), tuple(itm.body.getLinearVelocity()) for v in self.parent.views.values(): v.updateCamera() if wasEnabled != itm.isEnabled(): if wasEnabled: # print "sim disabling", itm try: self.parent.enabled.remove(itm) except ValueError: pass if not itm in self.parent.disabled: self.parent.disabled.append(itm) else: # print "sim enabling", itm try: self.parent.disabled.remove(itm) except ValueError: pass if not itm in self.parent.enabled: self.parent.enabled.append(itm) except: import traceback traceback.print_exc() class ViewChangeNotifier(pb.Referenceable): """ A remote reference that notifies the sim client that an item has gone in or out of view """ def __init__(self, parent, view): self.parent = parent self.view = view def remote_leftView(self, oid): """ This is called by the server when oid leaves the view """ if not self.parent.items.has_key(oid): return self.parent.removeItem(oid) @inlineCallbacks def remote_enteredView(self, oid): """ This is called by the server when oid enters the view """ if not self.parent.items.has_key(oid): item = yield self.view.viewObjects([oid]) self.parent.addItem(item[0]) class SimService(Service): """ Runs the client side of the simulation """ implements(ISimulationClient, IIterableService) maxips = 100 lasto = 0 lastd = 0 ecnt = 0 dcnt = 0 dtick = 0 coiterating = 0 pcid = None enabledLoop = None disabledLoop = None enabledPeriod = 0.05 disabledPeriod = 1.00 def __init__(self): self.views = [] self.enabled = [] self.disabled = [] self.realms = {} self.areas = {} self.items = {} self.areatranslations = {} self.arearotations = {} self.enabledState = {} def configure(self, nm, cf): """ Configure the interface with this config file """ def getItem(self, oid): """ Returns an item. Raises a key error if the item doesn't exist. """ try: return self.items[oid] except KeyError: return self.areas[oid] def getNotifier(self, view): """ Returns a new notifier for the given view """ return ViewChangeNotifier(self, view) @inlineCallbacks def addView(self, view, progressFunc=None): """ Add a new view, which means that we need to also update our list of items """ self.views.append(view) count = yield view.countObjectsInView() if count != 0: progInc = 1 / float(count) else: progressFunc = None limit = 10 for offset in range(0, count, limit): itemids = yield view.getObjectIDsInView(offset, limit) itemids = filter(lambda iid: not self.items.has_key(iid), itemids) items = yield view.viewObjects(itemids) dlist = [] for item in items: dfrd = self.addItem(item) dlist.append(dfrd) def gotResult(result): if progressFunc is not None: progressFunc(progInc) return result def gotError(error): deferr(error) return error dfrd.addCallback(gotResult) dfrd.addErrback(gotError) yield DeferredList(dlist) @inlineCallbacks def addItem(self, item): """ Add an item to this client """ if self.items.has_key(item.getID()): raise KeyError("Duplicate item added: %r" % (item.getID(),)) item.setParent(self) self.items[item.getID()] = item for area in item.areas.values(): if not self.areas.has_key(area.getID()): self.addArea(area) yield item.create() if item.isEnabled(): self.enabled.append(item) else: self.disabled.append(item) def addArea(self, area): """ Add an area to this client """ area.sim = self self.areas[area.getID()] = area if not self.realms.has_key(area.realm.getID()): self.realms[area.realm.getID()] = area.realm def removeItem(self, oid): """ Remove an item from the client """ if self.items[oid] in self.enabled: self.enabled.remove(self.items[oid]) else: self.disabled.remove(self.items[oid]) self.items[oid].destroy() del self.items[oid] def startService(self): """ Start the cooperator!! """ self.enabledLoop = LoopingCall(self.loopItems, True) self.disabledLoop = LoopingCall(self.loopItems, False) self.enabledLoop.start(self.enabledPeriod) self.disabledLoop.start(self.disabledPeriod) self.coiterating = 1 def loopItems(self, enabled): """ If start looping through enabled or disabled items depending on the value of enabled """ # print "loopy", len(self.enabled), len(self.disabled) pcid = self.parent.getLocalService(IPlayerClient).pcid if pcid is not None: try: ignore = [self.items[pcid]] except KeyError: ignore = None else: ignore = None if enabled: return coiterate(ItemIterator(self, self.enabled, ignore)) else: return coiterate(ItemIterator(self, self.disabled, ignore)) def enableItem(self, oid): """ Enable an item """ itm = self.items[oid] if not itm in self.enabled: self.enabled.append(itm) if itm in self.disabled: self.disabled.remove(itm) def requestIteration(self, item): """ Request iteration on an item. Here for compatibility with the server """ self.enableItem(item.getID()) def disableItem(self, oid): """ Disable an item """ itm = self.items[oid] if not itm in self.disabled: self.disabled.append(itm) if itm in self.enabled: self.enabled.remove(itm) def removeIteration(self, item): """ Request stopping of iteration on an item. Here for compatibility with the server """ self.disableItem(item.getID()) def iterate(self, time): """ Called once per tick of the server """ try: for r in self.realms.values(): r.iterate(time) for a in self.areas.values(): a.iterate(time) # update stats ps = self.parent.getLocalService(IPlayerClient) ps.updateStat(4, "IterEnable %d/%d" % (self.ecnt, len(self.enabled))) ps.updateStat(5, "IterDisable %d/%d" % (self.dcnt, len(self.disabled))) ps.updateStat(6, "Total %d" % len(self.items)) self.dcnt = self.ecnt = 0 if hasattr(ps, "pcid") and self.items.has_key(ps.pcid): p = self.items[ps.pcid].body.getPosition() a = self.items[ps.pcid].areas.keys() else: p = (0, 0, 0) a = "None" ps.updateStat(8, "Pos: %.1f %.1f %.1f %s" % (p[0], p[1], p[2], a)) return except: import traceback traceback.print_exc() def iteratePC(self, delta): """ Iterates our PC one step """ mypc = self.items.get(self.parent.getLocalService(IPlayerClient).pcid) if mypc is not None: mypc.iterate(delta) def getLocalRealm(self, realmID): """ Returns a realm if we have it locally. """ return self.realms[realmID]