209 lines
7.6 KiB
Python
209 lines
7.6 KiB
Python
|
# vim: set fileencoding=utf-8
|
||
|
import binascii
|
||
|
import copy
|
||
|
import base64
|
||
|
from typing import Any, Dict, List
|
||
|
|
||
|
from bemani.backend.bishi.base import BishiBashiBase
|
||
|
from bemani.backend.ess import EventLogHandler
|
||
|
from bemani.common import ValidatedDict, GameConstants, VersionConstants
|
||
|
from bemani.data import UserID
|
||
|
from bemani.protocol import Node
|
||
|
|
||
|
|
||
|
class TheStarBishiBashi(
|
||
|
EventLogHandler,
|
||
|
BishiBashiBase,
|
||
|
):
|
||
|
|
||
|
name = "The★BishiBashi"
|
||
|
version = VersionConstants.BISHI_BASHI_TSBB
|
||
|
|
||
|
@classmethod
|
||
|
def get_settings(cls) -> Dict[str, Any]:
|
||
|
"""
|
||
|
Return all of our front-end modifiably settings.
|
||
|
"""
|
||
|
return {
|
||
|
'bools': [
|
||
|
{
|
||
|
'name': 'Force Unlock Characters',
|
||
|
'tip': 'Force unlock all characters on select screen.',
|
||
|
'category': 'game_config',
|
||
|
'setting': 'force_unlock_characters',
|
||
|
},
|
||
|
],
|
||
|
}
|
||
|
|
||
|
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 != 'IBBDAT00':
|
||
|
# Not the right profile type requested
|
||
|
return
|
||
|
|
||
|
# Grab the shop name
|
||
|
try:
|
||
|
shopname = csvs[30].decode('shift-jis')
|
||
|
except Exception:
|
||
|
return
|
||
|
self.update_machine_name(shopname)
|
||
|
|
||
|
def handle_system_getmaster_request(self, request: Node) -> Node:
|
||
|
# System message
|
||
|
root = Node.void('system')
|
||
|
root.add_child(Node.s32('result', 0))
|
||
|
return root
|
||
|
|
||
|
def handle_playerdata_usergamedata_send_request(self, request: Node) -> Node:
|
||
|
# Look up user by refid
|
||
|
refid = request.child_value('data/eaid')
|
||
|
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
|
||
|
if userid is None:
|
||
|
root = Node.void('playerdata')
|
||
|
root.add_child(Node.s32('result', 1)) # Unclear if this is the right thing to do here.
|
||
|
return root
|
||
|
|
||
|
# Extract new profile info from old profile
|
||
|
oldprofile = self.get_profile(userid)
|
||
|
is_new = False
|
||
|
if oldprofile is None:
|
||
|
oldprofile = ValidatedDict()
|
||
|
is_new = True
|
||
|
newprofile = self.unformat_profile(userid, request, oldprofile, is_new)
|
||
|
|
||
|
# Write new profile
|
||
|
self.put_profile(userid, newprofile)
|
||
|
|
||
|
# Return success!
|
||
|
root = Node.void('playerdata')
|
||
|
root.add_child(Node.s32('result', 0))
|
||
|
return root
|
||
|
|
||
|
def handle_playerdata_usergamedata_recv_request(self, request: Node) -> Node:
|
||
|
# Look up user by refid
|
||
|
refid = request.child_value('data/eaid')
|
||
|
profiletype = request.child_value('data/recv_csv').split(',')[0]
|
||
|
profile = None
|
||
|
userid = None
|
||
|
if refid is not None:
|
||
|
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
|
||
|
if userid is not None:
|
||
|
profile = self.get_profile(userid)
|
||
|
if profile is not None:
|
||
|
return self.format_profile(userid, profiletype, profile)
|
||
|
else:
|
||
|
root = Node.void('playerdata')
|
||
|
root.add_child(Node.s32('result', 1)) # Unclear if this is the right thing to do here.
|
||
|
return root
|
||
|
|
||
|
def format_profile(self, userid: UserID, profiletype: str, profile: ValidatedDict) -> Node:
|
||
|
root = Node.void('playerdata')
|
||
|
root.add_child(Node.s32('result', 0))
|
||
|
player = Node.void('player')
|
||
|
root.add_child(player)
|
||
|
records = 0
|
||
|
|
||
|
for i in range(len(profile['strdatas'])):
|
||
|
strdata = profile['strdatas'][i]
|
||
|
bindata = profile['bindatas'][i]
|
||
|
|
||
|
# Figure out the profile type
|
||
|
csvs = strdata.split(b',')
|
||
|
if len(csvs) < 2:
|
||
|
# Not long enough to care about
|
||
|
continue
|
||
|
datatype = csvs[1].decode('ascii')
|
||
|
if datatype != profiletype:
|
||
|
# Not the right profile type requested
|
||
|
continue
|
||
|
|
||
|
game_config = self.get_game_config()
|
||
|
force_unlock_characters = game_config.get_bool('force_unlock_characters')
|
||
|
if force_unlock_characters:
|
||
|
csvs[11] = b'3ffffffffffff'
|
||
|
else:
|
||
|
# Reward characters based on playing other games on the network
|
||
|
hexdata = csvs[11].decode('ascii')
|
||
|
while (len(hexdata) & 1) != 0:
|
||
|
hexdata = '0' + hexdata
|
||
|
unlock_bits = [b for b in binascii.unhexlify(hexdata)]
|
||
|
while len(unlock_bits) < 7:
|
||
|
unlock_bits.insert(0, 0)
|
||
|
|
||
|
# Reverse the array, so indexing makes more sense
|
||
|
unlock_bits = unlock_bits[::-1]
|
||
|
|
||
|
# Figure out what other games were played by this user
|
||
|
profiles = self.data.local.user.get_games_played(userid)
|
||
|
|
||
|
# IIDX
|
||
|
if len([p for p in profiles if p[0] == GameConstants.IIDX]) > 0:
|
||
|
unlock_bits[1] = unlock_bits[1] | 0x10
|
||
|
|
||
|
# Pop'n
|
||
|
if len([p for p in profiles if p[0] == GameConstants.POPN_MUSIC]) > 0:
|
||
|
unlock_bits[1] = unlock_bits[1] | 0x60
|
||
|
|
||
|
# Jubeat
|
||
|
if len([p for p in profiles if p[0] == GameConstants.JUBEAT]) > 0:
|
||
|
unlock_bits[2] = unlock_bits[2] | 0x02
|
||
|
|
||
|
# DDR
|
||
|
if len([p for p in profiles if p[0] == GameConstants.DDR]) > 0:
|
||
|
unlock_bits[6] = unlock_bits[6] | 0x03
|
||
|
|
||
|
# GFDM characters exist, but this network has no support for
|
||
|
# GFDM or Gitadora, so the bits were never added.
|
||
|
|
||
|
# Reconstruct table
|
||
|
unlock_bits = unlock_bits[::-1]
|
||
|
csvs[11] = ''.join(['{:02x}'.format(x) for x in unlock_bits]).encode('ascii')
|
||
|
|
||
|
# This is a valid profile node for this type, lets return only the profile values
|
||
|
strdata = b','.join(csvs[2:])
|
||
|
record = Node.void('record')
|
||
|
player.add_child(record)
|
||
|
d = Node.string('d', base64.b64encode(strdata).decode('ascii'))
|
||
|
record.add_child(d)
|
||
|
d.add_child(Node.string('bin1', base64.b64encode(bindata).decode('ascii')))
|
||
|
|
||
|
# Remember that we had this record
|
||
|
records = records + 1
|
||
|
|
||
|
player.add_child(Node.u32('record_num', records))
|
||
|
return root
|
||
|
|
||
|
def unformat_profile(self, userid: UserID, request: Node, oldprofile: ValidatedDict, is_new: bool) -> ValidatedDict:
|
||
|
# Profile save request, data values are base64 encoded.
|
||
|
# d is a CSV, and bin1 is binary data.
|
||
|
newprofile = copy.deepcopy(oldprofile)
|
||
|
strdatas: List[bytes] = []
|
||
|
bindatas: List[bytes] = []
|
||
|
|
||
|
record = request.child('data/record')
|
||
|
for node in record.children:
|
||
|
if node.name != 'd':
|
||
|
continue
|
||
|
|
||
|
profile = base64.b64decode(node.value)
|
||
|
# Update the shop name if this is a new profile, since we know it came
|
||
|
# from this cabinet. This is the only source of truth for what the
|
||
|
# cabinet shop name is set to.
|
||
|
if is_new:
|
||
|
self.__update_shop_name(profile)
|
||
|
strdatas.append(profile)
|
||
|
bindatas.append(base64.b64decode(node.child_value('bin1')))
|
||
|
|
||
|
newprofile['strdatas'] = strdatas
|
||
|
newprofile['bindatas'] = bindatas
|
||
|
|
||
|
# Keep track of play statistics across all versions
|
||
|
self.update_play_statistics(userid)
|
||
|
|
||
|
return newprofile
|