"""ClientRepository module: contains the ClientRepository class""" from PandaModules import * from TaskManagerGlobal import * from MsgTypes import * from ShowBaseGlobal import * import Task import DirectNotifyGlobal import ClientDistClass import CRCache # The repository must import all known types of Distributed Objects #import DistributedObject #import DistributedToon import DirectObject class ClientRepository(DirectObject.DirectObject): notify = DirectNotifyGlobal.directNotify.newCategory("ClientRepository") TASK_PRIORITY = -30 def __init__(self, dcFileName): self.number2cdc={} self.name2cdc={} self.doId2do={} self.doId2cdc={} self.parseDcFile(dcFileName) self.cache=CRCache.CRCache() self.serverDelta = 0 # Set this to 'http' to establish a connection to the server # using the HTTPClient interface, which ultimately uses the # OpenSSL socket library (even though SSL is not involved). # This is not as robust a socket library as NSPR's, but the # HTTPClient interface does a good job of negotiating the # connection over an HTTP proxy if one is in use. # Set it to 'nspr' to use Panda's net interface # (e.g. QueuedConnectionManager, etc.) to establish the # connection, which ultimately uses the NSPR socket library. # This is a much better socket library, but it may be more # than you need for most applications; and the proxy support # is weak. # Set it to 'default' to use the HTTPClient interface if a # proxy is in place, but the NSPR interface if we don't have a # proxy. self.connectMethod = base.config.GetString('connect-method', 'default') self.connectHttp = None self.bootedIndex = None self.bootedText = None self.tcpConn = None return None def setServerDelta(self, delta): """ Indicates the approximate difference in seconds between the client's clock and the server's clock, in universal time (not including timezone shifts). This is mainly useful for reporting synchronization information to the logs; don't depend on it for any precise timing requirements. Also see Notify.setServerDelta(), which also accounts for a timezone shift. """ self.serverDelta = delta def getServerDelta(self): return self.serverDelta def parseDcFile(self, dcFileName): self.dcFile = DCFile() readResult = self.dcFile.read(dcFileName) if not readResult: self.notify.error("Could not read dcfile: " + dcFileName) self.hashVal = self.dcFile.getHash() return self.parseDcClasses(self.dcFile) def parseDcClasses(self, dcFile): numClasses = dcFile.getNumClasses() for i in range(0, numClasses): # Create a clientDistClass from the dcClass dcClass = dcFile.getClass(i) clientDistClass = ClientDistClass.ClientDistClass(dcClass) # List the cdc in the number and name dictionaries self.number2cdc[dcClass.getNumber()]=clientDistClass self.name2cdc[dcClass.getName()]=clientDistClass return None def connect(self, serverList, successCallback = None, successArgs = [], failureCallback = None, failureArgs = []): """ Attempts to establish a connection to the server. May return before the connection is established. The two callbacks represent the two functions to call (and their arguments) on success or failure, respectively. The failure callback also gets one additional parameter, which will be passed in first: the return status code giving reason for failure, if it is known. """ if self.hasProxy: self.notify.info("Connecting to gameserver via proxy: %s" % (self.proxy.cStr())) else: self.notify.info("Connecting to gameserver directly (no proxy)."); if self.connectMethod == 'http': self.connectHttp = 1 elif self.connectMethod == 'nspr': self.connectHttp = 0 else: self.connectHttp = self.hasProxy self.bootedIndex = None self.bootedText = None if self.connectHttp: # In the HTTP case, we can't just iterate through the list # of servers, because each server attempt requires # spawning a request and then coming back later to check # the success or failure. Instead, we start the ball # rolling by calling the connect callback, which will call # itself repeatedly until we establish a connection (or # run out of servers). ch = self.http.makeChannel(0) self.httpConnectCallback(ch, serverList, 0, successCallback, successArgs, failureCallback, failureArgs) else: self.qcm = QueuedConnectionManager() # A big old 20 second timeout. gameServerTimeoutMs = base.config.GetInt("game-server-timeout-ms", 20000) # Try each of the servers in turn. for url in serverList: self.notify.info("Connecting to %s via NSPR interface." % (url.cStr())) self.tcpConn = self.qcm.openTCPClientConnection( url.getServer(), url.getPort(), gameServerTimeoutMs) if self.tcpConn: self.tcpConn.setNoDelay(1) self.qcr=QueuedConnectionReader(self.qcm, 0) self.qcr.addConnection(self.tcpConn) minLag = config.GetFloat('min-lag', 0.) maxLag = config.GetFloat('max-lag', 0.) if minLag or maxLag: self.qcr.startDelay(minLag, maxLag) self.cw=ConnectionWriter(self.qcm, 0) self.startReaderPollTask() if successCallback: successCallback(*successArgs) return # Failed to connect. if failureCallback: failureCallback(0, *failureArgs) def httpConnectCallback(self, ch, serverList, serverIndex, successCallback, successArgs, failureCallback, failureArgs): if ch.isConnectionReady(): self.tcpConn = ch.getConnection() self.tcpConn.userManagesMemory = 1 self.startReaderPollTask() if successCallback: successCallback(*successArgs) elif serverIndex < len(serverList): # No connection yet, but keep trying. url = serverList[serverIndex] self.notify.info("Connecting to %s via HTTP interface." % (url.cStr())) ch.beginConnectTo(DocumentSpec(url)) ch.spawnTask(name = 'connect-to-server', callback = self.httpConnectCallback, extraArgs = [ch, serverList, serverIndex + 1, successCallback, successArgs, failureCallback, failureArgs]) else: # No more servers to try; we have to give up now. if failureCallback: failureCallback(ch.getStatusCode(), *failureArgs) def startReaderPollTask(self): # Stop any tasks we are running now self.stopReaderPollTask() taskMgr.add(self.readerPollUntilEmpty, "readerPollTask", priority=self.TASK_PRIORITY) return None def stopReaderPollTask(self): taskMgr.remove("readerPollTask") return None def readerPollUntilEmpty(self, task): while self.readerPollOnce(): pass return Task.cont def readerPollOnce(self): # we simulate the network plug being pulled by setting tcpConn # to None; enforce that condition if not self.tcpConn: return 0 # Make sure any recently-sent datagrams are flushed when the # time expires, if we're in collect-tcp mode. # Temporary try .. except for old Pandas. try: self.tcpConn.considerFlush() except: pass if self.connectHttp: datagram = Datagram() if self.tcpConn.receiveDatagram(datagram): self.handleDatagram(datagram) return 1 # Unable to receive a datagram: did we lose the connection? if self.tcpConn.isClosed(): self.tcpConn = None self.stopReaderPollTask() self.loginFSM.request("noConnection") return 0 else: self.ensureValidConnection() if self.qcr.dataAvailable(): datagram = NetDatagram() if self.qcr.getData(datagram): self.handleDatagram(datagram) return 1 return 0 def ensureValidConnection(self): # Was the connection reset? if self.connectHttp: pass else: if self.qcm.resetConnectionAvailable(): resetConnectionPointer = PointerToConnection() if self.qcm.getResetConnection(resetConnectionPointer): resetConn = resetConnectionPointer.p() self.qcm.closeConnection(resetConn) # if we've simulated a network plug pull, restore the # simulated plug self.restoreNetworkPlug() if self.tcpConn.this == resetConn.this: self.tcpConn = None self.stopReaderPollTask() self.loginFSM.request("noConnection") else: self.notify.warning("Lost unknown connection.") return None def handleDatagram(self, datagram): # This class is meant to be pure virtual, and any classes that # inherit from it need to make their own handleDatagram method pass def handleGenerateWithRequired(self, di): # Get the class Id classId = di.getArg(STUint16); # Get the DO Id doId = di.getArg(STUint32) # Look up the cdc cdc = self.number2cdc[classId] # Create a new distributed object, and put it in the dictionary distObj = self.generateWithRequiredFields(cdc, doId, di) return None def handleGenerateWithRequiredOther(self, di): # Get the class Id classId = di.getArg(STUint16); # Get the DO Id doId = di.getArg(STUint32) # Look up the cdc cdc = self.number2cdc[classId] # Create a new distributed object, and put it in the dictionary distObj = self.generateWithRequiredOtherFields(cdc, doId, di) return None def handleQuietZoneGenerateWithRequired(self, di): # Special handler for quiet zone generates -- we need to filter # Get the class Id classId = di.getArg(STUint16); # Get the DO Id doId = di.getArg(STUint32) # Look up the cdc cdc = self.number2cdc[classId] # If the class is a neverDisable class (which implies uberzone) we # should go ahead and generate it even though we are in the quiet zone if cdc.constructor.neverDisable: # Create a new distributed object, and put it in the dictionary distObj = self.generateWithRequiredFields(cdc, doId, di) return None def handleQuietZoneGenerateWithRequiredOther(self, di): # Special handler for quiet zone generates -- we need to filter # Get the class Id classId = di.getArg(STUint16); # Get the DO Id doId = di.getArg(STUint32) # Look up the cdc cdc = self.number2cdc[classId] # If the class is a neverDisable class (which implies uberzone) we # should go ahead and generate it even though we are in the quiet zone if cdc.constructor.neverDisable: # Create a new distributed object, and put it in the dictionary distObj = self.generateWithRequiredOtherFields(cdc, doId, di) return None def generateWithRequiredFields(self, cdc, doId, di): # Is it in our dictionary? if self.doId2do.has_key(doId): # If so, just update it. distObj = self.doId2do[doId] distObj.generate() distObj.updateRequiredFields(cdc, di) distObj.announceGenerate() # Is it in the cache? If so, pull it out, put it in the dictionaries, # and update it. elif self.cache.contains(doId): # If so, pull it out of the cache... distObj = self.cache.retrieve(doId) # put it in both dictionaries... self.doId2do[doId] = distObj self.doId2cdc[doId] = cdc # and update it. distObj.generate() distObj.updateRequiredFields(cdc, di) distObj.announceGenerate() # If it is not in the dictionary or the cache, then... else: # Construct a new one distObj = cdc.constructor(self) # Assign it an Id distObj.doId = doId # Put the new do in both dictionaries self.doId2do[doId] = distObj self.doId2cdc[doId] = cdc # Update the required fields distObj.generateInit() # Only called when constructed distObj.generate() distObj.updateRequiredFields(cdc, di) distObj.announceGenerate() return distObj def generateWithRequiredOtherFields(self, cdc, doId, di): # Is it in our dictionary? if self.doId2do.has_key(doId): # If so, just update it. distObj = self.doId2do[doId] distObj.generate() distObj.updateRequiredOtherFields(cdc, di) distObj.announceGenerate() # Is it in the cache? If so, pull it out, put it in the dictionaries, # and update it. elif self.cache.contains(doId): # If so, pull it out of the cache... distObj = self.cache.retrieve(doId) # put it in both dictionaries... self.doId2do[doId] = distObj self.doId2cdc[doId] = cdc # and update it. distObj.generate() distObj.updateRequiredOtherFields(cdc, di) distObj.announceGenerate() # If it is not in the dictionary or the cache, then... else: # Construct a new one distObj = cdc.constructor(self) # Assign it an Id distObj.doId = doId # Put the new do in both dictionaries self.doId2do[doId] = distObj self.doId2cdc[doId] = cdc # Update the required fields distObj.generateInit() # Only called when constructed distObj.generate() distObj.updateRequiredOtherFields(cdc, di) distObj.announceGenerate() return distObj def handleDisable(self, di): # Get the DO Id doId = di.getArg(STUint32) # disable it. self.disableDoId(doId) return None def disableDoId(self, doId): # Make sure the object exists if self.doId2do.has_key(doId): # Look up the object distObj = self.doId2do[doId] # remove the object from both dictionaries del(self.doId2do[doId]) del(self.doId2cdc[doId]) assert(len(self.doId2do) == len(self.doId2cdc)) # Only cache the object if it is a "cacheable" type # object; this way we don't clutter up the caches with # trivial objects that don't benefit from caching. if distObj.getCacheable(): self.cache.cache(distObj) else: distObj.deleteOrDelay() else: ClientRepository.notify.warning("Disable failed. DistObj " + str(doId) + " is not in dictionary") return None def handleDelete(self, di): # Get the DO Id doId = di.getArg(STUint32) self.deleteObject(doId) def deleteObject(self, doId): """deleteObject(self, doId) Removes the object from the client's view of the world. This should normally not be called 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 it is in the dictionaries, remove it. if self.doId2do.has_key(doId): obj = self.doId2do[doId] # Remove it from the dictionaries del(self.doId2do[doId]) del(self.doId2cdc[doId]) # Sanity check the dictionaries assert(len(self.doId2do) == len(self.doId2cdc)) # Disable, announce, and delete the object itself... # unless delayDelete is on... obj.deleteOrDelay() # If it is in the cache, remove it. elif self.cache.contains(doId): self.cache.delete(doId) # Otherwise, ignore it else: ClientRepository.notify.warning( "Asked to delete non-existent DistObj " + str(doId)) return None def handleUpdateField(self, di): # Get the DO Id doId = di.getArg(STUint32) #print("Updating " + str(doId)) # Find the DO do = self.doId2do.get(doId) cdc = self.doId2cdc.get(doId) if (do != None and cdc != None): # Let the cdc finish the job cdc.updateField(do, di) else: ClientRepository.notify.warning( "Asked to update non-existent DistObj " + str(doId)) return None def handleGoGetLost(self, di): # The server told us it's about to drop the connection on us. # Get ready! if (di.getRemainingSize() > 0): self.bootedIndex = di.getUint16() self.bootedText = di.getString() ClientRepository.notify.warning( "Server is booting us out (%d): %s" % (self.bootedIndex, self.bootedText)) else: self.bootedIndex = None self.bootedText = None ClientRepository.notify.warning( "Server is booting us out with no explanation.") def handleUnexpectedMsgType(self, msgType, di): if msgType == CLIENT_GO_GET_LOST: self.handleGoGetLost(di) else: currentLoginState = self.loginFSM.getCurrentState() if currentLoginState: currentLoginStateName = currentLoginState.getName() else: currentLoginStateName = "None" currentGameState = self.gameFSM.getCurrentState() if currentGameState: currentGameStateName = currentGameState.getName() else: currentGameStateName = "None" ClientRepository.notify.warning( "Ignoring unexpected message type: " + str(msgType) + " login state: " + currentLoginStateName + " game state: " + currentGameStateName) return None def sendSetShardMsg(self, shardId): datagram = Datagram() # Add message type datagram.addUint16(CLIENT_SET_SHARD) # Add shard id datagram.addUint32(shardId) # send the message self.send(datagram) return None def sendSetZoneMsg(self, zoneId): datagram = Datagram() # Add message type datagram.addUint16(CLIENT_SET_ZONE) # Add zone id datagram.addUint16(zoneId) # send the message self.send(datagram) return None def sendUpdate(self, do, fieldName, args, sendToId = None): # Get the DO id doId = do.doId # Get the cdc cdc = self.doId2cdc.get(doId, None) if cdc: # Let the cdc finish the job cdc.sendUpdate(self, do, fieldName, args, sendToId) def send(self, datagram): if self.notify.getDebug(): print "ClientRepository sending datagram:" datagram.dumpHex(ostream) if not self.tcpConn: self.notify.warning("Unable to send message after connection is closed.") return if self.connectHttp: if not self.tcpConn.sendDatagram(datagram): self.notify.warning("Could not send datagram.") else: self.cw.send(datagram, self.tcpConn) return None def replaceMethod(self, oldMethod, newFunction): foundIt = 0 import new # Iterate over the ClientDistClasses for cdc in self.number2cdc.values(): # Iterate over the ClientDistUpdates for cdu in cdc.allCDU: method = cdu.func # See if this is a match if (method and (method.im_func == oldMethod)): # Create a new unbound method out of this new function newMethod = new.instancemethod(newFunction, method.im_self, method.im_class) # Set the new method on the cdu cdu.func = newMethod foundIt = 1 return foundIt # debugging funcs for simulating a network-plug-pull def pullNetworkPlug(self): self.restoreNetworkPlug() self.notify.warning('*** SIMULATING A NETWORK-PLUG-PULL ***') self.hijackedTcpConn = self.tcpConn self.tcpConn = None def networkPlugPulled(self): return hasattr(self, 'hijackedTcpConn') def restoreNetworkPlug(self): if self.networkPlugPulled(): self.notify.info('*** RESTORING SIMULATED PULLED-NETWORK-PLUG ***') self.tcpConn = self.hijackedTcpConn del self.hijackedTcpConn def getAllOfType(self, type): # Returns a list of all DistributedObjects in the repository # of a particular type. result = [] for obj in self.doId2do.values(): if isinstance(obj, type): result.append(obj) return result