Add code from old fork

This commit is contained in:
Hay1tsme 2025-10-07 02:05:27 -04:00
parent df4a9c712d
commit a77ad50345
20 changed files with 1922 additions and 5 deletions

View File

@ -0,0 +1,7 @@
from bemani.backend.bst.factory import BSTFactory
from bemani.backend.bst.base import BSTBase
__all__ = [
"BSTFactory",
"BSTBase",
]

View File

@ -0,0 +1,23 @@
from bemani.common import Profile
from bemani.data.types import UserID
from bemani.backend.base import Base
from bemani.backend.core import CoreHandler, CardManagerHandler, PASELIHandler
from bemani.common import GameConstants
from bemani.protocol import Node
class BSTBase(CoreHandler, CardManagerHandler, PASELIHandler, Base):
game = GameConstants.BST
# Helper method that enables events based on the server config
def get_events(self) -> Node:
return Node.void('event_ctrl')
def update_score(self, extid, songid, chartid, loc, points, gauge,
max_combo, grade, medal, fanta_count, great_count, fine_count, miss_count) -> None:
return None
def unformat_player_profile(self, profile: Node, refid: str, extid: int, userid: UserID) -> Profile:
return None
def format_player_profile(self, profile: Profile, userid: UserID) -> Node:
return None

View File

@ -0,0 +1,402 @@
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('player2')
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('info2')
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('info2')
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:
lobby2 = Node.void('lobby2')
lobby2.add_child(Node.s32('interval', 120))
lobby2.add_child(Node.s32('interval_p', 120))
global_ip = "".join(str(e) + "." for e in request.child_value('e/ga'))[:-1],
local_ip = "".join(str(e) + "." for e in request.child_value('e/la'))[:-1],
session = self.data.local.lobby.get_play_session_info_by_ip(self.game, self.version, global_ip, local_ip)
userid = 0
requested_lobby_id = request.child_value('e/eid')
lobby = None
if userid is not None:
userid = session.get_int("userid")
if requested_lobby_id > 0:
# Get the detales of the requested lobby
lobby = self.data.local.lobby.get_lobby_by_id(self.game, self.version, requested_lobby_id)
if lobby is None:
# Make a new lobby
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': request.child_value('e/uid'),
'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'),
}
)
lobby = self.data.local.lobby.get_lobby(self.game, self.version, userid)
lobby2.add_child(Node.s32('eid', lobby.get_int('id')))
e = Node.void('e')
lobby2.add_child(e)
e.add_child(Node.s32('eid', lobby.get_int('id')))
e.add_child(Node.u8('ver', lobby.get_int('ver')))
e.add_child(Node.u16('mid', lobby.get_int('mid')))
e.add_child(Node.u8('rest', lobby.get_int('rest')))
e.add_child(Node.s32('uid', lobby.get_int('mmode')))
e.add_child(Node.s32('mmode', lobby.get_int('mmode')))
e.add_child(Node.s16('mg', lobby.get_int('mg')))
e.add_child(Node.s32('mopt', lobby.get_int('mopt')))
e.add_child(Node.string('lid', lobby.get_str('lid')))
e.add_child(Node.string('sn', lobby.get_str('sn')))
e.add_child(Node.u8('pref', lobby.get_int('pref')))
e.add_child(Node.s16('eatime', lobby.get_int('eatime')))
e.add_child(Node.u8_array('ga', lobby.get_int_array('ga', 4)))
e.add_child(Node.u16('gp', lobby.get_int('gp')))
e.add_child(Node.u8_array('la', lobby.get_int_array('la', 4)))
return lobby2
def handle_shop_setting_write_request(self, request: Node) -> Node:
shop2 = Node.void('shop2')
#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('player2')
# 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('player2')
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")

View File

