175 lines
6.6 KiB
Python
175 lines
6.6 KiB
Python
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, 'handle_{}_{}_request'.format(request.name, method))
|
|
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, 'handle_{}_request'.format(request.name))
|
|
except AttributeError:
|
|
handler = None
|
|
if handler is not None:
|
|
response = handler(request)
|
|
|
|
if response is None:
|
|
# Unrecognized handler
|
|
self.log("Unrecognized service {} method {}".format(request.name, 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
|