537 lines
22 KiB
Python
537 lines
22 KiB
Python
from typing import Any, Dict, Tuple
|
|
from typing_extensions import Final
|
|
|
|
from bemani.backend.reflec.base import ReflecBeatBase
|
|
|
|
from bemani.common import Profile, VersionConstants, ID, Time
|
|
from bemani.data import UserID
|
|
from bemani.protocol import Node
|
|
|
|
|
|
class ReflecBeat(ReflecBeatBase):
|
|
name: str = "REFLEC BEAT"
|
|
version: int = VersionConstants.REFLEC_BEAT
|
|
|
|
# Clear types according to the game
|
|
GAME_CLEAR_TYPE_NO_PLAY: Final[int] = 0
|
|
GAME_CLEAR_TYPE_PLAYED: Final[int] = 2
|
|
GAME_CLEAR_TYPE_FULL_COMBO: Final[int] = 3
|
|
|
|
# Reflec Beat has no profile succession
|
|
supports_expired_profiles: bool = False
|
|
|
|
@classmethod
|
|
def get_settings(cls) -> Dict[str, Any]:
|
|
"""
|
|
Return all of our front-end modifiably settings.
|
|
"""
|
|
return {
|
|
"bools": [
|
|
{
|
|
"name": "Force Song Unlock",
|
|
"tip": "Force unlock all songs.",
|
|
"category": "game_config",
|
|
"setting": "force_unlock_songs",
|
|
},
|
|
],
|
|
"ints": [],
|
|
}
|
|
|
|
def __db_to_game_clear_type(self, db_clear_type: int, db_combo_type: int) -> int:
|
|
if db_clear_type == self.CLEAR_TYPE_NO_PLAY:
|
|
return self.GAME_CLEAR_TYPE_NO_PLAY
|
|
if db_clear_type == self.CLEAR_TYPE_FAILED:
|
|
return self.GAME_CLEAR_TYPE_PLAYED
|
|
if db_clear_type in [
|
|
self.CLEAR_TYPE_CLEARED,
|
|
self.CLEAR_TYPE_HARD_CLEARED,
|
|
self.CLEAR_TYPE_S_HARD_CLEARED,
|
|
]:
|
|
if db_combo_type in [
|
|
self.COMBO_TYPE_NONE,
|
|
self.COMBO_TYPE_ALMOST_COMBO,
|
|
]:
|
|
return self.GAME_CLEAR_TYPE_PLAYED
|
|
if db_combo_type in [
|
|
self.COMBO_TYPE_FULL_COMBO,
|
|
self.COMBO_TYPE_FULL_COMBO_ALL_JUST,
|
|
]:
|
|
return self.GAME_CLEAR_TYPE_FULL_COMBO
|
|
|
|
raise Exception(f"Invalid db_combo_type {db_combo_type}")
|
|
raise Exception(f"Invalid db_clear_type {db_clear_type}")
|
|
|
|
def __game_to_db_clear_type(self, game_clear_type: int, game_achievement_rate: int) -> Tuple[int, int]:
|
|
if game_clear_type == self.GAME_CLEAR_TYPE_NO_PLAY:
|
|
return (self.CLEAR_TYPE_NO_PLAY, self.COMBO_TYPE_NONE)
|
|
if game_clear_type == self.GAME_CLEAR_TYPE_PLAYED:
|
|
if game_achievement_rate >= 7000:
|
|
return (self.CLEAR_TYPE_CLEARED, self.COMBO_TYPE_NONE)
|
|
else:
|
|
return (self.CLEAR_TYPE_FAILED, self.COMBO_TYPE_NONE)
|
|
if game_clear_type == self.GAME_CLEAR_TYPE_FULL_COMBO:
|
|
return (self.CLEAR_TYPE_CLEARED, self.COMBO_TYPE_FULL_COMBO)
|
|
|
|
raise Exception(f"Invalid game_clear_type {game_clear_type}")
|
|
|
|
def handle_log_pcb_status_request(self, request: Node) -> Node:
|
|
return Node.void("log")
|
|
|
|
def handle_log_opsetting_request(self, request: Node) -> Node:
|
|
return Node.void("log")
|
|
|
|
def handle_log_play_request(self, request: Node) -> Node:
|
|
return Node.void("log")
|
|
|
|
def handle_pcbinfo_get_request(self, request: Node) -> Node:
|
|
shop_id = ID.parse_machine_id(request.child_value("lid"))
|
|
machine = self.get_machine_by_id(shop_id)
|
|
if machine is not None:
|
|
machine_name = machine.name
|
|
close = machine.data.get_bool("close")
|
|
hour = machine.data.get_int("hour")
|
|
minute = machine.data.get_int("minute")
|
|
pref = machine.data.get_int("pref", self.get_machine_region())
|
|
else:
|
|
machine_name = ""
|
|
close = False
|
|
hour = 0
|
|
minute = 0
|
|
pref = self.get_machine_region()
|
|
|
|
root = Node.void("pcbinfo")
|
|
info = Node.void("info")
|
|
root.add_child(info)
|
|
|
|
info.add_child(Node.string("name", machine_name))
|
|
info.add_child(Node.s16("pref", pref))
|
|
info.add_child(Node.bool("close", close))
|
|
info.add_child(Node.u8("hour", hour))
|
|
info.add_child(Node.u8("min", minute))
|
|
|
|
return root
|
|
|
|
def handle_pcbinfo_set_request(self, request: Node) -> Node:
|
|
self.update_machine_name(request.child_value("info/name"))
|
|
self.update_machine_data(
|
|
{
|
|
"close": request.child_value("info/close"),
|
|
"hour": request.child_value("info/hour"),
|
|
"minute": request.child_value("info/min"),
|
|
"pref": request.child_value("info/pref"),
|
|
}
|
|
)
|
|
return Node.void("pcbinfo")
|
|
|
|
def __add_event_info(self, request: Node) -> None:
|
|
events: Dict[int, int] = {}
|
|
|
|
for _eventid, _phase in events.items():
|
|
data = Node.void("data")
|
|
request.add_child(data)
|
|
data.add_child(Node.s32("type", -1))
|
|
data.add_child(Node.s32("value", -1))
|
|
|
|
def handle_sysinfo_get_request(self, request: Node) -> Node:
|
|
root = Node.void("sysinfo")
|
|
trd = Node.void("trd")
|
|
root.add_child(trd)
|
|
|
|
# Add event info
|
|
self.__add_event_info(trd)
|
|
|
|
return root
|
|
|
|
def handle_sysinfo_fan_request(self, request: Node) -> Node:
|
|
sysinfo = Node.void("sysinfo")
|
|
sysinfo.add_child(Node.u8("pref", self.get_machine_region()))
|
|
sysinfo.add_child(Node.string("lid", request.child_value("lid")))
|
|
return sysinfo
|
|
|
|
def handle_lobby_entry_request(self, request: Node) -> Node:
|
|
root = Node.void("lobby")
|
|
|
|
# Create a lobby entry for this user
|
|
extid = request.child_value("e/uid")
|
|
userid = self.data.remote.user.from_extid(self.game, self.version, extid)
|
|
if userid is not None:
|
|
profile = self.get_profile(userid)
|
|
self.data.local.lobby.put_lobby(
|
|
self.game,
|
|
self.version,
|
|
userid,
|
|
{
|
|
"mid": request.child_value("e/mid"),
|
|
"ng": request.child_value("e/ng"),
|
|
"lid": request.child_value("e/lid"),
|
|
"sn": request.child_value("e/sn"),
|
|
"pref": request.child_value("e/pref"),
|
|
"ga": request.child_value("e/ga"),
|
|
"gp": request.child_value("e/gp"),
|
|
"la": request.child_value("e/la"),
|
|
},
|
|
)
|
|
lobby = self.data.local.lobby.get_lobby(
|
|
self.game,
|
|
self.version,
|
|
userid,
|
|
)
|
|
root.add_child(Node.s32("eid", lobby.get_int("id")))
|
|
e = Node.void("e")
|
|
root.add_child(e)
|
|
e.add_child(Node.s32("eid", lobby.get_int("id")))
|
|
e.add_child(Node.u16("mid", lobby.get_int("mid")))
|
|
e.add_child(Node.u8("ng", lobby.get_int("ng")))
|
|
e.add_child(Node.s32("uid", profile.extid))
|
|
e.add_child(Node.string("pn", profile.get_str("name")))
|
|
e.add_child(Node.s32("exp", profile.get_int("exp")))
|
|
e.add_child(Node.u8("mg", profile.get_int("mg")))
|
|
e.add_child(Node.s32("tid", lobby.get_int("tid")))
|
|
e.add_child(Node.string("tn", lobby.get_str("tn")))
|
|
e.add_child(Node.string("lid", lobby.get_str("lid")))
|
|
e.add_child(Node.string("sn", lobby.get_str("sn")))
|
|
e.add_child(Node.u8("pref", lobby.get_int("pref")))
|
|
e.add_child(Node.u8_array("ga", lobby.get_int_array("ga", 4)))
|
|
e.add_child(Node.u16("gp", lobby.get_int("gp")))
|
|
e.add_child(Node.u8_array("la", lobby.get_int_array("la", 4)))
|
|
|
|
return root
|
|
|
|
def handle_lobby_read_request(self, request: Node) -> Node:
|
|
root = Node.void("lobby")
|
|
|
|
# Look up all lobbies matching the criteria specified
|
|
mg = request.child_value("m_grade") # noqa: F841
|
|
extid = request.child_value("uid")
|
|
limit = request.child_value("max")
|
|
userid = self.data.remote.user.from_extid(self.game, self.version, extid)
|
|
if userid is not None:
|
|
lobbies = self.data.local.lobby.get_all_lobbies(self.game, self.version)
|
|
for user, lobby in lobbies:
|
|
if limit <= 0:
|
|
break
|
|
|
|
if user == userid:
|
|
# If we have our own lobby, don't return it
|
|
continue
|
|
|
|
profile = self.get_profile(user)
|
|
if profile is None:
|
|
# No profile info, don't return this lobby
|
|
continue
|
|
|
|
e = Node.void("e")
|
|
root.add_child(e)
|
|
e.add_child(Node.s32("eid", lobby.get_int("id")))
|
|
e.add_child(Node.u16("mid", lobby.get_int("mid")))
|
|
e.add_child(Node.u8("ng", lobby.get_int("ng")))
|
|
e.add_child(Node.s32("uid", profile.extid))
|
|
e.add_child(Node.string("pn", profile.get_str("name")))
|
|
e.add_child(Node.s32("exp", profile.get_int("exp")))
|
|
e.add_child(Node.u8("mg", profile.get_int("mg")))
|
|
e.add_child(Node.s32("tid", lobby.get_int("tid")))
|
|
e.add_child(Node.string("tn", lobby.get_str("tn")))
|
|
e.add_child(Node.string("lid", lobby.get_str("lid")))
|
|
e.add_child(Node.string("sn", lobby.get_str("sn")))
|
|
e.add_child(Node.u8("pref", lobby.get_int("pref")))
|
|
e.add_child(Node.u8_array("ga", lobby.get_int_array("ga", 4)))
|
|
e.add_child(Node.u16("gp", lobby.get_int("gp")))
|
|
e.add_child(Node.u8_array("la", lobby.get_int_array("la", 4)))
|
|
|
|
limit = limit - 1
|
|
|
|
return root
|
|
|
|
def handle_lobby_delete_request(self, request: Node) -> Node:
|
|
eid = request.child_value("eid")
|
|
self.data.local.lobby.destroy_lobby(eid)
|
|
return Node.void("lobby")
|
|
|
|
def handle_player_start_request(self, request: Node) -> Node:
|
|
# Add a dummy entry into the lobby setup so we can clean up on end play
|
|
refid = request.child_value("rid")
|
|
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
|
|
if userid is not None:
|
|
self.data.local.lobby.put_play_session_info(self.game, self.version, userid, {})
|
|
|
|
root = Node.void("player")
|
|
root.add_child(Node.bool("is_suc", True))
|
|
|
|
# Add event info
|
|
self.__add_event_info(root)
|
|
|
|
return root
|
|
|
|
def handle_player_delete_request(self, request: Node) -> Node:
|
|
return Node.void("player")
|
|
|
|
def handle_player_end_request(self, request: Node) -> Node:
|
|
# Destroy play session based on info from the request
|
|
refid = request.child_value("rid")
|
|
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
|
|
if userid is not None:
|
|
# Kill any lingering lobbies by this user
|
|
lobby = self.data.local.lobby.get_lobby(
|
|
self.game,
|
|
self.version,
|
|
userid,
|
|
)
|
|
if lobby is not None:
|
|
self.data.local.lobby.destroy_lobby(lobby.get_int("id"))
|
|
self.data.local.lobby.destroy_play_session_info(self.game, self.version, userid)
|
|
|
|
return Node.void("player")
|
|
|
|
def handle_player_read_request(self, request: Node) -> Node:
|
|
refid = request.child_value("rid")
|
|
profile = self.get_profile_by_refid(refid)
|
|
if profile:
|
|
return profile
|
|
return Node.void("player")
|
|
|
|
def handle_player_write_request(self, request: Node) -> Node:
|
|
refid = request.child_value("rid")
|
|
profile = self.put_profile_by_refid(refid, request)
|
|
root = Node.void("player")
|
|
|
|
if profile is None:
|
|
root.add_child(Node.s32("uid", 0))
|
|
else:
|
|
root.add_child(Node.s32("uid", profile.extid))
|
|
root.add_child(Node.s32("time", Time.now()))
|
|
return root
|
|
|
|
def format_profile(self, userid: UserID, profile: Profile) -> Node:
|
|
statistics = self.get_play_statistics(userid)
|
|
game_config = self.get_game_config()
|
|
achievements = self.data.local.user.get_achievements(self.game, self.version, userid)
|
|
scores = self.data.remote.music.get_scores(self.game, self.version, userid)
|
|
root = Node.void("player")
|
|
pdata = Node.void("pdata")
|
|
root.add_child(pdata)
|
|
|
|
base = Node.void("base")
|
|
pdata.add_child(base)
|
|
base.add_child(Node.s32("uid", profile.extid))
|
|
base.add_child(Node.string("name", profile.get_str("name")))
|
|
base.add_child(Node.s16("lv", profile.get_int("lvl")))
|
|
base.add_child(Node.s32("exp", profile.get_int("exp")))
|
|
base.add_child(Node.s16("mg", profile.get_int("mg")))
|
|
base.add_child(Node.s16("ap", profile.get_int("ap")))
|
|
base.add_child(Node.s32("flag", profile.get_int("flag")))
|
|
|
|
con = Node.void("con")
|
|
pdata.add_child(con)
|
|
con.add_child(Node.s32("day", statistics.today_plays))
|
|
con.add_child(Node.s32("cnt", statistics.total_plays))
|
|
con.add_child(Node.s32("last", statistics.last_play_timestamp))
|
|
con.add_child(Node.s32("now", Time.now()))
|
|
|
|
team = Node.void("team")
|
|
pdata.add_child(team)
|
|
team.add_child(Node.s32("id", -1))
|
|
team.add_child(Node.string("name", ""))
|
|
|
|
custom = Node.void("custom")
|
|
customdict = profile.get_dict("custom")
|
|
pdata.add_child(custom)
|
|
custom.add_child(Node.u8("bgm_m", customdict.get_int("bgm_m")))
|
|
custom.add_child(Node.u8("st_f", customdict.get_int("st_f")))
|
|
custom.add_child(Node.u8("st_bg", customdict.get_int("st_bg")))
|
|
custom.add_child(Node.u8("st_bg_b", customdict.get_int("st_bg_b")))
|
|
custom.add_child(Node.u8("eff_e", customdict.get_int("eff_e")))
|
|
custom.add_child(Node.u8("se_s", customdict.get_int("se_s")))
|
|
custom.add_child(Node.u8("se_s_v", customdict.get_int("se_s_v")))
|
|
|
|
released = Node.void("released")
|
|
pdata.add_child(released)
|
|
|
|
for item in achievements:
|
|
if item.type[:5] != "item_":
|
|
continue
|
|
itemtype = int(item.type[5:])
|
|
if game_config.get_bool("force_unlock_songs") and itemtype == 0:
|
|
# Don't echo unlocks when we're force unlocking, we'll do it later
|
|
continue
|
|
|
|
info = Node.void("info")
|
|
released.add_child(info)
|
|
info.add_child(Node.u8("type", itemtype))
|
|
info.add_child(Node.u16("id", item.id))
|
|
|
|
if game_config.get_bool("force_unlock_songs"):
|
|
songs = {song.id for song in self.data.local.music.get_all_songs(self.game, self.version)}
|
|
|
|
for songid in songs:
|
|
info = Node.void("info")
|
|
released.add_child(info)
|
|
info.add_child(Node.u8("type", 0))
|
|
info.add_child(Node.u16("id", songid))
|
|
|
|
# Scores
|
|
record = Node.void("record")
|
|
pdata.add_child(record)
|
|
|
|
for score in scores:
|
|
rec = Node.void("rec")
|
|
record.add_child(rec)
|
|
rec.add_child(Node.u16("mid", score.id))
|
|
rec.add_child(Node.u8("ng", score.chart))
|
|
rec.add_child(Node.s32("win", score.data.get_dict("stats").get_int("win")))
|
|
rec.add_child(Node.s32("lose", score.data.get_dict("stats").get_int("lose")))
|
|
rec.add_child(Node.s32("draw", score.data.get_dict("stats").get_int("draw")))
|
|
rec.add_child(
|
|
Node.u8(
|
|
"ct",
|
|
self.__db_to_game_clear_type(
|
|
score.data.get_int("clear_type"),
|
|
score.data.get_int("combo_type"),
|
|
),
|
|
)
|
|
)
|
|
rec.add_child(Node.s16("ar", int(score.data.get_int("achievement_rate") / 10)))
|
|
rec.add_child(Node.s16("bs", score.points))
|
|
rec.add_child(Node.s16("mc", score.data.get_int("combo")))
|
|
rec.add_child(Node.s16("bmc", score.data.get_int("miss_count")))
|
|
|
|
# In original ReflecBeat, the entire battle log was returned for each battle.
|
|
# We don't support storing all of that info, so don't return anything here.
|
|
blog = Node.void("blog")
|
|
pdata.add_child(blog)
|
|
|
|
# Comment (seems unused?)
|
|
pdata.add_child(Node.string("cmnt", ""))
|
|
|
|
return root
|
|
|
|
def unformat_profile(self, userid: UserID, request: Node, oldprofile: Profile) -> Profile:
|
|
game_config = self.get_game_config()
|
|
newprofile = oldprofile.clone()
|
|
|
|
newprofile.replace_int("lid", ID.parse_machine_id(request.child_value("lid")))
|
|
newprofile.replace_str("name", request.child_value("pdata/base/name"))
|
|
newprofile.replace_int("lvl", request.child_value("pdata/base/lv"))
|
|
newprofile.replace_int("exp", request.child_value("pdata/base/exp"))
|
|
newprofile.replace_int("mg", request.child_value("pdata/base/mg"))
|
|
newprofile.replace_int("ap", request.child_value("pdata/base/ap"))
|
|
newprofile.replace_int("flag", request.child_value("pdata/base/flag"))
|
|
|
|
customdict = newprofile.get_dict("custom")
|
|
custom = request.child("pdata/custom")
|
|
if custom:
|
|
customdict.replace_int("bgm_m", custom.child_value("bgm_m"))
|
|
customdict.replace_int("st_f", custom.child_value("st_f"))
|
|
customdict.replace_int("st_bg", custom.child_value("st_bg"))
|
|
customdict.replace_int("st_bg_b", custom.child_value("st_bg_b"))
|
|
customdict.replace_int("eff_e", custom.child_value("eff_e"))
|
|
customdict.replace_int("se_s", custom.child_value("se_s"))
|
|
customdict.replace_int("se_s_v", custom.child_value("se_s_v"))
|
|
newprofile.replace_dict("custom", customdict)
|
|
|
|
# Music unlocks and other stuff
|
|
released = request.child("pdata/released")
|
|
if released:
|
|
for child in released.children:
|
|
if child.name != "info":
|
|
continue
|
|
|
|
item_id = child.child_value("id")
|
|
item_type = child.child_value("type")
|
|
if game_config.get_bool("force_unlock_songs") and item_type == 0:
|
|
# Don't save unlocks when we're force unlocking
|
|
continue
|
|
|
|
self.data.local.user.put_achievement(
|
|
self.game,
|
|
self.version,
|
|
userid,
|
|
item_id,
|
|
f"item_{item_type}",
|
|
{},
|
|
)
|
|
|
|
# Grab any new records set during this play session. Reflec Beat original only sends
|
|
# the top record back for songs that were played at least once during the session.
|
|
# Note that it sends the top record, so if you play the song twice, it will return
|
|
# only one record. Also, if you get a lower score than a previous try, it will return
|
|
# the previous try. So, we must also look at the battle log for the actual play scores,
|
|
# and combine the data if we can.
|
|
savedrecords: Dict[int, Dict[int, Dict[str, int]]] = {}
|
|
songplays = request.child("pdata/record")
|
|
if songplays:
|
|
for child in songplays.children:
|
|
if child.name != "rec":
|
|
continue
|
|
|
|
songid = child.child_value("mid")
|
|
chart = child.child_value("ng")
|
|
|
|
# These don't get sent with the battle logs, so we try to construct
|
|
# the values here.
|
|
if songid not in savedrecords:
|
|
savedrecords[songid] = {}
|
|
savedrecords[songid][chart] = {
|
|
"achievement_rate": child.child_value("ar") * 10,
|
|
"points": child.child_value("bs"),
|
|
"combo": child.child_value("mc"),
|
|
"miss_count": child.child_value("bmc"),
|
|
"win": child.child_value("win"),
|
|
"lose": child.child_value("lose"),
|
|
"draw": child.child_value("draw"),
|
|
}
|
|
|
|
# Now, see the actual battles that were played. If we can, unify the data with a record.
|
|
# We only do that when the record achievement rate and score matches the battle achievement
|
|
# rate and score, so we know for a fact that that record was generated by this battle.
|
|
battlelogs = request.child("pdata/blog")
|
|
if battlelogs:
|
|
for child in battlelogs.children:
|
|
if child.name != "log":
|
|
continue
|
|
|
|
songid = child.child_value("mid")
|
|
chart = child.child_value("ng")
|
|
|
|
clear_type = child.child_value("myself/ct")
|
|
achievement_rate = child.child_value("myself/ar") * 10
|
|
points = child.child_value("myself/s")
|
|
|
|
clear_type, combo_type = self.__game_to_db_clear_type(clear_type, achievement_rate)
|
|
|
|
combo = None
|
|
miss_count = -1
|
|
stats = None
|
|
|
|
if songid in savedrecords:
|
|
if chart in savedrecords[songid]:
|
|
data = savedrecords[songid][chart]
|
|
|
|
if data["achievement_rate"] == achievement_rate and data["points"] == points:
|
|
# This is the same record! Use the stats from it to update our
|
|
# internal representation.
|
|
combo = data["combo"]
|
|
miss_count = data["miss_count"]
|
|
stats = {
|
|
"win": data["win"],
|
|
"lose": data["lose"],
|
|
"draw": data["draw"],
|
|
}
|
|
|
|
self.update_score(
|
|
userid,
|
|
songid,
|
|
chart,
|
|
points,
|
|
achievement_rate,
|
|
clear_type,
|
|
combo_type,
|
|
miss_count,
|
|
combo=combo,
|
|
stats=stats,
|
|
)
|
|
|
|
# Keep track of play statistics
|
|
self.update_play_statistics(userid)
|
|
|
|
return newprofile
|