def test_min_value_allocation(): allocator = UniqueIdAllocator(1, 5) for i in range(1, 5 + 1): assert allocator.allocate() == i assert allocator.allocate() == IndexEnd
def __init__(self, port=None, host=6660): self.active_clients = {} self.allocate_channel = UniqueIdAllocator(1000000, 1999999) PacketHandler.__init__(self) SocketHandler.__init__(self, port, host) self.configure()
def __init__(self): self._http_client = HTTPClient() max_http_requests = ConfigVariableInt('http-max-requests', 900).value self._request_allocator = UniqueIdAllocator(0, max_http_requests) self._poll_task = None self._requests = {}
def test_normal_allocation(): allocator = UniqueIdAllocator(0, 10) for i in range(10 + 1): assert allocator.allocate() == i assert allocator.allocate() == IndexEnd
def __init__(self, listenPort): BaseObjectManager.__init__(self, False) self.dcSuffix = 'AI' # The client that just sent us a message. self.clientSender = None self.listenPort = listenPort self.netSys = NetworkSystem() self.netCallbacks = NetworkCallbacks() self.netCallbacks.setCallback(self.__handleNetCallback) self.listenSocket = self.netSys.createListenSocket(listenPort) self.pollGroup = self.netSys.createPollGroup() self.clientIdAllocator = UniqueIdAllocator(0, 0xFFFF) self.objectIdAllocator = UniqueIdAllocator(0, 0xFFFF) self.numClients = 0 self.clientsByConnection = {} self.zonesToClients = {} self.snapshotMgr = FrameSnapshotManager() self.objectsByZoneId = {} base.setTickRate(sv_tickrate.getValue()) base.simTaskMgr.add(self.runFrame, "serverRunFrame", sort = -100)
def handleSetDoIdrange(self, di): self.doIdBase = di.getUint32() self.doIdLast = self.doIdBase + di.getUint32() self.doIdAllocator = UniqueIdAllocator(self.doIdBase, self.doIdLast - 1) self.ourChannel = self.doIdBase self.createReady()
def test_initial_reserve_id_exhaustion(): allocator = UniqueIdAllocator(1, 3) for i in range(1, 3 + 1): allocator.initial_reserve_id(i) assert allocator.allocate() == IndexEnd
def test_regular_is_allocated(): allocator = UniqueIdAllocator(1, 5) for i in range(1, 5 + 1): assert not allocator.is_allocated(i) for i in range(1, 5 + 1): assert allocator.is_allocated(allocator.allocate())
def test_free_unallocated(): allocator = UniqueIdAllocator(0, 2) assert allocator.allocate() == 0 assert allocator.free(0) for i in range(0, 2 + 1): assert not allocator.free(i)
class UnoGameAIPlayerMgr: def __init__(self, uno_game): self.game = uno_game self.players = [] self.idAllocator = UniqueIdAllocator(1, 4) def createPlayer(self, difficulty = None): npc_name = random.choice(NPCGlobals.NPC_DNA.keys()) player = UnoGameAIPlayer(npc_name, self.idAllocator.allocate(), self.game) player.generate() self.players.append(player) return player def removePlayer(self, player): self.players.remove(player) def getPlayers(self): return self.players def getPlayerByID(self, doId): for player in self.players: if player.getID() == doId: return player return None def generateHeadPanels(self): for avatar in self.players: gender = avatar.getGender() animal = avatar.getAnimal() head, color = avatar.getHeadStyle() r, g, b, a = color self.game.d_generateHeadPanel(gender, animal, head, [r, g, b], avatar.getID(), avatar.getName())
class UnoGameAIPlayerMgr: def __init__(self, uno_game): self.game = uno_game self.players = [] self.idAllocator = UniqueIdAllocator(1, 4) def createPlayer(self, difficulty=None): npc_name = random.choice(NPCGlobals.NPC_DNA.keys()) player = UnoGameAIPlayer(npc_name, self.idAllocator.allocate(), self.game) player.generate() self.players.append(player) return player def removePlayer(self, player): self.players.remove(player) def getPlayers(self): return self.players def getPlayerByID(self, doId): for player in self.players: if player.getID() == doId: return player return def generateHeadPanels(self): for avatar in self.players: gender = avatar.getGender() animal = avatar.getAnimal() head, color = avatar.getHeadStyle() r, g, b, a = color self.game.d_generateHeadPanel(gender, animal, head, [r, g, b], avatar.getID(), avatar.getName())
def __init__(self, baseChannel, stateServerChannel): CogInvasionInternalRepository.__init__( self, baseChannel, stateServerChannel, [ 'resources/phase_3/etc/direct.dc', 'resources/phase_3/etc/toon.dc' ], dcSuffix='AI') self.notify.setInfo(True) self.district = None self.zoneAllocator = UniqueIdAllocator(ZoneUtil.DynamicZonesBegin, ZoneUtil.DynamicZonesEnd) self.zoneDataStore = AIZoneDataStore() self.hoods = {} self.dnaStoreMap = {} self.dnaDataMap = {} self.districtNameMgr = self.generateGlobalObject( DO_ID_DISTRICT_NAME_MANAGER, 'DistrictNameManager') self.holidayMgr = self.generateGlobalObject(DO_ID_HOLIDAY_MANAGER, 'HolidayManager') self.uin = self.generateGlobalObject(DO_ID_UNIQUE_INTEREST_NOTIFIER, 'UniqueInterestNotifier') self.csm = self.generateGlobalObject(DO_ID_CLIENT_SERVICES_MANAGER, 'ClientServicesManager') self.statsMgr = self.generateGlobalObject(DO_ID_STATS_MANAGER, 'StatsManager') # Anything that is a DistributedAvatarAI (Toons, Suits, etc). # This is a per-zone list of avatars. self.avatars = {} self.numAvatars = 0 self.battleZones = {} if DO_SIMULATION: self.zonePhysics = {} self.bspLoader = BSPLoader() self.bspLoader.setAi(True) self.bspLoader.setMaterialsFile("phase_14/etc/materials.txt") #self.bspLoader.setTextureContentsFile("phase_14/etc/texturecontents.txt") #self.bspLoader.setServerEntityDispatcher(self) self.bspLoader.read( "phase_14/etc/sewer_entrance_room_indoors/sewer_entrance_room_indoors.bsp" ) PhysicsUtils.makeBulletCollFromGeoms(self.bspLoader.getResult(), enableNow=False)
def test_initial_reserve_id(): allocator = UniqueIdAllocator(1, 3) assert not allocator.is_allocated(2) allocator.initial_reserve_id(2) assert allocator.is_allocated(2) assert allocator.allocate() == 1 assert allocator.allocate() == 3 assert allocator.allocate() == IndexEnd
def __init__(self, baseChannel, stateServerChannel): CogInvasionInternalRepository.__init__( self, baseChannel, stateServerChannel, ['phase_3/etc/direct.dc', 'phase_3/etc/toon.dc'], dcSuffix='AI') self.notify.setInfo(True) self.district = None #self.aiWorld = AIWorld(render) # Used for cogs self.zoneAllocator = UniqueIdAllocator(CIGlobals.DynamicZonesBegin, CIGlobals.DynamicZonesEnd) self.zoneDataStore = AIZoneDataStore() self.hoods = {} self.dnaStoreMap = {} self.dnaDataMap = {} self.districtNameMgr = self.generateGlobalObject( DO_ID_DISTRICT_NAME_MANAGER, 'DistrictNameManager')
def __init__(self, air): DistributedObjectAI.__init__(self, air) self.air.roomZoneAllocator = UniqueIdAllocator( ZonesGlobals.ROOM_MIN_ID_ZONE, ZonesGlobals.ROOM_MAX_ID_ZONE) self.roomList = [] self.accept("Client-disconnect", self.handleClientDisconnect)
def test_free_initial_reserve_id(): allocator = UniqueIdAllocator(1, 3) allocator.initial_reserve_id(1) assert allocator.free(1) assert allocator.allocate() == 2 assert allocator.allocate() == 3 assert allocator.allocate() == 1 assert allocator.allocate() == IndexEnd
def test_fraction_used(): allocator = UniqueIdAllocator(1, 4) assert allocator.fraction_used() == 0 for fraction in (0.25, 0.5, 0.75, 1): allocator.allocate() assert allocator.fraction_used() == fraction assert allocator.allocate() == IndexEnd
def test_free(): allocator = UniqueIdAllocator(0, 0) assert allocator.allocate() == 0 assert allocator.is_allocated(0) assert allocator.free(0) assert not allocator.is_allocated(0)
def __init__(self, *args, **kwargs): ModClientRepository.__init__(self, *args, **kwargs) # Anything that is a DistributedAvatarAI (Toons, Suits, etc). # This is a per-zone list of avatars. self.avatars = {} self.numAvatars = 0 self.battleZones = {} self.districtId = 0 self.zoneDataStore = AIZoneDataStore() self.zoneAllocator = UniqueIdAllocator(DynamicZonesBegin, DynamicZonesEnd) self.netMessenger = NetMessenger(self) # We deal with attacks on the server side as well from src.coginvasion.attack.AttackManagerAI import AttackManagerAI self.attackMgr = AttackManagerAI()
def test_bounded_is_allocated(): allocator = UniqueIdAllocator(1, 5) for i in range(1, 5 + 1): assert allocator.is_allocated(allocator.allocate()) assert not allocator.is_allocated(0) # Out of range, left side assert not allocator.is_allocated(10) # Out of range, right side
def test_free_reallocation(): allocator = UniqueIdAllocator(1, 5) for i in range(1, 5 + 1): assert allocator.allocate() == i assert allocator.is_allocated(i) for i in range(1, 5 + 1): assert allocator.free(i) for i in range(1, 5 + 1): assert not allocator.is_allocated(i) assert allocator.allocate() == i assert allocator.allocate() == IndexEnd
def __init__(self): self.connection = None self.queue = queue.Queue() base_channel = 4000000 max_channels = 1000000 self.minChannel = base_channel self.maxChannel = base_channel + max_channels self.channelAllocator = UniqueIdAllocator(self.minChannel, self.maxChannel) self.zoneAllocator = UniqueIdAllocator(DynamicZonesBegin, DynamicZonesEnd) self._registedChannels = set() self.__contextCounter = 0 self.__callbacks = {} self.ourChannel = self.allocateChannel() self.doTable: Dict[int, 'DistributedObjectAI'] = {} self.zoneTable: Dict[int, set] = {} self.parentTable: Dict[int, set] = {} self.dcFile = parse_dc_file('toon.dc') self.currentSender = None self.loop = None self.net_thread = None self.hoods = None self.zoneDataStore = AIZoneData.AIZoneDataStore() self.vismap: Dict[int, Tuple[int]] = {} self.connected = Event()
def test_free_reallocation_mid(): allocator = UniqueIdAllocator(1, 5) for i in range(1, 5 + 1): assert allocator.allocate() == i assert allocator.is_allocated(i) assert allocator.free(2) assert allocator.free(3) assert allocator.allocate() == 2 assert allocator.allocate() == 3 assert allocator.allocate() == IndexEnd
def __init__(self): ShowBase.__init__(self) self.activeConnections = {} maxChannels = self.config.GetInt('max-channel-id', 1000000) self.channelAllocator = UniqueIdAllocator(0, 0+maxChannels-1) self.configManager = None self.logManager = LogManager() self.dcManager = DCManager() self.dcManager.readDCFile() self.notify.warning(str(self.dcManager.dclassesByName)) self.connectionManager = ConnectionManager() self.messageManager = MessageManager() self.doManager = DistributedObjectManager() self.interestManager = InterestManager()
class ClientRepository(ClientRepositoryBase): """ This is the open-source ClientRepository as provided by CMU. It communicates with the ServerRepository in this same directory. If you are looking for the VR Studio's implementation of the client repository, look to OTPClientRepository (elsewhere). """ notify = DirectNotifyGlobal.directNotify.newCategory("ClientRepository") # This is required by DoCollectionManager, even though it's not # used by this implementation. GameGlobalsId = 0 doNotDeallocateChannel = True def __init__(self, dcFileNames=None, dcSuffix='', connectMethod=None, threadedNet=None): ClientRepositoryBase.__init__(self, dcFileNames=dcFileNames, dcSuffix=dcSuffix, connectMethod=connectMethod, threadedNet=threadedNet) self.setHandleDatagramsInternally(False) base.finalExitCallbacks.append(self.shutdown) # The doId allocator. The CMU LAN server may choose to # send us a block of doIds. If it chooses to do so, then we # may create objects, using those doIds. self.doIdAllocator = None self.doIdBase = 0 self.doIdLast = 0 # The doIdBase of the client message currently being # processed. self.currentSenderId = None # Explicitly-requested interest zones. self.interestZones = [] def handleSetDoIdrange(self, di): self.doIdBase = di.getUint32() self.doIdLast = self.doIdBase + di.getUint32() self.doIdAllocator = UniqueIdAllocator(self.doIdBase, self.doIdLast - 1) self.ourChannel = self.doIdBase self.createReady() def createReady(self): # Now that we've got a doId range, we can safely generate new # distributed objects. messenger.send('createReady', taskChain='default') messenger.send(self.uniqueName('createReady'), taskChain='default') def handleRequestGenerates(self, di): # When new clients join the zone of an object, they need to hear # about it, so we send out all of our information about objects in # that particular zone. zone = di.getUint32() for obj in list(self.doId2do.values()): if obj.zoneId == zone: if (self.isLocalId(obj.doId)): self.resendGenerate(obj) def resendGenerate(self, obj): """ Sends the generate message again for an already-generated object, presumably to inform any newly-arrived clients of this object's current state. """ # get the list of "ram" fields that aren't # required. These are fields whose values should # persist even if they haven't been received # lately, so we have to re-broadcast these values # in case the new client hasn't heard their latest # values. extraFields = [] for i in range(obj.dclass.getNumInheritedFields()): field = obj.dclass.getInheritedField(i) if field.hasKeyword('broadcast') and field.hasKeyword( 'ram') and not field.hasKeyword('required'): if field.asMolecularField(): # It's a molecular field; this means # we have to pack the components. # Fortunately, we'll find those # separately through the iteration, so # we can ignore this field itself. continue extraFields.append(field.getName()) datagram = self.formatGenerate(obj, extraFields) self.send(datagram) def handleGenerate(self, di): self.currentSenderId = di.getUint32() zoneId = di.getUint32() classId = di.getUint16() doId = di.getUint32() # Look up the dclass dclass = self.dclassesByNumber[classId] distObj = self.doId2do.get(doId) if distObj and distObj.dclass == dclass: # We've already got this object. Probably this is just a # repeat-generate, synthesized for the benefit of someone # else who just entered the zone. Accept the new updates, # but don't make a formal generate. assert (self.notify.debug("performing generate-update for %s %s" % (dclass.getName(), doId))) dclass.receiveUpdateBroadcastRequired(distObj, di) dclass.receiveUpdateOther(distObj, di) return assert (self.notify.debug("performing generate for %s %s" % (dclass.getName(), doId))) dclass.startGenerate() # Create a new distributed object, and put it in the dictionary distObj = self.generateWithRequiredOtherFields(dclass, doId, di, 0, zoneId) dclass.stopGenerate() def allocateDoId(self): """ Returns a newly-allocated doId. Call freeDoId() when the object has been deleted. """ return self.doIdAllocator.allocate() def reserveDoId(self, doId): """ Removes the indicate doId from the available pool, as if it had been explicitly allocated. You may pass it to freeDoId() later if you wish. """ self.doIdAllocator.initialReserveId(doId) return doId def freeDoId(self, doId): """ Returns a doId back into the free pool for re-use. """ assert self.isLocalId(doId) self.doIdAllocator.free(doId) def storeObjectLocation(self, object, parentId, zoneId): # The CMU implementation doesn't use the DoCollectionManager # much. object.parentId = parentId object.zoneId = zoneId def createDistributedObject(self, className=None, distObj=None, zoneId=0, optionalFields=None, doId=None, reserveDoId=False): """ To create a DistributedObject, you must pass in either the name of the object's class, or an already-created instance of the class (or both). If you pass in just a class name (to the className parameter), then a default instance of the object will be created, with whatever parameters the default constructor supplies. Alternatively, if you wish to create some initial values different from the default, you can create the instance yourself and supply it to the distObj parameter, then that instance will be used instead. (It should be a newly-created object, not one that has already been manifested on the network or previously passed through createDistributedObject.) In either case, the new DistributedObject is returned from this method. This method will issue the appropriate network commands to make this object appear on all of the other clients. You should supply an initial zoneId in which to manifest the object. The fields marked "required" or "ram" will be broadcast to all of the other clients; if you wish to broadcast additional field values at this time as well, pass a list of field names in the optionalFields parameters. Normally, doId is None, to mean allocate a new doId for the object. If you wish to use a particular doId, pass it in here. If you also pass reserveDoId = True, this doId will be reserved from the allocation pool using self.reserveDoId(). You are responsible for ensuring this doId falls within the client's allowable doId range and has not already been assigned to another object. """ if not className: if not distObj: self.notify.error( "Must specify either a className or a distObj.") className = distObj.__class__.__name__ if doId is None: doId = self.allocateDoId() elif reserveDoId: self.reserveDoId(doId) dclass = self.dclassesByName.get(className) if not dclass: self.notify.error("Unknown distributed class: %s" % (distObj.__class__)) classDef = dclass.getClassDef() if classDef == None: self.notify.error("Could not create an undefined %s object." % (dclass.getName())) if not distObj: distObj = classDef(self) if not isinstance(distObj, classDef): self.notify.error("Object %s is not an instance of %s" % (distObj.__class__.__name__, classDef.__name__)) distObj.dclass = dclass distObj.doId = doId self.doId2do[doId] = distObj distObj.generateInit() distObj._retrieveCachedData() distObj.generate() distObj.setLocation(0, zoneId) distObj.announceGenerate() datagram = self.formatGenerate(distObj, optionalFields) self.send(datagram) return distObj def formatGenerate(self, distObj, extraFields): """ Returns a datagram formatted for sending the generate message for the indicated object. """ return distObj.dclass.clientFormatGenerateCMU(distObj, distObj.doId, distObj.zoneId, extraFields) def sendDeleteMsg(self, doId): datagram = PyDatagram() datagram.addUint16(OBJECT_DELETE_CMU) datagram.addUint32(doId) self.send(datagram) def sendDisconnect(self): if self.isConnected(): # Tell the game server that we're going: datagram = PyDatagram() # Add message type datagram.addUint16(CLIENT_DISCONNECT_CMU) # Send the message self.send(datagram) self.notify.info("Sent disconnect message to server") self.disconnect() self.stopHeartbeat() def setInterestZones(self, interestZoneIds): """ Changes the set of zones that this particular client is interested in hearing about. """ datagram = PyDatagram() # Add message type datagram.addUint16(CLIENT_SET_INTEREST_CMU) for zoneId in interestZoneIds: datagram.addUint32(zoneId) # send the message self.send(datagram) self.interestZones = interestZoneIds[:] def setObjectZone(self, distObj, zoneId): """ Moves the object into the indicated zone. """ distObj.b_setLocation(0, zoneId) assert distObj.zoneId == zoneId # Tell all of the clients monitoring the new zone that we've # arrived. self.resendGenerate(distObj) def sendSetLocation(self, doId, parentId, zoneId): datagram = PyDatagram() datagram.addUint16(OBJECT_SET_ZONE_CMU) datagram.addUint32(doId) datagram.addUint32(zoneId) self.send(datagram) def sendHeartbeat(self): datagram = PyDatagram() # Add message type datagram.addUint16(CLIENT_HEARTBEAT_CMU) # Send it! self.send(datagram) self.lastHeartbeat = globalClock.getRealTime() # This is important enough to consider flushing immediately # (particularly if we haven't run readerPollTask recently). self.considerFlush() def isLocalId(self, doId): """ Returns true if this doId is one that we're the owner of, false otherwise. """ return ((doId >= self.doIdBase) and (doId < self.doIdLast)) def haveCreateAuthority(self): """ Returns true if this client has been assigned a range of doId's it may use to create objects, false otherwise. """ return (self.doIdLast > self.doIdBase) def getAvatarIdFromSender(self): """ Returns the doIdBase of the client that originally sent the current update message. This is only defined when processing an update message or a generate message. """ return self.currentSenderId def handleDatagram(self, di): if self.notify.getDebug(): print("ClientRepository received datagram:") di.getDatagram().dumpHex(ostream) msgType = self.getMsgType() self.currentSenderId = None # These are the sort of messages we may expect from the public # Panda server. if msgType == SET_DOID_RANGE_CMU: self.handleSetDoIdrange(di) elif msgType == OBJECT_GENERATE_CMU: self.handleGenerate(di) elif msgType == OBJECT_UPDATE_FIELD_CMU: self.handleUpdateField(di) elif msgType == OBJECT_DISABLE_CMU: self.handleDisable(di) elif msgType == OBJECT_DELETE_CMU: self.handleDelete(di) elif msgType == REQUEST_GENERATES_CMU: self.handleRequestGenerates(di) else: self.handleMessageType(msgType, di) # If we're processing a lot of datagrams within one frame, we # may forget to send heartbeats. Keep them coming! self.considerHeartbeat() def handleMessageType(self, msgType, di): self.notify.error("unrecognized message type %s" % (msgType)) def handleUpdateField(self, di): # The CMU update message starts with an additional field, not # present in the Disney update message: the doIdBase of the # original sender. Extract that and call up to the parent. self.currentSenderId = di.getUint32() ClientRepositoryBase.handleUpdateField(self, di) def handleDisable(self, di): # Receives a list of doIds. while di.getRemainingSize() > 0: doId = di.getUint32() # We should never get a disable message for our own object. assert not self.isLocalId(doId) self.disableDoId(doId) def handleDelete(self, di): # Receives a single doId. doId = di.getUint32() self.deleteObject(doId) def deleteObject(self, doId): """ Removes the object from the client's view of the world. This should normally not be called directly except in the case of error recovery, since the server will normally be responsible for deleting and disabling objects as they go out of scope. After this is called, future updates by server on this object will be ignored (with a warning message). The object will become valid again the next time the server sends a generate message for this doId. This is not a distributed message and does not delete the object on the server or on any other client. """ if doId in self.doId2do: # If it is in the dictionary, remove it. obj = self.doId2do[doId] # Remove it from the dictionary del self.doId2do[doId] # Disable, announce, and delete the object itself... # unless delayDelete is on... obj.deleteOrDelay() if self.isLocalId(doId): self.freeDoId(doId) elif self.cache.contains(doId): # If it is in the cache, remove it. self.cache.delete(doId) if self.isLocalId(doId): self.freeDoId(doId) else: # Otherwise, ignore it self.notify.warning("Asked to delete non-existent DistObj " + str(doId)) def stopTrackRequestDeletedDO(self, *args): # No-op. Not entirely sure what this does on the VR Studio side. pass def sendUpdate(self, distObj, fieldName, args): """ Sends a normal update for a single field. """ dg = distObj.dclass.clientFormatUpdate(fieldName, distObj.doId, args) self.send(dg) def sendUpdateToChannel(self, distObj, channelId, fieldName, args): """ Sends a targeted update of a single field to a particular client. The top 32 bits of channelId is ignored; the lower 32 bits should be the client Id of the recipient (i.e. the client's doIdbase). The field update will be sent to the indicated client only. The field must be marked clsend or p2p, and may not be marked broadcast. """ datagram = distObj.dclass.clientFormatUpdate(fieldName, distObj.doId, args) dgi = PyDatagramIterator(datagram) # Reformat the packed datagram to change the message type and # add the target id. dgi.getUint16() dg = PyDatagram() dg.addUint16(CLIENT_OBJECT_UPDATE_FIELD_TARGETED_CMU) dg.addUint32(channelId & 0xffffffff) dg.appendData(dgi.getRemainingBytes()) self.send(dg)
def __init__(self, uno_game): self.game = uno_game self.players = [] self.idAllocator = UniqueIdAllocator(1, 4)
class AIRepository: def __init__(self): self.connection = None self.queue = queue.Queue() base_channel = 4000000 max_channels = 1000000 self.minChannel = base_channel self.maxChannel = base_channel + max_channels self.channelAllocator = UniqueIdAllocator(self.minChannel, self.maxChannel) self.zoneAllocator = UniqueIdAllocator(DynamicZonesBegin, DynamicZonesEnd) self._registedChannels = set() self.__contextCounter = 0 self.__callbacks = {} self.ourChannel = self.allocateChannel() self.doTable: Dict[int, 'DistributedObjectAI'] = {} self.zoneTable: Dict[int, set] = {} self.parentTable: Dict[int, set] = {} self.dcFile = parse_dc_file('toon.dc') self.currentSender = None self.loop = None self.net_thread = None self.hoods = None self.zoneDataStore = AIZoneData.AIZoneDataStore() self.vismap: Dict[int, Tuple[int]] = {} self.connected = Event() def run(self): self.net_thread = Thread(target=self.__event_loop) self.net_thread.start() self.connected.wait() self.createObjects() def _on_net_except(self, loop, context): print('Error on networking thread: %s' % context['message']) self.loop.stop() simbase.stop() def __event_loop(self): self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) self.loop.set_exception_handler(self._on_net_except) self.loop.run_until_complete( self.loop.create_connection(self._on_connect, '127.0.0.1', 46668)) self.loop.run_forever() def _on_connect(self): self.connection = AIProtocol(self) return self.connection def readUntilEmpty(self, task): while True: try: dg = self.queue.get(timeout=0.05) except queue.Empty: break else: self.handleDatagram(dg) return task.cont def handleDatagram(self, dg): dgi = dg.iterator() recipient_count = dgi.get_uint8() recipients = [dgi.get_channel() for _ in range(recipient_count)] self.currentSender = dgi.get_channel() msg_type = dgi.get_uint16() if msg_type == STATESERVER_OBJECT_ENTER_AI_RECV: if self.currentSender == self.ourChannel: return self.handleObjEntry(dgi) elif msg_type == STATESERVER_OBJECT_DELETE_RAM: self.handleObjExit(dgi) elif msg_type == STATESERVER_OBJECT_LEAVING_AI_INTEREST: pass elif msg_type == STATESERVER_OBJECT_CHANGE_ZONE: self.handleChangeZone(dgi) elif msg_type == STATESERVER_OBJECT_UPDATE_FIELD: if self.currentSender == self.ourChannel: return self.handleUpdateField(dgi) else: print('Unhandled msg type: ', msg_type) def handleChangeZone(self, dgi): do_id = dgi.get_uint32() new_parent = dgi.get_uint32() new_zone = dgi.get_uint32() # Should we only change location if the old location matches? old_parent = dgi.get_uint32() old_zone = dgi.get_uint32() self.doTable[do_id].location = (new_parent, new_zone) self.storeLocation(do_id, old_parent, old_zone, new_parent, new_zone) def storeLocation(self, doId, oldParent, oldZone, newParent, newZone): if not doId: return obj = self.doTable.get(doId) oldParentObj = self.doTable.get(oldParent) newParentObj = self.doTable.get(newParent) if oldParent != newParent and oldParentObj: oldParentObj.handleChildLeave(obj, oldZone) if oldParent and oldParent in self.parentTable and doId in self.parentTable[ oldParent]: self.parentTable[oldParent].remove(doId) if oldZone != newZone and oldParentObj: oldParentObj.handleChildLeaveZone(obj, oldZone) if oldZone and oldZone in self.zoneTable and doId in self.zoneTable[ oldZone]: self.zoneTable[oldZone].remove(doId) if newZone: self.zoneTable.setdefault(newZone, set()) self.zoneTable[newZone].add(doId) if newParent: self.parentTable.setdefault(newParent, set()) self.parentTable[newParent].add(doId) if newParent != oldParent and newParentObj: newParentObj.handleChildArrive(obj, newZone) if newZone != oldZone and newParentObj: newParentObj.handleChildArriveZone(obj, newZone) def sendLocation(self, do_id, old_parent: int, old_zone: int, new_parent: int, new_zone: int): dg = Datagram() dg.add_server_header([do_id], self.ourChannel, STATESERVER_OBJECT_SET_ZONE) dg.add_uint32(new_parent) dg.add_uint32(new_zone) dg.add_uint32(old_parent) dg.add_uint32(old_zone) self.send(dg) @staticmethod def isClientChannel(channel): return config['ClientAgent.MIN_CHANNEL'] <= channel <= config[ 'ClientAgent.MAX_CHANNEL'] def setInterest(self, client_channel, handle, context, parent_id, zones): dg = Datagram() dg.add_server_header([client_channel], self.ourChannel, CLIENT_AGENT_SET_INTEREST) dg.add_uint16(handle) dg.add_uint32(context) dg.add_uint32(parent_id) for zone in zones: dg.add_uint32(zone) self.send(dg) def removeInterest(self, client_channel, handle, context): dg = Datagram() dg.add_server_header([client_channel], self.ourChannel, CLIENT_AGENT_REMOVE_INTEREST) dg.add_uint16(handle) dg.add_uint32(context) self.send(dg) def handleUpdateField(self, dgi): do_id = dgi.get_uint32() field_number = dgi.get_uint16() # TODO: security check here for client senders. field = self.dcFile.fields[field_number]() self.currentSender = self.currentSender do = self.doTable[do_id] try: field.receive_update(do, dgi) except Exception as e: print( f'failed to handle field update: <{field}> from {self.currentAvatarSender}' ) import traceback traceback.print_exc() dgi.seek(0) print('datagram:', dgi.remaining_bytes()) @property def currentAvatarSender(self): return getAvatarIDFromChannel(self.currentSender) def handleObjEntry(self, dgi): do_id = dgi.get_uint32() parent_id = dgi.get_uint32() zone_id = dgi.get_uint32() dc_id = dgi.get_uint16() dclass = self.dcFile.classes[dc_id] if do_id in self.doTable: # This is a response from a generate by us. do = self.doTable[do_id] do.queueUpdates = False while do.updateQueue: dg = do.updateQueue.popleft() self.send(dg) return if dclass.name == 'DistributedToon': from .toon.DistributedToonAI import DistributedToonAI obj = DistributedToonAI(self) # Don't queue updates as this object was generated by the stateserver. obj.queueUpdates = False obj.do_id = do_id obj.parentId = parent_id obj.zoneId = zone_id dclass.receive_update_all_required(obj, dgi) self.doTable[obj.do_id] = obj self.storeLocation(do_id, 0, 0, parent_id, zone_id) obj.announceGenerate() else: print('unknown object entry: %s' % dclass.name) def handleObjExit(self, dgi): doId = dgi.get_uint32() try: do = self.doTable.pop(doId) except KeyError: print(f'Received delete for unknown object: {doId}!') return do.delete() def context(self): self.__contextCounter = (self.__contextCounter + 1) & 0xFFFFFFFF return self.__contextCounter def allocateChannel(self): return self.channelAllocator.allocate() def deallocateChannel(self, channel): self.channelAllocator.free(channel) def registerForChannel(self, channel): if channel in self._registedChannels: return self._registedChannels.add(channel) dg = Datagram() dg.add_server_control_header(CONTROL_SET_CHANNEL) dg.add_channel(channel) self.send(dg) def unregisterForChannel(self, channel): if channel not in self._registedChannels: return self._registedChannels.remove(channel) dg = Datagram() dg.add_server_control_header(CONTROL_REMOVE_CHANNEL) dg.add_channel(channel) self.send(dg) def send(self, dg): self.connection.send_datagram(dg) def generateWithRequired(self, do, parent_id, zone_id, optional=()): do_id = self.allocateChannel() self.generateWithRequiredAndId(do, do_id, parent_id, zone_id, optional) def generateWithRequiredAndId(self, do, do_id, parent_id, zone_id, optional=()): do.do_id = do_id self.doTable[do_id] = do dg = do.dclass.ai_format_generate(do, do_id, parent_id, zone_id, STATESERVERS_CHANNEL, self.ourChannel, optional) self.send(dg) do.location = (parent_id, zone_id) do.generate() do.announceGenerate() def createObjects(self): self.registerForChannel(self.ourChannel) from .Objects import ToontownDistrictAI, ToontownDistrictStatsAI, DistributedInGameNewsMgrAI, NewsManagerAI, FriendManagerAI from .TimeManagerAI import TimeManagerAI self.district = ToontownDistrictAI(self) self.district.name = 'Nutty River' self.generateWithRequired(self.district, OTP_DO_ID_TOONTOWN, OTP_ZONE_ID_DISTRICTS) post_remove = Datagram() post_remove.add_server_control_header(CONTROL_ADD_POST_REMOVE) post_remove.add_server_header([ STATESERVERS_CHANNEL, ], self.ourChannel, STATESERVER_SHARD_REST) post_remove.add_channel(self.ourChannel) self.send(post_remove) dg = Datagram() dg.add_server_header([STATESERVERS_CHANNEL], self.ourChannel, STATESERVER_ADD_AI_RECV) dg.add_uint32(self.district.do_id) dg.add_channel(self.ourChannel) self.send(dg) stats = ToontownDistrictStatsAI(self) stats.settoontownDistrictId(self.district.do_id) self.generateWithRequired(stats, OTP_DO_ID_TOONTOWN, OTP_ZONE_ID_DISTRICTS_STATS) dg = Datagram() dg.add_server_header([STATESERVERS_CHANNEL], self.ourChannel, STATESERVER_ADD_AI_RECV) dg.add_uint32(stats.do_id) dg.add_channel(self.ourChannel) self.send(dg) self.timeManager = TimeManagerAI(self) self.timeManager.generateWithRequired(OTP_ZONE_ID_MANAGEMENT) self.ingameNewsMgr = DistributedInGameNewsMgrAI(self) self.ingameNewsMgr.generateWithRequired(OTP_ZONE_ID_MANAGEMENT) self.newsManager = NewsManagerAI(self) self.newsManager.generateWithRequired(OTP_ZONE_ID_MANAGEMENT) self.friendManager = FriendManagerAI(self) self.friendManager.generateGlobalObject(OTP_ZONE_ID_MANAGEMENT) self.loadZones() self.district.b_setAvailable(True) def loadZones(self): from ai.hood.HoodDataAI import DDHoodAI, TTHoodAI, BRHoodAI, MMHoodAI, DGHoodAI, DLHoodAI self.hoods = [ DDHoodAI(self), TTHoodAI(self), BRHoodAI(self), MMHoodAI(self), DGHoodAI(self), DLHoodAI(self) ] for hood in self.hoods: print(f'{hood.__class__.__name__} starting up...') hood.startup() print('All zones loaded.') def requestDelete(self, do): dg = Datagram() dg.add_server_header([do.do_id], self.ourChannel, STATESERVER_OBJECT_DELETE_RAM) dg.add_uint32(do.do_id) self.send(dg) @staticmethod def getAvatarExitEvent(avId): return 'do-deleted-%d' % avId def allocateZone(self): return self.zoneAllocator.allocate() def deallocateZone(self, zone): self.zoneAllocator.free(zone) def getAvatarDisconnectReason(self, avId): return self.timeManager.disconnectCodes.get(avId)
class HTTPRest(object): """ Primary class for handling GET/POST requests with Panda's HTTPClient object """ notify = directNotify.newCategory('http-rest') def __init__(self): self._http_client = HTTPClient() max_http_requests = ConfigVariableInt('http-max-requests', 900).value self._request_allocator = UniqueIdAllocator(0, max_http_requests) self._poll_task = None self._requests = {} def setup(self): """ Performs setup operations on the http rest object """ self._poll_task = taskMgr.add( self.__update, '%s-update-task' % self.__class__.__name__) def __update(self, task): """ Performs update operations on the PandaHTTP instance """ for request_id in list(self._requests): # Check that this id is still valid if request_id not in self._requests: continue request = self._requests[request_id] request.update() return task.cont def destroy(self): """ Performs destruction operations on the PandaHTTP instance """ if self._poll_task: taskMgr.remove(self._poll_task) for request_id in list(self._requests): self.remove_request(request_id) def remove_request(self, request_id): """ Removes the request id form the PandaHTTP request list """ if request_id not in self._requests: return self._request_allocator.free(request_id) del self._requests[request_id] def get_request_status(self, request_id): """ Returns the requests current status """ return not request_id in self._requests def get_request(self, request_id): """ Returns the requested request if its present """ return self._requests.get(request_id, None) def perform_get_request(self, url, headers={}, content_type=None, callback=None): """ Performs an HTTP restful GET call and returns the request's unique itentifier """ self.notify.debug('Sending GET request: %s' % url) request_channel = self._http_client.make_channel(True) if content_type != None: request_channel.set_content_type(content_type) for header_key in headers: header_value = headers[header_key] request_channel.send_extra_header(header_key, header_value) request_channel.begin_get_document(DocumentSpec(url)) ram_file = Ramfile() request_channel.download_to_ram(ram_file, False) request_id = self._request_allocator.allocate() http_request = HTTPRequest(self, request_id, request_channel, ram_file, callback) self._requests[request_id] = http_request return request_id def perform_json_get_request(self, url, headers={}, callback=None): """ """ def json_wrapper(data): """ Wraps the callback to automatically perform json.load on the resulting data """ try: data = json.loads(data) except: self.notify.warning('Received invalid JSON results: %s' % data) callback(data) return self.perform_get_request(url=url, content_type='application/json', headers=headers, callback=json_wrapper) def perform_post_request(self, url, headers={}, content_type=None, post_body={}, callback=None): """ """ self.notify.debug('Sending POST request: %s' % url) request_channel = self._http_client.make_channel(True) if content_type != None: request_channel.set_content_type(content_type) for header_key in headers: header_value = headers[header_key] request_channel.send_extra_header(header_key, header_value) post_body = json.dumps(post_body) request_channel.begin_post_form(DocumentSpec(url), post_body) ram_file = Ramfile() request_channel.download_to_ram(ram_file, False) request_id = self._request_allocator.allocate() http_request = HTTPRequest(self, request_id, request_channel, ram_file, callback) self._requests[request_id] = http_request return request_id def perform_json_post_request(self, url, headers={}, post_body={}, callback=None): """ """ def json_wrapper(data): """ Wraps the callback to automatically perform json.load on the resulting data """ try: data = json.loads(data) except: self.notify.warning('Received invalid JSON results: %s' % data) callback(data) return self.perform_post_request(url=url, content_type='application/json', headers=headers, post_body=post_body, callback=json_wrapper)
class CogInvasionAIRepository(CogInvasionInternalRepository): notify = directNotify.newCategory("CogInvasionAIRepository") def __init__(self, baseChannel, stateServerChannel): CogInvasionInternalRepository.__init__( self, baseChannel, stateServerChannel, [ 'resources/phase_3/etc/direct.dc', 'resources/phase_3/etc/toon.dc' ], dcSuffix='AI') self.notify.setInfo(True) self.district = None self.zoneAllocator = UniqueIdAllocator(ZoneUtil.DynamicZonesBegin, ZoneUtil.DynamicZonesEnd) self.zoneDataStore = AIZoneDataStore() self.hoods = {} self.dnaStoreMap = {} self.dnaDataMap = {} self.districtNameMgr = self.generateGlobalObject( DO_ID_DISTRICT_NAME_MANAGER, 'DistrictNameManager') self.holidayMgr = self.generateGlobalObject(DO_ID_HOLIDAY_MANAGER, 'HolidayManager') self.uin = self.generateGlobalObject(DO_ID_UNIQUE_INTEREST_NOTIFIER, 'UniqueInterestNotifier') self.csm = self.generateGlobalObject(DO_ID_CLIENT_SERVICES_MANAGER, 'ClientServicesManager') self.statsMgr = self.generateGlobalObject(DO_ID_STATS_MANAGER, 'StatsManager') # Anything that is a DistributedAvatarAI (Toons, Suits, etc). # This is a per-zone list of avatars. self.avatars = {} self.numAvatars = 0 self.battleZones = {} if DO_SIMULATION: self.zonePhysics = {} self.bspLoader = BSPLoader() self.bspLoader.setAi(True) self.bspLoader.setMaterialsFile("phase_14/etc/materials.txt") #self.bspLoader.setTextureContentsFile("phase_14/etc/texturecontents.txt") #self.bspLoader.setServerEntityDispatcher(self) self.bspLoader.read( "phase_14/etc/sewer_entrance_room_indoors/sewer_entrance_room_indoors.bsp" ) PhysicsUtils.makeBulletCollFromGeoms(self.bspLoader.getResult(), enableNow=False) def getBattleZone(self, zoneId): return self.battleZones.get(zoneId, None) def getPhysicsWorld(self, zoneId): bz = self.getBattleZone(zoneId) if bz: return bz.physicsWorld return None def __update(self, task): # Do a simulation of having battles going on dt = globalClock.getDt() # For each simulated battle zone for zonePhysics in self.zonePhysics.values(): # How many suits are casting rays numSuits = 15 #random.randint(4, 15) zonePhysics.doPhysics(dt, 1, 0.016) for i in xrange(numSuits): zonePhysics.rayTestClosest((0, 0, 0), (0, 20, 0)) return task.cont def addAvatar(self, avatar, zoneId=None): if zoneId is None: if hasattr(avatar, 'getZoneId'): zoneId = avatar.getZoneId() else: zoneId = avatar.zoneId if not zoneId in self.avatars: self.avatars[zoneId] = [] self.avatars[zoneId].append(avatar) self.numAvatars += 1 if zoneId in self.battleZones: print "Adding avatar to battle zone at {0}".format(zoneId) avatar.battleZone = self.battleZones[zoneId] avatar.addToPhysicsWorld(avatar.battleZone.physicsWorld) if DO_SIMULATION: # Setup simulation physics environment for each # zone, we will pretend they are battle zones if zoneId not in self.zonePhysics: print "Making phys world in zone {0}".format(zoneId) physicsWorld = BulletWorld() # Panda units are in feet, so the gravity is 32 feet per second, # not 9.8 meters per second. physicsWorld.setGravity(Vec3(0, 0, -32.1740)) # Add the static collision world (worldspawn faces) PhysicsUtils.attachBulletNodes(self.bspLoader.getResult(), physicsWorld) # Add objects that would be dynamic # (Avatar capsules, weapon collisions, door collisions, etc) dynObjects = 50 #random.randint(15, 50) for i in xrange(dynObjects): box = BulletCapsuleShape(0.5, 1.0, ZUp) rbnode = BulletRigidBodyNode("testrb") rbnode.setKinematic(True) rbnode.setDeactivationEnabled(True) rbnode.setDeactivationTime(5.0) rbnode.addShape(box, TransformState.makePos((0, 0, 0))) NodePath(rbnode).setPos(random.uniform(-100, 100), random.uniform(-100, 100), random.uniform(-50, 50)) physicsWorld.attach(rbnode) self.zonePhysics[zoneId] = physicsWorld def removeAvatar(self, avatar): removed = False zoneOfAv = 0 for zoneId in self.avatars.keys(): if avatar in self.avatars[zoneId]: self.avatars[zoneId].remove(avatar) zoneOfAv = zoneId removed = True break if removed: self.numAvatars -= 1 if avatar.battleZone: print "Removing avatar from battle zone at {0}".format(zoneOfAv) avatar.removeFromPhysicsWorld(avatar.battleZone.physicsWorld) avatar.battleZone = None def handleCrash(self, e): raise e def gotDistrictName(self, name): self.notify.info("This District will be called: %s" % name) self.districtId = self.allocateChannel() self.notify.info("Generating shard; id = %s" % self.districtId) self.district = DistributedDistrictAI(self) self.district.generateWithRequiredAndId(self.districtId, self.getGameDoId(), 3) self.notify.info("Claiming ownership; channel = %s" % self.districtId) self.claimOwnership(self.districtId) self.notify.info('Setting District name %s' % name) self.district.b_setDistrictName(name) self.district.b_setPopRecord(0) self.notify.info("Generating time manager...") self.timeManager = TimeManagerAI(self) self.timeManager.generateWithRequired(2) self.areas = [ TTHoodAI, BRHoodAI, DLHoodAI, MLHoodAI, DGHoodAI, DDHoodAI, CTHoodAI, MGHoodAI ] self.areaIndex = 0 taskMgr.add(self.makeAreasTask, 'makeAreasTask') def makeAreasTask(self, task): if self.areaIndex >= len(self.areas): self.done() return task.done area = self.areas[self.areaIndex] area(self) self.areaIndex += 1 task.delayTime = 0.5 return task.again def done(self): self.notify.info("Setting shard available.") self.district.b_setAvailable(1) self.notify.info("Done.") if DO_SIMULATION: print "There are {0} avatars.".format(self.numAvatars) print "There are {0} zones.".format(len(self.zonePhysics.keys())) taskMgr.add(self.__update, "AIUpdate") def noDistrictNames(self): self.notify.error( "Cannot create District: There are no available names!") def handleConnected(self): CogInvasionInternalRepository.handleConnected(self) self.districtNameMgr.d_requestDistrictName() self.holidayMgr.d_srvRequestHoliday() def toonsAreInZone(self, zoneId): numToons = 0 for obj in self.doId2do.values(): if obj.__class__.__name__ in ToonClasses: if obj.zoneId == zoneId: numToons += 1 return numToons > 0 def shutdown(self): if DO_SIMULATION: taskMgr.remove("AIUpdate") for hood in self.hoods.values(): hood.shutdown() if self.timeManager: self.timeManager.requestDelete() self.timeManager = None if self.district: self.district.b_setAvailable(0) self.district.requestDelete() def claimOwnership(self, channel): dg = PyDatagram() dg.addServerHeader(channel, self.ourChannel, STATESERVER_OBJECT_SET_AI) dg.addChannel(self.ourChannel) self.send(dg) def allocateZone(self): return self.zoneAllocator.allocate() def deallocateZone(self, zone): self.zoneAllocator.free(zone) def getZoneDataStore(self): return self.zoneDataStore
class CAHandler(PacketHandler, SocketHandler): logger = Logger("ca_handler") def __init__(self, port=None, host=6660): self.active_clients = {} self.allocate_channel = UniqueIdAllocator(1000000, 1999999) PacketHandler.__init__(self) SocketHandler.__init__(self, port, host) self.configure() def configure(self): SocketHandler.connect_socket(self) self.logger.info("handler online") def setup_new_connection(self, connection): channel = self.allocate_channel.allocate() self.active_clients[channel] = connection self.register_channel(channel) def register_channel(self, channel): dg = PyDatagram() dg.addUint16(msg_types.CONTROL_SET_CHANNEL) dg.addUint64(channel) self.cWriter.send(dg, self.connection) def unregister_channel(self, channel): dg = PyDatagram() dg.addUint16(msg_types.CONTROL_REMOVE_CHANNEL) dg.addUint64(channel) self.cWriter.send(dg, self.connection) def handle_packet(self, dg): connection = dg.getConnection() dgi = PyDatagramIterator(dg) msg = dgi.getUint16() # begin handling messages here if msg == msg_types.CLIENT_HEARTBEAT: self.handle_client_heartbeat(dgi) elif msg == msg_types.CLIENT_LOGIN_2: self.handle_client_login(dgi, connection) elif msg == msg_types.CLIENT_DISCONNECT: self.handle_client_disconnect(dgi, connection) else: self.logger.warn("received unimplemented message type - %d" % msg) def handle_client_heartbeat(self, dgi): # TODO - handle and keep track of client heartbeats self.logger.debug("received client heartbeat") def handle_client_login(self, dgi, connection): # TODO - dynamically set user info from the DBServer token = dgi.getString() self.logger.debug("logging in user %s" % token) # TODO - sanity checks serverVersion = dgi.getString() hashVal = dgi.getInt32() dg = PyDatagram() dg.addUint16(msg_types.CLIENT_LOGIN_2_RESP) dg.addUint8(0) # returnCode dg.addString("") # errorString # begin account details dg.addString(token) # username dg.addUint8(0) # secretChatAllowed dg.addUint32(int(time.time())) # sec dg.addUint32(int(time.clock())) # usec dg.addUint8(1) # isPaid self.cWriter.send(dg, connection) def handle_client_disconnect(self, dgi, connection): # TODO - unregister channels self.logger.warn("client from %s has disconnected" % str(connection))
def test_inclusive_allocation(): allocator = UniqueIdAllocator(0, 0) assert allocator.allocate() == 0 assert allocator.allocate() == IndexEnd
class ClientRepository(ClientRepositoryBase): """ This is the open-source ClientRepository as provided by CMU. It communicates with the ServerRepository in this same directory. If you are looking for the VR Studio's implementation of the client repository, look to OTPClientRepository (elsewhere). """ notify = DirectNotifyGlobal.directNotify.newCategory("ClientRepository") # This is required by DoCollectionManager, even though it's not # used by this implementation. GameGlobalsId = 0 doNotDeallocateChannel = True def __init__(self, dcFileNames = None, dcSuffix = '', connectMethod = None, threadedNet = None): ClientRepositoryBase.__init__(self, dcFileNames = dcFileNames, dcSuffix = dcSuffix, connectMethod = connectMethod, threadedNet = threadedNet) self.setHandleDatagramsInternally(False) base.finalExitCallbacks.append(self.shutdown) # The doId allocator. The CMU LAN server may choose to # send us a block of doIds. If it chooses to do so, then we # may create objects, using those doIds. self.doIdAllocator = None self.doIdBase = 0 self.doIdLast = 0 # The doIdBase of the client message currently being # processed. self.currentSenderId = None # Explicitly-requested interest zones. self.interestZones = [] def handleSetDoIdrange(self, di): self.doIdBase = di.getUint32() self.doIdLast = self.doIdBase + di.getUint32() self.doIdAllocator = UniqueIdAllocator(self.doIdBase, self.doIdLast - 1) self.ourChannel = self.doIdBase self.createReady() def createReady(self): # Now that we've got a doId range, we can safely generate new # distributed objects. messenger.send('createReady', taskChain = 'default') messenger.send(self.uniqueName('createReady'), taskChain = 'default') def handleRequestGenerates(self, di): # When new clients join the zone of an object, they need to hear # about it, so we send out all of our information about objects in # that particular zone. zone = di.getUint32() for obj in self.doId2do.values(): if obj.zoneId == zone: if (self.isLocalId(obj.doId)): self.resendGenerate(obj) def resendGenerate(self, obj): """ Sends the generate message again for an already-generated object, presumably to inform any newly-arrived clients of this object's current state. """ # get the list of "ram" fields that aren't # required. These are fields whose values should # persist even if they haven't been received # lately, so we have to re-broadcast these values # in case the new client hasn't heard their latest # values. extraFields = [] for i in range(obj.dclass.getNumInheritedFields()): field = obj.dclass.getInheritedField(i) if field.hasKeyword('broadcast') and field.hasKeyword('ram') and not field.hasKeyword('required'): if field.asMolecularField(): # It's a molecular field; this means # we have to pack the components. # Fortunately, we'll find those # separately through the iteration, so # we can ignore this field itself. continue extraFields.append(field.getName()) datagram = self.formatGenerate(obj, extraFields) self.send(datagram) def handleGenerate(self, di): self.currentSenderId = di.getUint32() zoneId = di.getUint32() classId = di.getUint16() doId = di.getUint32() # Look up the dclass dclass = self.dclassesByNumber[classId] distObj = self.doId2do.get(doId) if distObj and distObj.dclass == dclass: # We've already got this object. Probably this is just a # repeat-generate, synthesized for the benefit of someone # else who just entered the zone. Accept the new updates, # but don't make a formal generate. assert(self.notify.debug("performing generate-update for %s %s" % (dclass.getName(), doId))) dclass.receiveUpdateBroadcastRequired(distObj, di) dclass.receiveUpdateOther(distObj, di) return assert(self.notify.debug("performing generate for %s %s" % (dclass.getName(), doId))) dclass.startGenerate() # Create a new distributed object, and put it in the dictionary distObj = self.generateWithRequiredOtherFields(dclass, doId, di, 0, zoneId) dclass.stopGenerate() def allocateDoId(self): """ Returns a newly-allocated doId. Call freeDoId() when the object has been deleted. """ return self.doIdAllocator.allocate() def reserveDoId(self, doId): """ Removes the indicate doId from the available pool, as if it had been explicitly allocated. You may pass it to freeDoId() later if you wish. """ self.doIdAllocator.initialReserveId(doId) return doId def freeDoId(self, doId): """ Returns a doId back into the free pool for re-use. """ assert self.isLocalId(doId) self.doIdAllocator.free(doId) def storeObjectLocation(self, object, parentId, zoneId): # The CMU implementation doesn't use the DoCollectionManager # much. object.parentId = parentId object.zoneId = zoneId def createDistributedObject(self, className = None, distObj = None, zoneId = 0, optionalFields = None, doId = None, reserveDoId = False): """ To create a DistributedObject, you must pass in either the name of the object's class, or an already-created instance of the class (or both). If you pass in just a class name (to the className parameter), then a default instance of the object will be created, with whatever parameters the default constructor supplies. Alternatively, if you wish to create some initial values different from the default, you can create the instance yourself and supply it to the distObj parameter, then that instance will be used instead. (It should be a newly-created object, not one that has already been manifested on the network or previously passed through createDistributedObject.) In either case, the new DistributedObject is returned from this method. This method will issue the appropriate network commands to make this object appear on all of the other clients. You should supply an initial zoneId in which to manifest the object. The fields marked "required" or "ram" will be broadcast to all of the other clients; if you wish to broadcast additional field values at this time as well, pass a list of field names in the optionalFields parameters. Normally, doId is None, to mean allocate a new doId for the object. If you wish to use a particular doId, pass it in here. If you also pass reserveDoId = True, this doId will be reserved from the allocation pool using self.reserveDoId(). You are responsible for ensuring this doId falls within the client's allowable doId range and has not already been assigned to another object. """ if not className: if not distObj: self.notify.error("Must specify either a className or a distObj.") className = distObj.__class__.__name__ if doId is None: doId = self.allocateDoId() elif reserveDoId: self.reserveDoId(doId) dclass = self.dclassesByName.get(className) if not dclass: self.notify.error("Unknown distributed class: %s" % (distObj.__class__)) classDef = dclass.getClassDef() if classDef == None: self.notify.error("Could not create an undefined %s object." % ( dclass.getName())) if not distObj: distObj = classDef(self) if not isinstance(distObj, classDef): self.notify.error("Object %s is not an instance of %s" % (distObj.__class__.__name__, classDef.__name__)) distObj.dclass = dclass distObj.doId = doId self.doId2do[doId] = distObj distObj.generateInit() distObj._retrieveCachedData() distObj.generate() distObj.setLocation(0, zoneId) distObj.announceGenerate() datagram = self.formatGenerate(distObj, optionalFields) self.send(datagram) return distObj def formatGenerate(self, distObj, extraFields): """ Returns a datagram formatted for sending the generate message for the indicated object. """ return distObj.dclass.clientFormatGenerateCMU(distObj, distObj.doId, distObj.zoneId, extraFields) def sendDeleteMsg(self, doId): datagram = PyDatagram() datagram.addUint16(OBJECT_DELETE_CMU) datagram.addUint32(doId) self.send(datagram) def sendDisconnect(self): if self.isConnected(): # Tell the game server that we're going: datagram = PyDatagram() # Add message type datagram.addUint16(CLIENT_DISCONNECT_CMU) # Send the message self.send(datagram) self.notify.info("Sent disconnect message to server") self.disconnect() self.stopHeartbeat() def setInterestZones(self, interestZoneIds): """ Changes the set of zones that this particular client is interested in hearing about. """ datagram = PyDatagram() # Add message type datagram.addUint16(CLIENT_SET_INTEREST_CMU) for zoneId in interestZoneIds: datagram.addUint32(zoneId) # send the message self.send(datagram) self.interestZones = interestZoneIds[:] def setObjectZone(self, distObj, zoneId): """ Moves the object into the indicated zone. """ distObj.b_setLocation(0, zoneId) assert distObj.zoneId == zoneId # Tell all of the clients monitoring the new zone that we've # arrived. self.resendGenerate(distObj) def sendSetLocation(self, doId, parentId, zoneId): datagram = PyDatagram() datagram.addUint16(OBJECT_SET_ZONE_CMU) datagram.addUint32(doId) datagram.addUint32(zoneId) self.send(datagram) def sendHeartbeat(self): datagram = PyDatagram() # Add message type datagram.addUint16(CLIENT_HEARTBEAT_CMU) # Send it! self.send(datagram) self.lastHeartbeat = globalClock.getRealTime() # This is important enough to consider flushing immediately # (particularly if we haven't run readerPollTask recently). self.considerFlush() def isLocalId(self, doId): """ Returns true if this doId is one that we're the owner of, false otherwise. """ return ((doId >= self.doIdBase) and (doId < self.doIdLast)) def haveCreateAuthority(self): """ Returns true if this client has been assigned a range of doId's it may use to create objects, false otherwise. """ return (self.doIdLast > self.doIdBase) def getAvatarIdFromSender(self): """ Returns the doIdBase of the client that originally sent the current update message. This is only defined when processing an update message or a generate message. """ return self.currentSenderId def handleDatagram(self, di): if self.notify.getDebug(): print "ClientRepository received datagram:" di.getDatagram().dumpHex(ostream) msgType = self.getMsgType() self.currentSenderId = None # These are the sort of messages we may expect from the public # Panda server. if msgType == SET_DOID_RANGE_CMU: self.handleSetDoIdrange(di) elif msgType == OBJECT_GENERATE_CMU: self.handleGenerate(di) elif msgType == OBJECT_UPDATE_FIELD_CMU: self.handleUpdateField(di) elif msgType == OBJECT_DISABLE_CMU: self.handleDisable(di) elif msgType == OBJECT_DELETE_CMU: self.handleDelete(di) elif msgType == REQUEST_GENERATES_CMU: self.handleRequestGenerates(di) else: self.handleMessageType(msgType, di) # If we're processing a lot of datagrams within one frame, we # may forget to send heartbeats. Keep them coming! self.considerHeartbeat() def handleMessageType(self, msgType, di): self.notify.error("unrecognized message type %s" % (msgType)) def handleUpdateField(self, di): # The CMU update message starts with an additional field, not # present in the Disney update message: the doIdBase of the # original sender. Extract that and call up to the parent. self.currentSenderId = di.getUint32() ClientRepositoryBase.handleUpdateField(self, di) def handleDisable(self, di): # Receives a list of doIds. while di.getRemainingSize() > 0: doId = di.getUint32() # We should never get a disable message for our own object. assert not self.isLocalId(doId) self.disableDoId(doId) def handleDelete(self, di): # Receives a single doId. doId = di.getUint32() self.deleteObject(doId) def deleteObject(self, doId): """ Removes the object from the client's view of the world. This should normally not be called directly except in the case of error recovery, since the server will normally be responsible for deleting and disabling objects as they go out of scope. After this is called, future updates by server on this object will be ignored (with a warning message). The object will become valid again the next time the server sends a generate message for this doId. This is not a distributed message and does not delete the object on the server or on any other client. """ if doId in self.doId2do: # If it is in the dictionary, remove it. obj = self.doId2do[doId] # Remove it from the dictionary del self.doId2do[doId] # Disable, announce, and delete the object itself... # unless delayDelete is on... obj.deleteOrDelay() if self.isLocalId(doId): self.freeDoId(doId) elif self.cache.contains(doId): # If it is in the cache, remove it. self.cache.delete(doId) if self.isLocalId(doId): self.freeDoId(doId) else: # Otherwise, ignore it self.notify.warning( "Asked to delete non-existent DistObj " + str(doId)) def stopTrackRequestDeletedDO(self, *args): # No-op. Not entirely sure what this does on the VR Studio side. pass def sendUpdate(self, distObj, fieldName, args): """ Sends a normal update for a single field. """ dg = distObj.dclass.clientFormatUpdate( fieldName, distObj.doId, args) self.send(dg) def sendUpdateToChannel(self, distObj, channelId, fieldName, args): """ Sends a targeted update of a single field to a particular client. The top 32 bits of channelId is ignored; the lower 32 bits should be the client Id of the recipient (i.e. the client's doIdbase). The field update will be sent to the indicated client only. The field must be marked clsend or p2p, and may not be marked broadcast. """ datagram = distObj.dclass.clientFormatUpdate( fieldName, distObj.doId, args) dgi = PyDatagramIterator(datagram) # Reformat the packed datagram to change the message type and # add the target id. dgi.getUint16() dg = PyDatagram() dg.addUint16(CLIENT_OBJECT_UPDATE_FIELD_TARGETED_CMU) dg.addUint32(channelId & 0xffffffff) dg.appendData(dgi.getRemainingBytes()) self.send(dg)