async def run_game_loop(self, interval: float = 0.02) -> None: # pylint: disable=function-redefined # pylint: disable=missing-docstring if self._game_state_store.get_game_state( ).game_status == GameStatus.get("Paused"): self._game_state_store.push_update( GameStateUpdate( self._game_state_store.get_game_state().time_order + 1, game_status=GameStatus.get("Active"))) game_state = self._game_state_store.get_game_state() dt = interval self._game_loop_is_running = True logger.info( f"State machine starting game loop with interval of {interval} seconds." ) while game_state.game_status == GameStatus.get("Active"): t0 = time.time() update_dict = self.time_step(game_state, dt) while not self._event_queue.empty(): event = await self._event_queue.get() event_update = await self._universal_event_handler.handle( event, game_state=game_state, dt=dt) update_dict.update(event_update) if time.time() - t0 > 0.95 * interval: break self._game_state_store.push_update( GameStateUpdate(game_state.time_order + 1, **update_dict)) game_state = self._game_state_store.get_game_state() dt = max(interval, time.time() - t0) await curio.sleep(max(0, interval - dt)) self.game_time += dt logger.info("Game loop stopped.") self._game_loop_is_running = False
def run(self, hostname: str, port: int): """Run state machine and server and bind the server to a given address. # Arguments hostname (str): hostname or IPv4 address the server will be bound to port (int): port number the server will be bound to """ self.game_state_machine.run_game_loop_in_thread() self.server.run(port, hostname, self.game_state_machine) self.game_state_machine.stop() logger.info("Backend successfully shut down.")
def __init__(self, initial_game_state: GameState, time_step_function, event_handlers: dict = None): logger.info("Assembling Backend ...") self.game_state_store = GameStateStore(initial_game_state) self.game_state_machine = GameStateMachine(self.game_state_store) setattr(self.game_state_machine, "time_step", time_step_function) if event_handlers is not None: for event_type, handler_function in event_handlers.items(): self.game_state_machine.register_event_handler( event_type, handler_function) self.server = Server(self.game_state_store) logger.info("Backend assembled and ready.")
async def stop(self, timeout: float = 1.0) -> bool: # pylint: disable=function-redefined # pylint: disable=missing-docstring logger.info("Trying to stop game loop ...") if self._game_state_store.get_game_state( ).game_status == GameStatus.get("Active"): self._game_state_store.push_update( GameStateUpdate( self._game_state_store.get_game_state().time_order + 1, game_status=GameStatus.get("Paused"))) t0 = time.time() while self._game_loop_is_running: if time.time() - t0 > timeout: break await curio.sleep(0) return not self._game_loop_is_running
def register_event_handler(self, event_type: str, event_handler_function) -> None: """Register an event handler for a specific event type. # Arguments event_type (str): string that identifies the events to be handled by this function event_handler_function (callable, coroutine): callback function or coroutine that will be invoked with the handler args and kwargs with which the incoming event has been dispatched # Raises TypeError: if `event_handler_function` is not callable """ logger.info( f"Registering event handler for events of type {event_type}.") if not callable(event_handler_function): raise TypeError( f"'{event_handler_function.__class__.__name__}' object is not callable." ) self._event_handlers[event_type] = event_handler_function
async def loop(self): # pylint: disable=function-redefined # pylint: disable=missing-docstring logger.info(f"Trying to connect to server ...") self._set_status("Connecting") async with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: send_loop_task = await curio.spawn(self._send_loop, sock) recv_loop_task = await curio.spawn(self._client_recv_loop, sock) event_loop_task = await curio.spawn(self._event_loop) # check for disconnect event while not self.status == ConnectionStatus.get("Disconnected"): command = await self._command_queue.get() if command == "shutdown": logger.info( f"Sending shutdown command to server at {self.remote_address}." ) await sock.sendto("shutdown".encode("utf-8"), self.remote_address) break elif command == "shut_me_down": break logger.info(f"Shutting down connection to {self.remote_address}.") await recv_loop_task.cancel() await send_loop_task.cancel() await event_loop_task.cancel() self._set_status("Disconnected")
def _throttling_state_machine(self, t: int, state: dict): """Calculate a new state for congestion avoidance.""" if self.quality == "good": if self.latency > self._latency_threshold: # switch to bad mode logger.warning(( f"Throttling down connection to {self.remote_address} because " f"latency ({self.latency}) is above latency threshold ({self._latency_threshold})." )) self.quality = "bad" self._package_interval = self._package_intervals["bad"] logger.debug( f"new package interval: {self._package_interval} seconds.") # if good conditions didn't last at least the throttle time, increase it if t - state["last_quality_change"] < state["throttle_time"]: state["throttle_time"] = min([ state["throttle_time"] * 2.0, self._max_throttle_time ]) state["last_quality_change"] = t # if good conditions lasted throttle time since last milestone elif t - state["last_good_quality_milestone"] > state[ "throttle_time"]: if self._package_interval > self._package_intervals["good"]: logger.info(( f"Throttling up connection to {self.remote_address} because latency ({self.latency}) " f"has been below latency threshold ({self._latency_threshold}) " f"for {state['throttle_time']} seconds.")) self._package_interval = self._package_intervals["good"] logger.debug( f"new package interval: {self._package_interval} seconds." ) state["throttle_time"] = max( [state["throttle_time"] / 2.0, self._min_throttle_time]) state["last_good_quality_milestone"] = t else: # self.quality == 'bad' if self.latency < self._latency_threshold: # switch to good mode self.quality = "good" state["last_quality_change"] = t state["last_good_quality_milestone"] = t
async def loop(cls, hostname: str, port: int, server, event_wire) -> None: """Continously orchestrate and operate connections to clients. This coroutine will keep listening for client packages, create new #ServerConnection objects when necessary and make sure all packages are handled by and sent via the right connection. It will return as soon as the server receives a shutdown message. # Arguments hostname (str): the hostname or IPv4 address to which to bind the server socket port (int): the port number to which to bind the server socket server (pygase.Server): the server for which this loop is run event_wire (pygase.GameStateMachine): object to which events are to be repeated (has to implement a `_push_event` method) """ logger.info(f"Trying to run server on {(hostname, port)} ...") async with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: sock.bind((hostname, port)) server._hostname, server._port = sock.getsockname() # pylint: disable=protected-access connection_tasks = curio.TaskGroup() logger.info( f"Server successfully started and listening to packages from clients on {(hostname, port)}." ) while True: data, client_address = await sock.recvfrom(Package._max_size) # pylint: disable=protected-access try: package = ClientPackage.from_datagram(data) # Create new connection if client is unknown. if not client_address in server.connections: logger.info( f"New client connection from {client_address}.") new_connection = cls( client_address, server._universal_event_handler, # pylint: disable=protected-access server.game_state_store, package.time_order, event_wire, ) await connection_tasks.spawn( new_connection._send_loop, sock # pylint: disable=protected-access ) await connection_tasks.spawn(new_connection._event_loop ) # pylint: disable=protected-access # For now, the first client connection becomes host. if server.host_client is None: logger.info( f"Setting {client_address} as client with host permissions." ) server.host_client = client_address server.connections[client_address] = new_connection elif server.connections[ client_address].status == ConnectionStatus.get( "Disconnected"): # Start sending packages again, which will also set status to "Connected". logger.info( f"Client reconnecting from {client_address}.") await connection_tasks.spawn( server.connections[client_address]._send_loop, sock # pylint: disable=protected-access ) for event in package.events: event.handler_kwargs["client_address"] = client_address await server.connections[client_address]._recv(package) # pylint: disable=protected-access except ProtocolIDMismatchError: # ignore all non-PyGaSe packages try: if data.decode( "utf-8" ) == "shutdown" and client_address == server.host_client: logger.info( f"Received shutdown command from host client {client_address}." ) break elif data.decode("utf-8") == "shut_me_down": break else: logger.warning("Received unknown package.") except UnicodeDecodeError: logger.warning("Received unknown package.") logger.info(f"Shutting down server on {(hostname, port)}.") await connection_tasks.cancel_remaining()
def _set_status(self, status: str): """Set `self.status` to a new #ConnectionStatus value.""" self.status = ConnectionStatus.get(status) logger.info( f"Status of connection to {self.remote_address} set to '{status}'." )