Coverage for /home/kale/kxgames/libraries/kxg/kxg/messages.py: 100%

# Coverage for /home/kale/kxgames/libraries/kxg/kxg/messages.py : 100%

## 98 statements 98 run 0 missing 1 excluded

Hot-keys on this page

r m x p toggle line displays

j k next/prev highlighted chunk

0 (zero) top of page

1 (one) first highlighted chunk

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