from pandac.PandaModules import * from MsgTypes import * from direct.directnotify import DirectNotifyGlobal from ConnectionRepository import ConnectionRepository from PyDatagram import PyDatagram from PyDatagramIterator import PyDatagramIterator class AstronDatabaseInterface: """ This class is part of Panda3D's new MMO networking framework. It interfaces with Astron database(s) to manage objects directly, rather than via DB-StateServers. Do not create this class directly; instead, use AstronInternalRepository's dbInterface attribute. """ notify = DirectNotifyGlobal.directNotify.newCategory("AstronDatabaseInterface") def __init__(self, air): self.air = air self._callbacks = {} def createObject(self, databaseId, dclass, fields={}, callback=None): """ Create an object in the specified database. databaseId specifies the control channel of the target database. dclass specifies the class of the object to be created. fields is a dict with any fields that should be stored in the object on creation. callback will be called with callback(doId) if specified. On failure, doId is 0. """ # Save the callback: ctx = self.air.getContext() self._callbacks[ctx] = callback # Pack up/count valid fields. fieldPacker = DCPacker() fieldCount = 0 for k,v in fields.items(): field = dclass.getFieldByName(k) if not field: self.notify.error('Creation request for %s object contains ' 'invalid field named %s' % (dclass.getName(), k)) fieldPacker.rawPackUint16(field.getNumber()) fieldPacker.beginPack(field) field.packArgs(fieldPacker, v) fieldPacker.endPack() fieldCount += 1 # Now generate and send the datagram: dg = PyDatagram() dg.addServerHeader(databaseId, self.air.ourChannel, DBSERVER_CREATE_OBJECT) dg.addUint32(ctx) dg.addUint16(dclass.getNumber()) dg.addUint16(fieldCount) dg.appendData(fieldPacker.getString()) self.air.send(dg) def handleCreateObjectResp(self, di): ctx = di.getUint32() doId = di.getUint32() if ctx not in self._callbacks: self.notify.warning('Received unexpected DBSERVER_CREATE_OBJECT_RESP' ' (ctx %d, doId %d)' % (ctx, doId)) return if self._callbacks[ctx]: self._callbacks[ctx](doId) del self._callbacks[ctx] def queryObject(self, databaseId, doId, callback): """ Query object `doId` out of the database. On success, the callback will be invoked as callback(dclass, fields) where dclass is a DCClass instance and fields is a dict. On failure, the callback will be invoked as callback(None, None). """ # Save the callback: ctx = self.air.getContext() self._callbacks[ctx] = callback # Generate and send the datagram: dg = PyDatagram() dg.addServerHeader(databaseId, self.air.ourChannel, DBSERVER_OBJECT_GET_ALL) dg.addUint32(ctx) dg.addUint32(doId) self.air.send(dg) def handleQueryObjectResp(self, di): ctx = di.getUint32() success = di.getUint8() if ctx not in self._callbacks: self.notify.warning('Received unexpected DBSERVER_OBJECT_GET_ALL_RESP' ' (ctx %d)' % (ctx)) return try: if not success: if self._callbacks[ctx]: self._callbacks[ctx](None, None) return dclassId = di.getUint16() dclass = self.air.dclassesByNumber.get(dclassId) if not dclass: self.notify.error('Received bad dclass %d in' ' DBSERVER_OBJECT_GET_ALL_RESP' % (dclassId)) fieldCount = di.getUint16() unpacker = DCPacker() unpacker.setUnpackData(di.getRemainingBytes()) fields = {} for x in xrange(fieldCount): fieldId = unpacker.rawUnpackInt16() field = dclass.getFieldByIndex(fieldId) if not field: self.notify.error('Received bad field %d in query for' ' %s object' % (fieldId, dclass.getName())) unpacker.beginUnpack(field) fields[field.getName()] = field.unpackArgs(unpacker) unpacker.endUnpack() if self._callbacks[ctx]: self._callbacks[ctx](dclass, fields) finally: del self._callbacks[ctx] def updateObject(self, databaseId, doId, dclass, newFields, oldFields=None, callback=None): """ Update field(s) on an object, optionally with the requirement that the fields must match some old value. databaseId and doId represent the database control channel and object ID for the update request. newFields is to be a dict of fieldname->value, representing the fields to add/change on the database object. oldFields, if specified, is a similarly-formatted dict that contains the expected older values. If the values do not match, the database will refuse to process the update. This is useful for guarding against race conditions. On success, the callback is called as callback(None). On failure, the callback is called as callback(dict), where dict contains the current object values. This is so that the updater can try again, basing its updates off of the new values. """ # Ensure that the keys in newFields and oldFields are the same if # oldFields is given... if oldFields is not None: if set(newFields.keys()) != set(oldFields.keys()): self.notify.error('newFields and oldFields must contain the same keys!') return fieldPacker = DCPacker() fieldCount = 0 for k,v in newFields.items(): field = dclass.getFieldByName(k) if not field: self.notify.error('Update for %s(%d) object contains invalid' ' field named %s' % (dclass.getName(), doId, k)) fieldPacker.rawPackUint16(field.getNumber()) if oldFields is not None: # Pack the old values: fieldPacker.beginPack(field) field.packArgs(fieldPacker, oldFields[k]) fieldPacker.endPack() fieldPacker.beginPack(field) field.packArgs(fieldPacker, v) fieldPacker.endPack() fieldCount += 1 # Generate and send the datagram: dg = PyDatagram() if oldFields is not None: ctx = self.air.getContext() self._callbacks[ctx] = callback if fieldCount == 1: dg.addServerHeader(databaseId, self.air.ourChannel, DBSERVER_OBJECT_SET_FIELD_IF_EQUALS) else: dg.addServerHeader(databaseId, self.air.ourChannel, DBSERVER_OBJECT_SET_FIELDS_IF_EQUALS) dg.addUint32(ctx) else: if fieldCount == 1: dg.addServerHeader(databaseId, self.air.ourChannel, DBSERVER_OBJECT_SET_FIELD) else: dg.addServerHeader(databaseId, self.air.ourChannel, DBSERVER_OBJECT_SET_FIELDS) dg.addUint32(doId) if fieldCount != 1: dg.addUint16(fieldCount) dg.appendData(fieldPacker.getString()) self.air.send(dg) if oldFields is None and callback is not None: # Why oh why did they ask for a callback if there's no oldFields? # Oh well, better honor their request: callback(None) def handleUpdateObjectResp(self, di, multi): ctx = di.getUint32() success = di.getUint8() if ctx not in self._callbacks: self.notify.warning('Received unexpected' ' DBSERVER_OBJECT_SET_FIELD(S)_IF_EQUALS_RESP' ' (ctx %d)' % (ctx)) return try: if success: if self._callbacks[ctx]: self._callbacks[ctx](None) return if not di.getRemainingSize(): # We failed due to other reasons. if self._callbacks[ctx]: return self._callbacks[ctx]({}) if multi: fieldCount = di.getUint16() else: fieldCount = 1 unpacker = DCPacker() unpacker.setUnpackData(di.getRemainingBytes()) fields = {} for x in xrange(fieldCount): fieldId = unpacker.rawUnpackInt16() field = self.air.getDcFile().getFieldByIndex(fieldId) if not field: self.notify.error('Received bad field %d in update' ' failure response message' % (fieldId)) unpacker.beginUnpack(field) fields[field.getName()] = field.unpackArgs(unpacker) unpacker.endUnpack() if self._callbacks[ctx]: self._callbacks[ctx](fields) finally: del self._callbacks[ctx] def handleDatagram(self, msgType, di): if msgType == DBSERVER_CREATE_OBJECT_RESP: self.handleCreateObjectResp(di) elif msgType == DBSERVER_OBJECT_GET_ALL_RESP: self.handleQueryObjectResp(di) elif msgType == DBSERVER_OBJECT_SET_FIELD_IF_EQUALS_RESP: self.handleUpdateObjectResp(di, False) elif msgType == DBSERVER_OBJECT_SET_FIELDS_IF_EQUALS_RESP: self.handleUpdateObjectResp(di, True)