1
0
mirror of synced 2024-12-25 04:04:51 +01:00
bemaniutils/bemani/backend/sdvx/gravitywars.py

554 lines
20 KiB
Python

# vim: set fileencoding=utf-8
from typing import Any, Dict, List, Optional
from typing_extensions import Final
from bemani.backend.ess import EventLogHandler
from bemani.backend.sdvx.base import SoundVoltexBase
from bemani.backend.sdvx.infiniteinfection import SoundVoltexInfiniteInfection
from bemani.common import ID, VersionConstants
from bemani.protocol import Node
class SoundVoltexGravityWars(
EventLogHandler,
SoundVoltexBase,
):
name: str = "SOUND VOLTEX III GRAVITY WARS"
version: int = VersionConstants.SDVX_GRAVITY_WARS
GAME_LIMITED_LOCKED: Final[int] = 1
GAME_LIMITED_UNLOCKABLE: Final[int] = 2
GAME_LIMITED_UNLOCKED: Final[int] = 3
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_HARD_CLEAR: Final[int] = 3
GAME_CLEAR_TYPE_ULTIMATE_CHAIN: Final[int] = 4
GAME_CLEAR_TYPE_PERFECT_ULTIMATE_CHAIN: Final[int] = 5
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
GAME_CATALOG_TYPE_SONG: Final[int] = 0
GAME_CATALOG_TYPE_APPEAL_CARD: Final[int] = 1
GAME_CATALOG_TYPE_CREW: Final[int] = 4
GAME_GAUGE_TYPE_SKILL: Final[int] = 1
@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",
},
{
"name": "Force Crew Card Unlock",
"tip": "Force unlock all crew and subcrew cards.",
"category": "game_config",
"setting": "force_unlock_crew",
},
],
}
def previous_version(self) -> Optional[SoundVoltexBase]:
return SoundVoltexInfiniteInfection(self.data, self.config, self.model)
def _get_skill_analyzer_courses(self) -> List[Dict[str, Any]]:
# This is overridden in S1/S2 code.
return []
def _get_skill_analyzer_seasons(self) -> Dict[int, str]:
# This is overridden in S1/S2 code.
return {}
def _get_extra_events(self) -> List[int]:
# This is overridden in S1/S2 code.
return []
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_HARD_CLEAR: self.CLEAR_TYPE_HARD_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_HARD_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 __get_skill_analyzer_skill_levels(self) -> Dict[int, str]:
return {
0: "Skill LEVEL 01 岳翔",
1: "Skill LEVEL 02 流星",
2: "Skill LEVEL 03 月衝",
3: "Skill LEVEL 04 瞬光",
4: "Skill LEVEL 05 天極",
5: "Skill LEVEL 06 烈風",
6: "Skill LEVEL 07 雷電",
7: "Skill LEVEL 08 麗華",
8: "Skill LEVEL 09 魔騎士",
9: "Skill LEVEL 10 剛力羅",
10: "Skill LEVEL 11 或帝滅斗",
11: "Skill LEVEL ∞(12) 暴龍天",
}
def handle_game_3_common_request(self, request: Node) -> Node:
game = Node.void("game_3")
limited = Node.void("music_limited")
game.add_child(limited)
# Song unlock config
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") in (
self.GAME_LIMITED_LOCKED,
self.GAME_LIMITED_UNLOCKABLE,
):
ids.add((song.id, song.chart))
for (songid, chart) in ids:
info = Node.void("info")
limited.add_child(info)
info.add_child(Node.s32("music_id", songid))
info.add_child(Node.u8("music_type", chart))
info.add_child(Node.u8("limited", self.GAME_LIMITED_UNLOCKED))
# Event config
event = Node.void("event")
game.add_child(event)
def enable_event(eid: int) -> None:
evt = Node.void("info")
event.add_child(evt)
evt.add_child(Node.u32("event_id", eid))
if not game_config.get_bool("disable_matching"):
enable_event(1) # Matching enabled
enable_event(2) # Floor Infection
enable_event(3) # Policy Break
enable_event(60) # BEMANI Summer Diary
for eventid in self._get_extra_events():
enable_event(eventid)
# Skill Analyzer config
skill_course = Node.void("skill_course")
game.add_child(skill_course)
seasons = self._get_skill_analyzer_seasons()
skillnames = self.__get_skill_analyzer_skill_levels()
courses = self._get_skill_analyzer_courses()
max_level: Dict[int, int] = {}
for course in courses:
max_level[course["level"]] = max(
course["season_id"], max_level.get(course["level"], -1)
)
for course in courses:
info = Node.void("info")
skill_course.add_child(info)
info.add_child(Node.s16("course_id", course.get("id", course["level"])))
info.add_child(Node.s16("level", course["level"]))
info.add_child(Node.s32("season_id", course["season_id"]))
info.add_child(Node.string("season_name", seasons[course["season_id"]]))
info.add_child(
Node.bool(
"season_new_flg", max_level[course["level"]] == course["season_id"]
)
)
info.add_child(
Node.string(
"course_name",
course.get("skill_name", skillnames.get(course["level"], "")),
)
)
info.add_child(Node.s16("course_type", 0))
info.add_child(
Node.s16("skill_name_id", course.get("skill_name_id", course["level"]))
)
info.add_child(
Node.bool(
"matching_assist", course["level"] >= 0 and course["level"] <= 6
)
)
info.add_child(Node.s16("gauge_type", self.GAME_GAUGE_TYPE_SKILL))
info.add_child(Node.s16("paseli_type", 0))
for trackno, trackdata in enumerate(course["tracks"]):
track = Node.void("track")
info.add_child(track)
track.add_child(Node.s16("track_no", trackno))
track.add_child(Node.s32("music_id", trackdata["id"]))
track.add_child(Node.s8("music_type", trackdata["type"]))
return game
def handle_game_3_exception_request(self, request: Node) -> Node:
return Node.void("game_3")
def handle_game_3_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_3")
game.add_child(Node.u32("nxt_time", 1000 * 5 * 60))
return game
def handle_game_3_lounge_request(self, request: Node) -> Node:
game = Node.void("game_3")
# Refresh interval in seconds.
game.add_child(Node.u32("interval", 10))
return game
def handle_game_3_entry_s_request(self, request: Node) -> Node:
game = Node.void("game_3")
# 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_3_entry_e_request(self, request: Node) -> Node:
# Lobby destroy method, eid node (u32) should be used
# to destroy any open lobbies.
return Node.void("game_3")
def handle_game_3_frozen_request(self, request: Node) -> Node:
game = Node.void("game_3")
game.add_child(Node.u8("result", 0))
return game
def handle_game_3_save_e_request(self, request: Node) -> Node:
# This has to do with Policy Break against ReflecBeat and
# floor infection, but we don't implement multi-game support so meh.
return Node.void("game_3")
def handle_game_3_play_e_request(self, request: Node) -> Node:
return Node.void("game_3")
def handle_game_3_buy_request(self, request: Node) -> Node:
refid = request.child_value("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)
currency_type = request.child_value("currency_type")
price = request.child_value("item/price")
if isinstance(price, list):
# Sometimes we end up buying more than one item at once
price = sum(price)
if currency_type == self.GAME_CURRENCY_PACKETS:
# This is a valid purchase
newpacket = packet - price
if newpacket < 0:
result = 1
else:
packet = newpacket
result = 0
elif currency_type == self.GAME_CURRENCY_BLOCKS:
# This is a valid purchase
newblock = block - price
if newblock < 0:
result = 1
else:
block = newblock
result = 0
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)
# If this was a song unlock, we should mark it as unlocked
item_type = request.child_value("item/item_type")
item_id = request.child_value("item/item_id")
param = request.child_value("item/param")
if not isinstance(item_type, list):
# Sometimes we buy multiple things at once. Make it easier by always assuming this.
item_type = [item_type]
item_id = [item_id]
param = [param]
for i in range(len(item_type)):
self.data.local.user.put_achievement(
self.game,
self.version,
userid,
item_id[i],
f"item_{item_type[i]}",
{
"param": param[i],
},
)
else:
# Unclear what to do here, return a bad response
packet = 0
block = 0
result = 1
game = Node.void("game_3")
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 handle_game_3_new_request(self, request: Node) -> Node:
refid = request.child_value("refid")
name = request.child_value("name")
loc = ID.parse_machine_id(request.child_value("locid"))
self.new_profile_by_refid(refid, name, loc)
root = Node.void("game_3")
return root
def handle_game_3_load_request(self, request: Node) -> Node:
refid = request.child_value("refid")
root = self.get_profile_by_refid(refid)
if root is not None:
return root
# Figure out if this user has an older profile or not
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
if userid is not None:
previous_game = self.previous_version()
else:
previous_game = None
if previous_game is not None:
profile = previous_game.get_profile(userid)
else:
profile = None
if profile is not None:
root = Node.void("game_3")
root.add_child(Node.u8("result", 2))
root.add_child(Node.string("name", profile.get_str("name")))
return root
else:
root = Node.void("game_3")
root.add_child(Node.u8("result", 1))
return root
def handle_game_3_save_request(self, request: Node) -> Node:
refid = request.child_value("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_3")
def handle_game_3_load_m_request(self, request: Node) -> Node:
refid = request.child_value("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 = []
# Output to the game
game = Node.void("game_3")
new = Node.void("new")
game.add_child(new)
for score in scores:
music = Node.void("music")
new.add_child(music)
music.add_child(Node.u32("music_id", score.id))
music.add_child(Node.u32("music_type", score.chart))
music.add_child(Node.u32("score", score.points))
music.add_child(Node.u32("cnt", score.plays))
music.add_child(
Node.u32(
"clear_type",
self.__db_to_game_clear_type(score.data.get_int("clear_type")),
)
)
music.add_child(
Node.u32(
"score_grade", self.__db_to_game_grade(score.data.get_int("grade"))
)
)
stats = score.data.get_dict("stats")
music.add_child(Node.u32("btn_rate", stats.get_int("btn_rate")))
music.add_child(Node.u32("long_rate", stats.get_int("long_rate")))
music.add_child(Node.u32("vol_rate", stats.get_int("vol_rate")))
return game
def handle_game_3_save_m_request(self, request: Node) -> Node:
refid = request.child_value("refid")
if refid is not None:
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
else:
userid = None
# Doesn't matter if userid is None here, that's an anonymous score
musicid = request.child_value("music_id")
chart = request.child_value("music_type")
points = request.child_value("score")
combo = request.child_value("max_chain")
clear_type = self.__game_to_db_clear_type(request.child_value("clear_type"))
grade = self.__game_to_db_grade(request.child_value("score_grade"))
stats = {
"btn_rate": request.child_value("btn_rate"),
"long_rate": request.child_value("long_rate"),
"vol_rate": request.child_value("vol_rate"),
"critical": request.child_value("critical"),
"near": request.child_value("near"),
"error": request.child_value("error"),
}
# Save the score
self.update_score(
userid,
musicid,
chart,
points,
clear_type,
grade,
combo,
stats,
)
# Return a blank response
return Node.void("game_3")
def handle_game_3_save_c_request(self, request: Node) -> Node:
refid = request.child_value("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:
course_id = request.child_value("crsid")
clear_type = request.child_value("ct")
achievement_rate = request.child_value("ar")
season_id = request.child_value("ssnid")
# Do not update the course achievement when old achievement rate is greater.
old = self.data.local.user.get_achievement(
self.game, self.version, userid, (season_id * 100) + course_id, "course"
)
if old is not None and old.get_int("achievement_rate") > achievement_rate:
return Node.void("game_3")
self.data.local.user.put_achievement(
self.game,
self.version,
userid,
(season_id * 100) + course_id,
"course",
{
"clear_type": clear_type,
"achievement_rate": achievement_rate,
},
)
# Return a blank response
return Node.void("game_3")