import copy from typing import Optional, Dict, Any from bemani.backend.base import Model, Base, Status from bemani.protocol import Node from bemani.data import Data class UnrecognizedPCBIDException(Exception): def __init__(self, pcbid: str, model: str, ip: str) -> None: self.pcbid = pcbid self.model = model self.ip = ip class Dispatch: """ Dispatch object responsible for taking a decoded tree of Node objects from a game, looking up config, dispatching it to the correct game class and then returning a response. """ def __init__(self, config: Dict[str, Any], data: Data, verbose: bool) -> None: """ Initialize the Dispatch object. Parameters: config - A dictionary of configuration used for various settigs. data - A Data singleton for DB access. verbose - Whether we get chatty to stdout or not. """ self.__verbose = verbose self.__data = data self.__config = config def log(self, msg: str, *args: Any, **kwargs: Any) -> None: """ Given a message, format it and print it. Note that this only prints to stdout if we were initialized with verbose = True. Parameters: msg - A formatstring that should be formatted with any optional arguments or keyword arguments. """ if self.__verbose: print(msg.format(*args, **kwargs)) def handle(self, tree: Node) -> Optional[Node]: """ Given a packet from a game, handle it and return a response. Parameters: tree - A Node representing the root of a tree. Expected to come from an external game. Returns: A Node representing the root of a response tree, or None if we had a problem parsing or generating a response. """ self.log("Received request:\n{}", tree) if tree.name != 'call': # Invalid request self.log("Invalid root node {}", tree.name) return None if len(tree.children) != 1: # Invalid request self.log("Invalid number of children for root node") return None modelstring = tree.attribute('model') model = Model.from_modelstring(modelstring) pcbid = tree.attribute('srcid') # If we are enforcing, bail out if we don't recognize thie ID pcb = self.__data.local.machine.get_machine(pcbid) if self.__config['server']['enforce_pcbid'] and pcb is None: self.log("Unrecognized PCBID {}", pcbid) raise UnrecognizedPCBIDException(pcbid, modelstring, self.__config['client']['address']) # If we don't have a Machine, but we aren't enforcing, we must create it if pcb is None: pcb = self.__data.local.machine.create_machine(pcbid) request = tree.children[0] config = copy.copy(self.__config) config['machine'] = { 'pcbid': pcbid, 'arcade': pcb.arcade, } # If the machine we looked up is in an arcade, override the global # paseli settings with the arcade paseli settings. if pcb.arcade is not None: arcade = self.__data.local.machine.get_arcade(pcb.arcade) if arcade is not None: config['paseli']['enabled'] = arcade.data.get_bool('paseli_enabled') config['paseli']['infinite'] = arcade.data.get_bool('paseli_infinite') if arcade.data.get_bool('mask_services_url'): # Mask the address, no matter what the server settings are config['server']['uri'] = None # If we don't have a server URI, we should add the default if 'uri' not in config['server']: config['server']['uri'] = None game = Base.create(self.__data, config, model) method = request.attribute('method') response = None # If we are enforcing, make sure the PCBID isn't specified to be # game-specific if self.__config['server']['enforce_pcbid'] and pcb.game is not None: if pcb.game != game.game: self.log("PCBID {} assigned to game {}, but connected from game {}", pcbid, pcb.game, game.game) raise UnrecognizedPCBIDException(pcbid, modelstring, self.__config['client']['address']) if pcb.version is not None: if pcb.version > 0 and pcb.version != game.version: self.log( "PCBID {} assigned to game {} version {}, but connected from game {} version {}", pcbid, pcb.game, pcb.version, game.game, game.version, ) raise UnrecognizedPCBIDException(pcbid, modelstring, self.__config['client']['address']) if pcb.version < 0 and (-pcb.version) < game.version: self.log( "PCBID {} assigned to game {} maximum version {}, but connected from game {} version {}", pcbid, pcb.game, -pcb.version, game.game, game.version, ) raise UnrecognizedPCBIDException(pcbid, modelstring, self.__config['client']['address']) # First, try to handle with specific service/method function try: handler = getattr(game, f'handle_{request.name}_{method}_request') except AttributeError: handler = None if handler is not None: response = handler(request) if response is None: # Now, try to pass it off to a generic service handler try: handler = getattr(game, f'handle_{request.name}_request') except AttributeError: handler = None if handler is not None: response = handler(request) if response is None: # Unrecognized handler self.log(f"Unrecognized service {request.name} method {method}") return None # Make sure we have a status value if one wasn't provided if 'status' not in response.attributes: response.set_attribute('status', str(Status.SUCCESS)) root = Node.void('response') root.add_child(response) root.set_attribute('dstid', pcbid) self.log("Sending response:\n{}", root) return root