@ -0,0 +1,869 @@
from typing import Dict, Any
from bemani.common import Profile, ValidatedDict, VersionConstants, ID, Time
from bemani.backend.bst.base import BSTBase
from bemani.common import VersionConstants, Time
from bemani.backend.ess import EventLogHandler
from bemani.common.constants import BroadcastConstants, GameConstants, VersionConstants
from bemani.common.validateddict import ValidatedDict
from bemani.data.types import UserID
from bemani.protocol import Node
from bemani.data import Song
class Beatstream2(EventLogHandler, BSTBase):
name = "BeatStream アニムトライヴ"
version = VersionConstants.BEATSTREAM_2
GRADE_AAA_RED = 0
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
@classmethod
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',
},
{
'name': 'Force Unlock Museca Crossover',
'tip': 'Gives the Illil avitar to players even if they haven\'t played Museca 1+1/2',
'category': 'game_config',
'setting': 'force_unlock_museca_xover',
},
{
'name': 'Force Unlock Floor Infection',
'tip': 'Force unlocks the Floor Infection Part 20 SDVX 3 crossover songs',
'category': 'game_config',
'setting': 'force_unlock_sdvx_xover',
},
{
'name': 'Enable Pop\'n Crossover',
'tip': 'Enables the BeaSt pop\'n tanabata matsuri event',
'category': 'game_config',
'setting': 'enable_popn_xover',
},
{
'name': 'Enable REFLEC Crossover',
'tip': 'Enables the HAPPY☆SUMMER CAMPAIGN event',
'category': 'game_config',
'setting': 'enable_reflec_xover',
},
]
}
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))
# Only enable if we have webhooks set up for this game
if self.config.webhooks.discord[self.game]:
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 if is_play_museca is True
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
def update_score(self, extid, songid, chartid, loc, points, gauge,
max_combo, grade, medal, fanta_count, great_count, fine_count, miss_count) -> None:
butils_userid = self.data.local.user.from_extid(self.game, self.version, extid)
if butils_userid is None:
return None
old_score = self.data.local.music.get_score(self.game, self.version, butils_userid, songid, chartid)
if old_score is not None:
new_record = old_score.points < points
new_points = max(old_score.points, points)
new_data = old_score.data
else:
new_record = True
new_points = points
new_data = ValidatedDict()
new_loc = loc
# Only update the location if it's a new high score
if not new_record:
new_loc = old_score.location
new_gauge = max(new_data.get_int('gauge'), gauge)
new_data.replace_int('gauge', new_gauge)
new_max_combo = max(new_data.get_int('max_combo'), max_combo)
new_data.replace_int('max_combo', new_max_combo)
# Grades get better as their value decreases
if "grade" in new_data:
new_grade = min(new_data.get_int('grade'), grade)
new_data.replace_int('grade', new_grade)
else:
new_data.replace_int('grade', grade)
new_medal = max(new_data.get_int('medal'), medal)
new_data.replace_int('medal', new_medal)
# Only replace notecoutns if we upscored
if new_record:
new_data.replace_int('fanta_count', fanta_count)
new_data.replace_int('great_count', great_count)
new_data.replace_int('fine_count', fine_count)
new_data.replace_int('miss_count', miss_count)
data = {
"gauge": gauge,
"max_combo": max_combo,
"grade": grade,
"medal" : medal,
"fanta_count": fanta_count,
"great_count": great_count,
"fine_count": fine_count,
"miss_count": miss_count
}
self.data.local.music.put_attempt(
self.game,
self.version,
butils_userid,
songid,
chartid,
loc,
points,
data,
new_record,
Time.now())
self.data.local.music.put_score(self.game,
self.version,
butils_userid,
songid,
chartid,
new_loc,
new_points,
new_data,
new_record,
Time.now())
# Helper method to formay a player profile as a Node that the game will accept
def format_player_profile(self, profile: Profile, userid: UserID) -> Node:
root = Node.void("player2")
pdata = Node.void('pdata')
root.add_child(pdata)
account = Node.void('account')
account.add_child(Node.s32('usrid', profile.extid))
account.add_child(Node.s32('is_takeover', profile.get_int("is_takeover")))
account.add_child(Node.s32('tpc', profile.get_int("tpc")))
account.add_child(Node.s32('dpc', profile.get_int("dpc")))
account.add_child(Node.s32('crd', profile.get_int("crd")))
account.add_child(Node.s32('brd', profile.get_int("brd")))
account.add_child(Node.s32('tdc', profile.get_int("tdc")))
account.add_child(Node.s32('intrvld', profile.get_int("intrvld")))
account.add_child(Node.s16('ver', profile.get_int("ver")))
account.add_child(Node.u64('pst', profile.get_int("pst")))
account.add_child(Node.u64('st', Time.now() * 1000))
account.add_child(Node.bool('ea', profile.get_int("ea", True)))
pdata.add_child(account)
base = Node.void('base')
base.add_child(Node.string('name', profile.get_str('name')))
base.add_child(Node.s8('brnk', profile.get_int('brnk')))
base.add_child(Node.s8('bcnum', profile.get_int('bcnum')))
base.add_child(Node.s8('lcnum', profile.get_int('lcnum')))
base.add_child(Node.s32('volt', profile.get_int('volt')))
base.add_child(Node.s32('gold', profile.get_int('gold')))
base.add_child(Node.s32('lmid', profile.get_int('lmid')))
base.add_child(Node.s8('lgrd', profile.get_int('lgrd')))
base.add_child(Node.s8('lsrt', profile.get_int('lsrt')))
base.add_child(Node.s8('ltab', profile.get_int('ltab')))
base.add_child(Node.s8('splv', profile.get_int('splv')))
base.add_child(Node.s8('pref', profile.get_int('pref')))
base.add_child(Node.s32('lcid', profile.get_int('lcid')))
base.add_child(Node.s32('hat', profile.get_int('hat')))
pdata.add_child(base)
survey = Node.void('survey')
survey.add_child(Node.s8('motivate', 0)) # Needs testing
pdata.add_child(survey)
item = Node.void('item')
hacker = Node.void('hacker')
course = Node.void('course')
play_log = Node.void('play_log')
achievements = self.data.local.user.get_achievements(self.game, self.version, userid)
for i in achievements:
if i.type[:4] == "item":
info = Node.void('info')
info.add_child(Node.s32('id', i.id))
info.add_child(Node.s32('type', i.data.get_int('type')))
info.add_child(Node.s32('param', i.data.get_int('param')))
info.add_child(Node.s32('count', i.data.get_int('count')))
item.add_child(info)
elif i.type == "hacker":
info = Node.void('info')
info.add_child(Node.s32('id', i.id))
info.add_child(Node.s8('state0', i.data.get_int("state0")))
info.add_child(Node.s8('state1', i.data.get_int("state1")))
info.add_child(Node.s8('state2', i.data.get_int("state2")))
info.add_child(Node.s8('state3', i.data.get_int("state3")))
info.add_child(Node.s8('state4', i.data.get_int("state4")))
info.add_child(Node.u64('update_time', i.data.get_int('update_time', Time.now() * 1000))) # update_time is required or the profile will fail
hacker.add_child(info)
elif i.type == "course":
info = Node.void('record')
info.add_child(Node.s32('course_id', i.data.get_int("course_id")))
info.add_child(Node.s32('play', i.data.get_int("clear"))) # Play_id?
info.add_child(Node.bool('is_touch', i.data.get_bool("is_touch", False))) # ???
info.add_child(Node.s32('clear', i.data.get_int("clear"))) # Not sure what it wants here...
info.add_child(Node.s32('gauge', i.data.get_int("gauge")))
info.add_child(Node.s32('score', i.data.get_int("score")))
info.add_child(Node.s32('grade', i.data.get_int("grade")))
info.add_child(Node.s32('medal', i.data.get_int("medal")))
info.add_child(Node.s32('combo', i.data.get_int("combo")))
course.add_child(info)
rate = Node.void('rate')
rate.add_child(Node.s32('course_id', i.data.get_int("course_id")))
rate.add_child(Node.s32('play_count', i.data.get_int("play_count")))
rate.add_child(Node.s32('clear_count', i.data.get_int("clear_count")))
course.add_child(rate)
elif i.type == "crysis":
crysis = Node.void('crysis')
crysis.add_child(Node.s32('id', i.id))
crysis.add_child(Node.s8('step', i.data.get_int("step")))
crysis.add_child(Node.s32('r_gauge', i.data.get_int("r_gauge")))
crysis.add_child(Node.s8('r_state', i.data.get_int("r_state")))
play_log.add_child(crysis)
pdata.add_child(item)
pdata.add_child(course)
pdata.add_child(hacker)
pdata.add_child(play_log)
customize = Node.void('customize')
customize.add_child(Node.u16_array('custom', profile.get_int_array('custom', 16)))
pdata.add_child(customize)
tips = Node.void('tips')
tips.add_child(Node.s32('last_tips', profile.get_int('last_tips')))
pdata.add_child(tips)
record = Node.void('record')
scores = self.data.local.music.get_scores(self.game, self.version, userid)
for i in scores:
rec = Node.void('rec')
rec.add_child(Node.s32('music_id', i.id))
rec.add_child(Node.s32('note_level', i.chart))
rec.add_child(Node.s32('play_count', i.plays))
rec.add_child(Node.s32('clear_count', i.data.get_int('clear_count')))
rec.add_child(Node.s32('best_gauge', i.data.get_int('gauge')))
rec.add_child(Node.s32('best_score', i.points))
rec.add_child(Node.s32('best_grade', i.data.get_int('grade')))
rec.add_child(Node.s32('best_medal', i.data.get_int('medal')))
record.add_child(rec)
pdata.add_child(record)
return root
# Helper method to unformat the player profile into a ValidatedDict for the DB
def unformat_player_profile(self, profile: Node, refid: str, extid: int, userid: UserID) -> Profile:
ret = Profile(self.game, self.version, refid, extid)
# Account
next_tpc = int(profile.child_value('account/tpc')) + 1
ret.replace_int('is_takeover', int(profile.child_value('account/is_takeover')))
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')))
ret.replace_int('st', int(profile.child_value('account/st')))
# 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')))
ret.replace_int('lcid', int(profile.child_value('base/lcid')))
ret.replace_int('hat', int(profile.child_value('base/hat')))
# Items stored as achievements
items = profile.child('item')
if items is not None:
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)
# Beast Crysis and multiplayer data
play_log = profile.child('play_log')
if play_log is not None:
for i in play_log.children:
if i.name == "crysis":
self.data.local.user.put_achievement(self.game, self.version, userid, i.child_value('id'),
"crysis", {"step": i.child_value('step'), "r_gauge": i.child_value('r_gauge'),
"r_state": i.child_value('r_state')})
elif i.name == "onmatch":
pass
# 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"), 'update_time': Time.now() * 1000
})
return ret
# First call when somebody cards in, returns the status of a few crossover events
def handle_player2_start_request(self, request: Node, is_continue: bool = False) -> Node:
userid = self.data.local.user.from_refid(self.game, self.version, request.child_value('rid'))
player2 = Node.void('player2')
play_id = 0
game_config = self.get_game_config()
if userid is not None:
if not is_continue:
self.data.local.lobby.put_play_session_info(
self.game,
self.version,
userid,
# This is needed due to lobby2/entry's lack of user info
{ "pcbid": self.config.machine.pcbid }
)
profile = self.data.local.user.get_profile(self.game, self.version, userid)
if profile is None:
profile = self.data.local.user.get_profile(self.game, self.version - 1, userid)
if profile is not None:
play_id = profile.get_int('tpc') + 1
# Session stuff, and resend global defaults
player2.add_child(Node.s32('plyid', play_id))
start_time = Node.u64('start_time', Time.now() * 1000)
player2.add_child(start_time)
# Crossover events
# HAPPY☆SUMMER CAMPAIGN possibly?
reflec_collabo = Node.bool('reflec_collabo', game_config.get_bool("enable_reflec_xover"))
player2.add_child(reflec_collabo)
# BeaSt pop'n tanabata matsuri possibly?
pop_collabo = Node.bool('pop_collabo', game_config.get_bool("enable_popn_xover"))
player2.add_child(pop_collabo)
# Floor Infection Part 20
floor_infection = Node.void('floor_infection')
player2.add_child(floor_infection)
fi_event = Node.void('event')
floor_infection.add_child(fi_event)
fi_event.add_child(Node.s32('infection_id', 20))
if game_config.get_bool("force_unlock_sdvx_xover"):
# TODO: Test if 7 actually is the magic number, given there's only 3 songs
fi_event.add_child(Node.s32('music_list', 7))
fi_event.add_child(Node.bool('is_complete', True))
else:
# TODO: Figure out how to track floor infection lol
fi_event.add_child(Node.s32('music_list', 7))
fi_event.add_child(Node.bool('is_complete', True))
# If you played Museca 1+1/2 at launch you got rewards in BST and other games
museca = Node.void('museca')
player2.add_child(museca)
if game_config.get_bool("enable_popn_xover"):
museca.add_child(Node.bool('is_play_museca', True))
else:
m112_profile = self.data.local.user.get_profile(GameConstants.MUSECA, VersionConstants.MUSECA_1_PLUS, userid)
museca.add_child(Node.bool('is_play_museca', m112_profile is not None))
return player2
# Called when carding in to get the player profile
def handle_player2_read_request(self, request: Node) -> Node:
refid = request.child_value('rid')
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
profile = self.get_profile(userid)
return self.format_player_profile(profile, userid)
# Called either when carding out or creating a new profile
def handle_player2_write_request(self, request: Node) -> Node:
refid = request.child_value('pdata/account/rid')
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
extid = self.data.local.user.get_extid(self.game, self.version, userid)
pdata = request.child('pdata')
reply = Node.void('player2')
profile = self.unformat_player_profile(pdata, refid, extid, userid)
self.put_profile(userid, profile) # Save the profile
node_uid = Node.s32('uid', extid) # Send the extid back as a node
reply.add_child(node_uid)
return reply
# Called whenever some kind of error happens.
def handle_pcb2_error_request(self, request: Node) -> Node:
return Node.void('pcb2')
# BST2 has to be special and have it's own boot method
def handle_pcb2_boot_request(self, request: Node) -> Node:
shop_id = ID.parse_machine_id(request.child_value('lid'))
pcbid = self.data.local.machine.from_machine_id(shop_id)
if pcbid is not None:
machine = self.data.local.machine.get_machine(pcbid)
machine_name = machine.name
close = machine.data.get_bool('close')
hour = machine.data.get_int('hour')
minute = machine.data.get_int('minute')
else:
return None
pcb2 = Node.void('pcb2')
sinfo = Node.void('sinfo')
pcb2.add_child(sinfo)
sinfo.add_child(Node.string('nm', machine_name))
sinfo.add_child(Node.bool('cl_enbl', close))
sinfo.add_child(Node.u8('cl_h', hour))
sinfo.add_child(Node.u8('cl_m', minute))
return pcb2
# Send a list of events and phases
def handle_info2_common_request(self, request: Node) -> Node:
info2 = Node.void('info2')
info2.set_attribute('status', '0')
event_ctrl = self.get_events()
info2.add_child(event_ctrl)
return info2
# Called when a player registeres a new profile when they have an account
def handle_player2_succeed_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 - 1, userid)
scores = self.data.local.music.get_scores(self.game, self.version - 1, userid)
achievements = self.data.local.user.get_achievements(self.game, self.version - 1, userid)
player2 = Node.void('player2')
play = Node.bool('play', profile.get_int('tpc') > 1 if profile else False)
player2.add_child(play)
data = Node.void('data')
player2.add_child(data)
name = Node.string('name', profile.get_str('name') if profile else "")
data.add_child(name)
record = Node.void('record')
hacker = Node.void('hacker')
phantom = Node.void('phantom')
for score in scores:
rec = Node.void('rec')
rec.add_child(Node.s32('music_id', score.id))
rec.add_child(Node.s32('note_level', score.chart))
rec.add_child(Node.s32('play_count', score.plays))
rec.add_child(Node.s32('clear_count', score.data.get_int('clear_count')))
rec.add_child(Node.s32('best_gauge', score.data.get_int('gauge')))
rec.add_child(Node.s32('best_score', score.points))
rec.add_child(Node.s32('best_grade', score.data.get_int('grade')))
rec.add_child(Node.s32('best_medal', score.data.get_int('medal')))
record.add_child(rec)
for ach in achievements:
if ach.type == "hacker":
info = Node.void('info')
info.add_child(Node.s32('id', ach.id))
info.add_child(Node.s8('state0', ach.data.get_int("state0")))
info.add_child(Node.s8('state1', ach.data.get_int("state1")))
info.add_child(Node.s8('state2', ach.data.get_int("state2")))
info.add_child(Node.s8('state3', ach.data.get_int("state3")))
info.add_child(Node.s8('state4', ach.data.get_int("state4")))
info.add_child(Node.u64('update_time', ach.data.get_int('update_time', Time.now() * 1000))) # update_time is required or the profile will fail
hacker.add_child(info)
elif ach.type == "phantom":
minfo = Node.void('minfo')
minfo.add_child(Node.s32('mid', ach.data.get_int("mid")))
minfo.add_child(Node.s64('cbit', ach.data.get_int("cbit")))
minfo.add_child(Node.bool('clr', ach.data.get_bool("clr")))
phantom.add_child(info)
player2.add_child(hacker)
player2.add_child(record)
player2.add_child(phantom)
return player2
# Called during boot
def handle_shop2_setting_write_request(self, request: Node) -> Node:
shop2 = Node.void('shop2')
#TODO: shop settings saving
return shop2
# Called after settings_write, not sure what it does
def handle_info2_music_count_read_request(self, request: Node) -> Node:
info2 = Node.void('info2')
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_info2_music_ranking_read_request(self, Request: Node) -> Node:
info2 = Node.void('info2')
ranking = Node.void('ranking')
ranking.add_child(Node.void('count'))
info2.add_child(ranking)
return info2
# Called on card out
def handle_player2_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('player2')
# Called after finishing a song
def handle_player2_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('player2')
# Called after finishing a song in a course
def handle_player2_course_stage_data_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('player2')
# Called after finishing a course
def handle_player2_course_data_write_request(self, request: Node) -> Node:
return Node.void('player2')
# Called frequently to see who's playing
def handle_lobby2_get_lobby_list_request(self, request: Node) -> Node:
lobby2 = Node.void('lobby2')
lobby2.add_child(Node.s32('interval_sec', 10))
lobbies = self.data.local.lobby.get_all_lobbies(self.game, self.version)
if lobbies is not None:
for (user, lobby) in lobbies:
e = Node.void('e')
lobby2.add_child(e)
e.add_child(Node.u16('mid', lobby.get_int('mid')))
e.add_child(Node.u64('eatime', lobby.get_int('eatime')))
return lobby2
# Called to destroy a lobby after it's use
def handle_lobby2_delete_request(self, request: Node) -> Node:
self.data.local.lobby.destroy_lobby(request.child_value("eid"))
return Node.void('lobby2')
# Called when matching starts
def handle_lobby2_entry_request(self, request: Node) -> Node:
lobby2 = Node.void('lobby2')
lobby2.add_child(Node.s32('interval', 120))
lobby2.add_child(Node.s32('interval_p', 120))
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 sends 0 identifiable user information, 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
lobby = self.data.local.lobby.get_lobby(self.game, self.version, requested_lobby_id)
else:
# Make a new lobby
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': request.child_value('e/uid'),
'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'),
}
)
# Pull the lobby details back down to get the ID
lobby = self.data.local.lobby.get_lobby(self.game, self.version, userid)
lobby2.add_child(Node.s32('eid', lobby.get_int('id')))
e = Node.void('e')
lobby2.add_child(e)
e.add_child(Node.s32('eid', lobby.get_int('id')))
e.add_child(Node.u8('ver', lobby.get_int('ver')))
e.add_child(Node.u16('mid', lobby.get_int('mid')))
e.add_child(Node.u8('rest', lobby.get_int('rest')))
e.add_child(Node.s32('uid', lobby.get_int('mmode')))
e.add_child(Node.s32('mmode', lobby.get_int('mmode')))
e.add_child(Node.s16('mg', lobby.get_int('mg')))
e.add_child(Node.s32('mopt', lobby.get_int('mopt')))
e.add_child(Node.string('lid', lobby.get_str('lid')))
e.add_child(Node.string('sn', lobby.get_str('sn')))
e.add_child(Node.u8('pref', lobby.get_int('pref')))
e.add_child(Node.s16('eatime', lobby.get_int('eatime')))
e.add_child(Node.u8_array('ga', lobby.get_int_array('ga', 4)))
e.add_child(Node.u16('gp', lobby.get_int('gp')))
e.add_child(Node.u8_array('la', lobby.get_int_array('la', 4)))
return lobby2
# Called when a player tries to continue another credit
def handle_player2_continue_request(self, request: Node) -> Node:
return self.handle_player2_start_request(request, True)
# Called when a user request an eamuse app screenshot
def handle_info2_result_image_write_request(self, request: Node) -> Node:
song: Song = self.data.local.music.get_song(self.game, self.version, request.child_value("music_id"),
request.child_value("music_level"))
grades = ["Red AAA", "AAA", "AA", "A", "B", "C", "D"]
medals = ["No Play", "Failed", "Saved", "Cleared", "Full Combo", "Perfect"]
card_data = {
BroadcastConstants.PLAYER_NAME: request.child_value("player_name"),
BroadcastConstants.SONG_NAME: request.child_value("music_title"),
BroadcastConstants.ARTIST_NAME: request.child_value("artist_name"),
BroadcastConstants.DIFFICULTY: request.child_value("music_level"),
BroadcastConstants.DIFFICULTY_LEVEL: song.data.get_int("difficulty"),
#BroadcastConstants.BEAST_RANK: request.child_value("beast_rank"),
BroadcastConstants.SCORE: request.child_value("score"),
#BroadcastConstants.BEST_SCORE: request.child_value("best_score"),
BroadcastConstants.GAUGE: float(request.child_value("gauge") / 10),
BroadcastConstants.MEDAL: medals[request.child_value("medal")],
BroadcastConstants.GRADE: grades[request.child_value("grade")],
BroadcastConstants.MAX_COMBO: request.child_value("max_combo"),
#BroadcastConstants.FANTASTIC: request.child_value("fanta"),
#BroadcastConstants.GREAT: request.child_value("great"),
#BroadcastConstants.FINE: request.child_value("fine"),
#BroadcastConstants.MISS: request.child_value("miss"),
}
self.data.triggers.broadcast_score(card_data, self.game, song)
return Node.void("info2")
# Called when matching
def handle_player2_matching_data_load_request(self, request: Node) -> Node:
root = Node.void('player_matching')
data = Node.void('data')
data.add_child(Node.s32('id', 0)) # player id?
data.add_child(Node.bool('fl', False)) # First Local
data.add_child(Node.bool('fo', False)) # First Online
root.add_child(root)
# Called when saving machine settings
def handle_shop2_info_write_request(self, request: Node) -> Node:
mech = self.data.local.machine.get_machine(self.config.machine.pcbid)
if mech is not None:
mech.data["close"] = request.child_value('sinfo/cl_enbl')
mech.data["hour"] = request.child_value('sinfo/cl_h')
mech.data["minute"] = request.child_value('sinfo/cl_m')
mech.data["pref"] = request.child_value('sinfo/prf')
mech.name = request.child_value('sinfo/nm')
mech.game = self.game
mech.version = self.version
self.data.local.machine.put_machine(mech)
return Node.void("shop2")

