556 lines
21 KiB
Python
556 lines
21 KiB
Python
# vim: set fileencoding=utf-8
|
|
from typing import Any, Dict, Optional, Tuple
|
|
from typing_extensions import Final
|
|
|
|
from bemani.backend.ess import EventLogHandler
|
|
from bemani.backend.sdvx.base import SoundVoltexBase
|
|
from bemani.common import Profile, VersionConstants, ID, intish
|
|
from bemani.data import Score, UserID
|
|
from bemani.protocol import Node
|
|
|
|
|
|
class SoundVoltexBooth(
|
|
EventLogHandler,
|
|
SoundVoltexBase,
|
|
):
|
|
name: str = "SOUND VOLTEX BOOTH"
|
|
version: int = VersionConstants.SDVX_BOOTH
|
|
|
|
GAME_LIMITED_LOCKED: Final[int] = 1
|
|
GAME_LIMITED_UNLOCKED: Final[int] = 2
|
|
|
|
GAME_CURRENCY_PACKETS: Final[int] = 0
|
|
GAME_CURRENCY_BLOCKS: Final[int] = 1
|
|
|
|
GAME_CLEAR_TYPE_NO_CLEAR: Final[int] = 1
|
|
GAME_CLEAR_TYPE_CLEAR: Final[int] = 2
|
|
GAME_CLEAR_TYPE_ULTIMATE_CHAIN: Final[int] = 3
|
|
GAME_CLEAR_TYPE_PERFECT_ULTIMATE_CHAIN: Final[int] = 4
|
|
|
|
GAME_GRADE_NO_PLAY: Final[int] = 0
|
|
GAME_GRADE_D: Final[int] = 1
|
|
GAME_GRADE_C: Final[int] = 2
|
|
GAME_GRADE_B: Final[int] = 3
|
|
GAME_GRADE_A: Final[int] = 4
|
|
GAME_GRADE_AA: Final[int] = 5
|
|
GAME_GRADE_AAA: Final[int] = 6
|
|
|
|
@classmethod
|
|
def get_settings(cls) -> Dict[str, Any]:
|
|
"""
|
|
Return all of our front-end modifiably settings.
|
|
"""
|
|
return {
|
|
"bools": [
|
|
{
|
|
"name": "Disable Online Matching",
|
|
"tip": "Disable online matching between games.",
|
|
"category": "game_config",
|
|
"setting": "disable_matching",
|
|
},
|
|
{
|
|
"name": "Force Song Unlock",
|
|
"tip": "Force unlock all songs.",
|
|
"category": "game_config",
|
|
"setting": "force_unlock_songs",
|
|
},
|
|
{
|
|
"name": "Force Appeal Card Unlock",
|
|
"tip": "Force unlock all appeal cards.",
|
|
"category": "game_config",
|
|
"setting": "force_unlock_cards",
|
|
},
|
|
],
|
|
}
|
|
|
|
def previous_version(self) -> Optional[SoundVoltexBase]:
|
|
return None
|
|
|
|
def __game_to_db_clear_type(self, clear_type: int) -> int:
|
|
return {
|
|
self.GAME_CLEAR_TYPE_NO_CLEAR: self.CLEAR_TYPE_FAILED,
|
|
self.GAME_CLEAR_TYPE_CLEAR: self.CLEAR_TYPE_CLEAR,
|
|
self.GAME_CLEAR_TYPE_ULTIMATE_CHAIN: self.CLEAR_TYPE_ULTIMATE_CHAIN,
|
|
self.GAME_CLEAR_TYPE_PERFECT_ULTIMATE_CHAIN: self.CLEAR_TYPE_PERFECT_ULTIMATE_CHAIN,
|
|
}[clear_type]
|
|
|
|
def __db_to_game_clear_type(self, clear_type: int) -> int:
|
|
return {
|
|
self.CLEAR_TYPE_NO_PLAY: self.GAME_CLEAR_TYPE_NO_CLEAR,
|
|
self.CLEAR_TYPE_FAILED: self.GAME_CLEAR_TYPE_NO_CLEAR,
|
|
self.CLEAR_TYPE_CLEAR: self.GAME_CLEAR_TYPE_CLEAR,
|
|
self.CLEAR_TYPE_HARD_CLEAR: self.GAME_CLEAR_TYPE_CLEAR,
|
|
self.CLEAR_TYPE_ULTIMATE_CHAIN: self.GAME_CLEAR_TYPE_ULTIMATE_CHAIN,
|
|
self.CLEAR_TYPE_PERFECT_ULTIMATE_CHAIN: self.GAME_CLEAR_TYPE_PERFECT_ULTIMATE_CHAIN,
|
|
}[clear_type]
|
|
|
|
def __game_to_db_grade(self, grade: int) -> int:
|
|
return {
|
|
self.GAME_GRADE_NO_PLAY: self.GRADE_NO_PLAY,
|
|
self.GAME_GRADE_D: self.GRADE_D,
|
|
self.GAME_GRADE_C: self.GRADE_C,
|
|
self.GAME_GRADE_B: self.GRADE_B,
|
|
self.GAME_GRADE_A: self.GRADE_A,
|
|
self.GAME_GRADE_AA: self.GRADE_AA,
|
|
self.GAME_GRADE_AAA: self.GRADE_AAA,
|
|
}[grade]
|
|
|
|
def __db_to_game_grade(self, grade: int) -> int:
|
|
return {
|
|
self.GRADE_NO_PLAY: self.GAME_GRADE_NO_PLAY,
|
|
self.GRADE_D: self.GAME_GRADE_D,
|
|
self.GRADE_C: self.GAME_GRADE_C,
|
|
self.GRADE_B: self.GAME_GRADE_B,
|
|
self.GRADE_A: self.GAME_GRADE_A,
|
|
self.GRADE_A_PLUS: self.GAME_GRADE_A,
|
|
self.GRADE_AA: self.GAME_GRADE_AA,
|
|
self.GRADE_AA_PLUS: self.GAME_GRADE_AA,
|
|
self.GRADE_AAA: self.GAME_GRADE_AAA,
|
|
self.GRADE_AAA_PLUS: self.GAME_GRADE_AAA,
|
|
self.GRADE_S: self.GAME_GRADE_AAA,
|
|
}[grade]
|
|
|
|
def handle_game_exception_request(self, request: Node) -> Node:
|
|
return Node.void("game")
|
|
|
|
def handle_game_entry_s_request(self, request: Node) -> Node:
|
|
game = Node.void("game")
|
|
# This should be created on the fly for a lobby that we're in.
|
|
game.add_child(Node.u32("entry_id", 1))
|
|
return game
|
|
|
|
def handle_game_lounge_request(self, request: Node) -> Node:
|
|
game = Node.void("game")
|
|
# Refresh interval in seconds.
|
|
game.add_child(Node.u32("interval", 10))
|
|
return game
|
|
|
|
def handle_game_entry_e_request(self, request: Node) -> Node:
|
|
# Lobby destroy method, eid attribute (u32) should be used
|
|
# to destroy any open lobbies.
|
|
return Node.void("game")
|
|
|
|
def handle_game_frozen_request(self, request: Node) -> Node:
|
|
game = Node.void("game")
|
|
game.set_attribute("result", "0")
|
|
return game
|
|
|
|
def handle_game_shop_request(self, request: Node) -> Node:
|
|
self.update_machine_name(request.child_value("shopname"))
|
|
|
|
# Respond with number of milliseconds until next request
|
|
game = Node.void("game")
|
|
game.add_child(Node.u32("nxt_time", 1000 * 5 * 60))
|
|
return game
|
|
|
|
def handle_game_common_request(self, request: Node) -> Node:
|
|
game = Node.void("game")
|
|
limited = Node.void("limited")
|
|
game.add_child(limited)
|
|
|
|
game_config = self.get_game_config()
|
|
if game_config.get_bool("force_unlock_songs"):
|
|
ids = set()
|
|
songs = self.data.local.music.get_all_songs(self.game, self.version)
|
|
for song in songs:
|
|
if song.data.get_int("limited") == self.GAME_LIMITED_LOCKED:
|
|
ids.add(song.id)
|
|
|
|
for songid in ids:
|
|
music = Node.void("music")
|
|
limited.add_child(music)
|
|
music.set_attribute("id", str(songid))
|
|
music.set_attribute("flag", str(self.GAME_LIMITED_UNLOCKED))
|
|
|
|
event = Node.void("event")
|
|
game.add_child(event)
|
|
|
|
def enable_event(eid: int) -> None:
|
|
evt = Node.void("info")
|
|
event.add_child(evt)
|
|
evt.set_attribute("id", str(eid))
|
|
|
|
if not game_config.get_bool("disable_matching"):
|
|
enable_event(3) # Matching enabled
|
|
enable_event(9) # Rank Soukuu
|
|
enable_event(13) # Year-end bonus
|
|
|
|
catalog = Node.void("catalog")
|
|
game.add_child(catalog)
|
|
songunlocks = self.data.local.game.get_items(self.game, self.version)
|
|
for unlock in songunlocks:
|
|
if unlock.type != "song_unlock":
|
|
continue
|
|
|
|
info = Node.void("info")
|
|
catalog.add_child(info)
|
|
info.set_attribute("id", str(unlock.id))
|
|
info.set_attribute("currency", str(self.GAME_CURRENCY_BLOCKS))
|
|
info.set_attribute("price", str(unlock.data.get_int("blocks")))
|
|
|
|
kacinfo = Node.void("kacinfo")
|
|
game.add_child(kacinfo)
|
|
kacinfo.add_child(Node.u32("note00", 0))
|
|
kacinfo.add_child(Node.u32("note01", 0))
|
|
kacinfo.add_child(Node.u32("note02", 0))
|
|
kacinfo.add_child(Node.u32("note10", 0))
|
|
kacinfo.add_child(Node.u32("note11", 0))
|
|
kacinfo.add_child(Node.u32("note12", 0))
|
|
kacinfo.add_child(Node.u32("rabbeat0", 0))
|
|
kacinfo.add_child(Node.u32("rabbeat1", 0))
|
|
|
|
return game
|
|
|
|
def handle_game_hiscore_request(self, request: Node) -> Node:
|
|
game = Node.void("game")
|
|
|
|
# Ranking system I think?
|
|
for i in range(1, 21):
|
|
ranking = Node.void("ranking")
|
|
game.add_child(ranking)
|
|
ranking.set_attribute("id", str(i))
|
|
|
|
hiscore = Node.void("hiscore")
|
|
game.add_child(hiscore)
|
|
hiscore.set_attribute("type", "1")
|
|
|
|
records = self.data.remote.music.get_all_records(self.game, self.version)
|
|
|
|
# Organize by song->chart
|
|
records_by_id: Dict[int, Dict[int, Tuple[UserID, Score]]] = {}
|
|
missing_users = []
|
|
for record in records:
|
|
userid, score = record
|
|
if score.id not in records_by_id:
|
|
records_by_id[score.id] = {}
|
|
|
|
records_by_id[score.id][score.chart] = record
|
|
missing_users.append(userid)
|
|
|
|
users = {
|
|
userid: profile
|
|
for (userid, profile) in self.get_any_profiles(missing_users)
|
|
}
|
|
|
|
# Output records
|
|
for songid in records_by_id:
|
|
music = Node.void("music")
|
|
hiscore.add_child(music)
|
|
music.set_attribute("id", str(songid))
|
|
|
|
for chart in records_by_id[songid]:
|
|
note = Node.void("note")
|
|
music.add_child(note)
|
|
note.set_attribute("type", str(chart))
|
|
|
|
userid, score = records_by_id[songid][chart]
|
|
note.set_attribute("score", str(score.points))
|
|
note.set_attribute("name", users[userid].get_str("name"))
|
|
|
|
return game
|
|
|
|
def handle_game_new_request(self, request: Node) -> Node:
|
|
refid = request.attribute("refid")
|
|
name = request.attribute("name")
|
|
loc = ID.parse_machine_id(request.attribute("locid"))
|
|
self.new_profile_by_refid(refid, name, loc)
|
|
|
|
root = Node.void("game")
|
|
return root
|
|
|
|
def handle_game_load_request(self, request: Node) -> Node:
|
|
refid = request.attribute("dataid")
|
|
root = self.get_profile_by_refid(refid)
|
|
if root is None:
|
|
root = Node.void("game")
|
|
root.set_attribute("none", "1")
|
|
return root
|
|
|
|
def handle_game_save_request(self, request: Node) -> Node:
|
|
refid = request.attribute("refid")
|
|
|
|
if refid is not None:
|
|
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
|
|
else:
|
|
userid = None
|
|
|
|
if userid is not None:
|
|
oldprofile = self.get_profile(userid)
|
|
newprofile = self.unformat_profile(userid, request, oldprofile)
|
|
else:
|
|
newprofile = None
|
|
|
|
if userid is not None and newprofile is not None:
|
|
self.put_profile(userid, newprofile)
|
|
|
|
return Node.void("game")
|
|
|
|
def handle_game_load_m_request(self, request: Node) -> Node:
|
|
refid = request.attribute("dataid")
|
|
|
|
if refid is not None:
|
|
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
|
|
else:
|
|
userid = None
|
|
|
|
if userid is not None:
|
|
scores = self.data.remote.music.get_scores(self.game, self.version, userid)
|
|
else:
|
|
scores = []
|
|
|
|
# Organize by song->chart
|
|
scores_by_id: Dict[int, Dict[int, Score]] = {}
|
|
for score in scores:
|
|
if score.id not in scores_by_id:
|
|
scores_by_id[score.id] = {}
|
|
|
|
scores_by_id[score.id][score.chart] = score
|
|
|
|
# Output to the game
|
|
game = Node.void("game")
|
|
for songid in scores_by_id:
|
|
music = Node.void("music")
|
|
game.add_child(music)
|
|
music.set_attribute("music_id", str(songid))
|
|
|
|
for chart in scores_by_id[songid]:
|
|
typenode = Node.void("type")
|
|
music.add_child(typenode)
|
|
typenode.set_attribute("type_id", str(chart))
|
|
|
|
score = scores_by_id[songid][chart]
|
|
typenode.set_attribute("score", str(score.points))
|
|
typenode.set_attribute("cnt", str(score.plays))
|
|
typenode.set_attribute(
|
|
"clear_type",
|
|
str(self.__db_to_game_clear_type(score.data.get_int("clear_type"))),
|
|
)
|
|
typenode.set_attribute(
|
|
"score_grade",
|
|
str(self.__db_to_game_grade(score.data.get_int("grade"))),
|
|
)
|
|
|
|
return game
|
|
|
|
def handle_game_save_m_request(self, request: Node) -> Node:
|
|
refid = request.attribute("dataid")
|
|
|
|
if refid is not None:
|
|
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
|
|
else:
|
|
userid = None
|
|
|
|
if userid is None:
|
|
return Node.void("game")
|
|
|
|
musicid = int(request.attribute("music_id"))
|
|
chart = int(request.attribute("music_type"))
|
|
score = int(request.attribute("score"))
|
|
combo = int(request.attribute("max_chain"))
|
|
grade = self.__game_to_db_grade(int(request.attribute("score_grade")))
|
|
clear_type = self.__game_to_db_clear_type(int(request.attribute("clear_type")))
|
|
|
|
# Save the score
|
|
self.update_score(
|
|
userid,
|
|
musicid,
|
|
chart,
|
|
score,
|
|
clear_type,
|
|
grade,
|
|
combo,
|
|
)
|
|
|
|
# No response necessary
|
|
return Node.void("game")
|
|
|
|
def handle_game_buy_request(self, request: Node) -> Node:
|
|
refid = request.attribute("refid")
|
|
|
|
if refid is not None:
|
|
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
|
|
else:
|
|
userid = None
|
|
|
|
if userid is not None:
|
|
profile = self.get_profile(userid)
|
|
else:
|
|
profile = None
|
|
|
|
if userid is not None and profile is not None:
|
|
# Look up packets and blocks
|
|
packet = profile.get_int("packet")
|
|
block = profile.get_int("block")
|
|
|
|
# Add on any additional we earned this round
|
|
packet = packet + (request.child_value("earned_gamecoin_packet") or 0)
|
|
block = block + (request.child_value("earned_gamecoin_block") or 0)
|
|
|
|
# Look up the item to get the actual price and currency used
|
|
item = self.data.local.game.get_item(
|
|
self.game,
|
|
self.version,
|
|
request.child_value("catalog_id"),
|
|
"song_unlock",
|
|
)
|
|
if item is not None:
|
|
currency_type = request.child_value("currency_type")
|
|
if currency_type == self.GAME_CURRENCY_PACKETS:
|
|
if "packets" in item:
|
|
# This is a valid purchase
|
|
newpacket = packet - item.get_int("packets")
|
|
if newpacket < 0:
|
|
result = 1
|
|
else:
|
|
packet = newpacket
|
|
result = 0
|
|
else:
|
|
# Bad transaction
|
|
result = 1
|
|
elif currency_type == self.GAME_CURRENCY_BLOCKS:
|
|
if "blocks" in item:
|
|
# This is a valid purchase
|
|
newblock = block - item.get_int("blocks")
|
|
if newblock < 0:
|
|
result = 1
|
|
else:
|
|
block = newblock
|
|
result = 0
|
|
else:
|
|
# Bad transaction
|
|
result = 1
|
|
else:
|
|
# Bad currency type
|
|
result = 1
|
|
|
|
if result == 0:
|
|
# Transaction is valid, update the profile with new packets and blocks
|
|
profile.replace_int("packet", packet)
|
|
profile.replace_int("block", block)
|
|
self.put_profile(userid, profile)
|
|
else:
|
|
# Bad catalog ID
|
|
result = 1
|
|
else:
|
|
# Unclear what to do here, return a bad response
|
|
packet = 0
|
|
block = 0
|
|
result = 1
|
|
|
|
game = Node.void("game")
|
|
game.add_child(Node.u32("gamecoin_packet", packet))
|
|
game.add_child(Node.u32("gamecoin_block", block))
|
|
game.add_child(Node.s8("result", result))
|
|
return game
|
|
|
|
def format_profile(self, userid: UserID, profile: Profile) -> Node:
|
|
game = Node.void("game")
|
|
|
|
# Generic profile stuff
|
|
game.add_child(Node.string("name", profile.get_str("name")))
|
|
game.add_child(Node.string("code", ID.format_extid(profile.extid)))
|
|
game.add_child(Node.u32("gamecoin_packet", profile.get_int("packet")))
|
|
game.add_child(Node.u32("gamecoin_block", profile.get_int("block")))
|
|
game.add_child(Node.u32("exp_point", profile.get_int("exp")))
|
|
game.add_child(Node.u32("m_user_cnt", profile.get_int("m_user_cnt")))
|
|
|
|
game_config = self.get_game_config()
|
|
if game_config.get_bool("force_unlock_cards"):
|
|
game.add_child(Node.bool_array("have_item", [True] * 512))
|
|
else:
|
|
game.add_child(
|
|
Node.bool_array(
|
|
"have_item",
|
|
[x > 0 for x in profile.get_int_array("have_item", 512)],
|
|
)
|
|
)
|
|
if game_config.get_bool("force_unlock_songs"):
|
|
game.add_child(Node.bool_array("have_note", [True] * 512))
|
|
else:
|
|
game.add_child(
|
|
Node.bool_array(
|
|
"have_note",
|
|
[x > 0 for x in profile.get_int_array("have_note", 512)],
|
|
)
|
|
)
|
|
|
|
# Last played stuff
|
|
lastdict = profile.get_dict("last")
|
|
last = Node.void("last")
|
|
game.add_child(last)
|
|
last.set_attribute("music_id", str(lastdict.get_int("music_id")))
|
|
last.set_attribute("music_type", str(lastdict.get_int("music_type")))
|
|
last.set_attribute("sort_type", str(lastdict.get_int("sort_type")))
|
|
last.set_attribute("headphone", str(lastdict.get_int("headphone")))
|
|
last.set_attribute("hispeed", str(lastdict.get_int("hispeed")))
|
|
last.set_attribute("appeal_id", str(lastdict.get_int("appeal_id")))
|
|
last.set_attribute("frame0", str(lastdict.get_int("frame0")))
|
|
last.set_attribute("frame1", str(lastdict.get_int("frame1")))
|
|
last.set_attribute("frame2", str(lastdict.get_int("frame2")))
|
|
last.set_attribute("frame3", str(lastdict.get_int("frame3")))
|
|
last.set_attribute("frame4", str(lastdict.get_int("frame4")))
|
|
|
|
return game
|
|
|
|
def unformat_profile(
|
|
self, userid: UserID, request: Node, oldprofile: Profile
|
|
) -> Profile:
|
|
newprofile = oldprofile.clone()
|
|
|
|
# Update experience and in-game currencies
|
|
earned_gamecoin_packet = request.child_value("earned_gamecoin_packet")
|
|
if earned_gamecoin_packet is not None:
|
|
newprofile.replace_int(
|
|
"packet", newprofile.get_int("packet") + earned_gamecoin_packet
|
|
)
|
|
earned_gamecoin_block = request.child_value("earned_gamecoin_block")
|
|
if earned_gamecoin_block is not None:
|
|
newprofile.replace_int(
|
|
"block", newprofile.get_int("block") + earned_gamecoin_block
|
|
)
|
|
gain_exp = request.child_value("gain_exp")
|
|
if gain_exp is not None:
|
|
newprofile.replace_int("exp", newprofile.get_int("exp") + gain_exp)
|
|
|
|
# Miscelaneous stuff
|
|
newprofile.replace_int("m_user_cnt", request.child_value("m_user_cnt"))
|
|
|
|
# Update user's unlock status if we aren't force unlocked
|
|
game_config = self.get_game_config()
|
|
if not game_config.get_bool("force_unlock_cards"):
|
|
have_item = request.child_value("have_item")
|
|
if have_item is not None:
|
|
newprofile.replace_int_array(
|
|
"have_item", 512, [1 if x else 0 for x in have_item]
|
|
)
|
|
if not game_config.get_bool("force_unlock_songs"):
|
|
have_note = request.child_value("have_note")
|
|
if have_note is not None:
|
|
newprofile.replace_int_array(
|
|
"have_note", 512, [1 if x else 0 for x in have_note]
|
|
)
|
|
|
|
# Grab last information.
|
|
lastdict = newprofile.get_dict("last")
|
|
lastdict.replace_int("headphone", request.child_value("headphone"))
|
|
lastdict.replace_int("hispeed", request.child_value("hispeed"))
|
|
lastdict.replace_int("appeal_id", request.child_value("appeal_id"))
|
|
lastdict.replace_int("frame0", request.child_value("frame0"))
|
|
lastdict.replace_int("frame1", request.child_value("frame1"))
|
|
lastdict.replace_int("frame2", request.child_value("frame2"))
|
|
lastdict.replace_int("frame3", request.child_value("frame3"))
|
|
lastdict.replace_int("frame4", request.child_value("frame4"))
|
|
last = request.child("last")
|
|
if last is not None:
|
|
lastdict.replace_int("music_id", intish(last.attribute("music_id")))
|
|
lastdict.replace_int("music_type", intish(last.attribute("music_type")))
|
|
lastdict.replace_int("sort_type", intish(last.attribute("sort_type")))
|
|
|
|
# Save back last information gleaned from results
|
|
newprofile.replace_dict("last", lastdict)
|
|
|
|
# Keep track of play statistics
|
|
self.update_play_statistics(userid)
|
|
|
|
return newprofile
|