# -*- test-case-name: mv3d.test.test_asset -*- # Copyright (C) 2006-2012 Mortal Coil Games # See LICENSE for details. """ Classes for Assets and AssetGroups An Asset can represent any static content available in the game. Some examples are Images, 3D Objects, Sounds, and Animations. The general idea is that AssetGroups will only live on Asset Servers. They will be referenced down to other servers when needed (or cached to slave servers). The individual Assets will be copied down to the end user as they will include a way to acquire the asset (via downloading, hard drive, osmosis, or whatnot) """ from twisted.internet import defer from zope.interface import implements from mv3d.util.classgen import ClassGenerator from mv3d.util.date import Date from mv3d.util.modifier import Modifiable from mv3d.resource.iasset import IAsset from mv3d.net.ha import HighlyAvailable, withSlaveUpdate from mv3d.util.persist import IDTuple, Text, SharedID, List, Persistable, \ UpgradeError, Integer from mv3d.util.guide import ChangeNotifier from mv3d.net.pb import Copyable from twisted.internet.defer import inlineCallbacks, returnValue from mv3d.util.iservice import IAssetClient from mv3d.client.ui.irenderer import Ogre3D, Panda3D class AssetReference(Persistable, Copyable): """ An AssetReference can be used to store a resilient reference to an asset. When sent over the network, it is sent as an ID, but when stored in the database, it uses the name of the asset. """ agid = Integer(autoSave=True, partialSave=True) aid = IDTuple(transmit=True, autoSave=True, partialSave=True) # transient=True ? name = Text(autoSave=True, partialSave=True, transmit=True) def __init__(self, agid, name): self.agid = agid self.name = name self.aid = None @classmethod def fromAsset(cls, asset): """ Create a new reference from an asset. """ aid = asset.getID() ref = cls(aid[0], asset.name) ref.aid = asset.aid return ref @inlineCallbacks def retrieveID(self, conductor): """ Looks up the asset id. """ asvc = conductor.getLocalService(IAssetClient) assetIDs = yield asvc.search(self.agid, Asset, Asset.name == self.name, onlyReturnIDs=True) if not assetIDs: raise KeyError("No asset with name %s found in group %d!" % ( self.name, self.agid)) if len(assetIDs) > 1: raise KeyError("%d assets have the name %s in group %d!" % ( len(assetIDs), self.name, self.agid)) self.aid = assetIDs[0] returnValue(self.aid) @inlineCallbacks def retrieveName(self, conductor): """ Look up the name """ asvc = conductor.getLocalService(IAssetClient) asset = yield asvc.getAsset(self.aid) self.name = asset.name self.agid = self.aid[0] returnValue(self.name) @classmethod @inlineCallbacks def updateStore(cls, store, conductor): """ Go through all references in the store and update the asset IDs. """ for ref in cls.xquery(store): yield ref.retrieveID() ref.save() def getAsset(self, conductor): """ Returns the asset for this reference. """ asvc = conductor.getLocalService(IAssetClient) return asvc.getAsset(self.aid) def acquire(self, conductor): """ Aquires the asset for this reference. """ asvc = conductor.getLocalService(IAssetClient) return asvc.acquireAsset(self.aid) def onCopied(self): """ Called when copied to another process. """ self.agid = self.aid[0] class Asset(Persistable, HighlyAvailable, Modifiable, ChangeNotifier): """ Assets will have 2 number IDs: id[0] = AssetGroup id id[1] = AssetId (within the group) Generally, users of this class will subclass it and then override the AcquireAsset function. """ implements(IAsset) classModifiers = dict(web=[ ClassGenerator("mv3d.server.editor.AssetEditor"), ClassGenerator("mv3d.server.editor.DeleteAsset"), ]) _baseType = SharedID() _schemaVersion = 2 aid = IDTuple(autoSave=True, partialSave=True, transmit=True) asset = None name = Text(default="", autoSave=True, partialSave=True, transmit=True) author = Text(default="", autoSave=True, partialSave=True, transmit=True) copyright = Text(default="", autoSave=True, partialSave=True, transmit=True) license = Text(default="", autoSave=True, partialSave=True, transmit=True) description = Text(default="", autoSave=True, partialSave=True, transmit=True) revisor = Text(default="", autoSave=True, partialSave=True, transmit=True) comment = Text(default="", autoSave=True, partialSave=True, transmit=True) checksum = Text(autoSave=True, partialSave=True, transmit=True) category = Text(autoSave=True, partialSave=True, transmit=True) tags = Text(autoSave=True, partialSave=True, transmit=True) dependencies = List(IDTuple(), transmit=True) parent = None def __init__(self): """ Initialize to default values The asset is set to 0 and added to the exclude list so it isn't sent over the network. """ Persistable.__init__(self) HighlyAvailable.__init__(self) Modifiable.__init__(self) self.date = Date() self.dependencies = [] @classmethod def upgrade(cls, oldData): """ Upgrade an asset from an old version to a new version. """ if oldData["_schemaVersion"] != 1: raise UpgradeError(oldData["_schemaVersion"], cls._schemaVersion, "Unknown version") newData = oldData.copy() newData["category"] = "" newData["tags"] = "" return newData def getID(self): """ return the full id for this asset """ return self.aid @withSlaveUpdate def setID(self, i): """ set the full id for this asset """ self.aid = i observe_setID = setID def _getGroup(self): """ Returns just the asset group part of the id """ if self.aid is None: return None return self.aid[0] def _setGroup(self, value): """ This is ignored since it's redundant but is required because the datastore will try to set it. """ agid = property(_getGroup, _setGroup) def getName(self): """ Returns my name """ return self.name @withSlaveUpdate def setName(self, n): """ Set my name to n. Names are non unique """ self.name = n observe_setName = setName def getAuthor(self): """ returns the author (str) """ return self.author @withSlaveUpdate def setAuthor(self, a): """ sets the author to a (str) """ self.author = a observe_setAuthor = setAuthor def getCopyright(self): """ Returns the copyright notice (str) """ return self.copyright @withSlaveUpdate def setCopyright(self, c): """ sets the copyright to c (str) """ self.copyright = c observe_setCopyright = setCopyright def getLicense(self): """ Returns the license information (str) """ return self.license @withSlaveUpdate def setLicense(self, l): """ sets the license to l (str) """ self.license = l observe_setLicense = setLicense def getDescription(self): """ Returns the description (str) """ return self.description @withSlaveUpdate def setDescription(self, d): """ sets the description to d (str) """ self.description = d observe_setDescription = setDescription def getRevisor(self): """ returns the revisor (str) """ return self.revisor @withSlaveUpdate def setRevisor(self, r): """ sets the revisor to r (str) """ self.revisor = r observe_setRevisor = setRevisor def getDate(self): """ returns the date (Date) """ return self.date @withSlaveUpdate def setDate(self, d): """ sets the date to d (anything parsable by Date) """ self.date = Date().parse(d) observe_setDate = setDate def getComment(self): """ returns the comment associated with this revision (str) """ return self.comment @withSlaveUpdate def setComment(self, c): """ Sets the comment associated with this revision to c (str) """ self.comment = c observe_setComment = setComment def getCompatibleRenderers(self): """ Returns a list of compatible renderers. """ return Ogre3D, Panda3D def haveAsset(self): """ Returns true if was have downloaded or otherwise acquired an asset """ return self.asset != 0 def acquireAsset(self, downloadList=None, **kw): """ This function will get the asset loaded by whatever means is appropriate. kw would be things like lod=10, bitrate="44k", etc. Normally, it'll return a deferred which will callback when the asset is finished downloading. If there is an instant error, it will return 0 right away. If the asset is already available, it'll return 1 right away. """ # this Asset class doesn't actually get any assets so # just return 1 if the asset is present return self.haveAsset() def getAsset(self): """ This function will return the reference to the asset if it has been loaded. If not, it returns 0. """ if self.haveAsset(): return self.asset return False def listDependencies(self): """ Return a list of the aids that we depend on """ return self.dependencies def acquireDependencies(self, asvc=None, downloadList=None, **kw): """ This function will make sure that all the dependencies of this asset are loaded... It probably returns a deferred """ if downloadList is None: downloadList = [self.aid] dd = [] i = asvc or self.parent for a in self.dependencies: if a in downloadList: raise ValueError("Asset %r has a circular reference to %r" % ( self.aid, a)) if a == self.aid: raise ValueError("Asset %r is a dependent of itself!" % ( self.aid,)) d = i.acquireAsset(a, downloadList[:], **kw) assert d != 0 if isinstance(d, defer.Deferred): dd.append(d) if len(dd): dl = defer.gatherResults(dd) return dl return True @withSlaveUpdate def addDependency(self, d): """ Add the aid of an asset that needs to be loaded before we can be loaded. """ self.dependencies.append(d) self.queueSave(selectAttributes=["dependencies"]) @withSlaveUpdate def setDependencies(self, d): """ Add the aid of an asset that needs to be loaded before we can be loaded. """ self.dependencies = d self.queueSave(selectAttributes=["dependencies"]) @withSlaveUpdate def remDependency(self, d): """ Remove d from the list of asset ids that we depend on return 0 if d isn't a current dependency. """ self.dependencies.remove(d) self.queueSave(selectAttributes=["dependencies"]) return True observe_addDependency = addDependency observe_remDependency = remDependency observe_setDependencies = setDependencies def getChecksum(self): """ Get the checksum for this asset """ return self.checksum @withSlaveUpdate def setChecksum(self, c): """ Get the checksum for this asset """ self.checksum = c observe_setChecksum = setChecksum def refreshChecksum(self): """ Causes the checksum to be reloaded for this asset """ def destroy(self): """ Destroy the asset """ def getClassGenerator(self): """ Returns a class generator for this asset class """ return ClassGenerator(classname=self.__class__.__name__, modulename=self.__class__.__module__) # define some basic types of assets class ImageAsset(Asset): """ An asset that points to an image """ _schemaVersion = 2 _baseType = SharedID() class MaterialAsset(Asset): """ An asset that points to a material type """ _schemaVersion = 2 _baseType = SharedID() class ShaderAsset(Asset): """ An asset that points to a shader file """ _schemaVersion = 2 _baseType = SharedID() class MeshAsset(Asset): """ An asset that points to a 3d mesh """ _schemaVersion = 2 _baseType = SharedID() class PhysicsAsset(Asset): """ An asset that points to a physics representation """ _schemaVersion = 2 _baseType = SharedID() class CodeAsset(Asset): """ An asset that points to source code """ _schemaVersion = 2 _baseType = SharedID() class SoundAsset(Asset): """ An asset for sound """ _schemaVersion = 2 _baseType = SharedID() class FileAsset(Asset): """ An asset for misc files """ _schemaVersion = 2 _baseType = SharedID()