ServerRepository.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. """ServerRepository module: contains the ServerRepository class"""
  2. from pandac.PandaModules import *
  3. #from TaskManagerGlobal import *
  4. from direct.distributed.MsgTypes import *
  5. from direct.task import Task
  6. from direct.directnotify import DirectNotifyGlobal
  7. from direct.distributed.PyDatagram import PyDatagram
  8. from direct.distributed.PyDatagramIterator import PyDatagramIterator
  9. import time
  10. import types
  11. class ServerRepository:
  12. """ This maintains the server-side connection with a Panda server.
  13. It is only for use with the Panda LAN server provided by CMU."""
  14. notify = DirectNotifyGlobal.directNotify.newCategory("ClientRepository")
  15. def __init__(self, tcpPort, udpPort, dcFileNames = None):
  16. self.qcm = QueuedConnectionManager()
  17. self.qcl = QueuedConnectionListener(self.qcm, 0)
  18. self.qcr = QueuedConnectionReader(self.qcm, 0)
  19. self.cw = ConnectionWriter(self.qcm, 0)
  20. self.tcpRendezvous = self.qcm.openTCPServerRendezvous(tcpPort, 10)
  21. print self.tcpRendezvous
  22. self.qcl.addConnection(self.tcpRendezvous)
  23. taskMgr.add(self.listenerPoll, "serverListenerPollTask")
  24. taskMgr.add(self.readerPollUntilEmpty, "serverReaderPollTask")
  25. taskMgr.add(self.clientHardDisconnectTask, "clientHardDisconnect")
  26. self.ClientIP = {}
  27. self.ClientZones = {}
  28. self.ClientDOIDbase = {}
  29. self.ClientObjects = {}
  30. self.DOIDnext = 1
  31. self.DOIDrange = 1000000
  32. self.DOIDtoClient = {}
  33. self.DOIDtoZones = {}
  34. self.DOIDtoDClass = {}
  35. self.ZonesToClients = {}
  36. self.ZonetoDOIDs = {}
  37. self.dcFile = DCFile()
  38. self.dcSuffix = ''
  39. self.readDCFile(dcFileNames)
  40. def importModule(self, dcImports, moduleName, importSymbols):
  41. """ Imports the indicated moduleName and all of its symbols
  42. into the current namespace. This more-or-less reimplements
  43. the Python import command. """
  44. module = __import__(moduleName, globals(), locals(), importSymbols)
  45. if importSymbols:
  46. # "from moduleName import symbolName, symbolName, ..."
  47. # Copy just the named symbols into the dictionary.
  48. if importSymbols == ['*']:
  49. # "from moduleName import *"
  50. if hasattr(module, "__all__"):
  51. importSymbols = module.__all__
  52. else:
  53. importSymbols = module.__dict__.keys()
  54. for symbolName in importSymbols:
  55. if hasattr(module, symbolName):
  56. dcImports[symbolName] = getattr(module, symbolName)
  57. else:
  58. raise StandardError, 'Symbol %s not defined in module %s.' % (symbolName, moduleName)
  59. else:
  60. # "import moduleName"
  61. # Copy the root module name into the dictionary.
  62. # Follow the dotted chain down to the actual module.
  63. components = moduleName.split('.')
  64. dcImports[components[0]] = module
  65. def readDCFile(self, dcFileNames = None):
  66. """
  67. Reads in the dc files listed in dcFileNames, or if
  68. dcFileNames is None, reads in all of the dc files listed in
  69. the Configrc file.
  70. """
  71. dcFile = self.dcFile
  72. dcFile.clear()
  73. self.dclassesByName = {}
  74. self.dclassesByNumber = {}
  75. self.hashVal = 0
  76. dcImports = {}
  77. if dcFileNames == None:
  78. readResult = dcFile.readAll()
  79. if not readResult:
  80. self.notify.error("Could not read dc file.")
  81. else:
  82. for dcFileName in dcFileNames:
  83. readResult = dcFile.read(Filename(dcFileName))
  84. if not readResult:
  85. self.notify.error("Could not read dc file: %s" % (dcFileName))
  86. self.hashVal = dcFile.getHash()
  87. # Now import all of the modules required by the DC file.
  88. for n in range(dcFile.getNumImportModules()):
  89. moduleName = dcFile.getImportModule(n)
  90. # Maybe the module name is represented as "moduleName/AI".
  91. suffix = moduleName.split('/')
  92. moduleName = suffix[0]
  93. if self.dcSuffix and self.dcSuffix in suffix[1:]:
  94. moduleName += self.dcSuffix
  95. importSymbols = []
  96. for i in range(dcFile.getNumImportSymbols(n)):
  97. symbolName = dcFile.getImportSymbol(n, i)
  98. # Maybe the symbol name is represented as "symbolName/AI".
  99. suffix = symbolName.split('/')
  100. symbolName = suffix[0]
  101. if self.dcSuffix and self.dcSuffix in suffix[1:]:
  102. symbolName += self.dcSuffix
  103. importSymbols.append(symbolName)
  104. self.importModule(dcImports, moduleName, importSymbols)
  105. # Now get the class definition for the classes named in the DC
  106. # file.
  107. for i in range(dcFile.getNumClasses()):
  108. dclass = dcFile.getClass(i)
  109. number = dclass.getNumber()
  110. className = dclass.getName() + self.dcSuffix
  111. # Does the class have a definition defined in the newly
  112. # imported namespace?
  113. classDef = dcImports.get(className)
  114. # Also try it without the dcSuffix.
  115. if classDef == None:
  116. className = dclass.getName()
  117. classDef = dcImports.get(className)
  118. if classDef == None:
  119. self.notify.info("No class definition for %s." % (className))
  120. else:
  121. if type(classDef) == types.ModuleType:
  122. if not hasattr(classDef, className):
  123. self.notify.error("Module %s does not define class %s." % (className, className))
  124. classDef = getattr(classDef, className)
  125. if type(classDef) != types.ClassType:
  126. self.notify.error("Symbol %s is not a class name." % (className))
  127. else:
  128. dclass.setClassDef(classDef)
  129. self.dclassesByName[className] = dclass
  130. if number >= 0:
  131. self.dclassesByNumber[number] = dclass
  132. # listens for new clients
  133. def listenerPoll(self, task):
  134. if self.qcl.newConnectionAvailable():
  135. rendezvous = PointerToConnection()
  136. netAddress = NetAddress()
  137. newConnection = PointerToConnection()
  138. retVal = self.qcl.getNewConnection(rendezvous, netAddress,
  139. newConnection)
  140. if retVal:
  141. # Crazy dereferencing
  142. newConnection=newConnection.p()
  143. self.qcr.addConnection(newConnection)
  144. # Add clients infomation to dictionary
  145. self.ClientIP[newConnection] = netAddress.getIpString()
  146. self.ClientZones[newConnection] = []
  147. self.ClientObjects[newConnection] = []
  148. self.lastConnection = newConnection
  149. self.sendDOIDrange(self.lastConnection)
  150. else:
  151. self.notify.warning(
  152. "getNewConnection returned false")
  153. return Task.cont
  154. # continuously polls for new messages on the server
  155. def readerPollUntilEmpty(self, task):
  156. while self.readerPollOnce():
  157. pass
  158. return Task.cont
  159. # checks for available messages to the server
  160. def readerPollOnce(self):
  161. availGetVal = self.qcr.dataAvailable()
  162. if availGetVal:
  163. datagram = NetDatagram()
  164. readRetVal = self.qcr.getData(datagram)
  165. if readRetVal:
  166. # need to send to message processing unit
  167. self.handleDatagram(datagram)
  168. else:
  169. self.notify.warning("getData returned false")
  170. return availGetVal
  171. # switching station for messages
  172. def handleDatagram(self, datagram):
  173. dgi = DatagramIterator(datagram)
  174. type = dgi.getUint16()
  175. if type == CLIENT_DISCONNECT:
  176. self.handleClientDisconnect(datagram.getConnection())
  177. elif type == CLIENT_SET_ZONE_CMU:
  178. self.handleSetZone(dgi, datagram.getConnection())
  179. elif type == CLIENT_REMOVE_ZONE:
  180. self.handleRemoveZone(dgi, datagram.getConnection())
  181. elif type == CLIENT_CREATE_OBJECT_REQUIRED:
  182. self.handleClientCreateObjectRequired(datagram, dgi)
  183. elif type == CLIENT_OBJECT_UPDATE_FIELD:
  184. self.handleClientUpdateField(datagram, dgi)
  185. elif type == CLIENT_OBJECT_DELETE:
  186. self.handleClientDeleteObject(datagram, dgi.getUint32())
  187. elif type == CLIENT_OBJECT_DISABLE:
  188. self.handleClientDisable(datagram, dgi.getUint32())
  189. else:
  190. self.notify.error("unrecognized message")
  191. # client wants to create an object, so we store appropriate data,
  192. # and then pass message along to corresponding zones
  193. def handleClientCreateObjectRequired(self, datagram, dgi):
  194. connection = datagram.getConnection()
  195. # no need to create a new message, just forward the received
  196. # message as it has the same msg type number
  197. zone = dgi.getUint32()
  198. classid = dgi.getUint16()
  199. doid = dgi.getUint32()
  200. rest = dgi.getRemainingBytes()
  201. datagram = NetDatagram()
  202. datagram.addUint16(CLIENT_CREATE_OBJECT_REQUIRED)
  203. datagram.addUint16(classid)
  204. datagram.addUint32(doid)
  205. datagram.appendData(rest)
  206. dclass = self.dclassesByNumber[classid]
  207. if self.ClientObjects[connection].count(doid) == 0:
  208. self.ClientObjects[connection].append(doid)
  209. self.DOIDtoZones[doid] = zone
  210. self.DOIDtoDClass[doid] = dclass
  211. if zone in self.ZonetoDOIDs:
  212. if self.ZonetoDOIDs[zone].count(doid)==0:
  213. self.ZonetoDOIDs[zone].append(doid)
  214. else:
  215. self.ZonetoDOIDs[zone] = [doid]
  216. self.sendToZoneExcept(zone, datagram, connection)
  217. # client wants to update an object, forward message along
  218. # to corresponding zone
  219. def handleClientUpdateField(self, datagram, dgi):
  220. connection = datagram.getConnection()
  221. doid = dgi.getUint32()
  222. fieldid = dgi.getUint16()
  223. dclass = self.DOIDtoDClass[doid]
  224. dcfield = dclass.getFieldByIndex(fieldid)
  225. if dcfield == None:
  226. self.notify.error(
  227. "Received update for field %s on object %s; no such field for class %s." % (
  228. fieldid, doid, dclass.getName()))
  229. return
  230. if (dcfield.hasKeyword('broadcast')):
  231. if (dcfield.hasKeyword('p2p')):
  232. self.sendToZoneExcept(self.DOIDtoZones[doid], datagram, 0)
  233. else:
  234. self.sendToZoneExcept(self.DOIDtoZones[doid], datagram, connection)
  235. elif (dcfield.hasKeyword('p2p')):
  236. doidbase = (doid / self.DOIDrange) * self.DOIDrange
  237. self.cw.send(datagram, self.DOIDtoClient[doidbase])
  238. else:
  239. self.notify.warning(
  240. "Message is not broadcast, p2p, or broadcast+p2p")
  241. # client disables an object, let everyone know who is in
  242. # that zone know about it
  243. def handleClientDisable(self, datagram, doid):
  244. # now send disable message to all clients that need to know
  245. if doid in self.DOIDtoZones:
  246. self.sendToZoneExcept(self.DOIDtoZones[doid], datagram, 0)
  247. # client deletes an object, let everyone who is in zone with
  248. # object know about it
  249. def handleClientDeleteObject(self, datagram, doid):
  250. if doid in self.DOIDtoZones:
  251. self.sendToZoneExcept(self.DOIDtoZones[doid], datagram, 0)
  252. self.ClientObjects[datagram.getConnection()].remove(doid)
  253. self.ZonetoDOIDs[self.DOIDtoZones[doid]].remove(doid)
  254. del self.DOIDtoZones[doid]
  255. del self.DOIDtoDClass[doid]
  256. def sendAvatarGenerate(self):
  257. datagram = PyDatagram()
  258. # Message type is 1
  259. datagram.addUint16(ALL_OBJECT_GENERATE_WITH_REQUIRED)
  260. # Avatar class type is 2
  261. datagram.addUint8(2)
  262. # A sample id
  263. datagram.addUint32(10)
  264. # The only required field is the zone field
  265. datagram.addUint32(999)
  266. self.cw.send(datagram, self.lastConnection)
  267. # sends the client the range of doid's that the client can use
  268. def sendDOIDrange(self, connection):
  269. # reuse DOID assignments if we can
  270. id = self.DOIDnext + self.DOIDrange
  271. self.DOIDnext = self.DOIDnext + self.DOIDrange
  272. self.DOIDtoClient[id] = connection
  273. self.ClientDOIDbase[connection] = id
  274. datagram = NetDatagram()
  275. datagram.addUint16(CLIENT_SET_DOID_RANGE)
  276. datagram.addUint32(id)
  277. datagram.addUint32(self.DOIDrange)
  278. print "Sending DOID range: ", id, self.DOIDrange
  279. self.cw.send(datagram, connection)
  280. # a client disconnected from us, we need to update our data, also tell other clients to remove
  281. # the disconnected clients objects
  282. def handleClientDisconnect(self, connection):
  283. if (self.ClientIP.has_key(connection)):
  284. del self.DOIDtoClient[self.ClientDOIDbase[connection]]
  285. for zone in self.ClientZones[connection]:
  286. if len(self.ZonesToClients[zone]) == 1:
  287. del self.ZonesToClients[zone]
  288. else:
  289. self.ZonesToClients[zone].remove(connection)
  290. for obj in self.ClientObjects[connection]:
  291. #create and send delete message
  292. datagram = NetDatagram()
  293. datagram.addUint16(CLIENT_OBJECT_DELETE_RESP)
  294. datagram.addUint32(obj)
  295. self.sendToZoneExcept(self.DOIDtoZones[obj], datagram, 0)
  296. self.ZonetoDOIDs[self.DOIDtoZones[obj]].remove(obj)
  297. del self.DOIDtoZones[obj]
  298. del self.DOIDtoDClass[obj]
  299. del self.ClientIP[connection]
  300. del self.ClientZones[connection]
  301. del self.ClientDOIDbase[connection]
  302. del self.ClientObjects[connection]
  303. # client told us its zone(s), store information
  304. def handleSetZone(self, dgi, connection):
  305. while dgi.getRemainingSize() > 0:
  306. ZoneID = dgi.getUint32()
  307. if self.ClientZones[connection].count(ZoneID) == 0:
  308. self.ClientZones[connection].append(ZoneID)
  309. if ZoneID in self.ZonesToClients:
  310. if self.ZonesToClients[ZoneID].count(connection) == 0:
  311. self.ZonesToClients[ZoneID].append(connection)
  312. else:
  313. self.ZonesToClients[ZoneID] = [connection]
  314. # We have a new member, need to get all of the data from clients who may have objects in this zone
  315. datagram = NetDatagram()
  316. datagram.addUint16(CLIENT_REQUEST_GENERATES)
  317. datagram.addUint32(ZoneID)
  318. self.sendToAll(datagram)
  319. print "SENDING REQUEST GENERATES (", ZoneID, ") TO ALL"
  320. # client has moved zones, need to update them
  321. def handleRemoveZone(self, dgi, connection):
  322. while dgi.getRemainingSize() > 0:
  323. ZoneID = dgi.getUint32()
  324. if self.ClientZones[connection].count(ZoneID) == 1:
  325. self.ClientZones[connection].remove(ZoneID)
  326. if ZoneID in self.ZonesToClients:
  327. if self.ZonesToClients[ZoneID].count(connection) == 1:
  328. self.ZonesToClients[ZoneID].remove(connection)
  329. for i in self.ZonetoDOIDs[ZoneID]:
  330. datagram = NetDatagram()
  331. datagram.addUint16(CLIENT_OBJECT_DELETE)
  332. datagram.addUint32(i)
  333. self.cw.send(datagram, connection)
  334. # client did not tell us he was leaving but we lost connection to him, so we need to update our data and tell others
  335. def clientHardDisconnectTask(self, task):
  336. for i in self.ClientIP.keys():
  337. if not self.qcr.isConnectionOk(i):
  338. self.handleClientDisconnect(i)
  339. return Task.cont
  340. # sends a message to everyone who is in the zone
  341. def sendToZoneExcept(self, ZoneID, datagram, connection):
  342. if ZoneID in self.ZonesToClients:
  343. for conn in self.ZonesToClients[ZoneID]:
  344. if (conn != connection):
  345. self.cw.send(datagram, conn)
  346. # sends a message to all connected clients
  347. def sendToAll(self, datagram):
  348. for client in self.ClientIP.keys():
  349. self.cw.send(datagram, client)