ClientRepository.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653
  1. """ClientRepository module: contains the ClientRepository class"""
  2. from PandaModules import *
  3. from TaskManagerGlobal import *
  4. from MsgTypes import *
  5. from ShowBaseGlobal import *
  6. import Task
  7. import DirectNotifyGlobal
  8. import ClientDistClass
  9. import CRCache
  10. # The repository must import all known types of Distributed Objects
  11. #import DistributedObject
  12. #import DistributedToon
  13. import DirectObject
  14. class ClientRepository(DirectObject.DirectObject):
  15. notify = DirectNotifyGlobal.directNotify.newCategory("ClientRepository")
  16. TASK_PRIORITY = -30
  17. def __init__(self, dcFileName):
  18. self.number2cdc={}
  19. self.name2cdc={}
  20. self.doId2do={}
  21. self.doId2cdc={}
  22. self.parseDcFile(dcFileName)
  23. self.cache=CRCache.CRCache()
  24. self.serverDelta = 0
  25. # Set this to 'http' to establish a connection to the server
  26. # using the HTTPClient interface, which ultimately uses the
  27. # OpenSSL socket library (even though SSL is not involved).
  28. # This is not as robust a socket library as NSPR's, but the
  29. # HTTPClient interface does a good job of negotiating the
  30. # connection over an HTTP proxy if one is in use.
  31. # Set it to 'nspr' to use Panda's net interface
  32. # (e.g. QueuedConnectionManager, etc.) to establish the
  33. # connection, which ultimately uses the NSPR socket library.
  34. # This is a much better socket library, but it may be more
  35. # than you need for most applications; and the proxy support
  36. # is weak.
  37. # Set it to 'default' to use the HTTPClient interface if a
  38. # proxy is in place, but the NSPR interface if we don't have a
  39. # proxy.
  40. self.connectMethod = base.config.GetString('connect-method', 'default')
  41. self.connectHttp = None
  42. self.qcm = None
  43. self.bootedIndex = None
  44. self.bootedText = None
  45. self.tcpConn = None
  46. # Reader statistics
  47. self.rsDatagramCount = 0
  48. self.rsUpdateObjs = {}
  49. self.rsLastUpdate = 0
  50. self.rsDoReport = base.config.GetBool('reader-statistics', 1)
  51. self.rsUpdateInterval = base.config.GetDouble('reader-statistics-interval', 10)
  52. return None
  53. def setServerDelta(self, delta):
  54. """
  55. Indicates the approximate difference in seconds between the
  56. client's clock and the server's clock, in universal time (not
  57. including timezone shifts). This is mainly useful for
  58. reporting synchronization information to the logs; don't
  59. depend on it for any precise timing requirements.
  60. Also see Notify.setServerDelta(), which also accounts for a
  61. timezone shift.
  62. """
  63. self.serverDelta = delta
  64. def getServerDelta(self):
  65. return self.serverDelta
  66. def parseDcFile(self, dcFileName):
  67. self.dcFile = DCFile()
  68. readResult = self.dcFile.read(dcFileName)
  69. if not readResult:
  70. self.notify.error("Could not read dcfile: " + dcFileName)
  71. self.hashVal = self.dcFile.getHash()
  72. return self.parseDcClasses(self.dcFile)
  73. def parseDcClasses(self, dcFile):
  74. numClasses = dcFile.getNumClasses()
  75. for i in range(0, numClasses):
  76. # Create a clientDistClass from the dcClass
  77. dcClass = dcFile.getClass(i)
  78. clientDistClass = ClientDistClass.ClientDistClass(dcClass)
  79. # List the cdc in the number and name dictionaries
  80. self.number2cdc[dcClass.getNumber()]=clientDistClass
  81. self.name2cdc[dcClass.getName()]=clientDistClass
  82. return None
  83. def connect(self, serverList,
  84. successCallback = None, successArgs = [],
  85. failureCallback = None, failureArgs = []):
  86. """
  87. Attempts to establish a connection to the server. May return
  88. before the connection is established. The two callbacks
  89. represent the two functions to call (and their arguments) on
  90. success or failure, respectively. The failure callback also
  91. gets one additional parameter, which will be passed in first:
  92. the return status code giving reason for failure, if it is
  93. known.
  94. """
  95. if self.hasProxy:
  96. self.notify.info("Connecting to gameserver via proxy: %s" % (self.proxy.cStr()))
  97. else:
  98. self.notify.info("Connecting to gameserver directly (no proxy).");
  99. if self.connectMethod == 'http':
  100. self.connectHttp = 1
  101. elif self.connectMethod == 'nspr':
  102. self.connectHttp = 0
  103. else:
  104. self.connectHttp = self.hasProxy
  105. self.bootedIndex = None
  106. self.bootedText = None
  107. if self.connectHttp:
  108. # In the HTTP case, we can't just iterate through the list
  109. # of servers, because each server attempt requires
  110. # spawning a request and then coming back later to check
  111. # the success or failure. Instead, we start the ball
  112. # rolling by calling the connect callback, which will call
  113. # itself repeatedly until we establish a connection (or
  114. # run out of servers).
  115. ch = self.http.makeChannel(0)
  116. self.httpConnectCallback(ch, serverList, 0,
  117. successCallback, successArgs,
  118. failureCallback, failureArgs)
  119. else:
  120. if self.qcm == None:
  121. self.qcm = QueuedConnectionManager()
  122. self.cw = ConnectionWriter(self.qcm, 0)
  123. self.qcr = QueuedConnectionReader(self.qcm, 0)
  124. minLag = config.GetFloat('min-lag', 0.)
  125. maxLag = config.GetFloat('max-lag', 0.)
  126. if minLag or maxLag:
  127. self.qcr.startDelay(minLag, maxLag)
  128. # A big old 20 second timeout.
  129. gameServerTimeoutMs = base.config.GetInt("game-server-timeout-ms",
  130. 20000)
  131. # Try each of the servers in turn.
  132. for url in serverList:
  133. self.notify.info("Connecting to %s via NSPR interface." % (url.cStr()))
  134. self.tcpConn = self.qcm.openTCPClientConnection(
  135. url.getServer(), url.getPort(),
  136. gameServerTimeoutMs)
  137. if self.tcpConn:
  138. self.tcpConn.setNoDelay(1)
  139. self.qcr.addConnection(self.tcpConn)
  140. self.startReaderPollTask()
  141. if successCallback:
  142. successCallback(*successArgs)
  143. return
  144. # Failed to connect.
  145. if failureCallback:
  146. failureCallback(0, *failureArgs)
  147. def disconnect(self):
  148. """Closes the previously-established connection.
  149. """
  150. self.notify.info("Closing connection to server.")
  151. if self.tcpConn != None:
  152. if self.connectHttp:
  153. self.tcpConn.close()
  154. else:
  155. self.qcm.closeConnection(self.tcpConn)
  156. self.tcpConn = None
  157. self.stopReaderPollTask()
  158. def httpConnectCallback(self, ch, serverList, serverIndex,
  159. successCallback, successArgs,
  160. failureCallback, failureArgs):
  161. if ch.isConnectionReady():
  162. self.tcpConn = ch.getConnection()
  163. self.tcpConn.userManagesMemory = 1
  164. self.startReaderPollTask()
  165. if successCallback:
  166. successCallback(*successArgs)
  167. elif serverIndex < len(serverList):
  168. # No connection yet, but keep trying.
  169. url = serverList[serverIndex]
  170. self.notify.info("Connecting to %s via HTTP interface." % (url.cStr()))
  171. ch.beginConnectTo(DocumentSpec(url))
  172. ch.spawnTask(name = 'connect-to-server',
  173. callback = self.httpConnectCallback,
  174. extraArgs = [ch, serverList, serverIndex + 1,
  175. successCallback, successArgs,
  176. failureCallback, failureArgs])
  177. else:
  178. # No more servers to try; we have to give up now.
  179. if failureCallback:
  180. failureCallback(ch.getStatusCode(), *failureArgs)
  181. def startReaderPollTask(self):
  182. # Stop any tasks we are running now
  183. self.stopReaderPollTask()
  184. taskMgr.add(self.readerPollUntilEmpty, "readerPollTask",
  185. priority=self.TASK_PRIORITY)
  186. return None
  187. def stopReaderPollTask(self):
  188. taskMgr.remove("readerPollTask")
  189. return None
  190. def readerPollUntilEmpty(self, task):
  191. while self.readerPollOnce():
  192. pass
  193. return Task.cont
  194. def readerPollOnce(self):
  195. # we simulate the network plug being pulled by setting tcpConn
  196. # to None; enforce that condition
  197. if not self.tcpConn:
  198. return 0
  199. # Make sure any recently-sent datagrams are flushed when the
  200. # time expires, if we're in collect-tcp mode.
  201. self.tcpConn.considerFlush()
  202. if self.rsDoReport:
  203. self.reportReaderStatistics()
  204. if self.connectHttp:
  205. datagram = Datagram()
  206. if self.tcpConn.receiveDatagram(datagram):
  207. if self.rsDoReport:
  208. self.rsDatagramCount += 1
  209. self.handleDatagram(datagram)
  210. return 1
  211. # Unable to receive a datagram: did we lose the connection?
  212. if self.tcpConn.isClosed():
  213. self.tcpConn = None
  214. self.stopReaderPollTask()
  215. self.loginFSM.request("noConnection")
  216. return 0
  217. else:
  218. self.ensureValidConnection()
  219. if self.qcr.dataAvailable():
  220. datagram = NetDatagram()
  221. if self.qcr.getData(datagram):
  222. if self.rsDoReport:
  223. self.rsDatagramCount += 1
  224. self.handleDatagram(datagram)
  225. return 1
  226. return 0
  227. def ensureValidConnection(self):
  228. # Was the connection reset?
  229. if self.connectHttp:
  230. pass
  231. else:
  232. if self.qcm.resetConnectionAvailable():
  233. resetConnectionPointer = PointerToConnection()
  234. if self.qcm.getResetConnection(resetConnectionPointer):
  235. resetConn = resetConnectionPointer.p()
  236. self.qcm.closeConnection(resetConn)
  237. # if we've simulated a network plug pull, restore the
  238. # simulated plug
  239. self.restoreNetworkPlug()
  240. if self.tcpConn.this == resetConn.this:
  241. self.tcpConn = None
  242. self.stopReaderPollTask()
  243. self.loginFSM.request("noConnection")
  244. else:
  245. self.notify.warning("Lost unknown connection.")
  246. return None
  247. def handleDatagram(self, datagram):
  248. # This class is meant to be pure virtual, and any classes that
  249. # inherit from it need to make their own handleDatagram method
  250. pass
  251. def reportReaderStatistics(self):
  252. now = globalClock.getRealTime()
  253. if now - self.rsLastUpdate < self.rsUpdateInterval:
  254. return
  255. self.rsLastUpdate = now
  256. self.notify.info("Received %s datagrams" % (self.rsDatagramCount))
  257. if self.rsUpdateObjs:
  258. self.notify.info("Updates: %s" % (self.rsUpdateObjs))
  259. self.rsDatagramCount = 0
  260. self.rsUpdateObjs = {}
  261. def handleGenerateWithRequired(self, di):
  262. # Get the class Id
  263. classId = di.getArg(STUint16);
  264. # Get the DO Id
  265. doId = di.getArg(STUint32)
  266. # Look up the cdc
  267. cdc = self.number2cdc[classId]
  268. # Create a new distributed object, and put it in the dictionary
  269. distObj = self.generateWithRequiredFields(cdc, doId, di)
  270. return None
  271. def handleGenerateWithRequiredOther(self, di):
  272. # Get the class Id
  273. classId = di.getArg(STUint16);
  274. # Get the DO Id
  275. doId = di.getArg(STUint32)
  276. # Look up the cdc
  277. cdc = self.number2cdc[classId]
  278. # Create a new distributed object, and put it in the dictionary
  279. distObj = self.generateWithRequiredOtherFields(cdc, doId, di)
  280. return None
  281. def handleQuietZoneGenerateWithRequired(self, di):
  282. # Special handler for quiet zone generates -- we need to filter
  283. # Get the class Id
  284. classId = di.getArg(STUint16);
  285. # Get the DO Id
  286. doId = di.getArg(STUint32)
  287. # Look up the cdc
  288. cdc = self.number2cdc[classId]
  289. # If the class is a neverDisable class (which implies uberzone) we
  290. # should go ahead and generate it even though we are in the quiet zone
  291. if cdc.constructor.neverDisable:
  292. # Create a new distributed object, and put it in the dictionary
  293. distObj = self.generateWithRequiredFields(cdc, doId, di)
  294. return None
  295. def handleQuietZoneGenerateWithRequiredOther(self, di):
  296. # Special handler for quiet zone generates -- we need to filter
  297. # Get the class Id
  298. classId = di.getArg(STUint16);
  299. # Get the DO Id
  300. doId = di.getArg(STUint32)
  301. # Look up the cdc
  302. cdc = self.number2cdc[classId]
  303. # If the class is a neverDisable class (which implies uberzone) we
  304. # should go ahead and generate it even though we are in the quiet zone
  305. if cdc.constructor.neverDisable:
  306. # Create a new distributed object, and put it in the dictionary
  307. distObj = self.generateWithRequiredOtherFields(cdc, doId, di)
  308. return None
  309. def generateWithRequiredFields(self, cdc, doId, di):
  310. # Is it in our dictionary?
  311. if self.doId2do.has_key(doId):
  312. # If so, just update it.
  313. distObj = self.doId2do[doId]
  314. distObj.generate()
  315. distObj.updateRequiredFields(cdc, di)
  316. distObj.announceGenerate()
  317. # Is it in the cache? If so, pull it out, put it in the dictionaries,
  318. # and update it.
  319. elif self.cache.contains(doId):
  320. # If so, pull it out of the cache...
  321. distObj = self.cache.retrieve(doId)
  322. # put it in both dictionaries...
  323. self.doId2do[doId] = distObj
  324. self.doId2cdc[doId] = cdc
  325. # and update it.
  326. distObj.generate()
  327. distObj.updateRequiredFields(cdc, di)
  328. distObj.announceGenerate()
  329. # If it is not in the dictionary or the cache, then...
  330. else:
  331. # Construct a new one
  332. distObj = cdc.constructor(self)
  333. # Assign it an Id
  334. distObj.doId = doId
  335. # Put the new do in both dictionaries
  336. self.doId2do[doId] = distObj
  337. self.doId2cdc[doId] = cdc
  338. # Update the required fields
  339. distObj.generateInit() # Only called when constructed
  340. distObj.generate()
  341. distObj.updateRequiredFields(cdc, di)
  342. distObj.announceGenerate()
  343. return distObj
  344. def generateWithRequiredOtherFields(self, cdc, doId, di):
  345. # Is it in our dictionary?
  346. if self.doId2do.has_key(doId):
  347. # If so, just update it.
  348. distObj = self.doId2do[doId]
  349. distObj.generate()
  350. distObj.updateRequiredOtherFields(cdc, di)
  351. distObj.announceGenerate()
  352. # Is it in the cache? If so, pull it out, put it in the dictionaries,
  353. # and update it.
  354. elif self.cache.contains(doId):
  355. # If so, pull it out of the cache...
  356. distObj = self.cache.retrieve(doId)
  357. # put it in both dictionaries...
  358. self.doId2do[doId] = distObj
  359. self.doId2cdc[doId] = cdc
  360. # and update it.
  361. distObj.generate()
  362. distObj.updateRequiredOtherFields(cdc, di)
  363. distObj.announceGenerate()
  364. # If it is not in the dictionary or the cache, then...
  365. else:
  366. # Construct a new one
  367. distObj = cdc.constructor(self)
  368. # Assign it an Id
  369. distObj.doId = doId
  370. # Put the new do in both dictionaries
  371. self.doId2do[doId] = distObj
  372. self.doId2cdc[doId] = cdc
  373. # Update the required fields
  374. distObj.generateInit() # Only called when constructed
  375. distObj.generate()
  376. distObj.updateRequiredOtherFields(cdc, di)
  377. distObj.announceGenerate()
  378. return distObj
  379. def handleDisable(self, di):
  380. # Get the DO Id
  381. doId = di.getArg(STUint32)
  382. # disable it.
  383. self.disableDoId(doId)
  384. return None
  385. def disableDoId(self, doId):
  386. # Make sure the object exists
  387. if self.doId2do.has_key(doId):
  388. # Look up the object
  389. distObj = self.doId2do[doId]
  390. # remove the object from both dictionaries
  391. del(self.doId2do[doId])
  392. del(self.doId2cdc[doId])
  393. assert(len(self.doId2do) == len(self.doId2cdc))
  394. # Only cache the object if it is a "cacheable" type
  395. # object; this way we don't clutter up the caches with
  396. # trivial objects that don't benefit from caching.
  397. if distObj.getCacheable():
  398. self.cache.cache(distObj)
  399. else:
  400. distObj.deleteOrDelay()
  401. else:
  402. ClientRepository.notify.warning("Disable failed. DistObj " +
  403. str(doId) +
  404. " is not in dictionary")
  405. return None
  406. def handleDelete(self, di):
  407. # Get the DO Id
  408. doId = di.getArg(STUint32)
  409. self.deleteObject(doId)
  410. def deleteObject(self, doId):
  411. """deleteObject(self, doId)
  412. Removes the object from the client's view of the world. This
  413. should normally not be called except in the case of error
  414. recovery, since the server will normally be responsible for
  415. deleting and disabling objects as they go out of scope.
  416. After this is called, future updates by server on this object
  417. will be ignored (with a warning message). The object will
  418. become valid again the next time the server sends a generate
  419. message for this doId.
  420. This is not a distributed message and does not delete the
  421. object on the server or on any other client.
  422. """
  423. # If it is in the dictionaries, remove it.
  424. if self.doId2do.has_key(doId):
  425. obj = self.doId2do[doId]
  426. # Remove it from the dictionaries
  427. del(self.doId2do[doId])
  428. del(self.doId2cdc[doId])
  429. # Sanity check the dictionaries
  430. assert(len(self.doId2do) == len(self.doId2cdc))
  431. # Disable, announce, and delete the object itself...
  432. # unless delayDelete is on...
  433. obj.deleteOrDelay()
  434. # If it is in the cache, remove it.
  435. elif self.cache.contains(doId):
  436. self.cache.delete(doId)
  437. # Otherwise, ignore it
  438. else:
  439. ClientRepository.notify.warning(
  440. "Asked to delete non-existent DistObj " + str(doId))
  441. return None
  442. def handleUpdateField(self, di):
  443. # Get the DO Id
  444. doId = di.getArg(STUint32)
  445. #print("Updating " + str(doId))
  446. if self.rsDoReport:
  447. self.rsUpdateObjs[doId] = self.rsUpdateObjs.get(doId, 0) + 1
  448. # Find the DO
  449. do = self.doId2do.get(doId)
  450. cdc = self.doId2cdc.get(doId)
  451. if (do != None and cdc != None):
  452. # Let the cdc finish the job
  453. cdc.updateField(do, di)
  454. else:
  455. ClientRepository.notify.warning(
  456. "Asked to update non-existent DistObj " + str(doId))
  457. return None
  458. def handleGoGetLost(self, di):
  459. # The server told us it's about to drop the connection on us.
  460. # Get ready!
  461. if (di.getRemainingSize() > 0):
  462. self.bootedIndex = di.getUint16()
  463. self.bootedText = di.getString()
  464. ClientRepository.notify.warning(
  465. "Server is booting us out (%d): %s" % (self.bootedIndex, self.bootedText))
  466. else:
  467. self.bootedIndex = None
  468. self.bootedText = None
  469. ClientRepository.notify.warning(
  470. "Server is booting us out with no explanation.")
  471. def handleUnexpectedMsgType(self, msgType, di):
  472. if msgType == CLIENT_GO_GET_LOST:
  473. self.handleGoGetLost(di)
  474. else:
  475. currentLoginState = self.loginFSM.getCurrentState()
  476. if currentLoginState:
  477. currentLoginStateName = currentLoginState.getName()
  478. else:
  479. currentLoginStateName = "None"
  480. currentGameState = self.gameFSM.getCurrentState()
  481. if currentGameState:
  482. currentGameStateName = currentGameState.getName()
  483. else:
  484. currentGameStateName = "None"
  485. ClientRepository.notify.warning(
  486. "Ignoring unexpected message type: " +
  487. str(msgType) +
  488. " login state: " +
  489. currentLoginStateName +
  490. " game state: " +
  491. currentGameStateName)
  492. return None
  493. def sendSetShardMsg(self, shardId):
  494. datagram = Datagram()
  495. # Add message type
  496. datagram.addUint16(CLIENT_SET_SHARD)
  497. # Add shard id
  498. datagram.addUint32(shardId)
  499. # send the message
  500. self.send(datagram)
  501. return None
  502. def sendSetZoneMsg(self, zoneId):
  503. datagram = Datagram()
  504. # Add message type
  505. datagram.addUint16(CLIENT_SET_ZONE)
  506. # Add zone id
  507. datagram.addUint16(zoneId)
  508. # send the message
  509. self.send(datagram)
  510. return None
  511. def sendUpdate(self, do, fieldName, args, sendToId = None):
  512. # Get the DO id
  513. doId = do.doId
  514. # Get the cdc
  515. cdc = self.doId2cdc.get(doId, None)
  516. if cdc:
  517. # Let the cdc finish the job
  518. cdc.sendUpdate(self, do, fieldName, args, sendToId)
  519. def send(self, datagram):
  520. if self.notify.getDebug():
  521. print "ClientRepository sending datagram:"
  522. datagram.dumpHex(ostream)
  523. if not self.tcpConn:
  524. self.notify.warning("Unable to send message after connection is closed.")
  525. return
  526. if self.connectHttp:
  527. if not self.tcpConn.sendDatagram(datagram):
  528. self.notify.warning("Could not send datagram.")
  529. else:
  530. self.cw.send(datagram, self.tcpConn)
  531. return None
  532. def replaceMethod(self, oldMethod, newFunction):
  533. foundIt = 0
  534. import new
  535. # Iterate over the ClientDistClasses
  536. for cdc in self.number2cdc.values():
  537. # Iterate over the ClientDistUpdates
  538. for cdu in cdc.allCDU:
  539. method = cdu.func
  540. # See if this is a match
  541. if (method and (method.im_func == oldMethod)):
  542. # Create a new unbound method out of this new function
  543. newMethod = new.instancemethod(newFunction,
  544. method.im_self,
  545. method.im_class)
  546. # Set the new method on the cdu
  547. cdu.func = newMethod
  548. foundIt = 1
  549. return foundIt
  550. # debugging funcs for simulating a network-plug-pull
  551. def pullNetworkPlug(self):
  552. self.restoreNetworkPlug()
  553. self.notify.warning('*** SIMULATING A NETWORK-PLUG-PULL ***')
  554. self.hijackedTcpConn = self.tcpConn
  555. self.tcpConn = None
  556. def networkPlugPulled(self):
  557. return hasattr(self, 'hijackedTcpConn')
  558. def restoreNetworkPlug(self):
  559. if self.networkPlugPulled():
  560. self.notify.info('*** RESTORING SIMULATED PULLED-NETWORK-PLUG ***')
  561. self.tcpConn = self.hijackedTcpConn
  562. del self.hijackedTcpConn
  563. def getAllOfType(self, type):
  564. # Returns a list of all DistributedObjects in the repository
  565. # of a particular type.
  566. result = []
  567. for obj in self.doId2do.values():
  568. if isinstance(obj, type):
  569. result.append(obj)
  570. return result