View File

@ -0,0 +1,32 @@
from typing import Dict, Optional, Any
from bemani.backend.base import Base, Factory
from bemani.backend.bst.beatstream2 import Beatstream2
from bemani.backend.bst.beatstream import Beatstream
from bemani.common import Model
from bemani.data import Data
class BSTFactory(Factory):
MANAGED_CLASSES = [
Beatstream2,
]
@classmethod
def register_all(cls) -> None:
for game in ['NBT']:
Base.register(game, BSTFactory)
@classmethod
def create(cls, data: Data, config: Dict[str, Any], model: Model, parentmodel: Optional[Model]=None) -> Optional[Base]:
if model.gamecode == 'NBT':
if model.version is None:
return None
if model.version <= 2015121600: # Beatstream 1
return Beatstream(data, config, model)
if model.version <= 2016111400 and model.version > 2015121600: # Beatstream 2
return Beatstream2(data, config, model)
# Unknown game version
return None

View File

@ -0,0 +1,5 @@
from bemani.client.bst.beatstream2 import Beatstream2Client
__all__ = [
"Beatstream2Client",
]

View File

@ -0,0 +1,261 @@
from typing import Optional
from bemani.client.base import BaseClient
from bemani.common.constants import GameConstants, VersionConstants
from bemani.common.time import Time
from bemani.common.validateddict import Profile
from bemani.data import Song
from bemani.protocol import Node
class Beatstream2Client(BaseClient):
name = 'TEST'
def verify_player2_start(self, refid: str) -> int:
call = self.call_node()
player2 = Node.void("player2")
player2.set_attribute('method', 'start')
player2.add_child(Node.string("rid", refid))
player2.add_child(Node.u8_array("ga", [192, 168, 1, 2]))
player2.add_child(Node.u16("gp", 1234))
player2.add_child(Node.u8_array("la", [192, 168, 1, 2]))
call.add_child(player2)
resp = self.exchange('', call)
self.assert_path(resp, "response/player2/plyid")
return resp.child_value("player2/plyid")
def verify_player2_read(self, refid: str) -> Profile:
call = self.call_node()
player2 = Node.void("player2")
player2.set_attribute('method', 'read')
player2.add_child(Node.string("rid", refid))
player2.add_child(Node.string("lid", "JP-1"))
player2.add_child(Node.s16("ver", 0))
call.add_child(player2)
resp = self.exchange('', call)
self.assert_path(resp, "response/player2/pdata/account/usrid")
self.assert_path(resp, "response/player2/pdata/base/name")
profile = resp.child("player2/pdata")
ret = Profile("bst", 2, refid, profile.child_value('account/usrid'))
ret.replace_int('usrid', int(profile.child_value('account/usrid')))
ret.replace_int('is_takeover', int(profile.child_value('account/is_takeover')))
ret.replace_int('tpc', profile.child_value('account/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')))
ret.replace_int('st', int(profile.child_value('account/st')))
# 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')))
ret.replace_int('lcid', int(profile.child_value('base/lcid')))
ret.replace_int('hat', int(profile.child_value('base/hat')))
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)
ret.replace_int('last_tips', profile.child_value('tips/last_tips'))
return ret
def verify_player2_stagedata_write(self, profile: Profile, song: Song, sid: int) -> None:
call = self.call_node()
player2 = Node.void("player2")
player2.set_attribute('method', 'stagedata_write')
player2.add_child(Node.s32('play_id', sid))
player2.add_child(Node.s32('continue_count', 0))
player2.add_child(Node.s32('stage_no', 0))
player2.add_child(Node.s32('user_id', profile.get_int('usrid')))
player2.add_child(Node.string('location_id', "JP-1"))
player2.add_child(Node.s32('select_music_id', song.id))
player2.add_child(Node.s32('select_grade', song.chart))
player2.add_child(Node.s32('result_clear_gauge', 1000))
player2.add_child(Node.s32('result_score', 888392))
player2.add_child(Node.s32('result_max_combo', 114))
player2.add_child(Node.s32('result_grade', 0))
player2.add_child(Node.s32('result_medal', 3))
player2.add_child(Node.s32('result_fanta', 68))
player2.add_child(Node.s32('result_great', 35))
player2.add_child(Node.s32('result_fine', 7))
player2.add_child(Node.s32('result_miss', 2))
call.add_child(player2)
resp = self.exchange('', call)
self.assert_path(resp, "response/player2/@status")
def verify_player2_write(self, profile: Profile, sid: int) -> None:
call = self.call_node()
player2 = Node.void("player2")
player2.set_attribute('method', 'write')
pdata = Node.void('pdata')
player2.add_child(pdata)
account = Node.void('account')
account.add_child(Node.s32('usrid', profile.get_int("usrid")))
account.add_child(Node.s32('is_takeover', profile.get_int("is_takeover")))
account.add_child(Node.s32('plyid', sid))
account.add_child(Node.s32('continue_cnt', 0))
account.add_child(Node.s32('tpc', profile.get_int("tpc")))
account.add_child(Node.s32('dpc', profile.get_int("dpc")))
account.add_child(Node.s32('crd', profile.get_int("crd")))
account.add_child(Node.s32('brd', profile.get_int("brd")))
account.add_child(Node.s32('tdc', profile.get_int("tdc")))
account.add_child(Node.string('rid', profile.refid))
account.add_child(Node.string('lid', "JP-1"))
account.add_child(Node.s32('intrvld', profile.get_int("intrvld")))
account.add_child(Node.s16('ver', profile.get_int("ver")))
account.add_child(Node.u64('pst', profile.get_int("pst")))
account.add_child(Node.u64('st', Time.now() * 1000))
account.add_child(Node.bool('ea', profile.get_int("ea", True)))
pdata.add_child(account)
base = Node.void('base')
base.add_child(Node.string('name', profile.get_str('name')))
base.add_child(Node.s8('brnk', profile.get_int('brnk')))
base.add_child(Node.s8('bcnum', profile.get_int('bcnum')))
base.add_child(Node.s8('lcnum', profile.get_int('lcnum')))
base.add_child(Node.s32('volt', profile.get_int('volt')))
base.add_child(Node.s32('gold', profile.get_int('gold')))
base.add_child(Node.s32('lmid', profile.get_int('lmid')))
base.add_child(Node.s8('lgrd', profile.get_int('lgrd')))
base.add_child(Node.s8('lsrt', profile.get_int('lsrt')))
base.add_child(Node.s8('ltab', profile.get_int('ltab')))
base.add_child(Node.s8('splv', profile.get_int('splv')))
base.add_child(Node.s8('pref', profile.get_int('pref')))
base.add_child(Node.s32('lcid', profile.get_int('lcid')))
base.add_child(Node.s32('hat', profile.get_int('hat')))
pdata.add_child(base)
pdata.add_child(Node.void("item"))
customize = Node.void('customize')
customize.add_child(Node.u16_array('custom', profile.get_int_array('custom', 16)))
pdata.add_child(customize)
tips = Node.void('tips')
tips.add_child(Node.s32('last_tips', profile.get_int('last_tips')))
pdata.add_child(tips)
pdata.add_child(Node.void("hacker"))
play_log = Node.void("play_log")
crisis = Node.void("crysis")
crisis.add_child(Node.s32("id", 0))
crisis.add_child(Node.s32("stage_no", 0))
crisis.add_child(Node.s8("step", 0))
crisis.add_child(Node.s32("r_gauge", 95))
crisis.add_child(Node.s8("r_state", 0))
play_log.add_child(crisis)
crisis = Node.void("crysis")
crisis.add_child(Node.s32("id", 0))
crisis.add_child(Node.s32("stage_no", 1))
crisis.add_child(Node.s8("step", 1))
crisis.add_child(Node.s32("r_gauge", 192))
crisis.add_child(Node.s8("r_state", 1))
play_log.add_child(crisis)
crisis = Node.void("crysis")
crisis.add_child(Node.s32("id", 0))
crisis.add_child(Node.s32("stage_no", 2))
crisis.add_child(Node.s8("step", 1))
crisis.add_child(Node.s32("r_gauge", 214))
crisis.add_child(Node.s8("r_state", 0))
play_log.add_child(crisis)
pdata.add_child(play_log)
call.add_child(player2)
resp = self.exchange('', call)
self.assert_path(resp, "response/player2/uid")
def verify_info2_result_image_write(self, profile: Profile, song: Song, sid: int) -> None:
call = self.call_node()
info2 = Node.void("info2")
info2.set_attribute('method', 'result_image_write')
info2.add_child(Node.s32("play_id", sid))
info2.add_child(Node.s32("continue_no", 0))
info2.add_child(Node.s32("stage_no", 0))
info2.add_child(Node.string("ref_id", profile.refid))
info2.add_child(Node.s32("beast_rank", 0))
info2.add_child(Node.string("player_name", profile.get_str("name")))
info2.add_child(Node.s32("music_id", song.id))
info2.add_child(Node.s32("music_grade", 0))
info2.add_child(Node.s32("music_level", song.chart))
info2.add_child(Node.string("music_title", song.name))
info2.add_child(Node.string("artist_name", song.artist))
info2.add_child(Node.s32("gauge", 995))
info2.add_child(Node.s32("grade", 0))
info2.add_child(Node.s32("score", 820567))
info2.add_child(Node.s32("best_score", 0))
info2.add_child(Node.s32("medal", 3))
info2.add_child(Node.bool("is_new_record", False))
info2.add_child(Node.s32("fanta", 81))
info2.add_child(Node.s32("great", 29))
info2.add_child(Node.s32("fine", 23))
info2.add_child(Node.s32("miss", 8))
call.add_child(info2)
resp = self.exchange('', call)
self.assert_path(resp, "response/info2/@status")
def verify_player2_end(self, refid: str) -> None:
call = self.call_node()
player2 = Node.void("player2")
player2.set_attribute('method', 'end')
player2.add_child(Node.string("rid", refid))
call.add_child(player2)
resp = self.exchange('', call)
self.assert_path(resp, "response/player2/@status")
def verify(self, cardid: Optional[str]) -> None:
# Make sure we can card in properly
refid = self.verify_cardmng_inquire(cardid, "query", True)
sid = self.verify_player2_start(refid)
profile = self.verify_player2_read(refid)
# Make sure courses, songs, and scorecards save properly
test_song = Song(GameConstants.BST, VersionConstants.BEATSTREAM_2,
129, 0, "チョコレートスマイル", "ちよこれえとすまいる", "", {})
self.verify_info2_result_image_write(profile, test_song, sid)
self.verify_player2_stagedata_write(profile, test_song, sid)
# Make sure profile saves properly, and game ends gracefully
self.verify_player2_write(profile, sid)
self.verify_player2_end(refid)

