88 lines
		
	
	
		
			3.0 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			88 lines
		
	
	
		
			3.0 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
|   | import asyncio | ||
|  | import sys | ||
|  | from contextlib import suppress | ||
|  | 
 | ||
|  | from aiohttp import ClientWebSocketResponse | ||
|  | from worlds._sc2common.bot import logger | ||
|  | from s2clientprotocol import sc2api_pb2 as sc_pb | ||
|  | 
 | ||
|  | from .data import Status | ||
|  | 
 | ||
|  | 
 | ||
|  | class ProtocolError(Exception): | ||
|  | 
 | ||
|  |     @property | ||
|  |     def is_game_over_error(self) -> bool: | ||
|  |         return self.args[0] in ["['Game has already ended']", "['Not supported if game has already ended']"] | ||
|  | 
 | ||
|  | 
 | ||
|  | class ConnectionAlreadyClosed(ProtocolError): | ||
|  |     pass | ||
|  | 
 | ||
|  | 
 | ||
|  | class Protocol: | ||
|  | 
 | ||
|  |     def __init__(self, ws): | ||
|  |         """
 | ||
|  |         A class for communicating with an SCII application. | ||
|  |         :param ws: the websocket (type: aiohttp.ClientWebSocketResponse) used to communicate with a specific SCII app | ||
|  |         """
 | ||
|  |         assert ws | ||
|  |         self._ws: ClientWebSocketResponse = ws | ||
|  |         self._status: Status = None | ||
|  | 
 | ||
|  |     async def __request(self, request): | ||
|  |         logger.debug(f"Sending request: {request !r}") | ||
|  |         try: | ||
|  |             await self._ws.send_bytes(request.SerializeToString()) | ||
|  |         except TypeError as exc: | ||
|  |             logger.exception("Cannot send: Connection already closed.") | ||
|  |             raise ConnectionAlreadyClosed("Connection already closed.") from exc | ||
|  |         logger.debug("Request sent") | ||
|  | 
 | ||
|  |         response = sc_pb.Response() | ||
|  |         try: | ||
|  |             response_bytes = await self._ws.receive_bytes() | ||
|  |         except TypeError as exc: | ||
|  |             if self._status == Status.ended: | ||
|  |                 logger.info("Cannot receive: Game has already ended.") | ||
|  |                 raise ConnectionAlreadyClosed("Game has already ended") from exc | ||
|  |             logger.error("Cannot receive: Connection already closed.") | ||
|  |             raise ConnectionAlreadyClosed("Connection already closed.") from exc | ||
|  |         except asyncio.CancelledError: | ||
|  |             # If request is sent, the response must be received before reraising cancel | ||
|  |             try: | ||
|  |                 await self._ws.receive_bytes() | ||
|  |             except asyncio.CancelledError: | ||
|  |                 logger.critical("Requests must not be cancelled multiple times") | ||
|  |                 sys.exit(2) | ||
|  |             raise | ||
|  | 
 | ||
|  |         response.ParseFromString(response_bytes) | ||
|  |         logger.debug("Response received") | ||
|  |         return response | ||
|  | 
 | ||
|  |     async def _execute(self, **kwargs): | ||
|  |         assert len(kwargs) == 1, "Only one request allowed by the API" | ||
|  | 
 | ||
|  |         response = await self.__request(sc_pb.Request(**kwargs)) | ||
|  | 
 | ||
|  |         new_status = Status(response.status) | ||
|  |         if new_status != self._status: | ||
|  |             logger.info(f"Client status changed to {new_status} (was {self._status})") | ||
|  |         self._status = new_status | ||
|  | 
 | ||
|  |         if response.error: | ||
|  |             logger.debug(f"Response contained an error: {response.error}") | ||
|  |             raise ProtocolError(f"{response.error}") | ||
|  | 
 | ||
|  |         return response | ||
|  | 
 | ||
|  |     async def ping(self): | ||
|  |         result = await self._execute(ping=sc_pb.RequestPing()) | ||
|  |         return result | ||
|  | 
 | ||
|  |     async def quit(self): | ||
|  |         with suppress(ConnectionAlreadyClosed, ConnectionResetError): | ||
|  |             await self._execute(quit=sc_pb.RequestQuit()) |