def process_message(self, message): """This is the main method of a plugin. It is called with an instance of fabula.Message. This method does not run in a thread. It should process the message quickly and return another instance of fabula.Message to be processed by the host and possibly to be sent to the remote host. The host Engine will try hard to call this method on a regular base, ideally once per frame. message.event_list thus might be empty. The default implementation calls the respective functions for the Events in message and returns Plugin.message_for_host. """ # Clear message for host # self.message_for_host = fabula.Message([]) if message.event_list: fabula.LOGGER.debug("processing message {}".format( message.event_list)) for event in message.event_list: # event_dict from EventProcessor base class # self.event_dict[event.__class__](event) return self.message_for_host
def _check_exit(self, connector_list): """Auxiliary method. Check if someone has left who is supposed to be there. """ for room in self.room_by_id.values(): # Create a list copy, for the same reason as above # active_list = list(room.active_clients.keys()) for connector in active_list: if not connector in connector_list: client_id = room.active_clients[connector] msg = "client '{}' {} has left without notice, removing" fabula.LOGGER.warning(msg.format(client_id, connector)) self.process_ExitEvent(fabula.ExitEvent(client_id), connector=connector) # Since we do this outside the loop, we have to clean # up to not confuse the next iteration # self.message_for_remote = fabula.Message([]) self._add_event_to_room_message( fabula.DeleteEvent(client_id), room) return
def __init__(self, host): """Initialise the plugin. host is an instance of fabula.core.Engine. """ fabula.eventprocessor.EventProcessor.__init__(self) self.host = host self.message_for_host = fabula.Message([])
def __init__(self, interface_instance): """Set up Engine attributes. Arguments: interface_instance An instance of a subclass of fabula.interfaces.Interface to communicate with the remote host. """ # First setup base class # fabula.eventprocessor.EventProcessor.__init__(self) self.interface = interface_instance self.logfile_name = "messages.log" self.plugin = None # The Engine examines the events of a received # Message and applies them. Events for special # consideration for the PluginEngine are collected # in a Message for the PluginEngine. # The Engine has to empty the Message once # the PluginEngine has processed all Events. # self.message_for_plugin = fabula.Message([]) # In self.message_for_remote we collect events to be # sent to the remote host in each loop. # self.message_for_remote = fabula.Message([]) # self.rack serves as a storage for deleted Entities # because there may be the need to respawn them. # self.rack = fabula.Rack() fabula.LOGGER.debug("complete") return
def __init__(self, interface_instance): """Initalisation. The Client must be instantiated with an instance of a subclass of fabula.interfaces.Interface which handles the connection to the server or supplies events in some other way. """ # Save the player id for Server and UserInterface. # self.client_id = "" # Setup Engine internals # Engine.__init__() calls EventProcessor.__init__() # fabula.core.Engine.__init__(self, interface_instance) # Now we have: # # self.rack # # self.rack serves as a storage for deleted Entities # because there may be the need to respawn them. # self.room keeps track of the map and active Entites. # An actual room is created when the first EnterRoomEvent is # encountered. # self.room = None # Override logfile name # Use PID for unique name. Two clients may run in the same directory. # self.logfile_name = "messages-client-{}-received.log".format( os.getpid()) # Set up flags used by self.run() # self.await_confirmation = False self.got_empty_message = False # Buffer for sent message # self.message_sent = fabula.Message([]) # Timestamp to detect server dropouts # self.timestamp = None # Remember the latest local MovesToEvent # self.movement_cache = [None, fabula.MovesToEvent(None, None)] fabula.LOGGER.debug("complete")
def grab_message(self): """Called by the local engine to obtain a new buffered message from the remote host. It must return an instance of fabula.Message, and it must do so immediately to avoid blocking. If there is no new message from remote, it must return an empty message. """ if self.messages_for_local: return self.messages_for_local.popleft() else: # A Message must be returned, so create an empty one # return fabula.Message([])
def run(self): """Main loop of the Client. This is a blocking method. It calls all the process methods to process events, and then the plugin. The loop will terminate when Client.plugin.exit_requested is True. """ # TODO: The current implementation is too overtrustful. # It should check much more thoroughly if the # affected Entites exist at all and so on. Since # this is a kind of precondition or even invariant, # it should be covered using a "design by contract" # approach to the whole thing instead of putting # neat "if" clauses here and there. fabula.LOGGER.info("starting") # By now we should have self.plugin. This is the reason why we do not # do this in __init__(). fabula.LOGGER.info("prompting the user for connection details") while self.client_id == "": self.client_id, connector = self.plugin.get_connection_details() if len(self.interface.connections.keys()): fabula.LOGGER.info( "interface is already connected to '{}', using connection". format(list(self.interface.connections.keys())[0])) else: if self.client_id == "exit requested": # partly copied from below fabula.LOGGER.info( "exit requested from UserInterface, shutting down interface" ) # stop the Client Interface thread # self.interface.shutdown() fabula.LOGGER.info("shutdown confirmed.") # TODO: possibly exit cleanly from the UserInterface here return elif connector is None: msg = "can not connect to Server: client_id == '{}', connector == {}" fabula.LOGGER.critical(msg.format(self.client_id, connector)) # TODO: exit properly # raise Exception(msg.format(self.client_id, connector)) else: fabula.LOGGER.info( "connecting client interface to '{}'".format(connector)) try: self.interface.connect(connector) except: fabula.LOGGER.critical("error connecting to server") fabula.LOGGER.info("shutting down interface") self.interface.shutdown() fabula.LOGGER.info("exiting") raise SystemExit # Use only the first connection in the ClientInterface # self.message_buffer = list(self.interface.connections.values())[0] init_event = fabula.InitEvent(self.client_id) fabula.LOGGER.info("sending {}".format(init_event)) self.message_buffer.send_message(fabula.Message([init_event])) # This requires a confirmation by ServerParametersEvent # self.await_confirmation = True # Local timestamp for messages # message_timestamp = None # Now loop # while not self.plugin.exit_requested: # grab_message must and will return a Message, but # it possibly has an empty event_list. # server_message = self.message_buffer.grab_message() if server_message.event_list: fabula.LOGGER.debug( "server incoming: {}".format(server_message)) # Log to logfile # TODO: this could be a method. But the we would have to maintain the message_timestamp in the instance. # Clear file and start Message log timer with first incoming # message # if message_timestamp is None: fabula.LOGGER.debug("Clearing log file") message_log_file = open(self.logfile_name, "wt") message_log_file.write("") message_log_file.close() fabula.LOGGER.debug("Starting message log timer") message_timestamp = datetime.datetime.today() message_log_file = open(self.logfile_name, "at") timedifference = datetime.datetime.today() - message_timestamp # Logging time difference in seconds and message, tab-separated, # terminated with double-newline. # timedifference as seconds + tenth of a second # message_log_file.write("{}\t{}\n\n".format( timedifference.seconds + timedifference.microseconds / 1000000.0, repr(server_message))) message_log_file.close() # Renew timestamp # message_timestamp = datetime.datetime.today() # Message was not empty # self.got_empty_message = False elif not self.got_empty_message: # TODO: The UserInterface currently locks up when there are no answers from the server. The Client should time out and exit politely if there is no initial answer from the server or there has been no answer for some time. fabula.LOGGER.debug("got an empty server_message.") # Set to True to log empty messages only once # self.got_empty_message = True # First handle the events in the Client, updating the room and # preparing self.message_for_plugin for the UserInterface # for current_event in server_message.event_list: # This is a bit of Python magic. # self.event_dict is a dict which maps classes to handling # functions. We use the class of the event supplied as # a key to call the appropriate handler, and hand over # the event. # These methods may add events for the plugin engine # to self.message_for_plugin # # TODO: This really should return a message, instead of giving one to write to # self.event_dict[current_event.__class__]( current_event, message=self.message_for_plugin) # Now that everything is set and stored, call the UserInterface to # process the messages. # Ideally we want to call process_message() once per frame, but the # call may take an almost arbitrary amount of time. # At least process_message() must be called regularly even if the # server Message and thus message_for_plugin are empty. # try: message_from_plugin = self.plugin.process_message( self.message_for_plugin) except: fabula.LOGGER.critical( "the UserInterface raised an exception:\n{}".format( traceback.format_exc())) fabula.LOGGER.info("shutting down interface") self.interface.shutdown() fabula.LOGGER.info("exiting") return # The UserInterface returned, the Server Message has been applied # and processed. Clean up. # self.message_for_plugin = fabula.Message([]) # TODO: possibly unset "AwaitConfirmation" flag # If there has been a Confirmation for a LookAt or TriesToManipulate, # unset "AwaitConfirmation" flag # The UserInterface might have collected some player input and # converted it to events. # if message_from_plugin.event_list: if self.await_confirmation: # Player input, but we still await confirmation. if self.timestamp == None: fabula.LOGGER.warning( "player attempt but still waiting - starting timer" ) self.timestamp = datetime.datetime.today() else: timedifference = datetime.datetime.today( ) - self.timestamp # TODO: This should also happen when a timeout occurs, not only after repeated player attempts. See `elif not self.got_empty_message` above. # if timedifference.seconds >= 3: fabula.LOGGER.warning( "waited 3s, still no confirmation, notifying user and resetting timer" ) self.timestamp = None msg = "The server does not reply.\nConsider to restart." perception_event = fabula.PerceptionEvent( self.client_id, msg) # Not catching reaction # self.plugin.process_message( fabula.Message([perception_event])) else: fabula.LOGGER.info( "still waiting for confirmation") else: # If we do not await a Confirmation anymore, we evaluate the # player input if any. # We queue player triggered events before possible events # from the Client. # TODO: Since MessageAppliedEvent is gone, are there any events left to be sent by the Client? # self.message_for_remote.event_list = ( message_from_plugin.event_list + self.message_for_remote.event_list) # Local movement tests # # TODO: Can't this be handled much easier by simply processing a MovesToEvent in UserInterface.collect_player_input? But then again, an UserInterface isn't supposed to check maps etc. # for event in message_from_plugin.event_list: if isinstance(event, fabula.TriesToMoveEvent): # TODO: similar to Server.process_TriesToMoveEvent() # Do we need to move / turn at all? # location = self.room.entity_locations[ event.identifier] difference = fabula.difference_2d( location, event.target_identifier) # In case of a TriesToMoveEvent, test and approve # the movement locally to allow for a smooth user # experience. We have the current map information # after all. # # Only allow certain vectors # if difference in ((0, 1), (0, -1), (1, 0), (-1, 0)): fabula.LOGGER.info( "applying TriesToMoveEvent locally") # TODO: event.identifier in self.room.entity_dict? event.target_identifier in fabula.DIRECTION_VECTOR? # TODO: code duplicated from Server.process_TriesToMoveEvent() # Test if a movement from the current entity # location to the new location on the map is # possible # # We queue the event in self.message_for_plugin, # so it will be rendered upon next call. That # way it bypasses the Client; its status will be # updated upon server confirmation. So we will # have a difference between client state and # presentation. We trust that it will be # resolved by the server in very short time. # try: if self.room.tile_is_walkable( event.target_identifier[:2]): self.movement_cache[ 0] = self.room.entity_locations[ event.identifier] moves_to_event = fabula.MovesToEvent( event.identifier, event.target_identifier[:2]) # Update room, needed for UserInterface # self.process_MovesToEvent( moves_to_event, message=self.message_for_plugin) # Remember event for crosscheck with # event from Server # self.movement_cache[1] = moves_to_event else: # Instead of #self.message_for_plugin.event_list.append(fabula.AttemptFailedEvent(event.identifier)) # we wait for the server to respond with # AttemptFailedEvent # pass except KeyError: # Instead of #self.message_for_plugin.event_list.append(fabula.AttemptFailedEvent(event.identifier)) # we wait for the server to respond with # AttemptFailedEvent # pass # If any of the Events to be sent is an AttemptEvent, we now # await confirmation # # TODO: has_attempt_event is used nowhere else. Remove? # has_attempt_event = False for event in message_from_plugin.event_list: if isinstance(event, fabula.AttemptEvent): fabula.LOGGER.info( "got attempt '{}' from Plugin: awaiting confirmation and discarding further user input" .format(event.__class__.__name__)) self.await_confirmation = True # Reset dropout timer # self.timestamp = None # If this iteration yielded any events, send them. # if self.message_for_remote.event_list: fabula.LOGGER.debug("server outgoing: %s" % self.message_for_remote) self.message_buffer.send_message(self.message_for_remote) # Save for possible re-send on dropout # self.last_message = self.message_for_remote # Clean up # self.message_for_remote = fabula.Message([]) # OK, done with the whole server message. # If no exit requested, grab the next one! # exit has been requested fabula.LOGGER.info("exit requested via plugin.exit_requested") fabula.LOGGER.info("sending ExitEvent to Server") self.message_buffer.send_message( fabula.Message([fabula.ExitEvent(self.client_id)])) fabula.LOGGER.info("shutting down interface") # stop the Client Interface thread # self.interface.shutdown() fabula.LOGGER.info("shutdown confirmed.") # TODO: possibly exit cleanly from the UserInterface here return
def process_InitEvent(self, event, **kwargs): """Check if we already have a room and entities. If yes, send the data. If not, pass on to the plugin. """ # TODO: update docstring # A guard clause first. # if event.identifier in self.room_by_client.keys(): msg = "Client id '{}' already exists in room_by_client == {}, sending ExitEvent at once and terminating connection" fabula.LOGGER.warning( msg.format(event.identifier, self.room_by_client)) try: self.interface.connections[kwargs["connector"]].send_message( fabula.Message([fabula.ExitEvent(event.identifier)])) except KeyError: msg = "connection to client '{}' not found, could not send Message" fabula.LOGGER.error(msg.format(kwargs["connector"])) fabula.LOGGER.debug("removing interface.connections[{}]".format( kwargs["connector"])) try: del self.interface.connections[kwargs["connector"]] except KeyError: fabula.LOGGER.warning("connection {} is already gone".format( kwargs["connector"])) return fabula.LOGGER.info("sending Server parameters") self.message_for_remote.event_list.append( fabula.ServerParametersEvent(event.identifier, self.action_time)) if len(self.room_by_id): # TODO: Spawning in first room in room_by_id by default. Is this ok as a convention, or do we need some way to configure that? Or a standard name for the first room to spawn in? room = list(self.room_by_id.values())[0] fabula.LOGGER.debug( "creating and processing EnterRoomEvent for new client") fabula.LOGGER.info( "Spawning client '{}' in first room '{}' by convention".format( event.identifier, room.identifier)) enter_room_event = fabula.EnterRoomEvent(event.identifier, room.identifier) # Process the Event right away, since it is not going through the # Plugin. This will also append the Event to self.message_for_remote. # self.process_EnterRoomEvent(enter_room_event, connector=kwargs["connector"], message=self.message_for_remote) fabula.LOGGER.info( "Sending existing floor_plan, entities and Rack") # TODO: It's not very clean to write to self.message_for_remote directly since the procces_() methods are not supposed to do so. # self.message_for_remote.event_list.extend( self._generate_room_rack_events(room.identifier)) # If a room has already been sent, the plugin should only spawn a new # player entity. # The Plugin will add RoomCompleteEvent when processing InitEvent. # kwargs["message"].event_list.append(event) return
def _add_event_to_room_message(self, event, room=None): """Add event to the respective Message in Server.message_by_room_id. If room is not given, it will be inferred from the event. This method will raise a RuntimeError if no fitting room can be found. Make sure to call this only for events for which a room can be found. """ if room is None: fabula.LOGGER.debug( "No room given, trying to infer from Event {}".format(event)) if (isinstance(event, fabula.SpawnEvent) or isinstance(event, fabula.ChangeMapElementEvent)): # Assuming Event.location == (x, y, "room_identifier") # room = self.room_by_id[event.location[2]] elif isinstance(event, fabula.EnterRoomEvent): # Room should be established # room = self.room_by_id[event.room_identifier] elif isinstance(event, fabula.ServerParametersEvent): room = self.room_by_client[event.client_identifier] elif "identifier" in event.__dict__.keys(): for current_room in self.room_by_id.values(): if event.identifier in current_room.entity_dict.keys(): room = current_room if room is None: msg = "Identifier '{}' of Event {} not found in any room: {}" msg = msg.format( event.identifier, event, [str(room) for room in self.room_by_id.values()]) fabula.LOGGER.error(msg) raise RuntimeError(msg) else: msg = "No Room found that fits Event {}".format(event) fabula.LOGGER.error(msg) raise RuntimeError(msg) fabula.LOGGER.debug("Inferred Room '{}'".format(room.identifier)) if room.identifier in self.message_by_room_id.keys(): self.message_by_room_id[room.identifier].event_list.append(event) else: self.message_by_room_id[room.identifier] = fabula.Message([event]) return
def _call_plugin(self, connector): """Auxiliary method, to be called from _main_loop(). Call Plugin and process Plugin message. """ # TODO: Check all code relying on connector being the origin of an Event - this might not be the case!!! # Put in a method to avoid duplication. # Must not take too long since the client is waiting. # Call Plugin even if there were no Events from the client to catch # Plugin-initiated Events. # message_from_plugin = self.plugin.process_message( self.message_for_plugin) # The plugin returned. Clean up. # self.message_for_plugin = fabula.Message([]) # Process events from the Plugin before returning them to the client. # We cannot just forward them since we may be required to change maps, # spawn entities etc. # Note that this time resulting events are queued for the remote host, # not the plugin. # # TODO: could we be required to send any new events to the Plugin? But this could become an infinite loop! # TODO: Again it's completely weird to give the Message as an argument. Functions should return Event lists instead. # for event in message_from_plugin.event_list: self.event_dict[event.__class__](event, message=self.message_for_remote, connector=connector) # If this iteration yielded any events, send them. # Message for remote host first # if self.message_for_remote.event_list: if not len(self.room_by_id): msg = "The plugin has not established any rooms. Cannot continue." fabula.LOGGER.critical(msg) raise RuntimeError(msg) # Sort Events by Room entered_room_id = None for event in self.message_for_remote.event_list: if isinstance(event, fabula.EnterRoomEvent): entered_room_id = event.room_identifier self._add_event_to_room_message(event) elif isinstance(event, fabula.RoomCompleteEvent): if entered_room_id is not None: self._add_event_to_room_message( event, self.room_by_id[entered_room_id]) entered_room_id = None else: msg = "No EnterRoomEvent preceding {}".format(event) fabula.LOGGER.error(msg) raise RuntimeError(msg) elif isinstance(event, fabula.DeleteEvent): # NOTE: HACK: as the Entity is already removed from all rooms, we have no way of knowing where it was before. So add DeleteEvent to all rooms. # [ self._add_event_to_room_message(event, current_room) for current_room in self.room_by_id.values() ] else: self._add_event_to_room_message(event) # Now process each room's message # for room_identifier, room_message in self.message_by_room_id.items( ): # Find active clients and build a message for each client # client_message_dict = {} for client_identifier in self.room_by_id[ room_identifier].active_clients.values(): client_message_dict[client_identifier] = fabula.Message([]) # Sort this room's events into client messages. Not everyone # might get the same ones. single_client = None for event in room_message.event_list: if isinstance(event, fabula.EnterRoomEvent): # Start collecting messages for single client only # single_client = event.client_identifier client_message_dict[single_client].event_list.append( event) elif isinstance(event, fabula.RoomCompleteEvent): client_message_dict[single_client].event_list.append( event) # Stop collecting messages for single client only # single_client = None # TODO: re-check the multiple room entity.id == client_id convention, and possibly make that visible in attribute names # elif (isinstance(event, fabula.SpawnEvent) and single_client is not None and event.entity.identifier == single_client): # This is supposed to be the SpawnEvent for the # new player - send to all # for message in client_message_dict.values(): message.event_list.append(event) else: if single_client is None: # That's for all, folks # for message in client_message_dict.values(): message.event_list.append(event) else: client_message_dict[ single_client].event_list.append(event) fabula.LOGGER.debug( "client_message_dict == {}".format(client_message_dict)) # Now. Off with them! # for connector, client_identifier in self.room_by_id[ room_identifier].active_clients.items(): message = client_message_dict[client_identifier] if len(message.event_list): fabula.LOGGER.debug("'{}' ({}) outgoing: {}".format( connector, client_identifier, message)) try: self.interface.connections[connector].send_message( message) except KeyError: msg = "connection to client '{}' not found, could not send Message" fabula.LOGGER.error(msg.format(connector)) # Clean up # self.message_for_remote = fabula.Message([]) self.message_by_room_id = {} return
def run(self): """Main method of the Server. This is a blocking method. It calls all the process methods to process events, and then the plugin. This method will print usage information and status reports to STDOUT. """ print("============================================================") print("Fabula {} Server".format(fabula.VERSION)) print("------------------------------------------------------------\n") # Listen on port 0xfab == 4011 :-) # connector = (self.ipaddress, 4011) fabula.LOGGER.info( "attempting to connect server interface to '{}'".format(connector)) try: self.interface.connect(connector) print("Listening on IP {}, port {}\n".format( connector[0], connector[1])) except: fabula.LOGGER.warning( "Exception in interface.connect() (server interface already connected?), continuing anyway" ) fabula.LOGGER.debug(traceback.format_exc()) print("Press [Ctrl] + [C] to stop the server.") fabula.LOGGER.info("starting main loop") # MAIN LOOP # TODO: Server.exit_requested is inconsistent with Client.plugin.exit_requested # while not self.exit_requested: self._main_loop() # exit has been requested # print("\nShutting down server.\n") fabula.LOGGER.info("exit flag set") fabula.LOGGER.info("sending ExitEvent to all connected Clients") for message_buffer in self.interface.connections.values(): message_buffer.send_message( fabula.Message([fabula.ExitEvent("server")])) # stop the Interface thread # fabula.LOGGER.info("shutting down interface") self.interface.shutdown() fabula.LOGGER.info("shutdown confirmed") # TODO: possibly exit cleanly from the plugin here print("Shutdown complete. A log file should be at fabula-server.log\n") return
def handle_messages(self, remote_message_buffer): """This background thread method transfers messages between local and remote MessageBuffer. It uses representations of the Events being sent to create new copies of the original ones. """ fabula.LOGGER.info("starting up") # Run thread as long as no shutdown is requested # while not self.shutdown_flag: # Get messages from remote # if remote_message_buffer.messages_for_remote: original_message = remote_message_buffer.messages_for_remote.popleft( ) new_message = fabula.Message([]) for event in original_message.event_list: if isinstance( event, fabula.SpawnEvent ) and event.entity.__class__ is not fabula.Entity: # These need special care. We need to have a canonic # fabula.Entity. Entity.clone() will produce one. # fabula.LOGGER.debug( "cloning canonical Entity from {}".format( event.entity)) event = fabula.SpawnEvent(event.entity.clone(), event.location) # Create new instances from string representations to avoid # concurrent access of client and server to the object # fabula.LOGGER.debug("evaluating: '{}'".format(repr(event))) try: new_message.event_list.append(eval(repr(event))) except SyntaxError: # Well, well, well. Some __repr__ does not return a # string that can be evaluated here! # fabula.LOGGER.error( "error: can not evaluate '{}', skipping".format( repr(event))) # Blindly use the first connection # list(self.connections.values())[0].messages_for_local.append( new_message) # No need to deliver messages to remote since it will grab them - # see above. # No need to run as fast as possible # sleep(1 / self.framerate) # Caught shutdown notification, stopping thread # fabula.LOGGER.info("shutting down") self.shutdown_confirmed = True raise SystemExit