View File

@ -21,6 +21,7 @@ class GameConstants(Enum):
POPN_MUSIC = "pnm"
REFLEC_BEAT = "reflec"
SDVX = "sdvx"
BST = "bst"
class VersionConstants:
@ -142,6 +143,9 @@ class VersionConstants:
SDVX_GRAVITY_WARS: Final[int] = 3
SDVX_HEAVENLY_HAVEN: Final[int] = 4
BEATSTREAM: Final[int] = 1
BEATSTREAM_2: Final[int] = 2
class APIConstants(Enum):
"""
@ -341,6 +345,17 @@ class BroadcastConstants(Enum):
COMBO = "Combo"
MEDAL = "Medal"
# Added for BST
DIFFICULTY_LEVEL = "Difficulty"
BEAST_RANK = "Beast Rank"
BEST_SCORE = "Best Score"
GAUGE = "Gauge"
MAX_COMBO = "Max Combo"
FANTASTIC = "Fantastic"
GREAT = "Great"
FINE = "Fine"
MISS = "Miss"
class _RegionConstants:
"""

View File

@ -27,6 +27,7 @@ class Triggers:
GameConstants.POPN_MUSIC: "Pop'n Music",
GameConstants.REFLEC_BEAT: "Reflec Beat",
GameConstants.SDVX: "Sound Voltex",
GameConstants.BST: "BeatStream",
}.get(game, "Unknown")
def has_broadcast_destination(self, game: GameConstants) -> bool:
@ -43,7 +44,7 @@ class Triggers:
self.broadcast_score_discord(data, game, song)
def broadcast_score_discord(self, data: Dict[BroadcastConstants, str], game: GameConstants, song: Song) -> None:
if game in {GameConstants.IIDX, GameConstants.POPN_MUSIC}:
if game in {GameConstants.IIDX, GameConstants.POPN_MUSIC, GameConstants.BST}:
now = datetime.now()
webhook = DiscordWebhook(url=self.config.webhooks.discord[game])

