ServerRepository.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731
  1. """ServerRepository module: contains the ServerRepository class"""
  2. from panda3d.core import *
  3. from panda3d.direct import *
  4. from direct.distributed.MsgTypesCMU import *
  5. from direct.task import Task
  6. from direct.task.TaskManagerGlobal import taskMgr
  7. from direct.directnotify import DirectNotifyGlobal
  8. from direct.distributed.PyDatagram import PyDatagram
  9. import inspect
  10. class ServerRepository:
  11. """ This maintains the server-side connection with a Panda server.
  12. It is only for use with the Panda LAN server provided by CMU."""
  13. notify = DirectNotifyGlobal.directNotify.newCategory("ServerRepository")
  14. class Client:
  15. """ This internal class keeps track of the data associated
  16. with each connected client. """
  17. def __init__(self, connection, netAddress, doIdBase):
  18. # The connection used to communicate with the client.
  19. self.connection = connection
  20. # The net address to the client, including IP address.
  21. # Used for reporting purposes only.
  22. self.netAddress = netAddress
  23. # The first doId in the range assigned to the client.
  24. # This also serves as a unique numeric ID for this client.
  25. # (It is sometimes called "avatarId" in some update
  26. # messages, even though the client is not required to use
  27. # this particular number as an avatar ID.)
  28. self.doIdBase = doIdBase
  29. # The set of zoneIds that the client explicitly has
  30. # interest in. The client will receive updates for all
  31. # distributed objects appearing in one of these zones.
  32. # (The client will also receive updates for all zones in
  33. # which any one of the distributed obejcts that it has
  34. # created still exist.)
  35. self.explicitInterestZoneIds = set()
  36. # The set of interest zones sent to the client at the last
  37. # update. This is the actual set of zones the client is
  38. # informed of. Changing the explicitInterestZoneIds,
  39. # above, creating or deleting objects in different zones,
  40. # or moving objects between zones, might influence this
  41. # set.
  42. self.currentInterestZoneIds = set()
  43. # A dictionary of doId -> Object, for distributed objects
  44. # currently in existence that were created by the client.
  45. self.objectsByDoId = {}
  46. # A dictionary of zoneId -> set([Object]), listing the
  47. # distributed objects assigned to each zone, of the
  48. # objects created by this client.
  49. self.objectsByZoneId = {}
  50. class Object:
  51. """ This internal class keeps track of the data associated
  52. with each extent distributed object. """
  53. def __init__(self, doId, zoneId, dclass):
  54. # The object's distributed ID.
  55. self.doId = doId
  56. # The object's current zone. Each object is associated
  57. # with only one zone.
  58. self.zoneId = zoneId
  59. # The object's class type.
  60. self.dclass = dclass
  61. # Note that the server does not store any other data about
  62. # the distributed objects; in particular, it doesn't
  63. # record its current fields. That is left to the clients.
  64. def __init__(self, tcpPort, serverAddress = None,
  65. udpPort = None, dcFileNames = None,
  66. threadedNet = None):
  67. if threadedNet is None:
  68. # Default value.
  69. threadedNet = ConfigVariableBool('threaded-net', False).value
  70. # Set up networking interfaces.
  71. numThreads = 0
  72. if threadedNet:
  73. numThreads = 1
  74. self.qcm = QueuedConnectionManager()
  75. self.qcl = QueuedConnectionListener(self.qcm, numThreads)
  76. self.qcr = QueuedConnectionReader(self.qcm, numThreads)
  77. self.cw = ConnectionWriter(self.qcm, numThreads)
  78. taskMgr.setupTaskChain('flushTask')
  79. if threadedNet:
  80. taskMgr.setupTaskChain('flushTask', numThreads = 1,
  81. threadPriority = TPLow, frameSync = True)
  82. self.tcpRendezvous = self.qcm.openTCPServerRendezvous(
  83. serverAddress or '', tcpPort, 10)
  84. self.qcl.addConnection(self.tcpRendezvous)
  85. taskMgr.add(self.listenerPoll, "serverListenerPollTask")
  86. taskMgr.add(self.readerPollUntilEmpty, "serverReaderPollTask")
  87. taskMgr.add(self.clientHardDisconnectTask, "clientHardDisconnect")
  88. # A set of clients that have recently been written to and may
  89. # need to be flushed.
  90. self.needsFlush = set()
  91. collectTcpInterval = ConfigVariableDouble('collect-tcp-interval').getValue()
  92. taskMgr.doMethodLater(collectTcpInterval, self.flushTask, 'flushTask',
  93. taskChain = 'flushTask')
  94. # A dictionary of connection -> Client object, tracking all of
  95. # the clients we currently have connected.
  96. self.clientsByConnection = {}
  97. # A similar dictionary of doIdBase -> Client object, indexing
  98. # by the client's doIdBase number instead.
  99. self.clientsByDoIdBase = {}
  100. # A dictionary of zoneId -> set([Client]), listing the clients
  101. # that have an interest in each zoneId.
  102. self.zonesToClients = {}
  103. # A dictionary of zoneId -> set([Object]), listing the
  104. # distributed objects assigned to each zone, globally.
  105. self.objectsByZoneId = {}
  106. # The number of doId's to assign to each client. Must remain
  107. # constant during server lifetime.
  108. self.doIdRange = base.config.GetInt('server-doid-range', 1000000)
  109. # An allocator object that assigns the next doIdBase to each
  110. # client.
  111. self.idAllocator = UniqueIdAllocator(0, 0xffffffff // self.doIdRange)
  112. self.dcFile = DCFile()
  113. self.dcSuffix = ''
  114. self.readDCFile(dcFileNames)
  115. def flushTask(self, task):
  116. """ This task is run periodically to flush any connections
  117. that might need it. It's only necessary in cases where
  118. collect-tcp is set true (if this is false, messages are sent
  119. immediately and do not require periodic flushing). """
  120. flush = self.needsFlush
  121. self.needsFlush = set()
  122. for client in flush:
  123. client.connection.flush()
  124. return Task.again
  125. def setTcpHeaderSize(self, headerSize):
  126. """Sets the header size of TCP packets. At the present, legal
  127. values for this are 0, 2, or 4; this specifies the number of
  128. bytes to use encode the datagram length at the start of each
  129. TCP datagram. Sender and receiver must independently agree on
  130. this."""
  131. self.qcr.setTcpHeaderSize(headerSize)
  132. self.cw.setTcpHeaderSize(headerSize)
  133. def getTcpHeaderSize(self):
  134. """Returns the current setting of TCP header size. See
  135. setTcpHeaderSize(). """
  136. return self.qcr.getTcpHeaderSize()
  137. def importModule(self, dcImports, moduleName, importSymbols):
  138. """ Imports the indicated moduleName and all of its symbols
  139. into the current namespace. This more-or-less reimplements
  140. the Python import command. """
  141. module = __import__(moduleName, globals(), locals(), importSymbols)
  142. if importSymbols:
  143. # "from moduleName import symbolName, symbolName, ..."
  144. # Copy just the named symbols into the dictionary.
  145. if importSymbols == ['*']:
  146. # "from moduleName import *"
  147. if hasattr(module, "__all__"):
  148. importSymbols = module.__all__
  149. else:
  150. importSymbols = module.__dict__.keys()
  151. for symbolName in importSymbols:
  152. if hasattr(module, symbolName):
  153. dcImports[symbolName] = getattr(module, symbolName)
  154. else:
  155. raise Exception('Symbol %s not defined in module %s.' % (symbolName, moduleName))
  156. else:
  157. # "import moduleName"
  158. # Copy the root module name into the dictionary.
  159. # Follow the dotted chain down to the actual module.
  160. components = moduleName.split('.')
  161. dcImports[components[0]] = module
  162. def readDCFile(self, dcFileNames = None):
  163. """
  164. Reads in the dc files listed in dcFileNames, or if
  165. dcFileNames is None, reads in all of the dc files listed in
  166. the Configrc file.
  167. """
  168. dcFile = self.dcFile
  169. dcFile.clear()
  170. self.dclassesByName = {}
  171. self.dclassesByNumber = {}
  172. self.hashVal = 0
  173. dcImports = {}
  174. if dcFileNames is None:
  175. readResult = dcFile.readAll()
  176. if not readResult:
  177. self.notify.error("Could not read dc file.")
  178. else:
  179. searchPath = getModelPath().getValue()
  180. for dcFileName in dcFileNames:
  181. pathname = Filename(dcFileName)
  182. vfs = VirtualFileSystem.getGlobalPtr()
  183. vfs.resolveFilename(pathname, searchPath)
  184. readResult = dcFile.read(pathname)
  185. if not readResult:
  186. self.notify.error("Could not read dc file: %s" % (pathname))
  187. self.hashVal = dcFile.getHash()
  188. # Now import all of the modules required by the DC file.
  189. for n in range(dcFile.getNumImportModules()):
  190. moduleName = dcFile.getImportModule(n)
  191. # Maybe the module name is represented as "moduleName/AI".
  192. suffix = moduleName.split('/')
  193. moduleName = suffix[0]
  194. if self.dcSuffix and self.dcSuffix in suffix[1:]:
  195. moduleName += self.dcSuffix
  196. importSymbols = []
  197. for i in range(dcFile.getNumImportSymbols(n)):
  198. symbolName = dcFile.getImportSymbol(n, i)
  199. # Maybe the symbol name is represented as "symbolName/AI".
  200. suffix = symbolName.split('/')
  201. symbolName = suffix[0]
  202. if self.dcSuffix and self.dcSuffix in suffix[1:]:
  203. symbolName += self.dcSuffix
  204. importSymbols.append(symbolName)
  205. self.importModule(dcImports, moduleName, importSymbols)
  206. # Now get the class definition for the classes named in the DC
  207. # file.
  208. for i in range(dcFile.getNumClasses()):
  209. dclass = dcFile.getClass(i)
  210. number = dclass.getNumber()
  211. className = dclass.getName() + self.dcSuffix
  212. # Does the class have a definition defined in the newly
  213. # imported namespace?
  214. classDef = dcImports.get(className)
  215. # Also try it without the dcSuffix.
  216. if classDef is None:
  217. className = dclass.getName()
  218. classDef = dcImports.get(className)
  219. if classDef is None:
  220. self.notify.debug("No class definition for %s." % (className))
  221. else:
  222. if inspect.ismodule(classDef):
  223. if not hasattr(classDef, className):
  224. self.notify.error("Module %s does not define class %s." % (className, className))
  225. classDef = getattr(classDef, className)
  226. if not inspect.isclass(classDef):
  227. self.notify.error("Symbol %s is not a class name." % (className))
  228. else:
  229. dclass.setClassDef(classDef)
  230. self.dclassesByName[className] = dclass
  231. if number >= 0:
  232. self.dclassesByNumber[number] = dclass
  233. # listens for new clients
  234. def listenerPoll(self, task):
  235. if self.qcl.newConnectionAvailable():
  236. rendezvous = PointerToConnection()
  237. netAddress = NetAddress()
  238. newConnection = PointerToConnection()
  239. retVal = self.qcl.getNewConnection(rendezvous, netAddress,
  240. newConnection)
  241. if not retVal:
  242. return Task.cont
  243. # Crazy dereferencing
  244. newConnection = newConnection.p()
  245. # Add clients information to dictionary
  246. id = self.idAllocator.allocate()
  247. doIdBase = id * self.doIdRange + 1
  248. self.notify.info(
  249. "Got client %s from %s" % (doIdBase, netAddress))
  250. client = self.Client(newConnection, netAddress, doIdBase)
  251. self.clientsByConnection[client.connection] = client
  252. self.clientsByDoIdBase[client.doIdBase] = client
  253. # Now we can start listening to that new connection.
  254. self.qcr.addConnection(newConnection)
  255. self.lastConnection = newConnection
  256. self.sendDoIdRange(client)
  257. return Task.cont
  258. def readerPollUntilEmpty(self, task):
  259. """ continuously polls for new messages on the server """
  260. while self.readerPollOnce():
  261. pass
  262. return Task.cont
  263. def readerPollOnce(self):
  264. """ checks for available messages to the server """
  265. availGetVal = self.qcr.dataAvailable()
  266. if availGetVal:
  267. datagram = NetDatagram()
  268. readRetVal = self.qcr.getData(datagram)
  269. if readRetVal:
  270. # need to send to message processing unit
  271. self.handleDatagram(datagram)
  272. return availGetVal
  273. def handleDatagram(self, datagram):
  274. """ switching station for messages """
  275. client = self.clientsByConnection.get(datagram.getConnection())
  276. if not client:
  277. # This shouldn't be possible, though it appears to happen
  278. # sometimes?
  279. self.notify.warning(
  280. "Ignoring datagram from unknown connection %s" % (datagram.getConnection()))
  281. return
  282. if self.notify.getDebug():
  283. self.notify.debug(
  284. "ServerRepository received datagram from %s:" % (client.doIdBase))
  285. #datagram.dumpHex(ostream)
  286. dgi = DatagramIterator(datagram)
  287. type = dgi.getUint16()
  288. if type == CLIENT_DISCONNECT_CMU:
  289. self.handleClientDisconnect(client)
  290. elif type == CLIENT_SET_INTEREST_CMU:
  291. self.handleClientSetInterest(client, dgi)
  292. elif type == CLIENT_OBJECT_GENERATE_CMU:
  293. self.handleClientCreateObject(datagram, dgi)
  294. elif type == CLIENT_OBJECT_UPDATE_FIELD:
  295. self.handleClientObjectUpdateField(datagram, dgi)
  296. elif type == CLIENT_OBJECT_UPDATE_FIELD_TARGETED_CMU:
  297. self.handleClientObjectUpdateField(datagram, dgi, targeted = True)
  298. elif type == OBJECT_DELETE_CMU:
  299. self.handleClientDeleteObject(datagram, dgi.getUint32())
  300. elif type == OBJECT_SET_ZONE_CMU:
  301. self.handleClientObjectSetZone(datagram, dgi)
  302. else:
  303. self.handleMessageType(type, dgi)
  304. def handleMessageType(self, msgType, di):
  305. self.notify.warning("unrecognized message type %s" % (msgType))
  306. def handleClientCreateObject(self, datagram, dgi):
  307. """ client wants to create an object, so we store appropriate
  308. data, and then pass message along to corresponding zones """
  309. connection = datagram.getConnection()
  310. zoneId = dgi.getUint32()
  311. classId = dgi.getUint16()
  312. doId = dgi.getUint32()
  313. client = self.clientsByConnection[connection]
  314. if self.getDoIdBase(doId) != client.doIdBase:
  315. self.notify.warning(
  316. "Ignoring attempt to create invalid doId %s from client %s" % (doId, client.doIdBase))
  317. return
  318. dclass = self.dclassesByNumber[classId]
  319. object = client.objectsByDoId.get(doId)
  320. if object:
  321. # This doId is already in use; thus, this message is
  322. # really just an update.
  323. if object.dclass != dclass:
  324. self.notify.warning(
  325. "Ignoring attempt to change object %s from %s to %s by client %s" % (
  326. doId, object.dclass.getName(), dclass.getName(), client.doIdBase))
  327. return
  328. self.setObjectZone(client, object, zoneId)
  329. else:
  330. if self.notify.getDebug():
  331. self.notify.debug(
  332. "Creating object %s of type %s by client %s" % (
  333. doId, dclass.getName(), client.doIdBase))
  334. object = self.Object(doId, zoneId, dclass)
  335. client.objectsByDoId[doId] = object
  336. client.objectsByZoneId.setdefault(zoneId, set()).add(object)
  337. self.objectsByZoneId.setdefault(zoneId, set()).add(object)
  338. self.updateClientInterestZones(client)
  339. # Rebuild the new datagram that we'll send on. We shim in the
  340. # doIdBase of the owner.
  341. dg = PyDatagram()
  342. dg.addUint16(OBJECT_GENERATE_CMU)
  343. dg.addUint32(client.doIdBase)
  344. dg.addUint32(zoneId)
  345. dg.addUint16(classId)
  346. dg.addUint32(doId)
  347. dg.appendData(dgi.getRemainingBytes())
  348. self.sendToZoneExcept(zoneId, dg, [client])
  349. def handleClientObjectUpdateField(self, datagram, dgi, targeted = False):
  350. """ Received an update request from a client. """
  351. connection = datagram.getConnection()
  352. client = self.clientsByConnection[connection]
  353. if targeted:
  354. targetId = dgi.getUint32()
  355. doId = dgi.getUint32()
  356. fieldId = dgi.getUint16()
  357. doIdBase = self.getDoIdBase(doId)
  358. owner = self.clientsByDoIdBase.get(doIdBase)
  359. object = owner and owner.objectsByDoId.get(doId)
  360. if not object:
  361. self.notify.warning(
  362. "Ignoring update for unknown object %s from client %s" % (
  363. doId, client.doIdBase))
  364. return
  365. dcfield = object.dclass.getFieldByIndex(fieldId)
  366. if dcfield is None:
  367. self.notify.warning(
  368. "Ignoring update for field %s on object %s from client %s; no such field for class %s." % (
  369. fieldId, doId, client.doIdBase, object.dclass.getName()))
  370. if client != owner:
  371. # This message was not sent by the object's owner.
  372. if not dcfield.hasKeyword('clsend') and not dcfield.hasKeyword('p2p'):
  373. self.notify.warning(
  374. "Ignoring update for %s.%s on object %s from client %s: not owner" % (
  375. object.dclass.getName(), dcfield.getName(), doId, client.doIdBase))
  376. return
  377. # We reformat the message slightly to insert the sender's
  378. # doIdBase.
  379. dg = PyDatagram()
  380. dg.addUint16(OBJECT_UPDATE_FIELD_CMU)
  381. dg.addUint32(client.doIdBase)
  382. dg.addUint32(doId)
  383. dg.addUint16(fieldId)
  384. dg.appendData(dgi.getRemainingBytes())
  385. if targeted:
  386. # A targeted update: only to the indicated client.
  387. target = self.clientsByDoIdBase.get(targetId)
  388. if not target:
  389. self.notify.warning(
  390. "Ignoring targeted update to %s for %s.%s on object %s from client %s: target not known" % (
  391. targetId,
  392. dclass.getName(), dcfield.getName(), doId, client.doIdBase))
  393. return
  394. self.cw.send(dg, target.connection)
  395. self.needsFlush.add(target)
  396. elif dcfield.hasKeyword('p2p'):
  397. # p2p: to object owner only
  398. self.cw.send(dg, owner.connection)
  399. self.needsFlush.add(owner)
  400. elif dcfield.hasKeyword('broadcast'):
  401. # Broadcast: to everyone except orig sender
  402. self.sendToZoneExcept(object.zoneId, dg, [client])
  403. elif dcfield.hasKeyword('reflect'):
  404. # Reflect: broadcast to everyone including orig sender
  405. self.sendToZoneExcept(object.zoneId, dg, [])
  406. else:
  407. self.notify.warning(
  408. "Message is not broadcast or p2p")
  409. def getDoIdBase(self, doId):
  410. """ Given a doId, return the corresponding doIdBase. This
  411. will be the owner of the object (clients may only create
  412. object doId's within their assigned range). """
  413. return int(doId / self.doIdRange) * self.doIdRange + 1
  414. def handleClientDeleteObject(self, datagram, doId):
  415. """ client deletes an object, let everyone who has interest in
  416. the object's zone know about it. """
  417. connection = datagram.getConnection()
  418. client = self.clientsByConnection[connection]
  419. object = client.objectsByDoId.get(doId)
  420. if not object:
  421. self.notify.warning(
  422. "Ignoring update for unknown object %s from client %s" % (
  423. doId, client.doIdBase))
  424. return
  425. self.sendToZoneExcept(object.zoneId, datagram, [])
  426. self.objectsByZoneId[object.zoneId].remove(object)
  427. if not self.objectsByZoneId[object.zoneId]:
  428. del self.objectsByZoneId[object.zoneId]
  429. client.objectsByZoneId[object.zoneId].remove(object)
  430. if not client.objectsByZoneId[object.zoneId]:
  431. del client.objectsByZoneId[object.zoneId]
  432. del client.objectsByDoId[doId]
  433. self.updateClientInterestZones(client)
  434. def handleClientObjectSetZone(self, datagram, dgi):
  435. """ The client is telling us the object is changing to a new
  436. zone. """
  437. doId = dgi.getUint32()
  438. zoneId = dgi.getUint32()
  439. connection = datagram.getConnection()
  440. client = self.clientsByConnection[connection]
  441. object = client.objectsByDoId.get(doId)
  442. if not object:
  443. # Don't know this object.
  444. self.notify.warning("Ignoring object location for %s: unknown" % (doId))
  445. return
  446. self.setObjectZone(client, object, zoneId)
  447. def setObjectZone(self, owner, object, zoneId):
  448. if object.zoneId == zoneId:
  449. # No change.
  450. return
  451. oldZoneId = object.zoneId
  452. self.objectsByZoneId[object.zoneId].remove(object)
  453. if not self.objectsByZoneId[object.zoneId]:
  454. del self.objectsByZoneId[object.zoneId]
  455. owner.objectsByZoneId[object.zoneId].remove(object)
  456. if not owner.objectsByZoneId[object.zoneId]:
  457. del owner.objectsByZoneId[object.zoneId]
  458. object.zoneId = zoneId
  459. self.objectsByZoneId.setdefault(zoneId, set()).add(object)
  460. owner.objectsByZoneId.setdefault(zoneId, set()).add(object)
  461. self.updateClientInterestZones(owner)
  462. # Any clients that are listening to oldZoneId but not zoneId
  463. # should receive a disable message: this object has just gone
  464. # out of scope for you.
  465. datagram = PyDatagram()
  466. datagram.addUint16(OBJECT_DISABLE_CMU)
  467. datagram.addUint32(object.doId)
  468. for client in self.zonesToClients[oldZoneId]:
  469. if client != owner:
  470. if zoneId not in client.currentInterestZoneIds:
  471. self.cw.send(datagram, client.connection)
  472. self.needsFlush.add(client)
  473. # The client is now responsible for sending a generate for the
  474. # object that just switched zones, to inform the clients that
  475. # are listening to the new zoneId but not the old zoneId.
  476. def sendDoIdRange(self, client):
  477. """ sends the client the range of doid's that the client can
  478. use """
  479. datagram = NetDatagram()
  480. datagram.addUint16(SET_DOID_RANGE_CMU)
  481. datagram.addUint32(client.doIdBase)
  482. datagram.addUint32(self.doIdRange)
  483. self.cw.send(datagram, client.connection)
  484. self.needsFlush.add(client)
  485. # a client disconnected from us, we need to update our data, also
  486. # tell other clients to remove the disconnected clients objects
  487. def handleClientDisconnect(self, client):
  488. for zoneId in client.currentInterestZoneIds:
  489. if len(self.zonesToClients[zoneId]) == 1:
  490. del self.zonesToClients[zoneId]
  491. else:
  492. self.zonesToClients[zoneId].remove(client)
  493. for object in client.objectsByDoId.values():
  494. #create and send delete message
  495. datagram = NetDatagram()
  496. datagram.addUint16(OBJECT_DELETE_CMU)
  497. datagram.addUint32(object.doId)
  498. self.sendToZoneExcept(object.zoneId, datagram, [])
  499. self.objectsByZoneId[object.zoneId].remove(object)
  500. if not self.objectsByZoneId[object.zoneId]:
  501. del self.objectsByZoneId[object.zoneId]
  502. client.objectsByDoId = {}
  503. client.objectsByZoneId = {}
  504. del self.clientsByConnection[client.connection]
  505. del self.clientsByDoIdBase[client.doIdBase]
  506. id = client.doIdBase // self.doIdRange
  507. self.idAllocator.free(id)
  508. self.qcr.removeConnection(client.connection)
  509. self.qcm.closeConnection(client.connection)
  510. def handleClientSetInterest(self, client, dgi):
  511. """ The client is specifying a particular set of zones it is
  512. interested in. """
  513. zoneIds = set()
  514. while dgi.getRemainingSize() > 0:
  515. zoneId = dgi.getUint32()
  516. zoneIds.add(zoneId)
  517. client.explicitInterestZoneIds = zoneIds
  518. self.updateClientInterestZones(client)
  519. def updateClientInterestZones(self, client):
  520. """ Something about the client has caused its set of interest
  521. zones to potentially change. Recompute them. """
  522. origZoneIds = client.currentInterestZoneIds
  523. newZoneIds = client.explicitInterestZoneIds | set(client.objectsByZoneId.keys())
  524. if origZoneIds == newZoneIds:
  525. # No change.
  526. return
  527. client.currentInterestZoneIds = newZoneIds
  528. addedZoneIds = newZoneIds - origZoneIds
  529. removedZoneIds = origZoneIds - newZoneIds
  530. for zoneId in addedZoneIds:
  531. self.zonesToClients.setdefault(zoneId, set()).add(client)
  532. # The client is opening interest in this zone. Need to get
  533. # all of the data from clients who may have objects in
  534. # this zone
  535. datagram = NetDatagram()
  536. datagram.addUint16(REQUEST_GENERATES_CMU)
  537. datagram.addUint32(zoneId)
  538. self.sendToZoneExcept(zoneId, datagram, [client])
  539. datagram = PyDatagram()
  540. datagram.addUint16(OBJECT_DISABLE_CMU)
  541. for zoneId in removedZoneIds:
  542. self.zonesToClients[zoneId].remove(client)
  543. # The client is abandoning interest in this zone. Any
  544. # objects in this zone should be disabled for the client.
  545. for object in self.objectsByZoneId.get(zoneId, []):
  546. datagram.addUint32(object.doId)
  547. self.cw.send(datagram, client.connection)
  548. self.needsFlush.add(client)
  549. def clientHardDisconnectTask(self, task):
  550. """ client did not tell us he was leaving but we lost connection to
  551. him, so we need to update our data and tell others """
  552. for client in list(self.clientsByConnection.values()):
  553. if not self.qcr.isConnectionOk(client.connection):
  554. self.handleClientDisconnect(client)
  555. return Task.cont
  556. def sendToZoneExcept(self, zoneId, datagram, exceptionList):
  557. """sends a message to everyone who has interest in the
  558. indicated zone, except for the clients on exceptionList."""
  559. if self.notify.getDebug():
  560. self.notify.debug(
  561. "ServerRepository sending to all in zone %s except %s:" % (zoneId, [c.doIdBase for c in exceptionList]))
  562. #datagram.dumpHex(ostream)
  563. for client in self.zonesToClients.get(zoneId, []):
  564. if client not in exceptionList:
  565. if self.notify.getDebug():
  566. self.notify.debug(
  567. " -> %s" % (client.doIdBase))
  568. self.cw.send(datagram, client.connection)
  569. self.needsFlush.add(client)
  570. def sendToAllExcept(self, datagram, exceptionList):
  571. """ sends a message to all connected clients, except for
  572. clients on exceptionList. """
  573. if self.notify.getDebug():
  574. self.notify.debug(
  575. "ServerRepository sending to all except %s:" % ([c.doIdBase for c in exceptionList],))
  576. #datagram.dumpHex(ostream)
  577. for client in self.clientsByConnection.values():
  578. if client not in exceptionList:
  579. if self.notify.getDebug():
  580. self.notify.debug(
  581. " -> %s" % (client.doIdBase))
  582. self.cw.send(datagram, client.connection)
  583. self.needsFlush.add(client)