PageRenderTime 44ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/kxg/messages.py

https://github.com/kxgames/GameEngine
Python | 279 lines | 266 code | 9 blank | 4 comment | 5 complexity | 67e5abed1fb8d90bec091563a617bfb4 MD5 | raw file
  1. #!/usr/bin/env python3
  2. from .errors import *
  3. class Message:
  4. # This class defers initializing all of its members until the appropriate
  5. # setter is called, rather than initializing everything in a constructor.
  6. # This is done to avoid sending unnecessary information over the network.
  7. class ErrorState:
  8. SOFT_SYNC_ERROR = 0
  9. HARD_SYNC_ERROR = 1
  10. def __repr__(self):
  11. return self.__class__.__name__ + '()'
  12. def was_sent(self):
  13. return hasattr(self, 'sender_id')
  14. def was_sent_by(self, actor_or_id):
  15. from .actors import Actor
  16. from .forums import IdFactory
  17. if isinstance(actor_or_id, Actor):
  18. id = actor_or_id.id
  19. elif isinstance(actor_or_id, IdFactory):
  20. id = actor_or_id.get()
  21. else:
  22. id = actor_or_id
  23. try:
  24. return self.sender_id == id
  25. except AttributeError:
  26. raise ApiUsageError("""\
  27. Can't ask who sent a message before it's been sent.
  28. This error means Message.was_sent_by() or
  29. Message.was_sent_by_referee() got called on a message that
  30. hadn't been sent yet. Normally you would only call these
  31. methods from within Message.on_check().""")
  32. def was_sent_by_referee(self):
  33. return self.was_sent_by(1)
  34. def tokens_to_add(self):
  35. yield from []
  36. def tokens_to_remove(self):
  37. yield from []
  38. def tokens_referenced(self):
  39. """
  40. Return a list of all the tokens that are referenced in this message.
  41. Tokens that haven't been assigned an id yet are searched recursively
  42. for tokens. So this method may return fewer results after the message
  43. is sent. This information is used by the game engine to catch mistakes
  44. like forgetting to add a token to the world or keeping a stale
  45. reference to a token after its been removed.
  46. """
  47. tokens = set()
  48. # Use the pickle machinery to find all the tokens contained at any
  49. # level of this message. When an object is being pickled, the Pickler
  50. # calls its persistent_id() method for each object it encounters. We
  51. # hijack this method to add every Token we encounter to a list.
  52. # This definitely feels like a hacky abuse of the pickle machinery, but
  53. # that notwithstanding this should be quite robust and quite fast.
  54. def persistent_id(obj):
  55. from .tokens import Token
  56. if isinstance(obj, Token):
  57. tokens.add(obj)
  58. # Recursively descend into tokens that haven't been assigned an
  59. # id yet, but not into tokens that have.
  60. return obj.id
  61. from pickle import Pickler
  62. from io import BytesIO
  63. # Use BytesIO to basically ignore the serialized data stream, since we
  64. # only care about visiting all the objects that would be pickled.
  65. pickler = Pickler(BytesIO())
  66. pickler.persistent_id = persistent_id
  67. pickler.dump(self)
  68. return tokens
  69. def on_check(self, world):
  70. """
  71. Confirm that the message is consistent with the `World`.
  72. This handler is called by actors. If no `MessageCheck` exception is
  73. raised, the message will be sent as usual. Otherwise, the behavior
  74. will depend on what kind of actor is handling the message. `Actor`
  75. (uniplayer and multiplayer clients) will simply not send the message.
  76. `ServerActor` (multiplayer server) will decide if the error should be
  77. handled by undoing the message or asking the clients to sync
  78. themselves.
  79. """
  80. raise NotImplementedError
  81. def on_prepare_sync(self, world, memento):
  82. """
  83. Determine how `on_check` failures on the server should be handled.
  84. When `on_check` fails on the server, it means that the client which
  85. sent the message is out of sync (since had it been in sync, it would've
  86. rejected the message locally). There are two ways to handle this
  87. situation, and the role of this handler is to decide which to use.
  88. The first is to reject the message. This is considered a "hard sync
  89. error". In this case, the out-of-sync client will be instructed to
  90. undo this message, and the rest of the clients will never be sent the
  91. message in the first place. This approach ensures that messages sent
  92. by the server are consistent with the server's `World`, but at the cost
  93. of requiring some messages to be undone, which may be jarring for the
  94. players. To indicate a hard sync error, return False from this
  95. handler. This is the default behavior.
  96. The second is to send the message with extra instructions on how to
  97. re-synchronize the clients. This is considered a "soft sync error".
  98. In this case, the message will be relayed to all clients as usual, but
  99. each client will call the `on_sync` handler upon receipt. Any extra
  100. information that might be helpful in resynchronizing the clients can be
  101. assigned to the *memento* argument, which will be sent to each client
  102. along with the message, and then passed to `on_sync`. To indicate a
  103. soft sync error, return True from this handler.
  104. """
  105. return False
  106. def on_execute(self, world):
  107. """
  108. Update the world with the information stored in the message.
  109. This handler is called by the forum on every machine running the game,
  110. before any signal-handling callbacks. It is allowed to make changes to
  111. the game world, but should not change the message itself.
  112. """
  113. pass
  114. def on_sync(self, world, memento):
  115. """
  116. Handle soft synchronization errors.
  117. See `on_prepare_sync` for more details on hard/soft synchronization
  118. errors. This handler should use any information put in the *memento*
  119. by `on_prepare_sync` to quietly re-synchronize the client with the
  120. server.
  121. """
  122. pass
  123. def on_undo(self, world):
  124. """
  125. Handle hard synchronization errors.
  126. See `on_prepare_sync` for more details or hard/soft synchronization
  127. error. This handler should undo whatever changes were made to the
  128. world in `on_execute`, preferably in a way that is as minimally
  129. disruptive to the player as possible. This handler will only called on
  130. the client that originally sent this message.
  131. """
  132. message_cls = self.__class__.__name__
  133. raise ApiUsageError("""\
  134. The message {self} was rejected by the server.
  135. This client attempted to send a {message_cls} message, but it
  136. was rejected by the server. To fix this error, either figure
  137. out why the client is getting out of sync with the server or
  138. implement a {message_cls}.on_undo() that undoes everything done
  139. in {message_cls}.on_execute().""")
  140. def _set_sender_id(self, id_factory):
  141. self.sender_id = id_factory.get()
  142. def _set_server_response_id(self, id):
  143. self._server_response_id = id
  144. def _get_server_response_id(self):
  145. return self._server_response_id
  146. def _set_server_response(self, server_response):
  147. self._server_response = server_response
  148. def _get_server_response(self):
  149. try:
  150. return self._server_response
  151. except AttributeError:
  152. return None
  153. def _assign_token_ids(self, id_factory):
  154. """
  155. Assign id numbers to any tokens that will be added to the world by this
  156. message.
  157. This method is called by `Actor` but not by `ServerActor`, so it's
  158. guaranteed to be called exactly once. In fact, this method is not
  159. really different from the constructor, except that an `IdFactory`
  160. instance is nicely provided. That's useful for assigning ids to tokens
  161. but probably nothing else. This method is called before `_check` so
  162. that `_check` can make sure that valid ids were assigned (although by
  163. default it doesn't).
  164. """
  165. for token in self.tokens_to_add():
  166. token._give_id(id_factory)
  167. def _check(self, world):
  168. self.on_check(world)
  169. def _prepare_sync(self, world, server_response):
  170. self._set_server_response(server_response)
  171. return self.on_prepare_sync(world, self._server_response)
  172. def _execute(self, world):
  173. # Deal with tokens to be created or destroyed.
  174. for token in self.tokens_to_add():
  175. world._add_token(token)
  176. # Save the id numbers for the tokens we're removing so we can restore
  177. # them if we need to undo this message.
  178. self._removed_token_ids = {}
  179. for token in self.tokens_to_remove():
  180. self._removed_token_ids[token] = token.id
  181. world._remove_token(token)
  182. # Let derived classes execute themselves.
  183. self.on_execute(world)
  184. def _sync(self, world):
  185. self.on_sync(world, self._server_response)
  186. def _undo(self, world):
  187. # The tokens in self.tokens_to_add() haven't been added to the world
  188. # yet, because the message was copied and pickled before it was
  189. # executed on the server. We need to access the tokens that are
  190. # actually in the world before we can remove them again.
  191. for token in self.tokens_to_add():
  192. real_token = world.get_token(token.id)
  193. world._remove_token(real_token)
  194. # The tokens in self.tokens_to_remove() have already been removed from
  195. # the world. We want to add them back, and we want to make sure they
  196. # end up with the id as before.
  197. for token in self.tokens_to_remove():
  198. token._id = self._removed_token_ids[token]
  199. world._add_token(token)
  200. # Let derived classes execute themselves.
  201. self.on_undo(world)
  202. class MessageCheck(Exception):
  203. pass
  204. @debug_only
  205. def require_message(object):
  206. require_instance(Message(), object)
  207. @debug_only
  208. def require_message_cls(cls):
  209. if not isinstance(cls, type) or not issubclass(cls, Message):
  210. try: wrong_thing = cls.__name__
  211. except: wrong_thing = cls
  212. raise ApiUsageError("""\
  213. expected Message subclass, but got {wrong_thing} instead.""")