# -*- test-case-name: mv3d.test.phys.test_body -*- # Copyright (C) 2007-2012 Mortal Coil Games # See LICENSE for details. """ Defines a wrapper around various ode related objects so that they automatically handle various things related to MV3D such as transitioning to new areas, persisting, sent over the network, etc. """ from array import array from time import time from platform import architecture import sys from twisted.spread import pb from zope.interface import implements #@UnresolvedImport from mv3d.net.pb import Cacheable, withClientUpdate from mv3d.net.security import Securable from mv3d.util.classgen import ClassGenerator, getClass from mv3d.util.math3d import Vector, Quaternion from mv3d.util.persist import Persistable, UntypedReference, List, IDTuple, \ AttributeProperty, FloatVector, Boolean, Text, Float, Referenced, \ MapAttribute, Pickled from twisted.internet.defer import maybeDeferred, gatherResults, inlineCallbacks, \ returnValue, Deferred, _DefGen_Return from mv3d.util.iservice import IAssetClient, IPlayerClient, IConductor from mv3d.phys.representation import Prefab from twisted.python.log import deferr from mv3d.phys.ibody import IBodyDynamicsAgregator, IBodyDynamicsReceiver, \ BodyError, IBody from mv3d.path.steer import Arrival, Seek from mv3d.util.conductor import findParent from mv3d.phys.iphys import IBody, ISpace, IBallJoint if "64bit" in architecture() and not "darwin" in sys.platform: byteSize = 8 else: byteSize = 4 class DefaultRanker: """ Just dumbly assign a rank of 1 to everything """ def __init__(self, agregator): self.agregator = agregator def rank(self, _bid, _pos, _rot, _lv, _av): return 1 def sent(self, bid): pass class TimeRanker: """ Rank updates by the last time the object was updated """ maxtime = 2.0 def __init__(self, agregator): self.agregator = agregator self.lastseen = {} def rank(self, bid, _pos, _rot, _lv, _av): last = self.lastseen.get(bid, 0.0) diff = time() - last if diff > self.maxtime: diff = self.maxtime return 1 - (diff / self.maxtime) def sent(self, bid): self.lastseen[bid] = time() class DefaultReducer: """ Send only the top x items """ limit = 10 def __init__(self, agregator): self.agregator = agregator def setLimit(self, limit): self.limit = limit def reduce(self, updates): """ Take a list of updates that are assigned by (rank, (bid, p, r, lv, av)) and only return ones that are < our rank """ # reduced = filter(lambda u: u[0] < self.rank, updates) updates.sort() return updates[:self.limit] class StringFlinger(Cacheable, Securable): """ A dynamics agregator that works by putting dynamics info into a string """ implements(IBodyDynamicsAgregator) sendevery = 10 itercount = 0 autoiter = True coiterator = None ranker = None noClients = 0 stoppedObserving = Cacheable.stoppedObserving def __init__(self, ranker=None, reducer=None, ignoreList=None): Cacheable.__init__(self) Securable.__init__(self) self.ignoreList = ignoreList or [] self.updates = {} self.bodies = {} self.nextid = 0 if ranker is not None: self.ranker = ranker else: self.ranker = TimeRanker(self) if reducer is not None: self.reducer = reducer else: self.reducer = DefaultReducer(self) self.addExclude("queue", "autoiter", "bodies", "nextid", "ranker", "reducer") def addBody(self, body): """ Add a new body to our list """ if body.objectid in self.ignoreList: return assert body not in self.bodies self.bodies[body] = self.nextid self.updateAllClients("addBody", self.nextid, body) self.nextid += 1 return self.nextid - 1 def removeBody(self, body): """ Remove a body from our list """ if body.objectid in self.ignoreList: return if self.bodies.has_key(body): self.updateAllClients("removeBody", self.bodies[body]) del self.bodies[body] def removeByOID(self, oid): """ Try and remove a body by it's objectid """ kill = [] for b in self.bodies.itervalues(): if hasattr(b, "objectid") and b.objectid == oid: kill.append(b) for b in kill: self.removeBody(b) def giveUpdate(self, body, p, r, lv, av): """ Queue up an update """ if hasattr(body, "objectid") and body.objectid in self.ignoreList: return try: bid = self.bodies[body] except KeyError: # need to add it bid = self.addBody(body) self.updates[bid] = (self.ranker.rank(bid, p, r, lv, av), bid, p, r, lv, av) def updateToString(self, data): """ Transform an update into a string """ ida = array("L", [data[1]]) dyna = array("f") for d in data[2:]: dyna.fromlist(list(d)) return ida.tostring() + dyna.tostring() def sendNextBatch(self): """ Process and send the next batch of updates """ batch = self.reducer.reduce(list(self.updates.values())) if not len(batch): return 0 [self.ranker.sent(u[1]) for u in batch] data = [self.updateToString(u) for u in batch] # string = "".join(data) self.updateAllClients("receiveBatch", "".join(data)) self.updates = {} return len(batch) def iterate(self, _): """ Send the next batch if it is time to do so """ return if self.itercount % self.sendevery == 0: self.sendNextBatch() self.itercount += 1 if not len(self.clients): self.noClients += 1 if self.noClients == 10: # stop all together raise StopIteration else: self.noClients = 0 def getStateToCacheAndObserveFor(self, perspective, observer): """ Invert our bodies dict """ st = Cacheable.getStateToCacheAndObserveFor(self, perspective, observer) # Reverse the bodies dict st["bodies"] = {} for k, v in self.bodies.items(): st["bodies"][v] = k return st def setCopyableState(self, state): """ Not allowed, so raise an error """ raise BodyError("Can't set the state of an IDynamicsBodyFlinger!") class StringCatcher(Cacheable): """ Receive dynamics updates for bodies """ implements(IBodyDynamicsReceiver) updatesize = 52 + byteSize bodies = None # setCopyableState = Cacheable.setCopyableState def setCopyableState(self, state): Cacheable.setCopyableState(self, state) def observe_addBody(self, bid, body=None): self.bodies[bid] = body def observe_removeBody(self, bid): del self.bodies[bid] def processUpdate(self, upstr): """ Take an string for one update and apply it to an object """ if hasattr(self.server, "lastups"): self.server.lastups += 1 bid = array("L", upstr[:byteSize])[0] dyna = array("f", upstr[byteSize:]) p = Vector(tuple(dyna[:3])) r = Quaternion(tuple(dyna[3:7])) lv = Vector(tuple(dyna[7:10])) av = Vector(tuple(dyna[10:13])) try: self.bodies[bid].updateDynamics(p, r, lv, av) except KeyError: # ignore non-existent body updates pass def observe_receiveBatch(self, data): """ Process a batch of updates """ for x in range(0, len(data), self.updatesize): up = data[x:x + self.updatesize] self.processUpdate(up) class BodyWithColliders(Persistable): """ todo: this class is dumb. please remove """ implements(IBody) _baseType = Referenced() colliders = List(UntypedReference()) objectid = None _allowedState = ["colliders"] def iterate(self): """ Iterate the body """ @withClientUpdate def addCollider(self, collider): """ Add a collider """ if self.colliders is None: self.colliders = [] collider.setOID(self.objectid) self.colliders.append(collider) self.queueSave(selectAttributes=["colliders"]) observe_addCollider = addCollider def getColliders(self): """ Return all of our colliders """ if self.colliders is None: self.colliders = [] return self.colliders def setPosition(self, pos): """ Set the position """ raise RuntimeError("Not Implemented") def setRotation(self, pos): """ Set the rotation """ raise RuntimeError("Not Implemented") def setLinearVelocity(self, pos): """ Set the LinearVelocity """ raise RuntimeError("Not Implemented") def setAngularVelocity(self, pos): """ Set the AngularVelocity """ raise RuntimeError("Not Implemented") def disable(self): """ Disable the body """ raise RuntimeError("Not Implemented") def enable(self): """ Enable the body """ raise RuntimeError("Not Implemented") class MobileBody(BodyWithColliders, Cacheable, Securable): """ A physical and visual body that can exist in multiple areas at the same time """ _schemaVersion = 2 areaid = IDTuple(autoSave=True, partialSave=True, transmit=True) objectid = IDTuple(autoSave=True, partialSave=True, transmit=True) # more attributes at the bottom of the class enabled = Boolean(default=True, transmit=True) joints = List(UntypedReference(), transmit=True) vobs = List(UntypedReference(), transmit=True) # V2 representationID = IDTuple(autoSave=True, partialSave=True, transmit=True) scale = FloatVector(default=(1, 1, 1), autoSave=True, partialSave=True, transmit=True) # V3 initial_position = FloatVector(default=(0, 0, 0), autoSave=True, partialSave=True) initial_rotation = FloatVector(default=(0, 0, 0), autoSave=True, partialSave=True) body = None space = None colliderspace = None area = None waterdepth = 0 world = None gradualOffset = 0 gradualUpdate = None forceClient = False parent = None representation = None useAggregator = True behavior = None def __init__(self, objectid=None): BodyWithColliders.__init__(self) Cacheable.__init__(self) Securable.__init__(self) self.objectid = objectid self.joints = [] self.colliders = [] self.vobs = [] self.otherspacecolliders = {} self.otherspacespaces = {} self.otherspacecolliderspaces = {} self.aggregators = [] self.iterators = [] @classmethod def upgrade(cls, oldData): oldVersion = oldData["_schemaVersion"] if oldVersion != 1: raise ValueError("Old version is %d, not 1" % oldVersion) newData = oldData.copy() newData["representationID"] = None newData["scale"] = "1.0, 1.0, 1.0" return newData def onLoad(self): self.otherspacecolliders = {} self.otherspacespaces = {} self.otherspacecolliderspaces = {} self.aggregators = [] def create(self, world, space=None): """ Create the body and optionally the colliders if space is defined """ self.world = world self.body = world.getPhysicsClass(IBody)(self.world) self.body.mvbody = self if self.initial_position is not None: self.body.setPosition(self.initial_position) if self.initial_rotation is not None: self.body.setOrientation(self.initial_rotation) if self.initial_linearVel is not None: self.body.setLinearVelocity(self.initial_linearVel) if self.initial_angularVel is not None: self.body.setAngularVelocity(self.initial_angularVel) if space is not None: self.enterSpace(space) if not self.enabled: self.body.disable() def destroy(self): """ Destroy the body """ self.world = None for c in self.colliders: c.destroy() if self.colliderspace is not None: self.colliderspace.destroy() self.body = None self.space = None for v in self.vobs: v.destroy() if self.representation is not None: self.representation.destroy() def enterSpace(self, space): """ Create colliders in this space """ assert self.body is not None if self.colliderspace is not None: self.colliderspace.destroy() self.colliderspace = self.world.getPhysicsClass(ISpace)(self.world) self.colliderspace.build(None, space) for c in self.colliders: c.destroy() c.setOID(self.objectid) c.build(body=self.body, space=self.colliderspace) self.space = space if self.representationID is not None: return self.buildRepresentation() @inlineCallbacks def buildRepresentation(self): """ On the server, build colliders. On the client, build the mesh as well. Note that combining representation and freestanding colliders will not work! """ try: if self.representation is None and self.representationID is not None: asvc = self.parent.parent.getLocalService(IAssetClient) self.representation = yield Prefab.loadFromAsset(asvc, self.representationID) if self.representation is None: returnValue(None) try: renderer = self.parent.parent.getLocalService(IPlayerClient).renderer except KeyError: renderer = None parentNode = None if self.area is not None: parentNode = getattr(self.area, "node", None) self.representation.build(self.parent.parent, space=self.colliderspace, position=self.position, rotation=self.rotation, scale=self.scale, oid=self.objectid, body=self.body, renderer=renderer, parentNode=parentNode) self.colliders.extend(self.representation.getColliders()) except: deferr() raise def enterSecondarySpace(self, ownerid, space): """ Create secondary colliders in this space """ self.otherspacecolliders[ownerid] = [] self.otherspacespaces[ownerid] = space if self.otherspacecolliderspaces.has_key(ownerid): self.otherspacecolliderspaces[ownerid].destroy() self.otherspacecolliderspaces[ownerid] = self.world.getPhysicsClass( ISpace)(self.world) self.otherspacecolliderspaces[ownerid].build(space=space) for collider in self.colliders: col = collider.copy() col.density = 0.0 # not sure what I was thinking here col.build(self.body, self.otherspacecolliderspaces[ownerid]) self.otherspacecolliders[ownerid].append(col) def enterArea(self, area): """ Determine the space most appropriate in are and enter it. Area should be an area object """ if hasattr(area, "sim"): self.parent = area.sim if self.body is None: self.create(area.world) #Hack! if self.isEnabled(): space = area.getEnabledSpace(self.getPosition()) else: space = area.getDisabledSpace(self.getPosition()) dfrds = [] dfrd = self.enterSpace(space) if isinstance(dfrd, Deferred): dfrds.append(dfrd) self.areaid = area.getID() self.area = area for vob in self.vobs: dfrds.append(maybeDeferred(vob.create, area, self.getPosition(), self.getRotation())) return gatherResults(dfrds) def leaveArea(self, area): """ This body is leaving the area, remove everything in the area """ if self.colliderspace is not None: self.colliderspace.destroy() for col in self.colliders: col.destroy() for vob in self.vobs: vob.leaveArea(area) self.space = None def leaveSecondarySpace(self, ownerid): """ Remove our secondary info from this space """ for col in self.otherspacecolliders[ownerid]: col.destroy() del self.otherspacecolliders[ownerid] del self.otherspacespaces[ownerid] self.otherspacecolliderspaces[ownerid].destroy() del self.otherspacecolliderspaces[ownerid] def hasSecondarySpace(self, ownerid): """ Returns true if we have colliders in this secondary space """ return ownerid in self.otherspacespaces.keys() def attach(self, other, joint): """ Attach this body to another one using the joint """ joint.attach(self, other) self.joints.append(joint) other.joints.append(joint) self.queueSave(selectAttributes=["joints"]) def setPosition(self, pos, permanent=True): """ Set the position of this body """ if self.body is not None: self.body.setPosition(pos) if permanent: self.updateVobs() return self.updateAllClients("setRotation", pos) def getPosition(self): """ Get the position of this body """ if self.body is not None: return self.body.getPosition() else: return (0, 0, 0) def setRotation(self, rot, permanent=True): """ Set the quaternion rotation of this body """ if self.body is not None: self.body.setOrientation(rot) if permanent: self.updateVobs() return self.updateAllClients("setRotation", rot) def getRotation(self): """ Get the quaternion rotation of this body """ if self.body is not None: return self.body.getOrientation() else: return (1, 0, 0, 0) @withClientUpdate def setLinearVelocity(self, lvel): """ Set the linear velocity of this body """ self.body.setLinearVelocity(lvel) def getLinearVelocity(self): """ Returns the linear velocity of this body """ if self.body is not None: return self.body.getLinearVelocity() else: return (0, 0, 0) @withClientUpdate def setAngularVelocity(self, avel): """ Set the angular Velocity of this body """ self.body.setAngularVelocity(avel) def getAngularVelocity(self): """ Get the angular velocity of this body """ if self.body is not None: return self.body.getAngularVelocity() else: return (0, 0, 0) def addForce(self, amount, _location=(0, 0, 0)): """ Add an instant force to the body. """ if self.body is None: return self.body.addForce(amount)#, location) def getMass(self): """ Get the mass of the body """ if self.body is None: return 1.0 return self.body.getMass().mass @withClientUpdate def enable(self): """ Set the state of this body to be enabled """ self.body.enable() self.setLinearVelocity(self.getLinearVelocity()) self.setAngularVelocity(self.getAngularVelocity()) if not self.enabled: if self.objectid is not None: self.parent.requestIteration(self) if self.area is not None: self.enterSpace(self.area.getEnabledSpace(self.getPosition())) self.enabled = True @withClientUpdate def disable(self): """ Set the state of this body to be disabled """ self.body.disable() self.setLinearVelocity((0, 0, 0)) self.setAngularVelocity((0, 0, 0)) self.setPosition(self.getPosition()) self.setRotation(self.getRotation()) if self.enabled and self.area is not None: self.parent.removeIteration(self) self.enterSpace(self.area.getDisabledSpace(self.getPosition())) self.enabled = False def isEnabled(self): """ Returns true if this body is enabled """ if self.body is None: return self.enabled return self.body.isEnabled() def updateDynamics(self, pos, rot, lvel, avel): """ Update the position, rotation, linear and angular velocities """ if self.body is None: self.initial_position = pos self.initial_rotation = rot else: dpos = Vector(self.getPosition()) - pos dst = dpos.squaredDistance((0, 0, 0)) if dst > 0.5 and dst < 100: self.gradualUpdate = dpos * -0.05 self.gradualOffset = 20 self.body.setLinearVel(lvel) self.body.setAngularVel(avel) self.body.setQuaternion(rot) return self.body.setPosition(pos) self.body.setQuaternion(rot) self.body.setLinearVel(lvel) self.body.setAngularVel(avel) def moveTo(self, destination, speed): """ Gradually move the body to the destination at no faster than the specified speed slow down to stop at the destination. When arrived the movement will be stopped and the deferred returned will fire back. """ return self.requestBehavior(Arrival(destination, speed)) def moveTowards(self, destination, speed, closeness=None): """ Gradually move the body to the destination at no faster than the specified speed. Callback the deferred when closeness (percent) of the initial distance between the body and the destination is met. If not specified, then the callback is only called when the move is cancelled. """ return self.requestBehavior(Seek(destination, speed, closeness)) def cancelMove(self): """ Cancel the currently in progres move. """ self.stopCurrentBehavior() def requestBehavior(self, behavior): """ Request a new behavior. Returns a deferred that will fire when the behavior is done/stopped. """ if self.behavior is not None: self.behavior.stop() self.behavior = behavior return behavior.start(self) def stopCurrentBehavior(self): """ Stops the current behavior """ if self.behavior is not None: self.behavior.stop() self.behavior = None def getPreferredSpaceType(self): """ Return our preferred space type 0 = Enabled 1 = Disabled """ if self.body is not None and self.body.isEnabled(): return 0 return 1 @withClientUpdate def addVisualObject(self, vob): """ Add a visual object """ if self.forceClient and hasattr(vob, "clientClass"): newVobClass = getClass(vob.clientClass) vob.__class__ = newVobClass self.vobs.append(vob) self.queueSave(selectAttributes=["vobs"]) @withClientUpdate def removeVisualObject(self, vob): """ Remove a visual object """ self.vobs.remove(vob) self.queueSave(selectAttributes=["vobs"]) @inlineCallbacks @withClientUpdate def setRepresentation(self, representationID): """ Set the representation ID """ self.representationID = representationID area = yield self.parent.getItem(self.areaid) self.leaveArea(area) yield self.enterArea(area) observe_removeVisualObject = removeVisualObject observe_addVisualObject = addVisualObject observe_setRepresentation = setRepresentation def getVisualObjects(self): """ Return our list of visual objects """ return self.vobs def updateVobs(self): """ Update all the visual objects """ p = self.getPosition() r = self.getRotation() if self.representation is not None: self.representation.setPosition(p) self.representation.setOrientation(r) for vob in self.vobs: if isinstance(vob, IVisualObjectView): vob.setPosition(p) vob.setRotation(r) def setWaterDepth(self, depth): """ Set the depth of water """ self.waterdepth = depth def getWaterDepth(self, _depth): """ Get the depth of water """ return self.waterdepth def requestIteration(self, obj): """ Every time this iterates, obj will iterate as well. """ self.iterators.append(obj) def removeIterationRequest(self, obj): """ Remove obj from the objects that will be iterated at the same time this is. """ self.iterators.remove(obj) def iterate(self, deltaTime=0.05): """ iterate this body """ for iterator in self.iterators[:]: try: iterator.iterate(deltaTime) except: deferr() self.iterators.remove(iterator) if self.gradualOffset > 0: self.gradualOffset -= 1 self.body.setPosition(self.gradualUpdate + self.body.getPosition()) if self.isEnabled(): if not self.enabled: self.enable() pos = self.body.getPosition() rot = self.body.getOrientation() lvel = self.body.getLinearVelocity() avel = self.body.getAngularVelocity() for aggr in self.aggregators: aggr.giveUpdate(self, pos, rot, lvel, avel) self.updateVobs() else: if self.enabled: self.disable() def iterateOtherArea(self, deltaTime, area): """ Iterate the body for another area. """ def getBoundingBox(self): """ Returns the bounding box for this body (AA, object space) """ bbox = self.colliderspace.getBoundingBox() #minx, maxx, miny, maxy, etc low = Vector(bbox[::2]) - self.getPosition() high = Vector(bbox[1::2]) - self.getPosition() return tuple(list(low) + list(high)) def getServerClassGenerators(self): """ Return class generators """ cgs = [ClassGenerator("mv3d.phys.body.StringCatcher", sourceclass="mv3d.phys.body.StringFlinger")] cgs.append(ClassGenerator().setFrom(self)) for vob in self.vobs: cgs.append(vob.getServerClassGenerators()) return cgs def getClientClassGenerators(self): """ Return class generators """ cgs = [ClassGenerator("mv3d.phys.body.StringCatcher", sourceclass="mv3d.phys.body.StringFlinger")] cgs.append(ClassGenerator().setFrom(self)) for v in self.vobs: cgs.append(v.getClientClassGenerators()) return cgs def getStateToCacheAndObserveFor(self, perspective, observer): st = Cacheable.getStateToCacheAndObserveFor(self, perspective, observer) assert perspective is not None if self.useAggregator: if not hasattr(perspective, "bodydynamicsaggregator"): perspective.bodydynamicsaggregator = StringFlinger() perspective.bodydynamicsaggregator.grantPermission("read", "all") perspective.bodydynamicsaggregator.grantPermission("reference", "all") findParent(perspective.parent, IConductor).addIterator( perspective.bodydynamicsaggregator.iterate) st["bodydynamicsreceiver"] = perspective.bodydynamicsaggregator self.aggregators.append(perspective.bodydynamicsaggregator) st["_auto_initial_position"] = self.getPosition() st["_auto_initial_rotation"] = self.getRotation() return st def stoppedObserving(self, perspective, observer): assert perspective is not None if self.useAggregator: perspective.bodydynamicsaggregator.removeBody(self) self.aggregators.remove(perspective.bodydynamicsaggregator) return Cacheable.stoppedObserving(self, perspective, observer) setCopyableState = Cacheable.setCopyableState observe_setRotation = setRotation observe_setPosition = setPosition observe_setLinearVelocity = setLinearVelocity observe_setAngularVelocity = setAngularVelocity observe_disable = disable observe_enable = enable def _setInitialPosition(self, value): """ used in persisting """ self.initial_position = value def _setInitialRotation(self, value): """ used in persisting """ self.initial_rotation = value def _setInitialLinearVel(self, value): """ used in persisting """ self.initial_linearVel = value def _setInitialAngularVel(self, value): """ used in persisting """ self.initial_angularVel = value position = AttributeProperty(FloatVector(), getPosition, _setInitialPosition) rotation = AttributeProperty(FloatVector(), getRotation, _setInitialRotation) linearVel = AttributeProperty(FloatVector(), getLinearVelocity, _setInitialLinearVel) angularVel = AttributeProperty(FloatVector(), getAngularVelocity, _setInitialAngularVel) class StationaryBody(BodyWithColliders, Cacheable, Securable): """ A body that is stationary """ getStateToCacheAndObserveFor = Cacheable.getStateToCacheAndObserveFor stoppedObserving = Cacheable.stoppedObserving setCopyableState = Cacheable.setCopyableState space = None representation = None sim = None world = None _baseType = Referenced() special = Boolean(default=False, autoSave=True, partialSave=True, transmit=True) colliderspace = None areaid = IDTuple(autoSave=True, partialSave=True, transmit=True) position = FloatVector(autoSave=True, partialSave=True, transmit=True) rotation = FloatVector(autoSave=True, partialSave=True, transmit=True) scale = FloatVector(autoSave=True, partialSave=True, transmit=True) vobs = List(UntypedReference(), transmit=True) representationID = IDTuple(autoSave=True, partialSave=True, transmit=True) colliders = List(UntypedReference(), transmit=True) _schemaVersion = 3 def __init__(self): BodyWithColliders.__init__(self) Cacheable.__init__(self) Securable.__init__(self) self.position = (0, 0, 0) self.rotation = (1, 0, 0, 0) self.scale = (1, 1, 1) self.vobs = [] self.colliders = [] self.transformedcolliders = [] @classmethod def upgrade(cls, oldData): oldVersion = oldData["_schemaVersion"] if oldVersion > 2: raise ValueError("Old version is %d, which is > 2" % oldVersion) newData = oldData.copy() if oldVersion < 2: newData["representationID"] = None newData["scale"] = "1.0, 1.0, 1.0" if oldVersion < 3: newData["colliders"] = [] return newData def onLoad(self): self.transformedcolliders = [] def create(self, world, space=None): """ Create the body and optionally the colliders if space is defined """ self.world = world if space is not None: return self.enterSpace(space) def destroy(self): """ Destroy the body """ self.world = None for collider in self.colliders: collider.destroy() for collider in self.transformedcolliders: collider.destroy() if self.colliderspace is not None: self.colliderspace.destroy() self.space = None for vobby in self.vobs: vobby.destroy() if self.representation is not None: self.representation.destroy() def getPreferredSpaceType(self): """ Return our preferred space type 1 = Disabled 2 = Special """ if self.special: return 2 return 1 @withClientUpdate def setPosition(self, pos): """ Set the position of this body """ self.position = pos self.updateVobs() self.enterSpace(self.space, repositionOnly=True) def getPosition(self): """ Get the position of this body """ return self.position @withClientUpdate def setRotation(self, rot): """ Set the quaternion rotation of this body """ self.rotation = rot self.updateVobs() self.enterSpace(self.space, repositionOnly=True) def getRotation(self): """ Get the quaternion rotation of this body """ return self.rotation @withClientUpdate def setScale(self, scale): """ Set the scale of this body """ self.scale = scale self.updateVobs() self.enterSpace(self.space, repositionOnly=True) def getScale(self): """ Returns the scale of the body """ return self.scale def getColliders(self): """ Return the transformed colliders if there are any otherwise return the defined colliders """ if len(self.transformedcolliders): return self.transformedcolliders return self.colliders def enable(self): """ Can't be enabled """ def disable(self): """ Can't be disabled """ def isEnabled(self): """ We can't be enabled """ return False def enterSpace(self, space, repositionOnly=False): """ Create colliders in this space """ if self.world is None: if hasattr(space, "world"): self.world = space.world else: # TODO: is this wise? return if self.colliderspace is not None: self.colliderspace.destroy() self.colliderspace = self.world.getPhysicsClass(ISpace)(self.world) self.colliderspace.build(space=space) for collider in self.transformedcolliders: collider.destroy() self.transformedcolliders = [] for collider in self.colliders: newc = collider.copy() if newc.position is not None: # TODO or not TODO: but this is wrong. needs to include # rotation newc.setPosition(collider.position + self.position) if newc.rotation is not None: newc.setRotation(Quaternion(collider.rotation) * self.rotation) newc.build(space=self.colliderspace) self.transformedcolliders.append(newc) self.space = space if self.representationID is not None: return self.buildRepresentation(repositionOnly) @inlineCallbacks def buildRepresentation(self, repositionOnly=False): """ On the server, build colliders """ try: if self.sim is None: returnValue(None) 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) if self.representation is None: returnValue(None) try: renderer = self.sim.parent.getLocalService(IPlayerClient).renderer except KeyError: renderer = None self.representation.position = self.position self.representation.rotation = Quaternion(self.rotation).toEuler() self.representation.scale = self.scale if repositionOnly: renderer = None 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) self.transformedcolliders.extend(self.representation.getColliders()) except _DefGen_Return: raise except: deferr() raise def enterArea(self, area): """ Determine the space most appropriate in are and enter it. Area should be an area object """ if self.special: space = area.getSpecialSpace(self.getPosition()) else: space = area.getDisabledSpace(self.getPosition()) self.areaid = area.getID() self.sim = area.sim esdfrd = self.enterSpace(space) dfrds = [] if isinstance(esdfrd, Deferred): dfrds.append(esdfrd) for vob in self.vobs: dfrds.append(maybeDeferred(vob.create, area, self.getPosition(), self.getRotation())) return gatherResults(dfrds) def leaveArea(self, _area): """ Remove the body from the given area """ self.destroy() @withClientUpdate def addVisualObject(self, vob): """ Add a visual object """ self.vobs.append(vob) self.queueSave(selectAttributes=["vobs"]) @withClientUpdate def removeVisualObject(self, vob): """ Remove a visual object """ self.vobs.remove(vob) self.queueSave(selectAttributes=["vobs"]) @inlineCallbacks @withClientUpdate def setRepresentation(self, representationID): """ Set the representation ID """ self.representationID = representationID area = yield self.sim.getItem(self.areaid) self.leaveArea(area) self.representation = None yield self.enterArea(area) observe_removeVisualObject = removeVisualObject observe_addVisualObject = addVisualObject observe_setRepresentation = setRepresentation def getVisualObjects(self): """ Get all the visual objects """ return self.vobs def updateVobs(self): """ Update all the visual objects """ p = self.getPosition() r = self.getRotation() for vob in self.vobs: if isinstance(vob, IVisualObjectView): vob.setPosition(p) vob.setRotation(r) def getBoundingBox(self): """ Returns the bounding box for this body (AA, object space) """ bbox = self.colliderspace.getBoundingBox() #minx, maxx, miny, maxy, etc low = Vector(bbox[::2]) - self.getPosition() high = Vector(bbox[1::2]) - self.getPosition() return tuple(list(low) + list(high)) def getServerClassGenerators(self): """ Return class generators """ cgs = [] cgs.append(ClassGenerator().setFrom(self)) for vob in self.vobs: cgs.append(vob.getServerClassGenerators()) return cgs def getClientClassGenerators(self): """ Return class generators """ cgs = [] cgs.append(ClassGenerator().setFrom(self)) for vob in self.vobs: cgs.append(vob.getClientClassGenerators()) return cgs observe_setRotation = setRotation observe_setPosition = setPosition observe_setScale = setScale class IJoint: """ A force that holds two Bodies together """ class Joint(IJoint, Cacheable): """ A standard joint """ type = None joint = None def __init__(self, type=None): Cacheable.__init__(self) self.type = type def attach(self, world, bodya, bodyb): self.createJoint(world) self.joint.attach(bodya.body, bodyb.body) def createBallJoint(self, world): self.joint = world.getPhysicsClass(IBallJoint)(world) def createJoint(self, world): assert self.type is not None return getattr(self, "create" + self.type)(world) def setAnchor(self, pos): self.joint.setAnchor(pos) class IVisualObject: """ This can be used to describe how an object looks to the client """ def enterArea(self, aid): """ Enter an area """ def destroy(self): """ Destroy this object """ def getAssets(self): """ Return all the assets required to display this object """ def addAsset(self, aid): """ Add an asset to the object """ def removeAsset(self, aid): """ Renive an asset from the object """ def setScale(self, scale): """ Set our scsale """ def startAnimation(self, name, loop=False, weight=100): """ Start an animation on this object """ def stopAnimation(self, name): """ Stop an animation on this object """ observe_setScale = setScale observe_startAnimation = startAnimation observe_stopAnimation = stopAnimation observe_addAsset = addAsset observe_removeAsset = removeAsset observe_enterArea = enterArea def getServerClassGenerators(self): """ Returns class generators for a server """ def getClientClassGenerators(self): """ Returns class generators for a client """ class IVisualObjectView(object): """ The client side of a visual object """ def observe_enterArea(self, aid): """ Enter an area """ def observe_leaveArea(self, aid): """ Leave an area """ def observe_addAsset(self, aid): """ Add an asset to the object """ def observe_setScale(self, scale): """ Set our scsale """ def observe_startAnimation(self, name, loop, weight): """ Start an animation """ def observe_stopAnimation(self, name): """ Stop an animation """ def setPosition(self, pos): """ Set the position of this object """ def setRotation(self, rot): """ Set the rotation of this object """ class BasicVisualObject(IVisualObject, Cacheable, Securable, Persistable): """ A basic visual object representation on the server side """ _schemaVersion = 2 stoppedObserving = Cacheable.stoppedObserving getStateToCacheAndObserveFor = Cacheable.getStateToCacheAndObserveFor setCopyableState = Cacheable.setCopyableState areaid = IDTuple(autoSave=True, partialSave=True, transmit=True) scale = FloatVector(autoSave=True, partialSave=True, transmit=True) posoffset = FloatVector(autoSave=True, partialSave=True, transmit=True) rotoffset = FloatVector(autoSave=True, partialSave=True, transmit=True) assets = List(IDTuple(), transmit=True) controllerID = IDTuple(autoSave=True, partialSave=True, transmit=True) controllerParameters = MapAttribute(Text(), Pickled(), transmit=True) clientClass = "mv3d.client.view.visual.BasicVisualObjectView" def __init__(self): Persistable.__init__(self) Cacheable.__init__(self) Securable.__init__(self) self.scale = Vector(1.0, 1.0, 1.0) self.posoffset = Vector(0, 0, 0) self.rotoffset = Quaternion(1, 0, 0, 0) self.assets = [] self.controllerParameters = {} @classmethod def upgrade(cls, oldData): oldVersion = oldData["_schemaVersion"] if oldVersion != 1: raise ValueError("Old version is %d, not 1" % oldVersion) newData = oldData.copy() newData["controllerID"] = None return newData def create(self, _area, _pos_, _rot): """ This does nothing on the server side """ def enterArea(self, area): """ Enter an area """ self.areaid = area.getID() def leaveArea(self, _area): """ Leave an area """ self.areaid = None def getAssets(self): """ Return all the assets """ return self.assets @withClientUpdate def setAssets(self, assets): """ Sets all the assets """ self.assets = assets @withClientUpdate def addAsset(self, aid): """ Add an asset """ self.assets.append(aid) self.save() @withClientUpdate def removeAsset(self, aid): """ Remove an asset from the object """ self.assets.remove(aid) self.save() @withClientUpdate def setScale(self, scale): """ Set our scsale """ self.scale = scale @withClientUpdate def setOffset(self, p, r): """ Set the offset for this node that gets applied when we set the position or rotation """ self.posoffset = p self.rotoffset = r @withClientUpdate def setControllerParameter(self, name, value): """ Sets the value of a parameter on the controller. """ self.controllerParameters[name] = value @withClientUpdate def sendControllerEvent(self, eventName, *args, **kwargs): """ Sends the controller an event. """ observe_setAssets = setAssets observe_setScale = setScale observe_setOffset = setOffset observe_setcontrollerParameter = setControllerParameter observe_sendControllerEvent = sendControllerEvent def getServerClassGenerators(self): """ Returns class generators for a server """ return ClassGenerator(classname="BasicVisualObject", modulename="mv3d.phys.body", sourceclass="mv3d.phys.body.BasicVisualObject") def getClientClassGenerators(self): """ Returns class generators for a client """ return ClassGenerator(classname="BasicVisualObjectView", modulename="mv3d.client.view.visual", sourceclass="mv3d.phys.body.BasicVisualObject") class WaterPlaneVisualObject(IVisualObject, Cacheable, Securable, Persistable): """ A plane that is made of water This is special because water is done with RTT """ stoppedObserving = Cacheable.stoppedObserving getStateToCacheAndObserveFor = Cacheable.getStateToCacheAndObserveFor setCopyableState = Cacheable.setCopyableState areaid = IDTuple(autoSave=True, partialSave=True, transmit=True) distance = Float(default=0.0, autoSave=True, partialSave=True, transmit=True) normal = FloatVector(autoSave=True, partialSave=True, transmit=True) assets = List(IDTuple(), transmit=True) def __init__(self): Persistable.__init__(self) Cacheable.__init__(self) Securable.__init__(self) self.normal = Vector(0, 1, 0) self.assets = [] def create(self, _area, _pos_, _rot): """ This does nothing on the server side """ def enterArea(self, area): """ Enter an area """ self.areaid = area.getID() def getAssets(self): """ Returns the list of assets """ return self.assets @withClientUpdate def addAsset(self, aid): """ Add an asset """ self.assets.append(aid) self.queueSave(selectAttributes=["assets"]) @withClientUpdate def removeAsset(self, aid): """ Remove an asset from the object """ self.assets.remove(aid) self.queueSave(selectAttributes=["assets"]) @withClientUpdate def setPlane(self, distance, normal): """ Set the plane """ self.distance = distance self.normal = normal observe_setPlane = setPlane def getServerClassGenerators(self): """ Returns class generators for a server """ return ClassGenerator(classname="WaterPlaneVisualObject", modulename="mv3d.phys.body", sourceclass="mv3d.phys.body.WaterPlaneVisualObject") def getClientClassGenerators(self): """ Returns class generators for a client """ return ClassGenerator(classname="WaterPlaneVisualObjectView", modulename="mv3d.client.view.visual", sourceclass="mv3d.phys.body.WaterPlaneVisualObject") pb.setUnjellyableForClass(StringFlinger, StringCatcher)