1
0
mirror of synced 2024-12-01 00:57:18 +01:00
bemaniutils/bemani/backend/dispatch.py

185 lines
6.6 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