1
0
mirror of synced 2024-11-30 16:54:30 +01:00
bemaniutils/BACKEND.md
2019-12-08 21:43:49 +00:00

99 lines
6.3 KiB
Markdown

The backend services component is responsible for parsing game requests
and generating an appropriate response. It is divided into a dispatch
layer and a series of folders each representing a game series. There is
also a data layer filled with database access functions. Each game
series knows how to dispatch a request to an appropriate game class
given a model string and the Node structure (as detailed in PROTOCOL).
Each game class inherits from the base backend class which provides common
functionality such as boot requests, PASELI, and card lookup routines.
Both the base backend class and the game classes use the data layer to
determine appropriate responses given a request.
A rough sketch of how the pieces fit together is as follows:
------------
| Dispatch |
------------
|
|
V
-------- -------------- --------------
| Base |<--->| Game Class |<--->| Data Class |
-------- -------------- --------------
Dispatch is responsible for taking a tree of Node objects and determining
which game class should handle it. It will extract the modelstring and
root Node's name in order to determine which game and what request is
being made. It will extract the PCBID of the request and optionally compare
it against known PCBIDs if enforcing mode is enabled. If the PCBID is tied
to an arcade, it will look up the details of that arcade in order to allow
an operator to override PASELI settings. Then, it create a database connection
using the Data class and instantiates the appropriate game class with the
Data class and modelstring provided on instantiation. Then, it attempts to
call the `handle_<request>_<method>_request` method on the instantiated game
class. The <request> portion represents the name of the root Node object, and
the <method> portion represents the string method found as an attribute on the
root node. If that doesn't exist, it falls back to the `handle_<request>_request`
method. Some series prefer to handle everything in one request method, manually
examining the method, and some prefer to have individual methods per request
and method combination. If neither of these exists, then it is assumed that
the game does not handle this packet and an appropriate error is generated.
Under most circumstances, code has been copied forward to new games in a series
instead of made common. This is because games tend to subtly change their
expectations of the network protocol under the assumption that the player will
never go back to older versions of the game or server. A conscious decision was
made to copy-paste code instead of trying to refactor it to be common, specifically
to represent the reality that while the code appears identical/similar enough,
its really up to the game engineers how to implement and things are subtly different.
In some cases, a packet truly doesn't change across versions and in that case
game series will chose to factor out common code, but in many cases the choice was
made to sacrifice ease of maintainability for stability across versions.
All game classes should inherit from Base. It has several
`handle_<request>_request` handlers for basic functions such as PASELI, bootup,
and card lookup routines. Base assumes that `has_profile`, `get_profile`,
`put_profile`, and `bind_profile` are subclassed by any game wishing to provide
complex profile handling, but provides default handlers which should work for any
simple game. It also provides `get_play_statistics` and `update_play_statistics`
which all games should use during their respective profile fetch and save. Base
handles looking up a user's PASELI balance if the PCBID of the request is in an
arcade and that arcade is set to non-infinite PASELI.
Each game class is expected to have a few properties. The 'game' property should
be a string name and is used for all DB operations when referring to a game
series. The 'version' property should be an integer and is used for all DB
operations when referring to a specific version of a game. Game code is free to
choose what these values are set to, but currently all games use constants
defined in `bemani/common/constants.py` for easier sharing with the BEMAPI REST
server and the frontend. This is how Base can provide statistics, profile lookup
and card manager services without knowing anything about what game is subclassin
g it. Aside from that, games are free to handle packets in any way they see fit.
In general, the tables provided by the database are indexed document stores.
Most BEMANI games are implemented entirely client-side and simply expect the
server to return the data it sent to it on the last profile save. For that to
work, there are a few database abstractions in use across various games. The
'profile' table stores JSON data given a specific game, so it is appropriate
for game-specific data. The 'game_settings' table stores JSON data given
a game series, so it is appropriate for data that is consistent across an
entire series. Note that this does not include scores! The 'achievement' table
stores JSON data given a specific game, an identifier and an identifier type.
This table is perfect for item data such as unlocks or event progress. The
'score' table stores JSON given a game and song ID, as does the 'score_history'
table. Note that the song ID is an internal representation and translated
through the 'music' table. This is to allow game series to renumber their
songs while still keeping score history across versions. It also allows the
server to preserve scores across multiple versions of a game. Note that all above
tables are accessed through the Data class instead of directly creating SQL
in the game classes.
In some cases, we access a remote version of the Data classes instead of the
MySQL version directly. The remote version in turn contacts any remote BEMAPI
REST servers as well as the local database and then sums up the information before
returning it to the game layer. In this way, we support fetching scores and
rivaling across networks using the BEMAPI REST API in a manner that is virtually
transparent to individual game implementations. Crucially, it is not used in
the API implementation itself nor in the frontend, ensuring that both only
respond with data that is contained on this instance directly.