View File

@ -856,6 +856,23 @@ def navigation() -> Dict[str, Any]:
"gamecode": GameConstants.SDVX.value,
},
)
if GameConstants.BST in g.config.support:
bst_entries = []
bst_entries.extend([
{
'label': 'Global Scores',
'uri': url_for('bst_pages.viewnetworkscores'),
}
])
pages.append(
{
'label': 'BeatStream',
'entries': bst_entries,
'base_uri': app.blueprints['bst_pages'].url_prefix,
'gamecode': GameConstants.BST.value,
},
)
# Admin pages
if user.admin:

View File

@ -0,0 +1,8 @@
from bemani.frontend.bst.endpoints import bst_pages
from bemani.frontend.bst.cache import BSTCache
__all__ = [
"BSTCache",
"bst_pages",
]

View File

@ -0,0 +1,18 @@
import copy
from typing import Any, Dict, Iterator, List, Tuple
from bemani.backend.bst import BSTFactory, BSTBase
from bemani.common import Profile, ValidatedDict, GameConstants, VersionConstants
from bemani.data import Attempt, Link, RemoteUser, Score, Song, UserID
from bemani.frontend.base import FrontendBase
class BSTFrontend(FrontendBase):
game: GameConstants = GameConstants.BST
def all_games(self) -> Iterator[Tuple[GameConstants, int, str]]:
yield from BSTFactory.all_games()
def update_name(self, profile: Profile, name: str) -> Profile:
newprofile = copy.deepcopy(profile)
newprofile.replace_str('name', name)
return newprofile

