Initial hacky implementation of SuperNova 2 support
Features: - e-Amuse card registration and login - Last song selection storage - Options storage - Score storage - Calorie storage - Events ToDo: - Expose functionality to frontend UI config (calorie display, events) - Identify why calorie values are not loading - Identify why last song not storing correctly for sn2 category - Doubles mode and course mode logic
This commit is contained in:
parent
223c93874c
commit
208c45f8f7
@ -127,6 +127,13 @@ class DDRBase(CoreHandler, CardManagerHandler, PASELIHandler, Base):
|
|||||||
"""
|
"""
|
||||||
return Node.void("game")
|
return Node.void("game")
|
||||||
|
|
||||||
|
def format_profile_part(self, userid: UserID, profile: Profile, part: str) -> Node:
|
||||||
|
"""
|
||||||
|
Base handler for a profile. Given a userid and a profile dictionary,
|
||||||
|
return a Node representing a profile. Should be overridden.
|
||||||
|
"""
|
||||||
|
return Node.void("game")
|
||||||
|
|
||||||
def format_scores(self, userid: UserID, profile: Profile, scores: List[Score]) -> Node:
|
def format_scores(self, userid: UserID, profile: Profile, scores: List[Score]) -> Node:
|
||||||
"""
|
"""
|
||||||
Base handler for a score list. Given a userid, profile and a score list,
|
Base handler for a score list. Given a userid, profile and a score list,
|
||||||
@ -142,6 +149,24 @@ class DDRBase(CoreHandler, CardManagerHandler, PASELIHandler, Base):
|
|||||||
"""
|
"""
|
||||||
return oldprofile
|
return oldprofile
|
||||||
|
|
||||||
|
def get_profile_by_refid_and_part(self, refid: Optional[str], part: Optional[str]) -> Optional[Node]:
|
||||||
|
"""
|
||||||
|
Given a RefID, return a formatted profile node. Basically every game
|
||||||
|
needs a profile lookup, even if it handles where that happens in
|
||||||
|
a different request. This is provided for code deduplication.
|
||||||
|
"""
|
||||||
|
if refid is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# First try to load the actual profile
|
||||||
|
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
|
||||||
|
profile = self.get_profile(userid)
|
||||||
|
if profile is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Now, return it
|
||||||
|
return self.format_profile_part(userid, profile, part)
|
||||||
|
|
||||||
def get_profile_by_refid(self, refid: Optional[str]) -> Optional[Node]:
|
def get_profile_by_refid(self, refid: Optional[str]) -> Optional[Node]:
|
||||||
"""
|
"""
|
||||||
Given a RefID, return a formatted profile node. Basically every game
|
Given a RefID, return a formatted profile node. Basically every game
|
||||||
|
3
bemani/backend/ddr/ddrsn2/__init__.py
Normal file
3
bemani/backend/ddr/ddrsn2/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from bemani.backend.ddr.ddrsn2.ddrsn2 import DDRSuperNova2
|
||||||
|
|
||||||
|
__all__ = ["DDRSuperNova2"]
|
26
bemani/backend/ddr/ddrsn2/battlerecord.py
Normal file
26
bemani/backend/ddr/ddrsn2/battlerecord.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import logging
|
||||||
|
from ctypes import *
|
||||||
|
|
||||||
|
|
||||||
|
class BattleRecordStruct(Structure):
|
||||||
|
_pack_ = 1
|
||||||
|
_fields_ = [
|
||||||
|
("name", c_char * 14),
|
||||||
|
("wins", c_uint16),
|
||||||
|
("loses", c_uint16),
|
||||||
|
("draws", c_uint16),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class BattleRecord:
|
||||||
|
@staticmethod
|
||||||
|
def create(name: str, wins: int, losses: int, draws: int) -> BattleRecordStruct:
|
||||||
|
this_name = name
|
||||||
|
if len(this_name) > 8:
|
||||||
|
this_name = this_name[:8]
|
||||||
|
logging.warning("name {} too long, truncating to {}", name, this_name)
|
||||||
|
elif len(this_name) < 8:
|
||||||
|
logging.warning("name too short, padding with spaces")
|
||||||
|
this_name = this_name.ljust(8, " ")
|
||||||
|
|
||||||
|
return BattleRecordStruct(this_name.encode("ascii"), wins, losses, draws)
|
566
bemani/backend/ddr/ddrsn2/ddrsn2.py
Normal file
566
bemani/backend/ddr/ddrsn2/ddrsn2.py
Normal file
@ -0,0 +1,566 @@
|
|||||||
|
# vim: set fileencoding=utf-8
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import html
|
||||||
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
from typing_extensions import Final
|
||||||
|
|
||||||
|
from bemani.backend.ddr.base import DDRBase
|
||||||
|
from bemani.backend.ddr.ddrsn2.eventinfo import EventInfo
|
||||||
|
from bemani.backend.ddr.ddrsn2.playerinfo import PlayerInfo
|
||||||
|
from bemani.backend.ddr.ddrsn2.scoreinfo import ScoreInfo
|
||||||
|
from bemani.backend.ddr.stubs import DDRSuperNova
|
||||||
|
from bemani.backend.ddr.common import (
|
||||||
|
DDRGameFriendHandler,
|
||||||
|
DDRGameLockHandler,
|
||||||
|
DDRGameLoadCourseHandler,
|
||||||
|
DDRGameLoadHandler,
|
||||||
|
DDRGameLogHandler,
|
||||||
|
DDRGameMessageHandler,
|
||||||
|
DDRGameNewHandler,
|
||||||
|
DDRGameOldHandler,
|
||||||
|
DDRGameRankingHandler,
|
||||||
|
DDRGameSaveCourseHandler,
|
||||||
|
DDRGameSaveHandler,
|
||||||
|
DDRGameScoreHandler,
|
||||||
|
DDRGameShopHandler,
|
||||||
|
DDRGameTraceHandler,
|
||||||
|
)
|
||||||
|
from bemani.common import Time, VersionConstants, Profile, intish
|
||||||
|
from bemani.data import Score, UserID
|
||||||
|
from bemani.protocol import Node
|
||||||
|
from bemani.backend.base import Status
|
||||||
|
|
||||||
|
|
||||||
|
class DDRSuperNova2(
|
||||||
|
DDRGameFriendHandler,
|
||||||
|
DDRGameLockHandler,
|
||||||
|
DDRGameLoadCourseHandler,
|
||||||
|
DDRGameLoadHandler,
|
||||||
|
DDRGameLogHandler,
|
||||||
|
DDRGameMessageHandler,
|
||||||
|
DDRGameOldHandler,
|
||||||
|
DDRGameNewHandler,
|
||||||
|
DDRGameRankingHandler,
|
||||||
|
DDRGameSaveCourseHandler,
|
||||||
|
DDRGameSaveHandler,
|
||||||
|
DDRGameScoreHandler,
|
||||||
|
DDRGameShopHandler,
|
||||||
|
DDRGameTraceHandler,
|
||||||
|
DDRBase,
|
||||||
|
):
|
||||||
|
name: str = "DanceDanceRevolution SuperNova 2"
|
||||||
|
version: int = VersionConstants.DDR_SUPERNOVA_2
|
||||||
|
|
||||||
|
GAME_STYLE_SINGLE: Final[int] = 0
|
||||||
|
GAME_STYLE_DOUBLE: Final[int] = 1
|
||||||
|
GAME_STYLE_VERSUS: Final[int] = 2
|
||||||
|
|
||||||
|
GAME_RANK_AAA: Final[int] = 0
|
||||||
|
GAME_RANK_AA: Final[int] = 1
|
||||||
|
GAME_RANK_A: Final[int] = 2
|
||||||
|
GAME_RANK_B: Final[int] = 3
|
||||||
|
GAME_RANK_C: Final[int] = 4
|
||||||
|
GAME_RANK_D: Final[int] = 5
|
||||||
|
GAME_RANK_E: Final[int] = 6
|
||||||
|
|
||||||
|
GAME_CHART_SINGLE_BEGINNER: Final[int] = 0
|
||||||
|
GAME_CHART_SINGLE_BASIC: Final[int] = 1
|
||||||
|
GAME_CHART_SINGLE_DIFFICULT: Final[int] = 2
|
||||||
|
GAME_CHART_SINGLE_EXPERT: Final[int] = 3
|
||||||
|
GAME_CHART_SINGLE_CHALLENGE: Final[int] = 4
|
||||||
|
GAME_CHART_DOUBLE_BASIC: Final[int] = 5
|
||||||
|
GAME_CHART_DOUBLE_DIFFICULT: Final[int] = 6
|
||||||
|
GAME_CHART_DOUBLE_EXPERT: Final[int] = 7
|
||||||
|
GAME_CHART_DOUBLE_CHALLENGE: Final[int] = 8
|
||||||
|
|
||||||
|
GAME_HALO_NONE: Final[int] = 0
|
||||||
|
GAME_HALO_FULL_COMBO: Final[int] = 1
|
||||||
|
GAME_HALO_PERFECT_COMBO: Final[int] = 2
|
||||||
|
GAME_HALO_MARVELOUS_COMBO: Final[int] = 3
|
||||||
|
|
||||||
|
GAME_MAX_SONGS: Final[int] = 600
|
||||||
|
|
||||||
|
def previous_version(self) -> Optional[DDRBase]:
|
||||||
|
return DDRSuperNova(self.data, self.config, self.model)
|
||||||
|
|
||||||
|
def game_to_db_rank(self, game_rank: int) -> int:
|
||||||
|
return {
|
||||||
|
self.GAME_RANK_AAA: self.RANK_AAA,
|
||||||
|
self.GAME_RANK_AA: self.RANK_AA,
|
||||||
|
self.GAME_RANK_A: self.RANK_A,
|
||||||
|
self.GAME_RANK_B: self.RANK_B,
|
||||||
|
self.GAME_RANK_C: self.RANK_C,
|
||||||
|
self.GAME_RANK_D: self.RANK_D,
|
||||||
|
self.GAME_RANK_E: self.RANK_E,
|
||||||
|
}[game_rank]
|
||||||
|
|
||||||
|
def db_to_game_rank(self, db_rank: int) -> int:
|
||||||
|
return {
|
||||||
|
self.RANK_AAA: self.GAME_RANK_AAA,
|
||||||
|
self.RANK_AA_PLUS: self.GAME_RANK_AA,
|
||||||
|
self.RANK_AA: self.GAME_RANK_AA,
|
||||||
|
self.RANK_AA_MINUS: self.GAME_RANK_A,
|
||||||
|
self.RANK_A_PLUS: self.GAME_RANK_A,
|
||||||
|
self.RANK_A: self.GAME_RANK_A,
|
||||||
|
self.RANK_A_MINUS: self.GAME_RANK_B,
|
||||||
|
self.RANK_B_PLUS: self.GAME_RANK_B,
|
||||||
|
self.RANK_B: self.GAME_RANK_B,
|
||||||
|
self.RANK_B_MINUS: self.GAME_RANK_C,
|
||||||
|
self.RANK_C_PLUS: self.GAME_RANK_C,
|
||||||
|
self.RANK_C: self.GAME_RANK_C,
|
||||||
|
self.RANK_C_MINUS: self.GAME_RANK_D,
|
||||||
|
self.RANK_D_PLUS: self.GAME_RANK_D,
|
||||||
|
self.RANK_D: self.GAME_RANK_D,
|
||||||
|
self.RANK_E: self.GAME_RANK_E,
|
||||||
|
}[db_rank]
|
||||||
|
|
||||||
|
def game_to_db_chart(self, game_chart: int) -> int:
|
||||||
|
return {
|
||||||
|
self.GAME_CHART_SINGLE_BEGINNER: self.CHART_SINGLE_BEGINNER,
|
||||||
|
self.GAME_CHART_SINGLE_BASIC: self.CHART_SINGLE_BASIC,
|
||||||
|
self.GAME_CHART_SINGLE_DIFFICULT: self.CHART_SINGLE_DIFFICULT,
|
||||||
|
self.GAME_CHART_SINGLE_EXPERT: self.CHART_SINGLE_EXPERT,
|
||||||
|
self.GAME_CHART_SINGLE_CHALLENGE: self.CHART_SINGLE_CHALLENGE,
|
||||||
|
self.GAME_CHART_DOUBLE_BASIC: self.CHART_DOUBLE_BASIC,
|
||||||
|
self.GAME_CHART_DOUBLE_DIFFICULT: self.CHART_DOUBLE_DIFFICULT,
|
||||||
|
self.GAME_CHART_DOUBLE_EXPERT: self.CHART_DOUBLE_EXPERT,
|
||||||
|
self.GAME_CHART_DOUBLE_CHALLENGE: self.CHART_DOUBLE_CHALLENGE,
|
||||||
|
}[game_chart]
|
||||||
|
|
||||||
|
def db_to_game_chart(self, db_chart: int) -> int:
|
||||||
|
return {
|
||||||
|
self.CHART_SINGLE_BEGINNER: self.GAME_CHART_SINGLE_BEGINNER,
|
||||||
|
self.CHART_SINGLE_BASIC: self.GAME_CHART_SINGLE_BASIC,
|
||||||
|
self.CHART_SINGLE_DIFFICULT: self.GAME_CHART_SINGLE_DIFFICULT,
|
||||||
|
self.CHART_SINGLE_EXPERT: self.GAME_CHART_SINGLE_EXPERT,
|
||||||
|
self.CHART_SINGLE_CHALLENGE: self.GAME_CHART_SINGLE_CHALLENGE,
|
||||||
|
self.CHART_DOUBLE_BASIC: self.GAME_CHART_DOUBLE_BASIC,
|
||||||
|
self.CHART_DOUBLE_DIFFICULT: self.GAME_CHART_DOUBLE_DIFFICULT,
|
||||||
|
self.CHART_DOUBLE_EXPERT: self.GAME_CHART_DOUBLE_EXPERT,
|
||||||
|
self.CHART_DOUBLE_CHALLENGE: self.GAME_CHART_DOUBLE_CHALLENGE,
|
||||||
|
}[db_chart]
|
||||||
|
|
||||||
|
def db_to_game_halo(self, db_halo: int) -> int:
|
||||||
|
if db_halo == self.HALO_MARVELOUS_FULL_COMBO:
|
||||||
|
combo_type = self.GAME_HALO_MARVELOUS_COMBO
|
||||||
|
elif db_halo == self.HALO_PERFECT_FULL_COMBO:
|
||||||
|
combo_type = self.GAME_HALO_PERFECT_COMBO
|
||||||
|
elif db_halo == self.HALO_GREAT_FULL_COMBO:
|
||||||
|
combo_type = self.GAME_HALO_FULL_COMBO
|
||||||
|
else:
|
||||||
|
combo_type = self.GAME_HALO_NONE
|
||||||
|
return combo_type
|
||||||
|
|
||||||
|
def sn2_to_db_halo(self, full: int, perf: int) -> int:
|
||||||
|
if full == 1 and perf == 1:
|
||||||
|
combo_type = self.HALO_PERFECT_FULL_COMBO
|
||||||
|
elif full == 1:
|
||||||
|
combo_type = self.HALO_GREAT_FULL_COMBO
|
||||||
|
else:
|
||||||
|
combo_type = self.HALO_NONE
|
||||||
|
return combo_type
|
||||||
|
|
||||||
|
def handle_game_common_request(self, request: Node) -> Node:
|
||||||
|
game = Node.void("game")
|
||||||
|
for flagid in range(256):
|
||||||
|
flag = Node.void("flag")
|
||||||
|
game.add_child(flag)
|
||||||
|
|
||||||
|
flag.set_attribute("id", str(flagid))
|
||||||
|
flag.set_attribute("s2", "0")
|
||||||
|
flag.set_attribute("s1", "0")
|
||||||
|
flag.set_attribute("t", "0")
|
||||||
|
|
||||||
|
hit_chart = self.data.local.music.get_hit_chart(self.game, self.music_version, self.GAME_MAX_SONGS)
|
||||||
|
counts_by_reflink = [0] * self.GAME_MAX_SONGS
|
||||||
|
for reflink, plays in hit_chart:
|
||||||
|
if reflink >= 0 and reflink < self.GAME_MAX_SONGS:
|
||||||
|
counts_by_reflink[reflink] = plays
|
||||||
|
game.add_child(Node.u32_array("cnt_music", counts_by_reflink))
|
||||||
|
|
||||||
|
return game
|
||||||
|
|
||||||
|
def handle_game_hiscore_request(self, request: Node) -> Node:
|
||||||
|
# This is almost identical to X3 and above, except X3 added a 'code' field
|
||||||
|
# that isn't present here. In the interest of correctness, keep a separate
|
||||||
|
# implementation here.
|
||||||
|
records = self.data.remote.music.get_all_records(self.game, self.music_version)
|
||||||
|
|
||||||
|
sortedrecords: Dict[int, Dict[int, Tuple[UserID, Score]]] = {}
|
||||||
|
missing_profiles = []
|
||||||
|
for userid, score in records:
|
||||||
|
if score.id not in sortedrecords:
|
||||||
|
sortedrecords[score.id] = {}
|
||||||
|
sortedrecords[score.id][score.chart] = (userid, score)
|
||||||
|
missing_profiles.append(userid)
|
||||||
|
users = {userid: profile for (userid, profile) in self.get_any_profiles(missing_profiles)}
|
||||||
|
|
||||||
|
game = Node.void("game")
|
||||||
|
for song in sortedrecords:
|
||||||
|
music = Node.void("music")
|
||||||
|
game.add_child(music)
|
||||||
|
music.set_attribute("reclink_num", str(song))
|
||||||
|
|
||||||
|
for chart in sortedrecords[song]:
|
||||||
|
userid, score = sortedrecords[song][chart]
|
||||||
|
try:
|
||||||
|
gamechart = self.db_to_game_chart(chart)
|
||||||
|
except KeyError:
|
||||||
|
# Don't support this chart in this game
|
||||||
|
continue
|
||||||
|
gamerank = self.db_to_game_rank(score.data.get_int("rank"))
|
||||||
|
combo_type = self.db_to_game_halo(score.data.get_int("halo"))
|
||||||
|
|
||||||
|
typenode = Node.void("type")
|
||||||
|
music.add_child(typenode)
|
||||||
|
typenode.set_attribute("diff", str(gamechart))
|
||||||
|
|
||||||
|
typenode.add_child(Node.string("name", users[userid].get_str("name")))
|
||||||
|
typenode.add_child(Node.u32("score", score.points))
|
||||||
|
typenode.add_child(Node.u16("area", users[userid].get_int("area", self.get_machine_region())))
|
||||||
|
typenode.add_child(Node.u8("rank", gamerank))
|
||||||
|
typenode.add_child(Node.u8("combo_type", combo_type))
|
||||||
|
|
||||||
|
return game
|
||||||
|
|
||||||
|
def handle_game_load_m_request(self, request: Node) -> Node:
|
||||||
|
extid = intish(request.attribute("code"))
|
||||||
|
refid = request.attribute("refid")
|
||||||
|
|
||||||
|
if extid is not None:
|
||||||
|
# Rival score loading
|
||||||
|
userid = self.data.remote.user.from_extid(self.game, self.version, extid)
|
||||||
|
else:
|
||||||
|
# Self score loading
|
||||||
|
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
|
||||||
|
|
||||||
|
if userid is not None:
|
||||||
|
scores = self.data.remote.music.get_scores(self.game, self.music_version, userid)
|
||||||
|
else:
|
||||||
|
scores = []
|
||||||
|
|
||||||
|
sortedscores: Dict[int, Dict[int, Score]] = {}
|
||||||
|
for score in scores:
|
||||||
|
if score.id not in sortedscores:
|
||||||
|
sortedscores[score.id] = {}
|
||||||
|
sortedscores[score.id][score.chart] = score
|
||||||
|
|
||||||
|
game = Node.void("game")
|
||||||
|
for song in sortedscores:
|
||||||
|
music = Node.void("music")
|
||||||
|
game.add_child(music)
|
||||||
|
music.set_attribute("reclink", str(song))
|
||||||
|
|
||||||
|
for chart in sortedscores[song]:
|
||||||
|
score = sortedscores[song][chart]
|
||||||
|
try:
|
||||||
|
gamechart = self.db_to_game_chart(chart)
|
||||||
|
except KeyError:
|
||||||
|
# Don't support this chart in this game
|
||||||
|
continue
|
||||||
|
gamerank = self.db_to_game_rank(score.data.get_int("rank"))
|
||||||
|
combo_type = self.db_to_game_halo(score.data.get_int("halo"))
|
||||||
|
|
||||||
|
typenode = Node.void("type")
|
||||||
|
music.add_child(typenode)
|
||||||
|
typenode.set_attribute("diff", str(gamechart))
|
||||||
|
|
||||||
|
typenode.add_child(Node.u32("score", score.points))
|
||||||
|
typenode.add_child(Node.u16("count", score.plays))
|
||||||
|
typenode.add_child(Node.u8("rank", gamerank))
|
||||||
|
typenode.add_child(Node.u8("combo_type", combo_type))
|
||||||
|
|
||||||
|
return game
|
||||||
|
|
||||||
|
def handle_game_save_m_request(self, request: Node) -> Node:
|
||||||
|
refid = request.attribute("refid")
|
||||||
|
songid = int(request.attribute("mid"))
|
||||||
|
chart = self.game_to_db_chart(int(request.attribute("mtype")))
|
||||||
|
|
||||||
|
# Calculate statistics
|
||||||
|
data = request.child("data")
|
||||||
|
points = int(data.attribute("score"))
|
||||||
|
combo = int(data.attribute("combo"))
|
||||||
|
rank = self.game_to_db_rank(int(data.attribute("rank")))
|
||||||
|
if int(data.attribute("full")) == 0:
|
||||||
|
halo = self.HALO_NONE
|
||||||
|
elif int(data.attribute("perf")) == 0:
|
||||||
|
halo = self.HALO_GREAT_FULL_COMBO
|
||||||
|
elif points < 1000000:
|
||||||
|
halo = self.HALO_PERFECT_FULL_COMBO
|
||||||
|
else:
|
||||||
|
halo = self.HALO_MARVELOUS_FULL_COMBO
|
||||||
|
trace = request.child_value("trace")
|
||||||
|
|
||||||
|
# Save the score, regardless of whether we have a refid. If we save
|
||||||
|
# an anonymous score, it only goes into the DB to count against the
|
||||||
|
# number of plays for that song/chart.
|
||||||
|
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
|
||||||
|
self.update_score(
|
||||||
|
userid,
|
||||||
|
songid,
|
||||||
|
chart,
|
||||||
|
points,
|
||||||
|
rank,
|
||||||
|
halo,
|
||||||
|
combo,
|
||||||
|
trace,
|
||||||
|
)
|
||||||
|
|
||||||
|
# No response needed
|
||||||
|
game = Node.void("game")
|
||||||
|
return game
|
||||||
|
|
||||||
|
def format_profile_part(self, userid: UserID, profile: Profile, part: str) -> Node:
|
||||||
|
root = Node.void("player")
|
||||||
|
root.set_attribute("part", part)
|
||||||
|
|
||||||
|
# Look up play stats we bridge to every mix
|
||||||
|
play_stats = self.get_play_statistics(userid)
|
||||||
|
|
||||||
|
# Load scoring data
|
||||||
|
scores = self.data.local.music.get_scores(self.game, self.version, userid)
|
||||||
|
|
||||||
|
if part == "0":
|
||||||
|
payload = bytearray(PlayerInfo.create(play_stats, profile, self.get_machine_region()))
|
||||||
|
size = 6144
|
||||||
|
|
||||||
|
elif part == "1":
|
||||||
|
payload = bytearray(ScoreInfo.create(scores, 1))
|
||||||
|
size = 7200
|
||||||
|
|
||||||
|
else:
|
||||||
|
payload = bytearray(ScoreInfo.create(scores, 2))
|
||||||
|
size = 7200
|
||||||
|
|
||||||
|
payload += bytearray([0] * (size - len(payload)))
|
||||||
|
|
||||||
|
root.set_attribute("encode", "0")
|
||||||
|
root.set_attribute("size", str(size))
|
||||||
|
root.set_attribute("md5c", hashlib.md5(payload).hexdigest().upper())
|
||||||
|
|
||||||
|
binary = Node.string("b", base64.b64encode(payload).decode("ascii"))
|
||||||
|
root.add_child(binary)
|
||||||
|
|
||||||
|
return root
|
||||||
|
|
||||||
|
def unformat_profile(self, userid: UserID, request: Node, oldprofile: Profile) -> Profile:
|
||||||
|
newprofile = oldprofile.clone()
|
||||||
|
play_stats = self.get_play_statistics(userid)
|
||||||
|
|
||||||
|
game = request.child("game")
|
||||||
|
opt = request.child("opt")
|
||||||
|
|
||||||
|
gr = newprofile.get_int_array("gr_s", 5)
|
||||||
|
|
||||||
|
gr1 = gr[0]
|
||||||
|
gr2 = gr[1]
|
||||||
|
gr3 = gr[2]
|
||||||
|
gr4 = gr[3]
|
||||||
|
gr5 = gr[4]
|
||||||
|
|
||||||
|
if game.attribute("gr1"):
|
||||||
|
gr1 = intish(game.attribute("gr1"))
|
||||||
|
if game.attribute("gr2"):
|
||||||
|
gr2 = intish(game.attribute("gr2"))
|
||||||
|
if game.attribute("gr3"):
|
||||||
|
gr3 = intish(game.attribute("gr3"))
|
||||||
|
if game.attribute("gr4"):
|
||||||
|
gr4 = intish(game.attribute("gr4"))
|
||||||
|
if game.attribute("gr5"):
|
||||||
|
gr5 = intish(game.attribute("gr5"))
|
||||||
|
|
||||||
|
newprofile.replace_int_array("gr_s", 5, [gr1, gr2, gr3, gr4, gr5])
|
||||||
|
|
||||||
|
last = newprofile.get_dict("last")
|
||||||
|
last.replace_int("cate", intish(game.attribute("cate")))
|
||||||
|
last.replace_int("mode", intish(game.attribute("mode")))
|
||||||
|
last.replace_int("type", intish(game.attribute("type")))
|
||||||
|
last.replace_int("sort", intish(game.attribute("sort")))
|
||||||
|
last.replace_int("music", intish(game.attribute("music")))
|
||||||
|
newprofile.replace_dict("last", last)
|
||||||
|
|
||||||
|
newprofile.replace_int_array(
|
||||||
|
"opt",
|
||||||
|
16,
|
||||||
|
[
|
||||||
|
intish(opt.attribute("op1")),
|
||||||
|
intish(opt.attribute("op2")),
|
||||||
|
intish(opt.attribute("op3")),
|
||||||
|
intish(opt.attribute("op4")),
|
||||||
|
intish(opt.attribute("op5")),
|
||||||
|
intish(opt.attribute("op6")),
|
||||||
|
intish(opt.attribute("op7")),
|
||||||
|
intish(opt.attribute("op8")),
|
||||||
|
intish(opt.attribute("op9")),
|
||||||
|
intish(opt.attribute("op10")),
|
||||||
|
intish(opt.attribute("op11")),
|
||||||
|
intish(opt.attribute("op12")),
|
||||||
|
intish(opt.attribute("op13")),
|
||||||
|
intish(opt.attribute("op14")),
|
||||||
|
intish(opt.attribute("op15")),
|
||||||
|
intish(opt.attribute("op16")),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
play_stats.increment_int("single_plays")
|
||||||
|
play_stats.increment_int(f"cnt_m{game.attribute('mode')}")
|
||||||
|
play_stats.replace_int("exp", play_stats.get_int("exp") + intish(game.attribute("exp")))
|
||||||
|
|
||||||
|
if game.attribute("weight"):
|
||||||
|
newprofile.replace_int("weight", intish(game.attribute("weight")))
|
||||||
|
|
||||||
|
if game.attribute("calory"):
|
||||||
|
if game.attribute("weight"):
|
||||||
|
weight = intish(game.attribute("weight"))
|
||||||
|
else:
|
||||||
|
weight = newprofile.get_int("weight", 0)
|
||||||
|
self.data.local.user.put_time_based_achievement(
|
||||||
|
self.game,
|
||||||
|
self.version,
|
||||||
|
userid,
|
||||||
|
0,
|
||||||
|
"workout",
|
||||||
|
{
|
||||||
|
"calories": intish(game.attribute("calory")),
|
||||||
|
"weight": weight,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update game flags
|
||||||
|
for child in request.children:
|
||||||
|
if child.name != "flag":
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
value = intish(child.attribute("data"))
|
||||||
|
offset = intish(child.attribute("off"))
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
flags = newprofile.get_int_array("flag", 256, [1] * 256)
|
||||||
|
if offset < 0 or offset >= len(flags):
|
||||||
|
continue
|
||||||
|
flags[offset] = value
|
||||||
|
newprofile.replace_int_array("flag", 256, flags)
|
||||||
|
|
||||||
|
# Keep track of play statistics
|
||||||
|
self.update_play_statistics(userid, play_stats)
|
||||||
|
|
||||||
|
# Store song score data
|
||||||
|
for child in request.children:
|
||||||
|
if child.name != "music":
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
combo = intish(child.attribute("combo"))
|
||||||
|
full = intish(child.attribute("full"))
|
||||||
|
id = intish(child.attribute("id"))
|
||||||
|
mode = intish(child.attribute("mode"))
|
||||||
|
perf = intish(child.attribute("perf"))
|
||||||
|
rank = intish(child.attribute("rank"))
|
||||||
|
score = intish(child.attribute("score"))
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.update_score(
|
||||||
|
userid,
|
||||||
|
id,
|
||||||
|
self.game_to_db_chart(mode),
|
||||||
|
score * 10,
|
||||||
|
self.game_to_db_rank(rank),
|
||||||
|
self.sn2_to_db_halo(full, perf),
|
||||||
|
combo,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Event Team Data
|
||||||
|
if game.attribute("team_i"):
|
||||||
|
newprofile.replace_int("team", intish(game.attribute("team_i")))
|
||||||
|
newprofile.replace_int(
|
||||||
|
"team_points", intish(game.attribute("team_p")) + newprofile.get_int("team_points", 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
return newprofile
|
||||||
|
|
||||||
|
def handle_player_new_request(self, request: Node) -> Node:
|
||||||
|
refid = request.attribute("ref_id")
|
||||||
|
name = request.attribute("name")
|
||||||
|
area = intish(request.attribute("area"))
|
||||||
|
|
||||||
|
formatted_name = html.unescape(name)
|
||||||
|
self.new_profile_by_refid(refid, formatted_name, area)
|
||||||
|
|
||||||
|
root = Node.void("player")
|
||||||
|
root.add_child(Node.s8("result", 2))
|
||||||
|
|
||||||
|
return root
|
||||||
|
|
||||||
|
def handle_player_get_request(self, request: Node) -> Node:
|
||||||
|
refid = request.attribute("ref_id")
|
||||||
|
part = request.attribute("part")
|
||||||
|
|
||||||
|
root = self.get_profile_by_refid_and_part(refid, part)
|
||||||
|
if root is None:
|
||||||
|
root = Node.void("player")
|
||||||
|
root.set_attribute("new", "1")
|
||||||
|
return root
|
||||||
|
|
||||||
|
def handle_player_set_request(self, request: Node) -> Node:
|
||||||
|
refid = request.attribute("ref_id")
|
||||||
|
|
||||||
|
self.put_profile_by_refid(refid, request)
|
||||||
|
|
||||||
|
root = Node.void("player")
|
||||||
|
root.add_child(Node.s8("result", 2))
|
||||||
|
|
||||||
|
return root
|
||||||
|
|
||||||
|
def handle_info_message_request(self, request: Node) -> Node:
|
||||||
|
size = 5772
|
||||||
|
message = "hello world"
|
||||||
|
payload = message.encode("ascii")
|
||||||
|
|
||||||
|
payload += bytearray([0] * (size - len(payload)))
|
||||||
|
|
||||||
|
b64 = base64.b64encode(payload).decode("ascii")
|
||||||
|
|
||||||
|
root = Node.void("info")
|
||||||
|
|
||||||
|
root.set_attribute("encode", "0")
|
||||||
|
root.set_attribute("size", str(size))
|
||||||
|
root.set_attribute("md5c", hashlib.md5(payload).hexdigest().upper())
|
||||||
|
|
||||||
|
binary = Node.string("b", b64)
|
||||||
|
root.add_child(binary)
|
||||||
|
|
||||||
|
return root
|
||||||
|
|
||||||
|
def handle_player_common_request(self, request: Node) -> Node:
|
||||||
|
size = 1920
|
||||||
|
payload = bytearray(EventInfo.create())
|
||||||
|
|
||||||
|
payload += bytearray([0] * (size - len(payload)))
|
||||||
|
|
||||||
|
b64 = base64.b64encode(payload).decode("ascii")
|
||||||
|
|
||||||
|
root = Node.void("player")
|
||||||
|
|
||||||
|
root.set_attribute("encode", "0")
|
||||||
|
root.set_attribute("size", str(size))
|
||||||
|
root.set_attribute("md5c", hashlib.md5(payload).hexdigest().upper())
|
||||||
|
|
||||||
|
binary = Node.string("b", b64)
|
||||||
|
root.add_child(binary)
|
||||||
|
|
||||||
|
return root
|
||||||
|
|
||||||
|
def handle_player_touch_request(self, request: Node) -> Node:
|
||||||
|
root = Node.void("player")
|
||||||
|
|
||||||
|
root.set_attribute("id", "1234")
|
||||||
|
|
||||||
|
return root
|
||||||
|
|
||||||
|
def handle_info_tenpo_request(self, request: Node) -> Node:
|
||||||
|
root = Node.void("tenpo")
|
||||||
|
return root
|
52
bemani/backend/ddr/ddrsn2/eventinfo.py
Normal file
52
bemani/backend/ddr/ddrsn2/eventinfo.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
from ctypes import *
|
||||||
|
|
||||||
|
from bemani.backend.ddr.ddrsn2.globalrankingstateentry import GlobalRankingStateEntryStruct
|
||||||
|
from bemani.backend.ddr.ddrsn2.unlockentry import UnlockEntryStruct
|
||||||
|
from bemani.backend.ddr.ddrsn2.zukinteams import ZukinTeams
|
||||||
|
|
||||||
|
|
||||||
|
class EventInfoStruct(Structure):
|
||||||
|
_pack_ = 1
|
||||||
|
_fields_ = [
|
||||||
|
("unlocks", UnlockEntryStruct * 256),
|
||||||
|
("global_ranking_state", GlobalRankingStateEntryStruct * 8),
|
||||||
|
("_unused", c_uint8 * 4), # Unused?
|
||||||
|
("event_ver", c_uint8), # 0x624
|
||||||
|
("event_end", c_uint8), # 0x625
|
||||||
|
("event_sel_team", c_uint8), # 0x626
|
||||||
|
("event_rankings", c_uint8 * 3), # 0x627
|
||||||
|
("event_avg", c_uint16), # 0x62a
|
||||||
|
("event_score_green", c_uint32), # 0x62c
|
||||||
|
("event_score_red", c_uint32), # 0x62c
|
||||||
|
("event_score_yellow", c_uint32), # 0x62c
|
||||||
|
("event_border", c_uint16), # 0x638
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class EventInfo:
|
||||||
|
@staticmethod
|
||||||
|
def create() -> EventInfoStruct:
|
||||||
|
p = EventInfoStruct()
|
||||||
|
p.event_ver = 5 # Event episode
|
||||||
|
p.event_end = 1 # Event has ended
|
||||||
|
p.event_sel_team = ZukinTeams.RED # Winning team
|
||||||
|
p.event_rankings[0] = ZukinTeams.RED
|
||||||
|
p.event_rankings[1] = ZukinTeams.GREEN
|
||||||
|
p.event_rankings[2] = ZukinTeams.YELLOW
|
||||||
|
p.event_avg = 123 # ?
|
||||||
|
p.event_score_green = 57312
|
||||||
|
p.event_score_red = 3516541
|
||||||
|
p.event_score_yellow = 5631
|
||||||
|
p.event_border = 123
|
||||||
|
|
||||||
|
for i in range(len(p.unlocks)):
|
||||||
|
p.unlocks[i].id = i & 0xFF
|
||||||
|
p.unlocks[i].enabled = 1
|
||||||
|
|
||||||
|
for i in range(0, len(p.global_ranking_state)):
|
||||||
|
p.global_ranking_state[i].id = i & 0xFF
|
||||||
|
p.global_ranking_state[i].state = 2
|
||||||
|
|
||||||
|
p.unlocks[0].enabled = 7 # Phase unlock max
|
||||||
|
|
||||||
|
return p
|
16
bemani/backend/ddr/ddrsn2/globalrankingstateentry.py
Normal file
16
bemani/backend/ddr/ddrsn2/globalrankingstateentry.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
from ctypes import *
|
||||||
|
|
||||||
|
|
||||||
|
class GlobalRankingStateEntryStruct(Structure):
|
||||||
|
_pack_ = 1
|
||||||
|
_fields_ = [
|
||||||
|
("id", c_int8),
|
||||||
|
("state", c_int8), # 0 = disabled, 1 = English ("Global Ranking"), 2 = Japanese ("Gachinko Dance Matsuri")
|
||||||
|
("_unused", c_int8 * 2),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class GlobalRankingStateEntry:
|
||||||
|
@staticmethod
|
||||||
|
def create(id: int, state: int) -> GlobalRankingStateEntryStruct:
|
||||||
|
return GlobalRankingStateEntryStruct(id, state)
|
92
bemani/backend/ddr/ddrsn2/playerinfo.py
Normal file
92
bemani/backend/ddr/ddrsn2/playerinfo.py
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
from ctypes import *
|
||||||
|
|
||||||
|
from bemani.backend.ddr.ddrsn2.battlerecord import BattleRecordStruct
|
||||||
|
from bemani.common import Profile, PlayStatistics
|
||||||
|
|
||||||
|
|
||||||
|
class PlayerInfoStruct(Structure):
|
||||||
|
_pack_ = 1
|
||||||
|
_fields_ = [
|
||||||
|
("count", c_uint16), # 0x00
|
||||||
|
("area", c_uint16), # 0x02
|
||||||
|
("title", c_uint16), # 0x04
|
||||||
|
("flag", c_uint16), # 0x06
|
||||||
|
("id", c_uint32), # 0x08
|
||||||
|
("exp", c_uint32), # 0x0c
|
||||||
|
("weight", c_uint16), # 0x10
|
||||||
|
("last_cate", c_uint8), # 0x12
|
||||||
|
("last_mode", c_uint8), # 0x13
|
||||||
|
("last_type", c_uint8), # 0x14
|
||||||
|
("last_sort", c_uint8), # 0x15
|
||||||
|
("last_music", c_uint8), # 0x16
|
||||||
|
("_unused1", c_char * 6), # 0x17 - 0x1b
|
||||||
|
("team", c_uint8), # 0x1c
|
||||||
|
("_unused2", c_char * 8), # 0x1d - 0x24
|
||||||
|
("takeover", c_uint8), # 0x25
|
||||||
|
("count_b", c_uint8), # 0x26
|
||||||
|
("_unused3", c_char * 2), # 0x27 - 0x29
|
||||||
|
("groove_radar", c_uint16 * 5), # 0x2a
|
||||||
|
("options", c_uint8 * 32), # 0x34
|
||||||
|
("name", c_char * 14), # 0x54
|
||||||
|
("_unused4", c_char * 2), # 0x62 - 0x63
|
||||||
|
# Bit is set to 1 = already displayed, 0 = not yet displayed
|
||||||
|
("unlock_prompt_bits", c_uint8 * 12), # 0x64 - 0x70
|
||||||
|
("_unused5", c_char * 20), # 0x70 - 0x83
|
||||||
|
("course", c_uint32 * 3), # 0x84
|
||||||
|
("_unused6", c_char * (0x1344 - 0x90)),
|
||||||
|
("battle_records", BattleRecordStruct * 5), # 0x1344
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class PlayerInfo:
|
||||||
|
@staticmethod
|
||||||
|
def create(play_stats: PlayStatistics, profile: Profile, machine_region: int) -> PlayerInfoStruct:
|
||||||
|
player = PlayerInfoStruct()
|
||||||
|
|
||||||
|
player.count = play_stats.get_int("single_plays")
|
||||||
|
player.area = profile.get_int("area", machine_region)
|
||||||
|
player.title = profile.get_int("title")
|
||||||
|
player.flag = profile.get_int("flag")
|
||||||
|
player.id = profile.extid
|
||||||
|
player.exp = play_stats.get_int("exp")
|
||||||
|
player.weight = profile.get_int("weight")
|
||||||
|
|
||||||
|
lastdict = profile.get_dict("last")
|
||||||
|
player.last_cate = lastdict.get_int("cate")
|
||||||
|
player.last_mode = lastdict.get_int("mode")
|
||||||
|
player.last_type = lastdict.get_int("type")
|
||||||
|
player.last_sort = lastdict.get_int("sort")
|
||||||
|
player.last_music = lastdict.get_int("music")
|
||||||
|
|
||||||
|
player.team = profile.get_int("team")
|
||||||
|
|
||||||
|
player.takeover = profile.get_int("takeover")
|
||||||
|
player.count_b = play_stats.get_int("battle_plays")
|
||||||
|
|
||||||
|
idx = 0
|
||||||
|
for entry in profile.get_int_array("gr_s", 5):
|
||||||
|
player.groove_radar[idx] = entry
|
||||||
|
idx += 1
|
||||||
|
|
||||||
|
idx = 0
|
||||||
|
|
||||||
|
# Empty option set is a non-zero opt array, unsure how to do this check with validated_dict cleanly?
|
||||||
|
if not profile.has_key("opt"):
|
||||||
|
profile.replace_int_array("opt", 16, [2, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 2, 0, 0, 0, 0])
|
||||||
|
|
||||||
|
for entry in profile.get_int_array("opt", 16):
|
||||||
|
player.options[idx] = entry
|
||||||
|
idx += 1
|
||||||
|
|
||||||
|
player.name = (profile.get_str("name").ljust(8, " ")[:8]).encode("ascii")
|
||||||
|
|
||||||
|
# Default, unlock everything?
|
||||||
|
for i in range(len(player.unlock_prompt_bits)):
|
||||||
|
player.unlock_prompt_bits[i] = 0xFF
|
||||||
|
|
||||||
|
idx = 0
|
||||||
|
for entry in profile.get_int_array("course", 3):
|
||||||
|
player.course[idx] = entry
|
||||||
|
idx += 1
|
||||||
|
|
||||||
|
return player
|
40
bemani/backend/ddr/ddrsn2/scoreinfo.py
Normal file
40
bemani/backend/ddr/ddrsn2/scoreinfo.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
from ctypes import *
|
||||||
|
|
||||||
|
from bemani.backend.ddr.ddrsn2.scorerecord import ScoreRecordStruct, ScoreRecord
|
||||||
|
from bemani.data import Score
|
||||||
|
|
||||||
|
|
||||||
|
class ScoreInfoStruct(Structure):
|
||||||
|
_pack_ = 1
|
||||||
|
_fields_ = [
|
||||||
|
("records", ScoreRecordStruct * 200),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ScoreInfo:
|
||||||
|
@staticmethod
|
||||||
|
def create(scores: list[Score], part: int) -> ScoreInfoStruct:
|
||||||
|
info = ScoreInfoStruct()
|
||||||
|
|
||||||
|
scores_by_song_id: dict[int, dict[int, Score]] = {}
|
||||||
|
|
||||||
|
for score in scores:
|
||||||
|
if score.id not in scores_by_song_id:
|
||||||
|
scores_by_song_id[score.id] = {}
|
||||||
|
|
||||||
|
scores_by_song_id[score.id][score.chart] = score
|
||||||
|
|
||||||
|
for i in range(len(info.records)):
|
||||||
|
if part == 2:
|
||||||
|
song_id = i + 200
|
||||||
|
else:
|
||||||
|
song_id = i
|
||||||
|
|
||||||
|
if song_id in scores_by_song_id:
|
||||||
|
score_record = ScoreRecord.create(scores_by_song_id[song_id])
|
||||||
|
else:
|
||||||
|
score_record = ScoreRecord.create(None)
|
||||||
|
|
||||||
|
info.records[i] = score_record
|
||||||
|
|
||||||
|
return info
|
115
bemani/backend/ddr/ddrsn2/scorerecord.py
Normal file
115
bemani/backend/ddr/ddrsn2/scorerecord.py
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import math
|
||||||
|
|
||||||
|
from ctypes import *
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from bemani.common import DBConstants
|
||||||
|
from bemani.data import Score
|
||||||
|
|
||||||
|
|
||||||
|
class ScoreRankRecordStruct(Structure):
|
||||||
|
_pack_ = 1
|
||||||
|
_fields_ = [
|
||||||
|
("lo_score", c_int8),
|
||||||
|
("score", c_int16),
|
||||||
|
("rank", c_int8, 5),
|
||||||
|
("unk1", c_int8, 1),
|
||||||
|
("yfc", c_int8, 1),
|
||||||
|
("ofc", c_int8, 1),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ScoreRecordStruct(Structure):
|
||||||
|
_pack_ = 1
|
||||||
|
_fields_ = [
|
||||||
|
("beginner", ScoreRankRecordStruct),
|
||||||
|
("basic", ScoreRankRecordStruct),
|
||||||
|
("difficult", ScoreRankRecordStruct),
|
||||||
|
("expert", ScoreRankRecordStruct),
|
||||||
|
("challenge", ScoreRankRecordStruct),
|
||||||
|
("double_basic", ScoreRankRecordStruct),
|
||||||
|
("double_difficult", ScoreRankRecordStruct),
|
||||||
|
("double_expert", ScoreRankRecordStruct),
|
||||||
|
("double_challenge", ScoreRankRecordStruct),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
GAME_RANK_AAA = 0
|
||||||
|
GAME_RANK_AA = 4
|
||||||
|
GAME_RANK_A = 8
|
||||||
|
GAME_RANK_B = 13
|
||||||
|
GAME_RANK_C = 16
|
||||||
|
GAME_RANK_D = 20
|
||||||
|
GAME_RANK_E = 24
|
||||||
|
GAME_RANK_NONE = 0xFF
|
||||||
|
|
||||||
|
|
||||||
|
class ScoreRecord:
|
||||||
|
@staticmethod
|
||||||
|
def blank() -> ScoreRecordStruct:
|
||||||
|
record = ScoreRecordStruct()
|
||||||
|
|
||||||
|
record.beginner.rank = GAME_RANK_NONE
|
||||||
|
record.basic.rank = GAME_RANK_NONE
|
||||||
|
record.difficult.rank = GAME_RANK_NONE
|
||||||
|
record.expert.rank = GAME_RANK_NONE
|
||||||
|
record.challenge.rank = GAME_RANK_NONE
|
||||||
|
|
||||||
|
record.double_basic.rank = GAME_RANK_NONE
|
||||||
|
record.double_difficult.rank = GAME_RANK_NONE
|
||||||
|
record.double_expert.rank = GAME_RANK_NONE
|
||||||
|
record.double_challenge.rank = GAME_RANK_NONE
|
||||||
|
|
||||||
|
return record
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create(scores: Optional[dict[int, Score]]) -> ScoreRecordStruct:
|
||||||
|
record = ScoreRecord.blank()
|
||||||
|
|
||||||
|
if scores is not None:
|
||||||
|
for chart in scores:
|
||||||
|
if chart == 0:
|
||||||
|
chart_rec = record.beginner
|
||||||
|
elif chart == 1:
|
||||||
|
chart_rec = record.basic
|
||||||
|
elif chart == 2:
|
||||||
|
chart_rec = record.difficult
|
||||||
|
elif chart == 3:
|
||||||
|
chart_rec = record.expert
|
||||||
|
else:
|
||||||
|
chart_rec = record.challenge
|
||||||
|
|
||||||
|
score = scores[chart]
|
||||||
|
|
||||||
|
formatted_score = math.floor(score.points / 10)
|
||||||
|
hi_score = formatted_score >> 8
|
||||||
|
|
||||||
|
lo_score = formatted_score ^ (hi_score << 8)
|
||||||
|
|
||||||
|
chart_rec.lo_score = lo_score
|
||||||
|
chart_rec.score = hi_score
|
||||||
|
|
||||||
|
rank = score.data.get_int("rank")
|
||||||
|
if rank == DBConstants.DDR_RANK_AAA:
|
||||||
|
chart_rec.rank = GAME_RANK_AAA
|
||||||
|
elif rank == DBConstants.DDR_RANK_AA:
|
||||||
|
chart_rec.rank = GAME_RANK_AA
|
||||||
|
elif rank == DBConstants.DDR_RANK_A:
|
||||||
|
chart_rec.rank = GAME_RANK_A
|
||||||
|
elif rank == DBConstants.DDR_RANK_B:
|
||||||
|
chart_rec.rank = GAME_RANK_B
|
||||||
|
elif rank == DBConstants.DDR_RANK_C:
|
||||||
|
chart_rec.rank = GAME_RANK_C
|
||||||
|
elif rank == DBConstants.DDR_RANK_D:
|
||||||
|
chart_rec.rank = GAME_RANK_D
|
||||||
|
else:
|
||||||
|
chart_rec.rank = GAME_RANK_E
|
||||||
|
|
||||||
|
halo = score.data.get_int("halo")
|
||||||
|
if halo == DBConstants.DDR_HALO_PERFECT_FULL_COMBO:
|
||||||
|
chart_rec.yfc = 1
|
||||||
|
chart_rec.ofc = 1
|
||||||
|
elif halo == DBConstants.DDR_HALO_GREAT_FULL_COMBO:
|
||||||
|
chart_rec.yfc = 1
|
||||||
|
|
||||||
|
return record
|
21
bemani/backend/ddr/ddrsn2/unlockentry.py
Normal file
21
bemani/backend/ddr/ddrsn2/unlockentry.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
from ctypes import *
|
||||||
|
|
||||||
|
|
||||||
|
class UnlockEntryStruct(Structure):
|
||||||
|
_pack_ = 1
|
||||||
|
_fields_ = [
|
||||||
|
("id", c_uint8),
|
||||||
|
("enabled", c_uint8),
|
||||||
|
# Set to 1 to enable. This is only read if the other flags check out. Set all other flags to 0 to force unlock.
|
||||||
|
("flag1", c_uint8), # Must be less than some value passed to check function (always 0x3d6???)
|
||||||
|
("flag2", c_uint8), # Some kind of "unlock type" flag? If 0 then other flags aren't checked.
|
||||||
|
("flag3", c_uint8), # Unused??
|
||||||
|
("flag4", c_uint8),
|
||||||
|
# If 0, flag2 (if flag2 is non-0) must match the type of unlock looked for by the checker function. If non-0, flag4 * 10 must be <= param4 of checker function (when is this used?)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class UnlockEntry:
|
||||||
|
@staticmethod
|
||||||
|
def create(id: int, enabled: int, flag1: int, flag2: int, flag3: int, flag4: int) -> UnlockEntryStruct:
|
||||||
|
return UnlockEntryStruct(id, enabled, flag1, flag2, flag3, flag4)
|
7
bemani/backend/ddr/ddrsn2/zukinteams.py
Normal file
7
bemani/backend/ddr/ddrsn2/zukinteams.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from enum import IntEnum
|
||||||
|
|
||||||
|
|
||||||
|
class ZukinTeams(IntEnum):
|
||||||
|
GREEN = 1
|
||||||
|
RED = 2
|
||||||
|
YELLOW = 3
|
13
bemani/backend/ddr/ddrx.py
Normal file
13
bemani/backend/ddr/ddrx.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from bemani.backend.ddr.base import DDRBase
|
||||||
|
from bemani.backend.ddr.ddrsn2 import DDRSuperNova2
|
||||||
|
from bemani.common import VersionConstants
|
||||||
|
|
||||||
|
|
||||||
|
class DDRX(DDRBase):
|
||||||
|
name: str = "DanceDanceRevolution X"
|
||||||
|
version: int = VersionConstants.DDR_X
|
||||||
|
|
||||||
|
def previous_version(self) -> Optional[DDRBase]:
|
||||||
|
return DDRSuperNova2(self.data, self.config, self.model)
|
@ -3,7 +3,6 @@ from typing import Dict, List, Optional, Tuple
|
|||||||
from typing_extensions import Final
|
from typing_extensions import Final
|
||||||
|
|
||||||
from bemani.backend.ddr.base import DDRBase
|
from bemani.backend.ddr.base import DDRBase
|
||||||
from bemani.backend.ddr.stubs import DDRX
|
|
||||||
from bemani.backend.ddr.common import (
|
from bemani.backend.ddr.common import (
|
||||||
DDRGameFriendHandler,
|
DDRGameFriendHandler,
|
||||||
DDRGameLockHandler,
|
DDRGameLockHandler,
|
||||||
@ -20,6 +19,7 @@ from bemani.backend.ddr.common import (
|
|||||||
DDRGameShopHandler,
|
DDRGameShopHandler,
|
||||||
DDRGameTraceHandler,
|
DDRGameTraceHandler,
|
||||||
)
|
)
|
||||||
|
from bemani.backend.ddr.ddrx import DDRX
|
||||||
from bemani.common import Time, VersionConstants, Profile, intish
|
from bemani.common import Time, VersionConstants, Profile, intish
|
||||||
from bemani.data import Score, UserID
|
from bemani.data import Score, UserID
|
||||||
from bemani.protocol import Node
|
from bemani.protocol import Node
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
from typing import List, Optional, Type
|
from typing import List, Optional, Type
|
||||||
|
|
||||||
from bemani.backend.base import Base, Factory
|
from bemani.backend.base import Base, Factory
|
||||||
|
from bemani.backend.ddr.ddrx import DDRX
|
||||||
from bemani.backend.ddr.stubs import (
|
from bemani.backend.ddr.stubs import (
|
||||||
DDRX,
|
|
||||||
DDRSuperNova2,
|
|
||||||
DDRSuperNova,
|
DDRSuperNova,
|
||||||
DDRExtreme,
|
DDRExtreme,
|
||||||
DDR7thMix,
|
DDR7thMix,
|
||||||
@ -14,6 +13,7 @@ from bemani.backend.ddr.stubs import (
|
|||||||
DDR2ndMix,
|
DDR2ndMix,
|
||||||
DDR1stMix,
|
DDR1stMix,
|
||||||
)
|
)
|
||||||
|
from bemani.backend.ddr.ddrsn2 import DDRSuperNova2
|
||||||
from bemani.backend.ddr.ddrx2 import DDRX2
|
from bemani.backend.ddr.ddrx2 import DDRX2
|
||||||
from bemani.backend.ddr.ddrx3 import DDRX3
|
from bemani.backend.ddr.ddrx3 import DDRX3
|
||||||
from bemani.backend.ddr.ddr2013 import DDR2013
|
from bemani.backend.ddr.ddr2013 import DDR2013
|
||||||
@ -47,7 +47,7 @@ class DDRFactory(Factory):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def register_all(cls) -> None:
|
def register_all(cls) -> None:
|
||||||
for gamecode in ["HDX", "JDX", "KDX", "MDX"]:
|
for gamecode in ["GDJ", "HDX", "JDX", "KDX", "MDX"]:
|
||||||
Base.register(gamecode, DDRFactory)
|
Base.register(gamecode, DDRFactory)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -69,6 +69,8 @@ class DDRFactory(Factory):
|
|||||||
return VersionConstants.DDR_A20
|
return VersionConstants.DDR_A20
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if model.gamecode == "GDJ":
|
||||||
|
return DDRSuperNova2(data, config, model)
|
||||||
if model.gamecode == "HDX":
|
if model.gamecode == "HDX":
|
||||||
return DDRX(data, config, model)
|
return DDRX(data, config, model)
|
||||||
if model.gamecode == "JDX":
|
if model.gamecode == "JDX":
|
||||||
|
@ -5,22 +5,6 @@ from bemani.backend.ddr.base import DDRBase
|
|||||||
from bemani.common import VersionConstants
|
from bemani.common import VersionConstants
|
||||||
|
|
||||||
|
|
||||||
class DDRX(DDRBase):
|
|
||||||
name: str = "DanceDanceRevolution X"
|
|
||||||
version: int = VersionConstants.DDR_X
|
|
||||||
|
|
||||||
def previous_version(self) -> Optional[DDRBase]:
|
|
||||||
return DDRSuperNova2(self.data, self.config, self.model)
|
|
||||||
|
|
||||||
|
|
||||||
class DDRSuperNova2(DDRBase):
|
|
||||||
name: str = "DanceDanceRevolution SuperNova 2"
|
|
||||||
version: int = VersionConstants.DDR_SUPERNOVA_2
|
|
||||||
|
|
||||||
def previous_version(self) -> Optional[DDRBase]:
|
|
||||||
return DDRSuperNova(self.data, self.config, self.model)
|
|
||||||
|
|
||||||
|
|
||||||
class DDRSuperNova(DDRBase):
|
class DDRSuperNova(DDRBase):
|
||||||
name: str = "DanceDanceRevolution SuperNova"
|
name: str = "DanceDanceRevolution SuperNova"
|
||||||
version: int = VersionConstants.DDR_SUPERNOVA
|
version: int = VersionConstants.DDR_SUPERNOVA
|
||||||
|
@ -444,6 +444,12 @@ class ValidatedDict(dict):
|
|||||||
else:
|
else:
|
||||||
self[name] = self[name] + 1
|
self[name] = self[name] + 1
|
||||||
|
|
||||||
|
def has_key(self, name: str) -> bool:
|
||||||
|
if name in self:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class Profile(ValidatedDict):
|
class Profile(ValidatedDict):
|
||||||
"""
|
"""
|
||||||
|
@ -4,7 +4,7 @@ from sqlalchemy.types import String, Integer, JSON # type: ignore
|
|||||||
from sqlalchemy.dialects.mysql import BIGINT as BigInteger # type: ignore
|
from sqlalchemy.dialects.mysql import BIGINT as BigInteger # type: ignore
|
||||||
from typing import Optional, Dict, List, Tuple, Any
|
from typing import Optional, Dict, List, Tuple, Any
|
||||||
|
|
||||||
from bemani.common import GameConstants, Time
|
from bemani.common import GameConstants, Time, VersionConstants
|
||||||
from bemani.data.exceptions import ScoreSaveException
|
from bemani.data.exceptions import ScoreSaveException
|
||||||
from bemani.data.mysql.base import BaseData, metadata
|
from bemani.data.mysql.base import BaseData, metadata
|
||||||
from bemani.data.types import Score, Attempt, Song, UserID
|
from bemani.data.types import Score, Attempt, Song, UserID
|
||||||
@ -81,6 +81,15 @@ music = Table(
|
|||||||
|
|
||||||
|
|
||||||
class MusicData(BaseData):
|
class MusicData(BaseData):
|
||||||
|
def __get_next_music_id(self) -> int:
|
||||||
|
cursor = self.execute("SELECT MAX(id) AS next_id FROM `music`")
|
||||||
|
result = cursor.fetchone()
|
||||||
|
try:
|
||||||
|
return result["next_id"] + 1
|
||||||
|
except TypeError:
|
||||||
|
# Nothing in DB
|
||||||
|
return 1
|
||||||
|
|
||||||
def __get_musicid(self, game: GameConstants, version: int, songid: int, songchart: int) -> int:
|
def __get_musicid(self, game: GameConstants, version: int, songid: int, songchart: int) -> int:
|
||||||
"""
|
"""
|
||||||
Given a game/version/songid/chart, look up the unique music ID for this song.
|
Given a game/version/songid/chart, look up the unique music ID for this song.
|
||||||
@ -105,8 +114,37 @@ class MusicData(BaseData):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
if cursor.rowcount != 1:
|
if cursor.rowcount != 1:
|
||||||
# music doesn't exist
|
if game == GameConstants.DDR and version == VersionConstants.DDR_SUPERNOVA_2:
|
||||||
raise Exception(f"Song {songid} chart {songchart} doesn't exist for game {game} version {version}")
|
# No currently implemented way to import supernova 2 songlist, so insert records to the DB on the fly
|
||||||
|
insertSql = "INSERT INTO music (id, songid, chart, game, version, name, artist, genre) VALUES (:id, :songid, :chart, :game, :version, :name, :artist, :genre)"
|
||||||
|
|
||||||
|
self.execute(
|
||||||
|
insertSql,
|
||||||
|
{
|
||||||
|
"id": self.__get_next_music_id(),
|
||||||
|
"songid": songid,
|
||||||
|
"chart": songchart,
|
||||||
|
"game": game.value,
|
||||||
|
"version": version,
|
||||||
|
"name": "test",
|
||||||
|
"artist": "artist",
|
||||||
|
"genre": "genre",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
cursor = self.execute(
|
||||||
|
sql,
|
||||||
|
{
|
||||||
|
"songid": songid,
|
||||||
|
"chart": songchart,
|
||||||
|
"game": game.value,
|
||||||
|
"version": version,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# music doesn't exist
|
||||||
|
raise Exception(f"Song {songid} chart {songchart} doesn't exist for game {game} version {version}")
|
||||||
|
|
||||||
result = cursor.fetchone()
|
result = cursor.fetchone()
|
||||||
return result["id"]
|
return result["id"]
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user