# -*- test-case-name: mv3d.test.service.test_gateway -*- # Copyright (C) 2006-2012 Mortal Coil Games # See LICENSE for details. """ Class for Gateways A gateway is a connection between two things. They are one way (you'll need two of them to represent a doorway that you can go in and out of. I say 'things' because even though most of the time, 'things' will be Areas, it is also possible to use any other Container type object in theory. You'll want to subclass the Gateway class with some sort of object class before doing anything with it. """ from zope.interface import implements from mv3d.util.math3d import Vector, Quaternion from mv3d.util.persist import IDTuple, Boolean, List, FloatVector, UpgradeError from twisted.internet.defer import inlineCallbacks, returnValue, Deferred, \ _DefGen_Return from mv3d.util.conductor import findParent from mv3d.util.iservice import IConductor, IAssetClient, IPlayerClient from twisted.python.log import deferr from twisted.internet import reactor from mv3d.phys.body import StationaryBody from mv3d.util.classgen import ClassGenerator from mv3d.phys.ibody import IGateway, ITranslatingGateway from mv3d.phys.representation import Prefab from mv3d.phys.iphys import ISpace from mv3d.net.pb import withClientUpdate class Gateway(object): """ A class that defines a method of connecting two areas together """ implements(IGateway) destinationID = IDTuple(autoSave=True, partialSave=True, transmit=True) canSeeThrough = Boolean(autoSave=True, partialSave=True, transmit=True) canCollideThrough = Boolean(autoSave=True, partialSave=True, transmit=True) siblings = List(IDTuple(), transmit=True) def __init__(self): self.destinationID = None self.canSeeThrough = True self.canCollideThrough = True self.siblings = [] def getDistanceFromObserver(self, item): """ Return the distance between us and item """ return Vector(self.getPosition()).distance(item.getPosition()) @inlineCallbacks def getDestination(self): """ Returns the destination area. """ assert self.destinationID is not None assert self.sim is not None returnValue((yield self.sim.getItem(self.destinationID))) @inlineCallbacks def traverse(self, obj): """ Push an object though the gateway. """ if (self.destinationID is None or self.destinationID in obj.listContainedBy()): # support for already being in the area and also using a gateway # as an in area teleporter. return dest = yield self.getDestination() for area in obj.listContainedBy(): area = yield self.sim.getItem(area) yield area.takeOut(obj.getID()) yield dest.contain(obj.getID()) @withClientUpdate def addSibling(self, s): """ Add a sibling to this gateway. Basically, a sibling is the gateway on the other side is an oid """ self.siblings.append(s) def observe_addSibling(self, s): """ Add a sibling to this gateway. Basically, a sibling is the gateway on the other side is an oid """ self.addSibling(self, s) @withClientUpdate def setDestination(self, areaID): """ Sets the destination area id. """ self.destinationID = areaID observe_setDestination = setDestination @withClientUpdate def setCanCollideThrough(self, canCollideThrough): """ Sets the value of canCollideThrough """ self.canCollideThrough = canCollideThrough observe_canCollideThrough = setCanCollideThrough @withClientUpdate def setCanSeeThrough(self, canSeeThrough): """ Sets the value of canSeeThrough """ self.canSeeThrough = canSeeThrough observe_canSeeThrough = setCanSeeThrough class TranslatingGateway(Gateway): """ A gateway that includes a space translation """ implements(ITranslatingGateway) translation = FloatVector(autoSave=True, partialSave=True, transmit=True) rotationDelta = FloatVector(autoSave=True, partialSave=True, transmit=True) def __init__(self): Gateway.__init__(self) self.translation = (0, 0, 0) self.rotationDelta = (1, 0, 0, 0) def getTranslation(self): """ Get the translation amount """ return self.translation def getRotationDelta(self): """ Get the rotation amount """ return self.rotationDelta @withClientUpdate def setTranslation(self, translation): """ Sets the value of translation """ self.translation = translation observe_setTranslation = setTranslation @withClientUpdate def setRotationDelta(self, rotationDelta): """ Sets the value of rotationDelta """ self.rotationDelta = rotationDelta observe_setRotationDelta = setRotationDelta def translatePoint(self, p): """ Translate a point through the gateway """ p = Vector(p) return (p + self.translation) def translateRotation(self, r): """ Translate a rotation through the gateway """ r = Quaternion(r) return r * Quaternion(self.rotationDelta) def unTranslatePoint(self, p): """ Translate a point back from the gateway """ return Vector(p) - self.translation def unTranslateRotation(self, r): """ Translate a rotation back from the gateway """ return Quaternion(r) * Quaternion(self.rotationDelta).inverse() def translateBody(self, body): """ Translate a body through the gateway """ body.setPosition(self.translatePoint(body.getPosition()), False) body.setRotation(self.translateRotation(body.getRotation()), False) def unTranslateBody(self, body): """ Translate a body back through the gateway """ body.setPosition(self.unTranslatePoint(body.getPosition()), False) body.setRotation(self.unTranslateRotation(body.getRotation()), False) @inlineCallbacks def traverse(self, obj): """ Send an object through the gateway.. """ yield Gateway.traverse(self, obj) if Vector(self.translation).length() > 0.01: obj.setPosition(self.translatePoint(obj.getPosition())) if self.rotationDelta != (1, 0, 0, 0): obj.setRotation(self.translateRotation(obj.getRotation())) class StationaryGatewayBody(TranslatingGateway, StationaryBody): """ A physical gateway has a physical form """ _schemaVersion = 4 nearRepresentationID = IDTuple(autoSave=True, partialSave=True, transmit=True) nearRepresentation = None nearspace = None colliderspace = None nearcolliderspace = None world = None def __init__(self): StationaryBody.__init__(self) TranslatingGateway.__init__(self) self.recentlypassed = {} self.nearbodies = [] self.traversing = [] self.transformedcolliders = [] self.nearTransformedColliders = [] @classmethod def upgrade(cls, oldData): """ Upgrade from an old version to a new version. """ if oldData["_schemaVersion"] >= 4: raise UpgradeError("Unknown version %d" % oldData["_schemaVersion"]) newData = oldData.copy() if oldData["_schemaVersion"] < 4: newData["rotationDelta"] = "1,0,0,0" return newData def onLoad(self): """ Called when loaded from the store """ self.traversing = [] return StationaryBody.onLoad(self) def create(self, world, space=None): """ Create the body and optionally the colliders if space is defined """ self.world = world self.nearspace = world.getPhysicsClass(ISpace)(self.world) self.nearspace.build() self.colliderspace = world.getPhysicsClass(ISpace)(self.world) self.nearcolliderspace = world.getPhysicsClass(ISpace)(self.world) if space is not None: return self.enterSpace(space) def destroy(self): """ Destroy the body """ self.colliderspace.destroy() for collider in self.transformedcolliders: collider.destroy() for collider in self.nearTransformedColliders: collider.destroy() self.nearspace.destroy() @inlineCallbacks def removeCollisionNotify(): try: area = yield self.getDestination() except KeyError: return try: area.stopNotifyingOnCollide(self.collideNearSpace) except ValueError: pass removeCollisionNotify().addErrback(deferr) def enterArea(self, area): """ Called when this gateway enters an area """ self.areaid = area.getID() self.sim = area.sim self.world = area.world if self.nearspace is None: self.create(area.world) return self.enterSpace(area.getSpecialSpace(self.getPosition())) def enterSpace(self, space, repositionOnly=False): """ Create colliders and representation in the space. """ if space == self.space: return self.space = space if self.colliderspace is not None: self.colliderspace.destroy() self.nearcolliderspace.destroy() else: self.create(self.space.world) self.colliderspace.build(space=space) self.nearcolliderspace.build(space=space) for collider in self.transformedcolliders: collider.destroy() collider.oncollide = self.hitGateway collider.build(space=self.colliderspace) for collider in self.nearTransformedColliders: collider.destroy() collider.oncollide = self.hitNearGateway collider.build(space=self.nearcolliderspace) if not self.transformedcolliders + self.nearTransformedColliders: # we haven't constructed our representation yet. return self.buildRepresentation(repositionOnly) def leaveArea(self, _area): """ Called when this gateway leaves an area """ self.areaid = None return self.leaveSpace() def leaveSpace(self): """ Leave the current space. """ self.space = None if self.colliderspace is not None: self.colliderspace.destroy() if self.nearcolliderspace is not None: self.nearcolliderspace.destroy() for collider in self.transformedcolliders: collider.destroy() for collider in self.nearTransformedColliders: collider.destroy() @inlineCallbacks def buildRepresentation(self, repositionOnly=False): """ On the server, build colliders and on the client also build visual objects. """ try: if self.sim is None: returnValue(None) # first build the gateway representation if self.representation is None and self.representationID is not None: asvc = self.sim.parent.getLocalService(IAssetClient) self.representation = yield Prefab.loadFromAsset(asvc, self.representationID) renderer = None try: psvc = self.sim.parent.getLocalService(IPlayerClient) renderer = psvc.renderer except KeyError: pass if repositionOnly: renderer = None if self.representation is not None: self.representation.position = self.position self.representation.rotation = Quaternion( self.rotation).toEuler() self.representation.scale = self.scale yield self.representation.build(self.sim.parent, space=self.colliderspace, position=(0, 0, 0), rotation=(1, 0, 0, 0), scale=(1, 1, 1), oid=self.objectid, renderer=renderer, colliderArgs=dict( oncollide=self.hitGateway)) self.transformedcolliders = self.representation.getColliders() # now build the near representation if (self.nearRepresentation is None and self.nearRepresentationID is not None): asvc = self.sim.parent.getLocalService(IAssetClient) self.nearRepresentation = yield Prefab.loadFromAsset( asvc, self.nearRepresentationID) if self.nearRepresentation is None: returnValue(None) self.nearRepresentation.position = self.position self.nearRepresentation.rotation = Quaternion( self.rotation).toEuler() self.nearRepresentation.scale = self.scale yield self.nearRepresentation.build(self.sim.parent, space=self.nearcolliderspace, position=(0, 0, 0), rotation=(1, 0, 0, 0), scale=(1, 1, 1), oid=self.objectid, renderer=renderer, colliderArgs=dict( oncollide=self.hitNearGateway)) self.nearTransformedColliders = ( self.nearRepresentation.getColliders()) except _DefGen_Return: raise except: deferr() raise @inlineCallbacks @withClientUpdate def setNearRepresentation(self, nearRepresentationID): """ Set the near representation ID """ self.nearRepresentationID = nearRepresentationID area = yield self.sim.getItem(self.areaid) self.leaveArea(area) self.representation = None yield self.enterArea(area) observe_setNearRepresentation = setNearRepresentation def hitGateway(self, geom, _mygeom): """ Called when someone hits our gateway collider """ if self.priority != 1: return True if not geom.oid: return True collisions = [] depth = 0 for collider in self.transformedcolliders: if collider.geom: collision = self.world.getContactsForGeoms(geom, collider.geom) collisions += collision if not len(collisions): #they really didn't hit us return True for cc in collisions: _p, _n, d, _g1, _g2 = cc.getContactGeomParams() depth += d depth /= float(len(collisions)) @inlineCallbacks def handleCollision(): obj = yield self.sim.getItem(geom.oid) if self.destinationID in obj.listContainedBy(): return if self.recentlypassed.has_key(geom.oid): lastpass, lastdepth = self.recentlypassed[geom.oid] if findParent(self.sim, IConductor).tick - lastpass > 15: del self.recentlypassed[geom.oid] yield self.traverse(obj) return self.recentlypassed[geom.oid] = ( findParent(self.sim, IConductor).tick, depth) if depth <= lastdepth: return yield self.traverse(obj) handleCollision().addErrback(deferr) return True def hitNearGateway(self, geom, _mygeom): """ Called when someone is near our gateway """ if not geom.oid: return True collisions = [] for collider in self.nearTransformedColliders: if collider.geom: collision = self.world.getContactsForGeoms(geom, collider.geom) collisions += collision if not len(collisions): return True if hasattr(geom.getBody(), "mvbody"): body = geom.getBody().mvbody else: return True @inlineCallbacks def handleCollision(): if not len(self.nearbodies): area = yield self.getDestination() area.notifyOnCollide(self.collideNearSpace) if body.hasSecondarySpace(self.objectid): for idx in range(len(self.nearbodies)): if self.nearbodies[idx][0] == body: self.nearbodies[idx] = (body, findParent(self.sim, IConductor).tick) return body.enterSecondarySpace(self.objectid, self.nearspace) self.nearbodies.append((body, findParent(self.sim, IConductor).tick)) handleCollision().addErrback(deferr) return True @inlineCallbacks def traverse(self, obj): """ Send an object through the gateway """ if obj in self.traversing: return self.traversing.append(obj) # HACK: make sure that we are running outside of the collision callback try: dfrd = Deferred() reactor.callLater(0.0, dfrd.callback, None) #@UndefinedVariable yield dfrd yield TranslatingGateway.traverse(self, obj) for sibID in self.siblings: sibling = yield self.sim.getItem(sibID) # TODO: this assumes the sibling is in the same process sibling.recentlypassed[obj.getID()] = (findParent(self.sim, IConductor).tick, 9999999) finally: self.traversing.remove(obj) def translateAllBodies(self): """ Translate all bodies in our space """ for body, _tick in self.nearbodies: self.translateBody(body) def unTranslateAllBodies(self): """ Untranslate a body back through the gateway """ for body, _tick in self.nearbodies: self.unTranslateBody(body) @inlineCallbacks def collideNearSpace(self, cmaker): """ Collide our near space with the space of the area we point to """ dest = yield self.getDestination() self.translateAllBodies() try: aspace = dest.getEnabledSpace() dspace = dest.getDisabledSpace() cmaker.collide(self.nearspace, aspace) cmaker.collide(self.nearspace, dspace) for body, _tick in self.nearbodies: body.iterateOtherArea(0.05, dest) finally: self.unTranslateAllBodies() def getServerClassGenerators(self): """ Return class generators """ cgs = [] cgs.append(ClassGenerator().setFrom(self)) return cgs def getClientClassGenerators(self): """ Return class generators """ cgs = [] cgs.append(ClassGenerator().setFrom(self)) return cgs