View File

@ -0,0 +1,16 @@
from flask_caching import Cache # type: ignore
from bemani.data import Config, Data
from bemani.frontend.app import app
from bemani.frontend.bst.bst import BSTFrontend
class BSTCache:
@classmethod
def preload(cls, data: Data, config: Config) -> None:
cache = Cache(app, config={
'CACHE_TYPE': 'filesystem',
'CACHE_DIR': config.cache_dir,
})
frontend = BSTFrontend(data, config, cache)
frontend.get_all_songs(force_db_load=True)

View File

@ -0,0 +1,52 @@
import re
from typing import Any, Dict, List, Optional
from flask import Blueprint, request, Response, url_for, abort
from bemani.common import ID, GameConstants
from bemani.data import Link, UserID
from bemani.frontend.app import loginrequired, jsonify, render_react
from bemani.frontend.bst.bst import BSTFrontend
from bemani.frontend.templates import templates_location
from bemani.frontend.static import static_location
from bemani.frontend.types import g
bst_pages = Blueprint(
'bst_pages',
__name__,
url_prefix=f'/{GameConstants.BST.value}',
template_folder=templates_location,
static_folder=static_location,
)
@bst_pages.route("/scores")
@loginrequired
def viewnetworkscores() -> Response:
frontend = BSTFrontend(g.data, g.config, g.cache)
network_scores = frontend.get_network_scores(limit=100)
if len(network_scores['attempts']) > 10:
network_scores['attempts'] = frontend.round_to_ten(network_scores['attempts'])
return render_react(
'Global DDR Scores',
'bst/scores.react.js',
{
'attempts': network_scores['attempts'],
'songs': frontend.get_all_songs(),
'players': network_scores['players'],
'versions': {version: name for (game, version, name) in frontend.all_games()},
'shownames': True,
'shownewrecords': False,
},
{
'refresh': url_for('bst_pages.listnetworkscores'),
'player': url_for('bst_pages.viewplayer', userid=-1),
'individual_score': url_for('bst_pages.viewtopscores', musicid=-1),
},
)
@bst_pages.route('/scores/list')
@jsonify
@loginrequired
def listnetworkscores() -> Dict[str, Any]:
frontend = BSTFrontend(g.data, g.config, g.cache)
return frontend.get_network_scores()

