mirror of
https://github.com/DragonMinded/bemaniutils.git
synced 2026-04-25 07:59:25 -05:00
408 lines
16 KiB
Python
408 lines
16 KiB
Python
import copy
|
|
import random
|
|
import struct
|
|
from typing import Optional, Dict, Any, List, Tuple
|
|
import time
|
|
|
|
from bemani.backend.bst.base import BSTBase
|
|
|
|
from bemani.common import Profile, ValidatedDict, VersionConstants, ID, Time
|
|
from bemani.backend.ess import EventLogHandler
|
|
from bemani.data import Data, UserID, Score
|
|
from bemani.protocol import Node
|
|
|
|
class Beatstream(EventLogHandler, BSTBase):
|
|
name = "BeatStream"
|
|
version = VersionConstants.BEATSTREAM
|
|
#TODO: Beatstream 1 support
|
|
|
|
GRADE_AAA = 1
|
|
GRADE_AA = 2
|
|
GRADE_A = 3
|
|
GRADE_B = 4
|
|
GRADE_C = 5
|
|
GRADE_D = 6
|
|
|
|
MEDAL_NOPLAY = 0
|
|
MEDAL_FAILED = 1
|
|
MEDAL_SAVED = 2
|
|
MEDAL_CLEAR = 3
|
|
MEDAL_FC = 4
|
|
MEDAL_PERFECT = 5
|
|
|
|
def get_settings(cls) -> Dict[str, Any]:
|
|
"""
|
|
Return all of our front-end modifiably settings.
|
|
"""
|
|
return {
|
|
'bools': [
|
|
{
|
|
'name': 'Enable Local Matching',
|
|
'tip': 'Enable local matching between games.',
|
|
'category': 'game_config',
|
|
'setting': 'enable_local_match',
|
|
},
|
|
{
|
|
'name': 'Enable Global Matching',
|
|
'tip': 'Enable global matching between games.',
|
|
'category': 'game_config',
|
|
'setting': 'enable_global_match',
|
|
},
|
|
]
|
|
}
|
|
|
|
def get_events(self) -> Node:
|
|
root = super().get_events()
|
|
game_config = self.get_game_config()
|
|
|
|
# Campaign
|
|
data = Node.void('data')
|
|
data.add_child(Node.s32('type', 0))
|
|
data.add_child(Node.s32('phase', 18))
|
|
root.add_child(data)
|
|
|
|
# Beast crissis
|
|
data = Node.void('data')
|
|
data.add_child(Node.s32('type', 1))
|
|
data.add_child(Node.s32('phase', 4))
|
|
root.add_child(data)
|
|
|
|
# 5th KAC screen on the demo reel
|
|
data = Node.void('data')
|
|
data.add_child(Node.s32('type', 3))
|
|
data.add_child(Node.s32('phase', 1))
|
|
root.add_child(data)
|
|
|
|
# Eamuse app screenshots
|
|
data = Node.void('data')
|
|
data.add_child(Node.s32('type', 3))
|
|
data.add_child(Node.s32('phase', 2))
|
|
root.add_child(data)
|
|
|
|
# Enables continues
|
|
data = Node.void('data')
|
|
data.add_child(Node.s32('type', 3))
|
|
data.add_child(Node.s32('phase', 3))
|
|
root.add_child(data)
|
|
|
|
# Allows 3 stage with paseli
|
|
data = Node.void('data')
|
|
data.add_child(Node.s32('type', 3))
|
|
data.add_child(Node.s32('phase', 4))
|
|
root.add_child(data)
|
|
|
|
# Local matching at start of credit enable
|
|
data = Node.void('data')
|
|
data.add_child(Node.s32('type', 3))
|
|
data.add_child(Node.s32('phase', 7))
|
|
if game_config.get_bool('enable_local_match'):
|
|
root.add_child(data)
|
|
|
|
# Controlls floor infection on attract screen ONLY
|
|
data = Node.void('data')
|
|
data.add_child(Node.s32('type', 3))
|
|
data.add_child(Node.s32('phase', 8))
|
|
root.add_child(data)
|
|
|
|
# Global matching during song loading enable
|
|
data = Node.void('data')
|
|
data.add_child(Node.s32('type', 3))
|
|
data.add_child(Node.s32('phase', 12))
|
|
if game_config.get_bool('enable_global_match'):
|
|
root.add_child(data)
|
|
|
|
# Unlocks the bemani rockin fes songs
|
|
data = Node.void('data')
|
|
data.add_child(Node.s32('type', 3))
|
|
data.add_child(Node.s32('phase', 13))
|
|
root.add_child(data)
|
|
|
|
# Enables Illil partner addition
|
|
data = Node.void('data')
|
|
data.add_child(Node.s32('type', 3))
|
|
data.add_child(Node.s32('phase', 16))
|
|
root.add_child(data)
|
|
|
|
# Controls notifs when carding in
|
|
data = Node.void('data')
|
|
data.add_child(Node.s32('type', 4))
|
|
data.add_child(Node.s32('phase', 31))
|
|
root.add_child(data)
|
|
|
|
# Courses
|
|
# 1 = 1-12, 2 = 13 and 14, 3 = 15, 4 = kami
|
|
# any other value disables courses
|
|
# need to set 1-4 to enable all courses
|
|
for x in range(1,5):
|
|
data = Node.void('data')
|
|
data.add_child(Node.s32('type', 7))
|
|
data.add_child(Node.s32('phase', x))
|
|
root.add_child(data)
|
|
|
|
# Beast Hacker
|
|
data = Node.void('data')
|
|
data.add_child(Node.s32('type', 8))
|
|
data.add_child(Node.s32('phase', 10)) # Phase 1 - 9, plus unused 10th
|
|
root.add_child(data)
|
|
|
|
# First play free
|
|
data = Node.void('data')
|
|
data.add_child(Node.s32('type', 1100))
|
|
data.add_child(Node.s32('phase', 2))
|
|
root.add_child(data)
|
|
|
|
return root
|
|
|
|
# Helper method to unformat the player profile into a ValidatedDict for the DB
|
|
def unformat_player_profile(self, profile: Node) -> Profile:
|
|
userid = self.data.local.user.from_extid(self.game, self.version, profile.child_value('account/usrid'))
|
|
ret = Profile(self.game, self.version, profile.child_value('account/rid'), profile.child_value('account/usrid'))
|
|
|
|
# Account
|
|
next_tpc = int(profile.child_value('account/tpc')) + 1
|
|
ret.replace_int('usrid', int(profile.child_value('account/usrid')))
|
|
ret.replace_int('tpc', next_tpc)
|
|
ret.replace_int('dpc', int(profile.child_value('account/dpc')))
|
|
ret.replace_int('crd', int(profile.child_value('account/crd')))
|
|
ret.replace_int('brd', int(profile.child_value('account/brd')))
|
|
ret.replace_int('tdc', int(profile.child_value('account/tdc')))
|
|
ret.replace_str('lid', profile.child_value('account/lid'))
|
|
ret.replace_int('ver', int(profile.child_value('account/ver')))
|
|
|
|
# Base
|
|
ret.replace_str('name', profile.child_value('base/name'))
|
|
ret.replace_int('brnk', int(profile.child_value('base/brnk')))
|
|
ret.replace_int('bcnum', int(profile.child_value('base/bcnum')))
|
|
ret.replace_int('lcnum', int(profile.child_value('base/lcnum')))
|
|
ret.replace_int('volt', int(profile.child_value('base/volt')))
|
|
ret.replace_int('gold', int(profile.child_value('base/gold')))
|
|
ret.replace_int('lmid', int(profile.child_value('base/lmid')))
|
|
ret.replace_int('lgrd', int(profile.child_value('base/lgrd')))
|
|
ret.replace_int('lsrt', int(profile.child_value('base/lsrt')))
|
|
ret.replace_int('ltab', int(profile.child_value('base/ltab')))
|
|
ret.replace_int('splv', int(profile.child_value('base/splv')))
|
|
ret.replace_int('pref', int(profile.child_value('base/pref')))
|
|
|
|
# Base2
|
|
ret.replace_int('lcid', int(profile.child_value('base2/lcid')))
|
|
ret.replace_int('hat', int(profile.child_value('base2/hat')))
|
|
|
|
# Items stored as achievements
|
|
items = profile.child('item')
|
|
if items is not None and int(profile.child_value('account/usrid')) != 0:
|
|
for i in items.children:
|
|
self.data.local.user.put_achievement(self.game, self.version, userid, i.child_value('id'),
|
|
f"item_{i.child_value('type')}", {"type": i.child_value('type'), "param": i.child_value('param'),
|
|
"count": i.child_value('count')})
|
|
|
|
# Customize
|
|
custom = profile.child_value('customize/custom')
|
|
if custom is not None:
|
|
customize = []
|
|
for i in custom:
|
|
customize.append(i)
|
|
ret.replace_int_array('custom', 16, custom)
|
|
|
|
# Tips
|
|
ret.replace_int('last_tips', profile.child_value('tips/last_tips'))
|
|
|
|
# Beast hacker
|
|
hacker = profile.child("hacker")
|
|
for x in hacker.children:
|
|
self.data.local.user.put_achievement(self.game, self.version, userid, x.child_value("id"), "hacker", {
|
|
"state0": x.child_value("state0"), "state1": x.child_value("state1"), "state2": x.child_value("state2"),
|
|
"state3": x.child_value("state3"), "state4": x.child_value("state4")
|
|
})
|
|
return ret
|
|
|
|
def handle_info_stagedata_write_request(self, request: Node) -> Node:
|
|
userid = request.child_value('user_id')
|
|
musicid = request.child_value('select_music_id')
|
|
chartid = request.child_value('select_grade')
|
|
location = self.get_machine_id()
|
|
points = request.child_value('result_score')
|
|
gauge = request.child_value('result_clear_gauge')
|
|
max_combo = request.child_value('result_max_combo')
|
|
grade = request.child_value('result_grade')
|
|
medal = request.child_value('result_medal')
|
|
fantastic_count = request.child_value('result_fanta')
|
|
great_count = request.child_value('result_great')
|
|
fine_count = request.child_value('result_fine')
|
|
miss_count = request.child_value('result_miss')
|
|
|
|
self.update_score(userid, musicid, chartid, location, points, gauge,
|
|
max_combo, grade, medal, fantastic_count, great_count, fine_count, miss_count)
|
|
|
|
return Node.void('player')
|
|
|
|
def handle_pcb_boot_request(self, request: Node) -> Node:
|
|
machine = self.data.local.machine.get_machine(self.config['machine']['pcbid'])
|
|
arcade = self.data.local.machine.get_arcade(machine.arcade)
|
|
pcb2 = Node.void('pcb')
|
|
pcb2.set_attribute('status', '0')
|
|
sinfo = Node.void('sinfo')
|
|
pcb2.add_child(sinfo)
|
|
|
|
sinfo_nm = Node.string('nm', arcade.name)
|
|
sinfo_cl_enbl = Node.bool('cl_enbl', False)
|
|
sinfo_cl_h = Node.u8('cl_h', 0)
|
|
sinfo_cl_m = Node.u8('cl_m', 0)
|
|
sinfo_shop_flag = Node.bool('shop_flag', True)
|
|
|
|
sinfo.add_child(sinfo_nm)
|
|
sinfo.add_child(sinfo_cl_enbl)
|
|
sinfo.add_child(sinfo_cl_h)
|
|
sinfo.add_child(sinfo_cl_m)
|
|
sinfo.add_child(sinfo_shop_flag)
|
|
|
|
return pcb2
|
|
|
|
def handle_info_common_request(self, request: Node) -> Node:
|
|
info2 = Node.void('info')
|
|
info2.set_attribute('status', '0')
|
|
|
|
event_ctrl = self.get_events()
|
|
info2.add_child(event_ctrl)
|
|
|
|
return info2
|
|
|
|
# Called after settings_write, not sure what it does
|
|
def handle_info_music_count_read_request(self, request: Node) -> Node:
|
|
info2 = Node.void('info')
|
|
record = Node.void('record')
|
|
record.add_child(Node.void('rec'))
|
|
record.add_child(Node.void('rate'))
|
|
info2.add_child(record)
|
|
return info2
|
|
|
|
# Called after music_count_read. Might have something to do with song popularity?
|
|
def handle_info_music_ranking_read_request(self, Request: Node) -> Node:
|
|
info2 = Node.void('info')
|
|
ranking = Node.void('ranking')
|
|
ranking.add_child(Node.void('count'))
|
|
info2.add_child(ranking)
|
|
return info2
|
|
|
|
# First call when somebody cards in, returns the status of a few crossover events
|
|
def handle_player_start_request(self, request: Node) -> Node:
|
|
userid = self.data.local.user.from_refid(self.game, self.version, request.child_value('rid'))
|
|
profile = self.data.local.user.get_profile(self.game, self.version, userid)
|
|
player = Node.void('player')
|
|
|
|
plytime = Node.s32('plyid', 0)
|
|
if profile is not None:
|
|
plytime = Node.s32('plyid', profile.get_int("plyid", 1))
|
|
|
|
player.add_child(plytime)
|
|
|
|
start_time = Node.u64('start_time', Time.now() * 1000)
|
|
player.add_child(start_time)
|
|
|
|
return player
|
|
|
|
def handle_lobby_entry_request(self, request: Node) -> Node:
|
|
lobby = Node.void('lobby')
|
|
lobby.add_child(Node.s32('interval', 120))
|
|
lobby.add_child(Node.s32('interval_p', 120))
|
|
selected_lobby = None
|
|
userid = 0
|
|
|
|
sessions = self.data.local.lobby.get_all_play_session_infos(self.game, self.version)
|
|
requested_lobby_id = request.child_value('e/eid')
|
|
|
|
# Beatstream always sends a uid of 0 in testing, so this is how we have to pull the UserID
|
|
for usr, sesh in sessions:
|
|
if sesh.get_str('pcbid') == self.config.machine.pcbid:
|
|
userid = usr
|
|
break
|
|
|
|
if requested_lobby_id > 0:
|
|
# Get the detales of the requested lobby
|
|
for l in self.data.local.lobby.get_all_lobbies(self.game, self.version) or []:
|
|
if l[1].get_int('id') == requested_lobby_id:
|
|
selected_lobby = l
|
|
break
|
|
|
|
else:
|
|
# Make a new lobby
|
|
extid = self.data.local.user.get_extid(self.game, self.version, userid)
|
|
self.data.local.lobby.put_lobby(
|
|
self.game,
|
|
self.version,
|
|
userid,
|
|
{
|
|
'ver': request.child_value('e/ver'),
|
|
'mid': request.child_value('e/mid'),
|
|
'rest': request.child_value('e/rest'),
|
|
'uid': extid,
|
|
'mmode': request.child_value('e/mmode'),
|
|
'mg': request.child_value('e/mg'),
|
|
'mopt': request.child_value('e/mopt'),
|
|
'lid': request.child_value('e/lid'),
|
|
'sn': request.child_value('e/sn'),
|
|
'pref': request.child_value('e/pref'),
|
|
'eatime': request.child_value('e/eatime'),
|
|
'ga': request.child_value('e/ga'),
|
|
'gp': request.child_value('e/gp'),
|
|
'la': request.child_value('e/la'),
|
|
}
|
|
)
|
|
|
|
selected_lobby = self.data.local.lobby.get_lobby(self.game, self.version, userid)
|
|
|
|
lobby.add_child(Node.s32('eid', selected_lobby.get_int('id')))
|
|
e = Node.void('e')
|
|
lobby.add_child(e)
|
|
e.add_child(Node.s32('eid', selected_lobby.get_int('id')))
|
|
e.add_child(Node.u8('ver', selected_lobby.get_int('ver')))
|
|
e.add_child(Node.u16('mid', selected_lobby.get_int('mid')))
|
|
e.add_child(Node.u8('rest', selected_lobby.get_int('rest')))
|
|
e.add_child(Node.s32('uid', selected_lobby.get_int('uid')))
|
|
e.add_child(Node.s32('mmode', selected_lobby.get_int('mmode')))
|
|
e.add_child(Node.s16('mg', selected_lobby.get_int('mg')))
|
|
e.add_child(Node.s32('mopt', selected_lobby.get_int('mopt')))
|
|
e.add_child(Node.string('lid', selected_lobby.get_str('lid')))
|
|
e.add_child(Node.string('sn', selected_lobby.get_str('sn')))
|
|
e.add_child(Node.u8('pref', selected_lobby.get_int('pref')))
|
|
e.add_child(Node.s16('eatime', selected_lobby.get_int('eatime')))
|
|
e.add_child(Node.u8_array('ga', selected_lobby.get_int_array('ga', 4)))
|
|
e.add_child(Node.u16('gp', selected_lobby.get_int('gp')))
|
|
e.add_child(Node.u8_array('la', selected_lobby.get_int_array('la', 4)))
|
|
|
|
return lobby
|
|
|
|
def handle_shop_setting_write_request(self, request: Node) -> Node:
|
|
shop2 = Node.void('shop')
|
|
#TODO: shop settings saving
|
|
return shop2
|
|
|
|
def handle_player_end_request(self, request: Node) -> Node:
|
|
self.data.local.lobby.destroy_play_session_info(self.game, self.version,
|
|
self.data.local.user.from_refid(self.game, self.version, request.child_value("rid")))
|
|
return Node.void('player')
|
|
|
|
# Called either when carding out or creating a new profile
|
|
def handle_player_write_request(self, request: Node) -> Node:
|
|
refid = request.child_value('pdata/account/rid')
|
|
extid = request.child_value('pdata/account/usrid')
|
|
pdata = request.child('pdata')
|
|
reply = Node.void('player')
|
|
|
|
profile = self.unformat_player_profile(pdata)
|
|
userid = self.data.remote.user.from_refid(self.game, self.version, refid) # Get the userid for the refid
|
|
|
|
# The game always wants the extid sent back, so we only have to look it up if it's 0
|
|
if extid == 0:
|
|
extid = self.data.local.user.get_extid(self.game, self.version, userid) # Get the extid for the profile we just saved
|
|
profile.replace_int('usrid', extid) # Replace the extid in the profile with the one generated by butils
|
|
|
|
self.put_profile(userid, profile) # Save the profile
|
|
|
|
node_uid = Node.s32('uid', extid) # Send it back as a node
|
|
reply.add_child(node_uid)
|
|
|
|
return reply
|
|
|
|
def handle_info_matching_data_write(self, request: Node) -> Node:
|
|
return Node.void("info")
|