177 lines
6.4 KiB
Python
177 lines
6.4 KiB
Python
from typing import Optional, Any
|
|
|
|
from bemani.backend.base import Base, Status
|
|
from bemani.common import Model
|
|
from bemani.protocol import Node
|
|
from bemani.data import Config, 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: Config, 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 = self.__config.clone()
|
|
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
|
|
|
|
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 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, 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, 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, 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}_requests")
|
|
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
|