View File

@ -0,0 +1 @@
/*** @jsx React.DOM */

View File

@ -12,6 +12,7 @@ from bemani.backend.sdvx import SoundVoltexFactory
from bemani.backend.reflec import ReflecBeatFactory
from bemani.backend.museca import MusecaFactory
from bemani.backend.mga import MetalGearArcadeFactory
from bemani.backend.bst import BSTFactory
from bemani.common import GameConstants, cache
from bemani.data import Config, Data
@ -81,3 +82,5 @@ def register_games(config: Config) -> None:
MetalGearArcadeFactory.register_all()
if GameConstants.DANCE_EVOLUTION in config.support:
DanceEvolutionFactory.register_all()
if GameConstants.BST in config.support:
BSTFactory.register_all()

View File

@ -17,6 +17,7 @@ from bemani.frontend.sdvx import sdvx_pages
from bemani.frontend.reflec import reflec_pages
from bemani.frontend.museca import museca_pages
from bemani.frontend.danevo import danevo_pages
from bemani.frontend.bst import bst_pages
from bemani.utils.config import (
load_config as base_load_config,
instantiate_cache as base_instantiate_cache,
@ -50,6 +51,8 @@ def register_blueprints() -> None:
app.register_blueprint(museca_pages)
if GameConstants.DANCE_EVOLUTION in config.support:
app.register_blueprint(danevo_pages)
if GameConstants.BST in config.support:
app.register_blueprint(bst_pages)
def register_games() -> None:

View File

@ -6207,6 +6207,168 @@ class ImportDanceEvolution(ImportBase):
self.finish_batch()
class ImportBst(ImportBase):
def __init__ (
self,
config: Config,
version: str,
no_combine: bool,
update: bool
) -> None:
if version in ['1', '2']:
actual_version = {
'1': VersionConstants.BEATSTREAM,
'2': VersionConstants.BEATSTREAM_2,
}.get(version, -1)
elif version == 'all':
actual_version = None
if actual_version in [
None,
VersionConstants.BEATSTREAM,
VersionConstants.BEATSTREAM_2,
]:
self.charts = [0, 1, 2, 3]
else:
raise Exception("Unsupported Beatstream version! Please use one of the following: 1, 2.")
super().__init__(config, GameConstants.BST, actual_version, no_combine, update)
def scrape(self, infile:str) -> List[Dict[str, Any]]:
if self.version is None:
raise Exception('Can\'t scrape database for \'all\' version!')
songs = []
with open(infile, encoding='utf_16_le') as musicdb:
songs = []
reader = csv.reader(musicdb, delimiter='', quotechar='"')
for row in reader:
if self.version == VersionConstants.BEATSTREAM:
if 'MusicInfoData' in row[0]:
continue
if 'EOF' in row[0]:
continue
songid = int(row[0])
name = row[1]
genre = row[3]
bpm_min = float(row[7])
bpm_max = float(row[8])
light = (row[9])
medium = (row[10])
beast = (row[11])
nightmare = (row[12])
artist = row[13]
if self.version == VersionConstants.BEATSTREAM_2:
if 'MusicInfoData' in row[0]:
continue
if 'EOF' in row[0]:
continue
songid = int(row[0])
name = row[1]
genre = row[4]
bpm_min = float(row[8])
bpm_max = float(row[9])
light = row[10]
medium = row[11]
beast = row[12]
nightmare = row[13]
artist = row[14]
song = {
'id': songid,
'title': name,
'artist': artist,
'genre': genre,
'bpm_min': bpm_min,
'bpm_max': bpm_max,
'difficulty': {
'light': light,
'medium': medium,
'beast': beast,
'nightmare': nightmare,
},
}
songs.append(song)
return songs
def lookup(self, server: str, token: str) -> List[Dict[str, Any]]:
if self.version is None:
raise Exception('Can\'t look up database for \'all\' version!')
# Grab music info from remote server
music = self.remote_music(server, token)
songs = music.get_all_songs(self.game, self.version)
lut: Dict[int, Dict[str, Any]] = {}
chart_map = {
0: 'light',
1: 'medium',
2: 'beast',
3: 'nightmare',
}
# Format it the way we expect
for song in songs:
if song.chart not in chart_map:
# Ignore charts on songs we don't support/care about.
continue
if song.id not in lut:
lut[song.id] = {
'id': song.id,
'title': song.name,
'artist': song.artist,
'genre': song.genre,
'bpm_min': song.data.get_int('bpm_min'),
'bpm_max': song.data.get_int('bpm_max'),
'difficulty': {
'light': 0,
'medium': 1,
'beast': 2,
'nightmare': 3,
},
}
lut[song.id]['difficulty'][chart_map[song.chart]] = song.data.get_int('difficulty')
# Reassemble the data
reassembled_songs = [val for _, val in lut.items()]
return reassembled_songs
def import_music_db(self, songs: List[Dict[str, Any]]) -> None:
if self.version is None:
raise Exception('Can\'t import database for \'all\' version!')
chart_map: Dict[int, str] = {
0: 'light',
1: 'medium',
2: 'beast',
3: 'nightmare',
}
for song in songs:
songid = song['id']
self.start_batch()
for chart in self.charts:
# First, try to find in the DB from another version
old_id = self.get_music_id_for_song(songid, chart)
# First, try to find in the DB from another version
if self.no_combine or old_id is None:
# Insert original
print(f"New entry for {songid} chart {chart}")
next_id = self.get_next_music_id()
else:
# Insert pointing at same ID so scores transfer
print(f"Reused entry for {songid} chart {chart}")
next_id = old_id
data = {
'difficulty': song['difficulty'][chart_map[chart]],
'bpm_min': song['bpm_min'],
'bpm_max': song['bpm_max'],
}
self.insert_music_id_for_song(next_id, songid, chart, song['title'], song['artist'], song['genre'], data)
self.finish_batch()
def main() -> None:
parser = argparse.ArgumentParser(description="Import Game Music DB")
parser.add_argument(
@ -6465,6 +6627,15 @@ def main() -> None:
danevo.import_music_db(songs)
danevo.close()
if series == GameConstants.BST:
bst = ImportBst(config, args.version, args.no_combine, args.update)
if args.csv:
songs = bst.scrape(args.csv)
else:
raise Exception('No musicdb.csv provided! Please provide --csv')
bst.import_music_db(songs)
bst.close()
else:
raise CLIException("Unsupported game series!")

View File

@ -61,6 +61,7 @@ from bemani.client.reflec import (
from bemani.client.bishi import TheStarBishiBashiClient
from bemani.client.mga import MetalGearArcadeClient
from bemani.client.danevo import DanceEvolutionClient
from bemani.client.bst.beatstream2 import Beatstream2Client
def get_client(proto: ClientProtocol, pcbid: str, game: str, config: Dict[str, Any]) -> BaseClient:
@ -322,6 +323,12 @@ def get_client(proto: ClientProtocol, pcbid: str, game: str, config: Dict[str, A
pcbid,
config,
)
if game == 'beatstream2':
return Beatstream2Client(
proto,
pcbid,
config
)
raise Exception(f"Unknown game {game}")
@ -559,6 +566,10 @@ def mainloop(
"name": "Dance Evolution Arcade",
"model": "KDM:J:B:A:2016080100",
"avs": "2.15.5 r6251",
},'beatstream2': {
'name': 'BeatStream 2',
'model': 'NBT:J:A:A:2016111400',
'avs': '2.16.7 r7487'
},
}
if action == "list":
@ -683,6 +694,7 @@ def main() -> None:
"reflec-6": "reflec-volzza2",
"mga": "metal-gear-arcade",
"danevo": "dance-evolution",
'bst2': 'beatstream2',
}.get(game, game)
mainloop(args.address, args.port, args.config, action, game, args.cardid, args.verbose)

View File

@ -49,10 +49,9 @@ server:
# Delete this to disable this support.
webhooks:
discord:
iidx:
- "https://discord.com/api/webhooks/1232122131321321321/eauihfafaewfhjaveuijaewuivhjawueihoi"
pnm:
- "https://discord.com/api/webhooks/1232122131321321321/eauihfafaewfhjaveuijaewuivhjawueihoi"
iidx: "https://discord.com/api/webhooks/1232122131321321321/eauihfafaewfhjaveuijaewuivhjawueihoi"
pnm: "https://discord.com/api/webhooks/1232122131321321321/eauihfafaewfhjaveuijaewuivhjawueihoi"
bst: "https://discord.com/api/webhooks/1232122131321321321/eauihfafaewfhjaveuijaewuivhjawueihoi"
# Assets URLs. These allow for in-game asset rendering on the front end. Delete this to disable asset rendering.
assets:
@ -89,6 +88,8 @@ support:
reflec: True
# SDVX frontend/backend enabled.
sdvx: True
# Beatstream frontend/backend enable.
bst: True
# Key used to encrypt cookies, should be unique per network instance.
secret_key: 'this_is_a_secret_please_change_me'