mirror of
https://github.com/DragonMinded/bemaniutils.git
synced 2026-09-14 13:25:13 -05:00
First stab at actual profile documentation as well as round tripping extracted fields.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import base64
|
||||
from typing import Any, Dict
|
||||
from typing_extensions import Final
|
||||
|
||||
from bemani.backend.ess import EventLogHandler
|
||||
from bemani.backend.danevo.base import DanceEvolutionBase
|
||||
@@ -14,6 +15,73 @@ class DanceEvolution(
|
||||
name: str = "Dance Evolution"
|
||||
version: int = VersionConstants.DANCE_EVOLUTION
|
||||
|
||||
# Dance Evolution is a mess that only uses profile blobs for everything from the server.
|
||||
# This includes scores/records, and your profile and class advancement. DATA01 includes
|
||||
# standard profile info in the CSV portion much like every ESS game. DATA03 includes, for
|
||||
# some reason, the ID of the songs you just played as well as the score, but not the chart.
|
||||
# DATA04 includes your cumulative score earned across all plays. DATA01-DATA05 as well as
|
||||
# DATA11-DATA15 hold the records for songs in the binary portion, where 01-05 indicate the
|
||||
# chart difficulty for the song and the offset into the binary data can be calculated by
|
||||
# the song's offset in the music DB. RDATA01 appears to be for attempts, and seems to
|
||||
# include information in the binary of previous attempts at songs, ordered by attempt.
|
||||
# This, however, does not include the chart played, so it can't be used to inform the backend
|
||||
# of attempts.
|
||||
|
||||
# RDATA running score list includes history chunks that are 32 bytes in length. The game
|
||||
# never sends trailing zeros for a data structure, even if it internally recognizes them,
|
||||
# so this may have length not divisible by 32 if there are no non-zero bytes after a certain
|
||||
# location. The correct thing to do is to fill with assumed zero bytes to a 32-byte boundary.
|
||||
# The first 4 bytes of any history chunk are the score in little endian. The next byte is the
|
||||
# song ID. Note that the chart does not appear anywhere in this structure to my knowledge.
|
||||
# I have no idea what the rest of the bytes are, but most stay zeros and many are the same
|
||||
# no matter what.
|
||||
|
||||
# DATA01-05 store the records for songs, including the high score, the letter grade earned,
|
||||
# and whether the song has been played (a record exists), cleared and whether a full combo
|
||||
# has been earned. The first 63 songs are stored in DATA01-05 (songs with ID 0-62). Song
|
||||
# ID 63 and above are stored in DATA11-15 instead, but in an identical format. Each record
|
||||
# is 8 bytes long and can be found by multiplying the music ID by 8. To get to songs in the
|
||||
# second chunk, first subtract 63 from the song ID and then multiply that value by 8. The
|
||||
# first 4 bytes are the high score, stored in little-endian. The next byte is the combo
|
||||
# achieved. The next byte is always observed to be 0x04, and the one after that 0x00. Finally,
|
||||
# the last byte is the letter grade and full combo marker. Bit 4 set indicates the player
|
||||
# earned a full combo. Bits 3-1 are a 3 bit integer indicating the letter grade. Bit 0 is
|
||||
# unused. The game considers any letter grade other than 0 to be a pass.
|
||||
|
||||
# The game stores records for each chart in a different DATAXX location. See the below chart
|
||||
# for exact storage locations. Note that yes, stealth and master are reversed from how they
|
||||
# are presented in game.
|
||||
#
|
||||
# DATA01/11 - Light
|
||||
# DATA02/12 - Standard
|
||||
# DATA03/13 - Extreme
|
||||
# DATA04/14 - Stealth
|
||||
# DATA05/15 - Master
|
||||
|
||||
# The letter grades are believed to be as follows.
|
||||
#
|
||||
# 0 - Failed. Game displays a white gem to indicate the song was played.
|
||||
# 1 - E. Game displays a colored gem matching the chart to indicate the song was cleared.
|
||||
# 2 - D. Game displays a colored gem matching the chart to indicate the song was cleared.
|
||||
# 3 - C. Game displays a colored gem matching the chart to indicate the song was cleared.
|
||||
# 4 - B. Game displays a colored gem matching the chart to indicate the song was cleared.
|
||||
# 5 - A. Game displays a colored gem matching the chart to indicate the song was cleared.
|
||||
# 6 - AA. Game displays a colored gem matching the chart to indicate the song was cleared.
|
||||
# 7 - AAA. Game displays a colored gem matching the chart to indicate the song was cleared.
|
||||
|
||||
DATA01_CLASS_OFFSET: Final[int] = 2
|
||||
DATA01_GOLD_OFFSET: Final[int] = 3
|
||||
DATA01_NAME_OFFSET: Final[int] = 25
|
||||
DATA01_AREA_OFFSET: Final[int] = 26
|
||||
DATA01_SHOP_OFFSET: Final[int] = 27
|
||||
|
||||
DATA03_FIRST_SONG_OFFSET: Final[int] = 13
|
||||
DATA03_FIRST_HIGH_SCORE_OFFSET: Final[int] = 14
|
||||
DATA03_SECOND_SONG_OFFSET: Final[int] = 15
|
||||
DATA03_SECOND_HIGH_SCORE_OFFSET: Final[int] = 16
|
||||
|
||||
DATA04_TOTAL_SCORE_EARNED_OFFSET: Final[int] = 9
|
||||
|
||||
@classmethod
|
||||
def get_settings(cls) -> Dict[str, Any]:
|
||||
"""
|
||||
@@ -21,6 +89,25 @@ class DanceEvolution(
|
||||
"""
|
||||
return {}
|
||||
|
||||
def __update_shop_name(self, profiledata: bytes) -> None:
|
||||
# Figure out the profile type
|
||||
csvs = profiledata.split(b",")
|
||||
if len(csvs) < 2:
|
||||
# Not long enough to care about
|
||||
return
|
||||
datatype = csvs[1].decode("ascii")
|
||||
if datatype != "DATA01":
|
||||
# Not the right profile type requested
|
||||
return
|
||||
|
||||
# Grab the shop name, which is offset based on storage, not based on the
|
||||
# game's sending this to us now.
|
||||
try:
|
||||
shopname = csvs[self.DATA01_SHOP_OFFSET + 2].decode("shift-jis")
|
||||
except Exception:
|
||||
return
|
||||
self.update_machine_name(shopname)
|
||||
|
||||
def handle_tax_get_phase_request(self, request: Node) -> Node:
|
||||
tax = Node.void("tax")
|
||||
tax.add_child(Node.s32("phase", 0))
|
||||
@@ -55,7 +142,9 @@ class DanceEvolution(
|
||||
|
||||
if reqtype == "S_SRVMSG":
|
||||
# Known keys include: INFO, ARK_ARR0, ARK_HAS0, SONGOPEN, IRDATA, EVTMSG3, WEEKLYSO
|
||||
root.add_child(Node.string("strdata1", ""))
|
||||
strdata = ""
|
||||
|
||||
root.add_child(Node.string("strdata1", base64.b64encode(strdata.encode("ascii")).decode("ascii")))
|
||||
root.add_child(Node.string("strdata2", ""))
|
||||
root.add_child(Node.u64("updatedate", Time.now() * 1000))
|
||||
root.add_child(Node.s32("result", 1))
|
||||
@@ -65,6 +154,9 @@ class DanceEvolution(
|
||||
|
||||
return root
|
||||
|
||||
def _to_hex(self, number: int) -> str:
|
||||
return hex(number)[2:]
|
||||
|
||||
def handle_playerdata_usergamedata_recv_request(self, request: Node) -> Node:
|
||||
playerdata = Node.void("playerdata")
|
||||
|
||||
@@ -97,9 +189,21 @@ class DanceEvolution(
|
||||
usergamedata = profile.get_dict("usergamedata")
|
||||
for ptype in profiletypes:
|
||||
if ptype in usergamedata:
|
||||
splits = usergamedata[ptype]["strdata"].split(b",")
|
||||
|
||||
if ptype == "DATA01":
|
||||
# Common profile stuff.
|
||||
splits[self.DATA01_NAME_OFFSET] = profile.get_str("name").encode('shift-jis')
|
||||
splits[self.DATA01_AREA_OFFSET] = profile.get_str("area").encode('shift-jis')
|
||||
splits[self.DATA01_CLASS_OFFSET] = self._to_hex(profile.get_int("class", 1)).encode('shift-jis')
|
||||
splits[self.DATA01_GOLD_OFFSET] = self._to_hex(profile.get_int("gold", 0)).encode('shift-jis')
|
||||
elif ptype == "DATA04":
|
||||
# Cumulative score.
|
||||
splits[self.DATA04_TOTAL_SCORE_EARNED_OFFSET] = self._to_hex(profile.get_int("cumulative_score", 0)).encode('shift-jis')
|
||||
|
||||
dnode = Node.string(
|
||||
"d",
|
||||
base64.b64encode(usergamedata[ptype]["strdata"]).decode("ascii"),
|
||||
base64.b64encode(b",".join(splits)).decode("ascii"),
|
||||
)
|
||||
dnode.add_child(
|
||||
Node.string(
|
||||
@@ -126,7 +230,11 @@ class DanceEvolution(
|
||||
|
||||
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
|
||||
if userid is not None:
|
||||
profile = self.get_profile(userid) or Profile(self.game, self.version, refid, 0)
|
||||
profile = self.get_profile(userid)
|
||||
is_new = False
|
||||
if profile is None:
|
||||
profile = Profile(self.game, self.version, refid, 0)
|
||||
is_new = True
|
||||
usergamedata = profile.get_dict("usergamedata")
|
||||
|
||||
for record in request.child("data/record").children:
|
||||
@@ -136,11 +244,26 @@ class DanceEvolution(
|
||||
strdata = base64.b64decode(record.value)
|
||||
bindata = base64.b64decode(record.child_value("bin1"))
|
||||
|
||||
# Update the shop name if this is a new profile, since we know on new profiles that
|
||||
# this value came from the game itself.
|
||||
if is_new:
|
||||
self.__update_shop_name(strdata)
|
||||
|
||||
# Grab and format the profile objects
|
||||
strdatalist = strdata.split(b",")
|
||||
profiletype = strdatalist[1].decode("utf-8")
|
||||
strdatalist = strdatalist[2:]
|
||||
|
||||
if profiletype == "DATA01":
|
||||
# Extract relevant info so that it's in the profile normally.
|
||||
profile.replace_str("name", strdatalist[self.DATA01_NAME_OFFSET].decode('shift-jis'))
|
||||
profile.replace_int("class", int(strdatalist[self.DATA01_CLASS_OFFSET].decode('shift-jis'), 16))
|
||||
profile.replace_int("gold", int(strdatalist[self.DATA01_GOLD_OFFSET].decode('shift-jis'), 16))
|
||||
profile.replace_str("area", strdatalist[self.DATA01_AREA_OFFSET].decode('shift-jis'))
|
||||
elif profiletype == "DATA04":
|
||||
# Keep track of this for fun, because hey, why not?
|
||||
profile.replace_int("cumulative_score", int(strdatalist[self.DATA04_TOTAL_SCORE_EARNED_OFFSET].decode('shift-jis'), 16))
|
||||
|
||||
usergamedata[profiletype] = {
|
||||
"strdata": b",".join(strdatalist),
|
||||
"bindata": bindata,
|
||||
@@ -150,5 +273,9 @@ class DanceEvolution(
|
||||
profile.replace_int("write_time", Time.now())
|
||||
self.put_profile(userid, profile)
|
||||
|
||||
# Keep track of play statistics across all versions, but don't do it for the initial profile save.
|
||||
if not is_new:
|
||||
self.update_play_statistics(userid)
|
||||
|
||||
playerdata.add_child(Node.s32("result", 0))
|
||||
return playerdata
|
||||
|
||||
Reference in New Issue
Block a user