396 lines
16 KiB
Python
396 lines
16 KiB
Python
|
import random
|
|||
|
import time
|
|||
|
from typing import Optional, Dict, List, Tuple, Any
|
|||
|
|
|||
|
from bemani.client.base import BaseClient
|
|||
|
from bemani.protocol import Node
|
|||
|
|
|||
|
|
|||
|
class PopnMusicSunnyParkClient(BaseClient):
|
|||
|
NAME = 'TEST'
|
|||
|
|
|||
|
def verify_game_active(self) -> None:
|
|||
|
call = self.call_node()
|
|||
|
|
|||
|
# Construct node
|
|||
|
game = Node.void('game')
|
|||
|
call.add_child(game)
|
|||
|
game.set_attribute('method', 'active')
|
|||
|
|
|||
|
# Add minimum amount of stuff so server accepts
|
|||
|
game.add_child(Node.s8('event', 0))
|
|||
|
|
|||
|
# Swap with server
|
|||
|
resp = self.exchange('pnm20/game', call)
|
|||
|
|
|||
|
# Verify that response is correct
|
|||
|
self.assert_path(resp, "response/game/@status")
|
|||
|
|
|||
|
def verify_game_get(self) -> None:
|
|||
|
call = self.call_node()
|
|||
|
|
|||
|
# Construct node
|
|||
|
game = Node.void('game')
|
|||
|
call.add_child(game)
|
|||
|
game.set_attribute('location_id', 'JP-1')
|
|||
|
game.set_attribute('method', 'get')
|
|||
|
game.add_child(Node.s8('event', 0))
|
|||
|
|
|||
|
# Swap with server
|
|||
|
resp = self.exchange('pnm20/game', call)
|
|||
|
|
|||
|
# Verify that response is correct
|
|||
|
self.assert_path(resp, "response/game")
|
|||
|
|
|||
|
for name in [
|
|||
|
'ir_phase',
|
|||
|
'music_open_phase',
|
|||
|
'collabo_phase',
|
|||
|
'personal_event_phase',
|
|||
|
'shop_event_phase',
|
|||
|
'netvs_phase',
|
|||
|
'card_phase',
|
|||
|
'other_phase',
|
|||
|
'local_matching_enable',
|
|||
|
'n_matching_sec',
|
|||
|
'l_matching_sec',
|
|||
|
'is_check_cpu',
|
|||
|
'week_no',
|
|||
|
]:
|
|||
|
node = resp.child('game').child(name)
|
|||
|
|
|||
|
if node is None:
|
|||
|
raise Exception('Missing node \'{}\' in response!'.format(name))
|
|||
|
if node.data_type != 's32':
|
|||
|
raise Exception('Node \'{}\' has wrong data type!'.format(name))
|
|||
|
|
|||
|
sel_ranking = resp.child('game').child('sel_ranking')
|
|||
|
up_ranking = resp.child('game').child('up_ranking')
|
|||
|
|
|||
|
for nodepair in [('sel_ranking', sel_ranking), ('up_ranking', up_ranking)]:
|
|||
|
name = nodepair[0]
|
|||
|
node = nodepair[1]
|
|||
|
|
|||
|
if node is None:
|
|||
|
raise Exception('Missing node \'{}\' in response!'.format(name))
|
|||
|
if node.data_type != 's16':
|
|||
|
raise Exception('Node \'{}\' has wrong data type!'.format(name))
|
|||
|
if not node.is_array:
|
|||
|
raise Exception('Node \'{}\' is not array!'.format(name))
|
|||
|
if len(node.value) != 5:
|
|||
|
raise Exception('Node \'{}\' is wrong array length!'.format(name))
|
|||
|
|
|||
|
def verify_playerdata_get(self, ref_id: str, msg_type: str) -> Optional[Dict[str, Any]]:
|
|||
|
call = self.call_node()
|
|||
|
|
|||
|
# Construct node
|
|||
|
playerdata = Node.void('playerdata')
|
|||
|
call.add_child(playerdata)
|
|||
|
playerdata.set_attribute('method', 'get')
|
|||
|
if msg_type == 'new':
|
|||
|
playerdata.set_attribute('model', self.config['old_profile_model'].split(':')[0])
|
|||
|
|
|||
|
playerdata.add_child(Node.string('ref_id', ref_id))
|
|||
|
playerdata.add_child(Node.string('shop_name', ''))
|
|||
|
playerdata.add_child(Node.s8('pref', 51))
|
|||
|
if msg_type == 'new':
|
|||
|
playerdata.add_child(Node.s32('ir_num', 0))
|
|||
|
elif msg_type == 'query':
|
|||
|
playerdata.add_child(Node.s32('gakuen', 2))
|
|||
|
playerdata.add_child(Node.s32('zoo', 1))
|
|||
|
playerdata.add_child(Node.s32('floor_infection', 1))
|
|||
|
playerdata.add_child(Node.s32('triple_journey', 1))
|
|||
|
playerdata.add_child(Node.s32('baseball', 1))
|
|||
|
|
|||
|
# Swap with server
|
|||
|
resp = self.exchange('pnm20/playerdata', call)
|
|||
|
|
|||
|
if msg_type == 'new':
|
|||
|
# Verify that response is correct
|
|||
|
self.assert_path(resp, "response/playerdata/@status")
|
|||
|
|
|||
|
status = int(resp.child('playerdata').attribute('status'))
|
|||
|
if status != 109:
|
|||
|
raise Exception('Reference ID \'{}\' returned invalid status \'{}\''.format(ref_id, status))
|
|||
|
|
|||
|
# No score data
|
|||
|
return None
|
|||
|
elif msg_type == 'query':
|
|||
|
# Verify that the response is correct
|
|||
|
self.assert_path(resp, "response/playerdata/base/name")
|
|||
|
self.assert_path(resp, "response/playerdata/base/g_pm_id")
|
|||
|
self.assert_path(resp, "response/playerdata/base/my_best")
|
|||
|
self.assert_path(resp, "response/playerdata/base/latest_music")
|
|||
|
self.assert_path(resp, "response/playerdata/avatar")
|
|||
|
self.assert_path(resp, "response/playerdata/avatar_add")
|
|||
|
self.assert_path(resp, "response/playerdata/netvs")
|
|||
|
self.assert_path(resp, "response/playerdata/sp_data")
|
|||
|
self.assert_path(resp, "response/playerdata/hiscore")
|
|||
|
|
|||
|
name = resp.child('playerdata').child('base').child('name').value
|
|||
|
if name != self.NAME:
|
|||
|
raise Exception('Invalid name \'{}\' returned for Ref ID \'{}\''.format(name, ref_id))
|
|||
|
|
|||
|
# Extract and return score data
|
|||
|
self.assert_path(resp, "response/playerdata/base/clear_medal")
|
|||
|
|
|||
|
def transform_medals(medal: int) -> Tuple[int, int, int, int]:
|
|||
|
return (
|
|||
|
(medal >> 0) & 0xF,
|
|||
|
(medal >> 4) & 0xF,
|
|||
|
(medal >> 8) & 0xF,
|
|||
|
(medal >> 12) & 0xF,
|
|||
|
)
|
|||
|
|
|||
|
medals = [transform_medals(medal) for medal in resp.child('playerdata').child('base').child('clear_medal').value]
|
|||
|
|
|||
|
hiscore = resp.child('playerdata').child('hiscore').value
|
|||
|
hiscores = []
|
|||
|
for i in range(0, len(hiscore) * 8, 17):
|
|||
|
byte_offset = int(i / 8)
|
|||
|
bit_offset = int(i % 8)
|
|||
|
|
|||
|
value = hiscore[byte_offset]
|
|||
|
value = value + (hiscore[byte_offset + 1] << 8)
|
|||
|
value = value + (hiscore[byte_offset + 2] << 16)
|
|||
|
|
|||
|
value = value >> bit_offset
|
|||
|
hiscores.append(value & 0x1FFFF)
|
|||
|
|
|||
|
scores = [(hiscores[x], hiscores[x + 1], hiscores[x + 2], hiscores[x + 3]) for x in range(0, len(hiscores), 4)]
|
|||
|
|
|||
|
return {'medals': medals, 'scores': scores}
|
|||
|
|
|||
|
else:
|
|||
|
raise Exception('Unrecognized message type \'{}\''.format(msg_type))
|
|||
|
|
|||
|
def verify_playerdata_set(self, ref_id: str, scores: List[Dict[str, Any]]) -> None:
|
|||
|
call = self.call_node()
|
|||
|
|
|||
|
# Construct node
|
|||
|
playerdata = Node.void('playerdata')
|
|||
|
call.add_child(playerdata)
|
|||
|
playerdata.set_attribute('method', 'set')
|
|||
|
playerdata.set_attribute('ref_id', ref_id)
|
|||
|
playerdata.set_attribute('shop_name', '')
|
|||
|
|
|||
|
# Add required children
|
|||
|
playerdata.add_child(Node.s16('chara', 1543))
|
|||
|
|
|||
|
# Add requested scores
|
|||
|
for score in scores:
|
|||
|
stage = Node.void('stage')
|
|||
|
playerdata.add_child(stage)
|
|||
|
stage.add_child(Node.s16('no', score['id']))
|
|||
|
stage.add_child(Node.u8('sheet', score['chart']))
|
|||
|
stage.add_child(Node.u16('n_data', (score['medal'] << (4 * score['chart']))))
|
|||
|
stage.add_child(Node.s32('score', score['score']))
|
|||
|
|
|||
|
# Swap with server
|
|||
|
resp = self.exchange('pnm20/playerdata', call)
|
|||
|
|
|||
|
# Verify nodes that cause crashes if they don't exist
|
|||
|
self.assert_path(resp, "response/playerdata/name")
|
|||
|
|
|||
|
name = resp.child('playerdata').child('name').value
|
|||
|
if name != self.NAME:
|
|||
|
raise Exception('Invalid name \'{}\' returned for Ref ID \'{}\''.format(name, ref_id))
|
|||
|
|
|||
|
def verify_playerdata_new(self, ref_id: str) -> None:
|
|||
|
call = self.call_node()
|
|||
|
|
|||
|
# Construct node
|
|||
|
playerdata = Node.void('playerdata')
|
|||
|
call.add_child(playerdata)
|
|||
|
playerdata.set_attribute('method', 'new')
|
|||
|
|
|||
|
playerdata.add_child(Node.string('ref_id', ref_id))
|
|||
|
playerdata.add_child(Node.string('name', self.NAME))
|
|||
|
playerdata.add_child(Node.string('shop_name', ''))
|
|||
|
playerdata.add_child(Node.s8('pref', 51))
|
|||
|
playerdata.add_child(Node.s8('gakuen', 2))
|
|||
|
playerdata.add_child(Node.s8('zoo', 1))
|
|||
|
playerdata.add_child(Node.s8('floor_infection', 1))
|
|||
|
playerdata.add_child(Node.s8('triple_journey', 1))
|
|||
|
playerdata.add_child(Node.s8('baseball', 1))
|
|||
|
|
|||
|
# Swap with server
|
|||
|
resp = self.exchange('pnm20/playerdata', call)
|
|||
|
|
|||
|
# Verify nodes that cause crashes if they don't exist
|
|||
|
self.assert_path(resp, "response/playerdata/base")
|
|||
|
|
|||
|
def verify(self, cardid: Optional[str]) -> None:
|
|||
|
# Verify boot sequence is okay
|
|||
|
self.verify_services_get(
|
|||
|
expected_services=[
|
|||
|
'pcbtracker',
|
|||
|
'pcbevent',
|
|||
|
'local',
|
|||
|
'message',
|
|||
|
'facility',
|
|||
|
'cardmng',
|
|||
|
'package',
|
|||
|
'posevent',
|
|||
|
'pkglist',
|
|||
|
'dlstatus',
|
|||
|
'eacoin',
|
|||
|
'lobby',
|
|||
|
'ntp',
|
|||
|
'keepalive'
|
|||
|
]
|
|||
|
)
|
|||
|
paseli_enabled = self.verify_pcbtracker_alive()
|
|||
|
self.verify_message_get()
|
|||
|
self.verify_package_list()
|
|||
|
self.verify_facility_get()
|
|||
|
self.verify_pcbevent_put()
|
|||
|
self.verify_game_active()
|
|||
|
self.verify_game_get()
|
|||
|
|
|||
|
# Verify card registration and profile lookup
|
|||
|
if cardid is not None:
|
|||
|
card = cardid
|
|||
|
else:
|
|||
|
card = self.random_card()
|
|||
|
print("Generated random card ID {} for use.".format(card))
|
|||
|
|
|||
|
if cardid is None:
|
|||
|
self.verify_cardmng_inquire(card, msg_type='unregistered', paseli_enabled=paseli_enabled)
|
|||
|
ref_id = self.verify_cardmng_getrefid(card)
|
|||
|
if len(ref_id) != 16:
|
|||
|
raise Exception('Invalid refid \'{}\' returned when registering card'.format(ref_id))
|
|||
|
if ref_id != self.verify_cardmng_inquire(card, msg_type='new', paseli_enabled=paseli_enabled):
|
|||
|
raise Exception('Invalid refid \'{}\' returned when querying card'.format(ref_id))
|
|||
|
self.verify_playerdata_get(ref_id, msg_type='new')
|
|||
|
self.verify_playerdata_new(ref_id)
|
|||
|
else:
|
|||
|
print("Skipping new card checks for existing card")
|
|||
|
ref_id = self.verify_cardmng_inquire(card, msg_type='query', paseli_enabled=paseli_enabled)
|
|||
|
|
|||
|
# Verify pin handling and return card handling
|
|||
|
self.verify_cardmng_authpass(ref_id, correct=True)
|
|||
|
self.verify_cardmng_authpass(ref_id, correct=False)
|
|||
|
if ref_id != self.verify_cardmng_inquire(card, msg_type='query', paseli_enabled=paseli_enabled):
|
|||
|
raise Exception('Invalid refid \'{}\' returned when querying card'.format(ref_id))
|
|||
|
|
|||
|
if cardid is None:
|
|||
|
# Verify score handling
|
|||
|
scores = self.verify_playerdata_get(ref_id, msg_type='query')
|
|||
|
if scores is None:
|
|||
|
raise Exception('Expected to get scores back, didn\'t get anything!')
|
|||
|
for medal in scores['medals']:
|
|||
|
for i in range(4):
|
|||
|
if medal[i] != 0:
|
|||
|
raise Exception('Got nonzero medals count on a new card!')
|
|||
|
for score in scores['scores']:
|
|||
|
for i in range(4):
|
|||
|
if score[i] != 0:
|
|||
|
raise Exception('Got nonzero scores count on a new card!')
|
|||
|
|
|||
|
for phase in [1, 2]:
|
|||
|
if phase == 1:
|
|||
|
dummyscores = [
|
|||
|
# An okay score on a chart
|
|||
|
{
|
|||
|
'id': 987,
|
|||
|
'chart': 2,
|
|||
|
'medal': 5,
|
|||
|
'score': 76543,
|
|||
|
},
|
|||
|
# A good score on an easier chart of the same song
|
|||
|
{
|
|||
|
'id': 987,
|
|||
|
'chart': 0,
|
|||
|
'medal': 6,
|
|||
|
'score': 99999,
|
|||
|
},
|
|||
|
# A bad score on a hard chart
|
|||
|
{
|
|||
|
'id': 741,
|
|||
|
'chart': 3,
|
|||
|
'medal': 2,
|
|||
|
'score': 45000,
|
|||
|
},
|
|||
|
# A terrible score on an easy chart
|
|||
|
{
|
|||
|
'id': 742,
|
|||
|
'chart': 1,
|
|||
|
'medal': 2,
|
|||
|
'score': 1,
|
|||
|
},
|
|||
|
]
|
|||
|
# Random score to add in
|
|||
|
songid = random.randint(907, 950)
|
|||
|
chartid = random.randint(0, 3)
|
|||
|
score = random.randint(0, 100000)
|
|||
|
medal = random.choice([1, 2, 3, 5, 6, 7, 9, 10, 11, 15])
|
|||
|
dummyscores.append({
|
|||
|
'id': songid,
|
|||
|
'chart': chartid,
|
|||
|
'medal': medal,
|
|||
|
'score': score,
|
|||
|
})
|
|||
|
if phase == 2:
|
|||
|
dummyscores = [
|
|||
|
# A better score on the same chart
|
|||
|
{
|
|||
|
'id': 987,
|
|||
|
'chart': 2,
|
|||
|
'medal': 5,
|
|||
|
'score': 98765,
|
|||
|
},
|
|||
|
# A worse score on another same chart
|
|||
|
{
|
|||
|
'id': 987,
|
|||
|
'chart': 0,
|
|||
|
'medal': 3,
|
|||
|
'score': 12345,
|
|||
|
'expected_score': 99999,
|
|||
|
'expected_medal': 6,
|
|||
|
},
|
|||
|
]
|
|||
|
|
|||
|
self.verify_playerdata_set(ref_id, dummyscores)
|
|||
|
scores = self.verify_playerdata_get(ref_id, msg_type='query')
|
|||
|
for score in dummyscores:
|
|||
|
newscore = scores['scores'][score['id']][score['chart']]
|
|||
|
newmedal = scores['medals'][score['id']][score['chart']]
|
|||
|
|
|||
|
if 'expected_score' in score:
|
|||
|
expected_score = score['expected_score']
|
|||
|
else:
|
|||
|
expected_score = score['score']
|
|||
|
if 'expected_medal' in score:
|
|||
|
expected_medal = score['expected_medal']
|
|||
|
else:
|
|||
|
expected_medal = score['medal']
|
|||
|
|
|||
|
if newscore != expected_score:
|
|||
|
raise Exception('Expected a score of \'{}\' for song \'{}\' chart \'{}\' but got score \'{}\''.format(
|
|||
|
expected_score, score['id'], score['chart'], newscore,
|
|||
|
))
|
|||
|
if newmedal != expected_medal:
|
|||
|
raise Exception('Expected a medal of \'{}\' for song \'{}\' chart \'{}\' but got medal \'{}\''.format(
|
|||
|
expected_medal, score['id'], score['chart'], newmedal,
|
|||
|
))
|
|||
|
|
|||
|
# Sleep so we don't end up putting in score history on the same second
|
|||
|
time.sleep(1)
|
|||
|
else:
|
|||
|
print("Skipping score checks for existing card")
|
|||
|
|
|||
|
# Verify paseli handling
|
|||
|
if paseli_enabled:
|
|||
|
print("PASELI enabled for this PCBID, executing PASELI checks")
|
|||
|
else:
|
|||
|
print("PASELI disabled for this PCBID, skipping PASELI checks")
|
|||
|
return
|
|||
|
|
|||
|
sessid, balance = self.verify_eacoin_checkin(card)
|
|||
|
if balance == 0:
|
|||
|
print("Skipping PASELI consume check because card has 0 balance")
|
|||
|
else:
|
|||
|
self.verify_eacoin_consume(sessid, balance, random.randint(0, balance))
|
|||
|
self.verify_eacoin_checkout(sessid)
|