1
0
mirror of synced 2024-11-27 23:50:47 +01:00

Merge remote-tracking branch 'upstream/trunk' into trunk

This commit is contained in:
cracrayol 2021-09-06 19:09:47 +02:00
commit ede06b3e68
20 changed files with 429 additions and 49 deletions

View File

@ -1,6 +1,7 @@
from abc import ABC
from abc import ABC, abstractmethod
import traceback
from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Type
from typing_extensions import Final
from bemani.common import Model, ValidatedDict, Profile, PlayStatistics, GameConstants, Time
from bemani.data import Config, Data, Machine, UserID, RemoteUser
@ -14,14 +15,14 @@ class Status:
"""
List of statuses we return to the game for various reasons.
"""
SUCCESS = 0
NO_PROFILE = 109
NOT_ALLOWED = 110
NOT_REGISTERED = 112
INVALID_PIN = 116
SUCCESS: Final[int] = 0
NO_PROFILE: Final[int] = 109
NOT_ALLOWED: Final[int] = 110
NOT_REGISTERED: Final[int] = 112
INVALID_PIN: Final[int] = 116
class Factory:
class Factory(ABC):
"""
The base class every game factory inherits from. Defines a create method
which should return some game class which can handle packets. Game classes
@ -29,16 +30,17 @@ class Factory:
Dispatch will look up in order to handle calls.
"""
MANAGED_CLASSES: List[Type["Base"]] = []
MANAGED_CLASSES: List[Type["Base"]]
@classmethod
@abstractmethod
def register_all(cls) -> None:
"""
Subclasses of this class should use this function to register themselves
with Base, using Base.register(). Factories specify the game code that
they support, which Base will use when routing requests.
"""
raise Exception('Override this in subclass!')
raise NotImplementedError('Override this in subclass!')
@classmethod
def run_scheduled_work(cls, data: Data, config: Config) -> None:
@ -84,6 +86,7 @@ class Factory:
yield (game.game, game.version, game.get_settings())
@classmethod
@abstractmethod
def create(cls, data: Data, config: Config, model: Model, parentmodel: Optional[Model]=None) -> Optional['Base']:
"""
Given a modelstring and an optional parent model, return an instantiated game class that can handle a packet.
@ -102,7 +105,7 @@ class Factory:
A subclass of Base that hopefully has a handle_<call>_request method on it, for the particular
call that Dispatch wants to resolve, or None if we can't look up a game.
"""
raise Exception('Override this in subclass!')
raise NotImplementedError('Override this in subclass!')
class Base(ABC):

View File

@ -18,6 +18,7 @@ class PopnMusicBase(CoreHandler, CardManagerHandler, PASELIHandler, Base):
game = GameConstants.POPN_MUSIC
# Play medals, as saved into/loaded from the DB
PLAY_MEDAL_NO_PLAY = DBConstants.POPN_MUSIC_PLAY_MEDAL_NO_PLAY
PLAY_MEDAL_CIRCLE_FAILED = DBConstants.POPN_MUSIC_PLAY_MEDAL_CIRCLE_FAILED
PLAY_MEDAL_DIAMOND_FAILED = DBConstants.POPN_MUSIC_PLAY_MEDAL_DIAMOND_FAILED
PLAY_MEDAL_STAR_FAILED = DBConstants.POPN_MUSIC_PLAY_MEDAL_STAR_FAILED
@ -188,6 +189,7 @@ class PopnMusicBase(CoreHandler, CardManagerHandler, PASELIHandler, Base):
"""
# Range check medals
if medal not in [
self.PLAY_MEDAL_NO_PLAY,
self.PLAY_MEDAL_CIRCLE_FAILED,
self.PLAY_MEDAL_DIAMOND_FAILED,
self.PLAY_MEDAL_STAR_FAILED,

View File

@ -276,6 +276,8 @@ class PopnMusicEclale(PopnMusicBase):
self.CHART_TYPE_EX,
]:
continue
if score.data.get_int('medal') == self.PLAY_MEDAL_NO_PLAY:
continue
points = score.points
medal = score.data.get_int('medal')
@ -364,6 +366,8 @@ class PopnMusicEclale(PopnMusicBase):
self.CHART_TYPE_EX,
]:
continue
if score.data.get_int('medal') == self.PLAY_MEDAL_NO_PLAY:
continue
points = score.points
medal = score.data.get_int('medal')
@ -458,6 +462,8 @@ class PopnMusicEclale(PopnMusicBase):
self.CHART_TYPE_EX,
]:
continue
if score.data.get_int('medal') == self.PLAY_MEDAL_NO_PLAY:
continue
music = Node.void('music')
root.add_child(music)

View File

@ -198,6 +198,8 @@ class PopnMusicFantasia(PopnMusicBase):
self.CHART_TYPE_EX,
]:
continue
if score.data.get_int('medal') == self.PLAY_MEDAL_NO_PLAY:
continue
clear_medal[score.id] = clear_medal[score.id] | self.__format_medal_for_score(score)
hiscore_index = (score.id * 4) + {
@ -289,6 +291,8 @@ class PopnMusicFantasia(PopnMusicBase):
self.CHART_TYPE_EX,
]:
continue
if score.data.get_int('medal') == self.PLAY_MEDAL_NO_PLAY:
continue
clear_medal[score.id] = clear_medal[score.id] | self.__format_medal_for_score(score)
hiscore_index = (score.id * 4) + {
@ -567,6 +571,8 @@ class PopnMusicFantasia(PopnMusicBase):
self.CHART_TYPE_EX,
]:
continue
if score.data.get_int('medal') == self.PLAY_MEDAL_NO_PLAY:
continue
clear_medal[score.id] = clear_medal[score.id] | self.__format_medal_for_score(score)
hiscore_index = (score.id * 4) + {

View File

@ -1,12 +1,12 @@
# vim: set fileencoding=utf-8
import copy
from typing import Dict, List
from typing import Any, Dict, List
from bemani.backend.popn.base import PopnMusicBase
from bemani.backend.popn.sunnypark import PopnMusicSunnyPark
from bemani.backend.base import Status
from bemani.common import Profile, VersionConstants, ID
from bemani.common import ValidatedDict, Profile, VersionConstants, ID
from bemani.data import UserID, Link
from bemani.protocol import Node
@ -23,6 +23,7 @@ class PopnMusicLapistoria(PopnMusicBase):
GAME_CHART_TYPE_EX = 3
# Medal type, as returned from the game
GAME_PLAY_MEDAL_NO_PLAY = 0
GAME_PLAY_MEDAL_CIRCLE_FAILED = 1
GAME_PLAY_MEDAL_DIAMOND_FAILED = 2
GAME_PLAY_MEDAL_STAR_FAILED = 3
@ -41,48 +42,101 @@ class PopnMusicLapistoria(PopnMusicBase):
def previous_version(self) -> PopnMusicBase:
return PopnMusicSunnyPark(self.data, self.config, self.model)
@classmethod
def get_settings(cls) -> Dict[str, Any]:
"""
Return all of our front-end modifiably settings.
"""
return {
'ints': [
{
'name': 'Story Mode',
'tip': 'Story mode phase for all players.',
'category': 'game_config',
'setting': 'story_phase',
'values': {
0: 'Disabled',
1: 'Phase 1',
2: 'Phase 2',
3: 'Phase 3',
4: 'Phase 4',
5: 'Phase 5',
6: 'Phase 6',
7: 'Phase 7',
8: 'Phase 8',
9: 'Phase 9',
10: 'Phase 10',
11: 'Phase 11',
12: 'Phase 12',
13: 'Phase 13',
14: 'Phase 14',
15: 'Phase 15',
16: 'Phase 16',
17: 'Phase 17',
18: 'Phase 18',
19: 'Phase 19',
20: 'Phase 20',
21: 'Phase 21',
22: 'Phase 22',
23: 'Phase 23',
24: 'Phase 24',
},
},
],
'bools': [
{
'name': 'Force Song Unlock',
'tip': 'Force unlock all songs.',
'category': 'game_config',
'setting': 'force_unlock_songs',
},
],
}
def handle_info22_common_request(self, request: Node) -> Node:
# TODO: Hook these up to config so we can change this
game_config = self.get_game_config()
story_phase = game_config.get_int('story_phase')
phases = {
# Unknown event (0-16)
0: 0,
# Unknown event (0-11)
1: 0,
# Default song phase availability (0-16)
0: 16,
# Card phase (0-11)
1: 11,
# Pop'n Aura, max (0-11) (remove all aura requirements)
2: 11,
# Story (0-24)
3: 1,
# BEMANI ruins Discovery! (0-2)
3: story_phase,
# BEMANI ruins Discovery! 0 = off, 1 = active, 2 = off
4: 0,
# Unknown event, something to do with net taisen (0-2)
5: 0,
5: 2,
# Unknown event (0-1)
6: 0,
6: 1,
# Unknown event (0-1)
7: 0,
7: 1,
# Unknown event (0-1)
8: 0,
# Unknown event (0-11)
9: 0,
# Unknown event (0-2)
8: 1,
# Course mode phase (0-11)
9: 11,
# Pon's Fate Purification Plan, 0 = off, 1 = active, 2 = off
10: 0,
# Unknown event (0-3)
11: 0,
11: 3,
# Unknown event (0-1)
12: 0,
# Unknown event (0-2)
13: 0,
12: 1,
# Appears to be unlocks for course mode including KAC stuff.
13: 2,
# Unknown event (0-4)
14: 0,
14: 4,
# Unknown event (0-2)
15: 0,
15: 2,
# Unknown event (0-2)
16: 0,
16: 2,
# Unknown event (0-12)
17: 0,
# Unknown event (0-2)
18: 0,
# Unknown event (0-7)
18: 2,
# Bemani Summer Diary, 0 = off, 1-6 are phases, 7 = off
19: 0,
}
@ -198,6 +252,7 @@ class PopnMusicLapistoria(PopnMusicBase):
rivalid = links[no].other_userid
rivalprofile = profiles[rivalid]
scores = self.data.remote.music.get_scores(self.game, self.version, rivalid)
achievements = self.data.local.user.get_achievements(self.game, self.version, rivalid)
# First, output general profile info.
friend = Node.void('friend')
@ -234,6 +289,7 @@ class PopnMusicLapistoria(PopnMusicBase):
}[score.chart]))
music.set_attribute('score', str(points))
music.set_attribute('clearmedal', str({
self.PLAY_MEDAL_NO_PLAY: self.GAME_PLAY_MEDAL_NO_PLAY,
self.PLAY_MEDAL_CIRCLE_FAILED: self.GAME_PLAY_MEDAL_CIRCLE_FAILED,
self.PLAY_MEDAL_DIAMOND_FAILED: self.GAME_PLAY_MEDAL_DIAMOND_FAILED,
self.PLAY_MEDAL_STAR_FAILED: self.GAME_PLAY_MEDAL_STAR_FAILED,
@ -247,6 +303,27 @@ class PopnMusicLapistoria(PopnMusicBase):
self.PLAY_MEDAL_PERFECT: self.GAME_PLAY_MEDAL_PERFECT,
}[medal]))
for course in achievements:
if course.type == 'course':
total_score = course.data.get_int('total_score')
clear_medal = course.data.get_int('clear_medal')
clear_norma = course.data.get_int('clear_norma')
stage1_score = course.data.get_int('stage1_score')
stage2_score = course.data.get_int('stage2_score')
stage3_score = course.data.get_int('stage3_score')
stage4_score = course.data.get_int('stage4_score')
coursenode = Node.void('course')
friend.add_child(coursenode)
coursenode.set_attribute('course_id', str(course.id))
coursenode.set_attribute('clear_medal', str(clear_medal))
coursenode.set_attribute('clear_norma', str(clear_norma))
coursenode.set_attribute('stage1_score', str(stage1_score))
coursenode.set_attribute('stage2_score', str(stage2_score))
coursenode.set_attribute('stage3_score', str(stage3_score))
coursenode.set_attribute('stage4_score', str(stage4_score))
coursenode.set_attribute('total_score', str(total_score))
return root
def handle_player22_conversion_request(self, request: Node) -> Node:
@ -287,6 +364,7 @@ class PopnMusicLapistoria(PopnMusicBase):
'bad': request.child_value('bad')
}
medal = {
self.GAME_PLAY_MEDAL_NO_PLAY: self.PLAY_MEDAL_NO_PLAY,
self.GAME_PLAY_MEDAL_CIRCLE_FAILED: self.PLAY_MEDAL_CIRCLE_FAILED,
self.GAME_PLAY_MEDAL_DIAMOND_FAILED: self.PLAY_MEDAL_DIAMOND_FAILED,
self.GAME_PLAY_MEDAL_STAR_FAILED: self.PLAY_MEDAL_STAR_FAILED,
@ -302,6 +380,119 @@ class PopnMusicLapistoria(PopnMusicBase):
self.update_score(userid, songid, chart, points, medal, combo=combo, stats=stats)
return root
def handle_player22_write_course_request(self, request: Node) -> Node:
refid = request.child_value('ref_id')
root = Node.void('player22')
if refid is None:
return root
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
if userid is None:
return root
# Grab info that we want to update
total_score = request.child_value('total_score') or 0
course_id = request.child_value('course_id')
if course_id is not None:
machine = self.data.local.machine.get_machine(self.config.machine.pcbid)
pref = request.child_value('pref') or 51
profile = self.get_profile(userid) or Profile(self.game, self.version, refid, 0)
course = self.data.local.user.get_achievement(
self.game,
self.version,
userid,
course_id,
"course",
) or ValidatedDict({})
stage_scores: Dict[int, int] = {}
for child in request.children:
if child.name != 'stage':
continue
stage = child.child_value('stage')
score = child.child_value('score')
if isinstance(stage, int) and isinstance(score, int):
stage_scores[stage] = score
# Update the scores if this was a new high score.
if total_score > course.get_int('total_score'):
course.replace_int('total_score', total_score)
course.replace_int('stage1_score', stage_scores.get(0, 0))
course.replace_int('stage2_score', stage_scores.get(1, 0))
course.replace_int('stage3_score', stage_scores.get(2, 0))
course.replace_int('stage4_score', stage_scores.get(3, 0))
# Only update ojamas used if this was an updated score.
course.replace_int('clear_norma', request.child_value('clear_norma'))
# Only udpate what location and prefecture this was scored in
# if we updated our score.
course.replace_int('pref', pref)
course.replace_int('lid', machine.arcade)
# Update medal and combo values.
course.replace_int('max_combo', max(course.get_int('max_combo'), request.child_value('max_combo')))
course.replace_int('clear_medal', max(course.get_int('clear_medal'), request.child_value('clear_medal')))
# Add one to the play count for this course.
course.increment_int('play_cnt')
self.data.local.user.put_achievement(
self.game,
self.version,
userid,
course_id,
"course",
course,
)
# Now, attempt to calculate ranking for this user for this run.
all_courses = self.data.local.user.get_all_achievements(self.game, self.version, course_id, "course")
global_ranking = sorted(all_courses, key=lambda entry: entry[1].data.get_int('total_score'), reverse=True)
pref_ranking = [c for c in global_ranking if c[1].data.get_int('pref') == pref]
local_ranking = [c for c in global_ranking if c[1].data.get_int('lid') == machine.arcade]
global_rank = len(global_ranking)
pref_rank = len(pref_ranking)
local_rank = len(local_ranking)
for i, rank in enumerate(global_ranking):
if userid == rank[0]:
global_rank = i + 1
break
for i, rank in enumerate(pref_ranking):
if userid == rank[0]:
pref_rank = i + 1
break
for i, rank in enumerate(local_ranking):
if userid == rank[0]:
local_rank = i + 1
break
# Now, return it all.
for rank_type, personal_rank, count in [
('all_ranking', global_rank, len(global_ranking)),
('pref_ranking', pref_rank, len(pref_ranking)),
('location_ranking', local_rank, len(local_ranking)),
]:
ranknode = Node.void(rank_type)
root.add_child(ranknode)
ranknode.add_child(Node.string('name', profile.get_str('name', 'なし')))
ranknode.add_child(Node.s16('chara_num', profile.get_int('chara', -1)))
ranknode.add_child(Node.s32('stage1_score', stage_scores.get(0, 0)))
ranknode.add_child(Node.s32('stage2_score', stage_scores.get(1, 0)))
ranknode.add_child(Node.s32('stage3_score', stage_scores.get(2, 0)))
ranknode.add_child(Node.s32('stage4_score', stage_scores.get(3, 0)))
ranknode.add_child(Node.s32('total_score', total_score))
ranknode.add_child(Node.s16('player_count', count))
ranknode.add_child(Node.s16('player_rank', personal_rank))
return root
def format_profile(self, userid: UserID, profile: Profile) -> Node:
root = Node.void('player22')
@ -380,6 +571,7 @@ class PopnMusicLapistoria(PopnMusicBase):
music.add_child(Node.s16('cnt', score.plays))
music.add_child(Node.s32('score', points))
music.add_child(Node.u8('clear_type', {
self.PLAY_MEDAL_NO_PLAY: self.GAME_PLAY_MEDAL_NO_PLAY,
self.PLAY_MEDAL_CIRCLE_FAILED: self.GAME_PLAY_MEDAL_CIRCLE_FAILED,
self.PLAY_MEDAL_DIAMOND_FAILED: self.GAME_PLAY_MEDAL_DIAMOND_FAILED,
self.PLAY_MEDAL_STAR_FAILED: self.GAME_PLAY_MEDAL_STAR_FAILED,
@ -474,6 +666,17 @@ class PopnMusicLapistoria(PopnMusicBase):
customize.add_child(Node.u16('comment_1', customize_dict.get_int('comment_1')))
customize.add_child(Node.u16('comment_2', customize_dict.get_int('comment_2')))
game_config = self.get_game_config()
if game_config.get_bool('force_unlock_songs'):
songs = self.data.local.music.get_all_songs(self.game, self.version)
for song in songs:
item = Node.void('item')
root.add_child(item)
item.add_child(Node.u8('type', 0))
item.add_child(Node.u16('id', song.id))
item.add_child(Node.u16('param', 15))
item.add_child(Node.bool('is_new', False))
# Set up achievements
achievements = self.data.local.user.get_achievements(self.game, self.version, userid)
for achievement in achievements:
@ -481,6 +684,20 @@ class PopnMusicLapistoria(PopnMusicBase):
itemtype = achievement.data.get_int('type')
param = achievement.data.get_int('param')
# Maximum for each type is as follows:
# 0, 1423 - These are song unlocks as far as I can tell, matches Eclale/UsaNeko.
# 1, 2040
# 2, 510
# 3, 173
# 4, 40
# 5, 24
# 6, 24
# 7, 4158
if game_config.get_bool('force_unlock_songs') and itemtype == 0:
# We already sent song unlocks in the force unlock section above.
continue
item = Node.void('item')
root.add_child(item)
item.add_child(Node.u8('type', itemtype))
@ -518,6 +735,34 @@ class PopnMusicLapistoria(PopnMusicBase):
story.add_child(Node.bool('is_cleared', cleared))
story.add_child(Node.u32('clear_chapter', clear_chapter))
elif achievement.type == 'course':
total_score = achievement.data.get_int('total_score')
max_combo = achievement.data.get_int('max_combo')
play_cnt = achievement.data.get_int('play_cnt')
clear_medal = achievement.data.get_int('clear_medal')
clear_norma = achievement.data.get_int('clear_norma')
stage1_score = achievement.data.get_int('stage1_score')
stage2_score = achievement.data.get_int('stage2_score')
stage3_score = achievement.data.get_int('stage3_score')
stage4_score = achievement.data.get_int('stage4_score')
course = Node.void('course')
root.add_child(course)
course.add_child(Node.s16('course_id', achievement.id))
course.add_child(Node.u8('clear_medal', clear_medal))
course.add_child(Node.u8('clear_norma', clear_norma))
course.add_child(Node.s32('stage1_score', stage1_score))
course.add_child(Node.s32('stage2_score', stage2_score))
course.add_child(Node.s32('stage3_score', stage3_score))
course.add_child(Node.s32('stage4_score', stage4_score))
course.add_child(Node.s32('total_score', total_score))
course.add_child(Node.s16('max_cmbo', max_combo)) # Yes, it is misspelled.
course.add_child(Node.s16('play_cnt', play_cnt))
course.add_child(Node.s16('all_rank', 1)) # Unclear what this does.
# There are also course_rank nodes, but it doesn't appear they get displayed
# to the user anywhere.
return root
def unformat_profile(self, userid: UserID, request: Node, oldprofile: Profile) -> Profile:
@ -584,6 +829,7 @@ class PopnMusicLapistoria(PopnMusicBase):
self.update_play_statistics(userid)
# Extract achievements
game_config = self.get_game_config()
for node in request.children:
if node.name == 'item':
if not node.child_value('is_new'):
@ -594,6 +840,10 @@ class PopnMusicLapistoria(PopnMusicBase):
itemtype = node.child_value('type')
param = node.child_value('param')
if game_config.get_bool('force_unlock_songs') and itemtype == 0:
# If we enabled force song unlocks, don't save songs to the profile.
continue
self.data.local.user.put_achievement(
self.game,
self.version,
@ -698,6 +948,7 @@ class PopnMusicLapistoria(PopnMusicBase):
music.add_child(Node.u8('clear_type', 0))
music.add_child(Node.s32('old_score', points))
music.add_child(Node.u8('old_clear_type', {
self.PLAY_MEDAL_NO_PLAY: self.GAME_PLAY_MEDAL_NO_PLAY,
self.PLAY_MEDAL_CIRCLE_FAILED: self.GAME_PLAY_MEDAL_CIRCLE_FAILED,
self.PLAY_MEDAL_DIAMOND_FAILED: self.GAME_PLAY_MEDAL_DIAMOND_FAILED,
self.PLAY_MEDAL_STAR_FAILED: self.GAME_PLAY_MEDAL_STAR_FAILED,

View File

@ -175,6 +175,8 @@ class PopnMusicSunnyPark(PopnMusicBase):
self.CHART_TYPE_EX,
]:
continue
if score.data.get_int('medal') == self.PLAY_MEDAL_NO_PLAY:
continue
clear_medal[score.id] = clear_medal[score.id] | self.__format_medal_for_score(score)
hiscore_index = (score.id * 4) + {
@ -348,6 +350,8 @@ class PopnMusicSunnyPark(PopnMusicBase):
self.CHART_TYPE_EX,
]:
continue
if score.data.get_int('medal') == self.PLAY_MEDAL_NO_PLAY:
continue
clear_medal[score.id] = clear_medal[score.id] | self.__format_medal_for_score(score)
@ -653,6 +657,8 @@ class PopnMusicSunnyPark(PopnMusicBase):
self.CHART_TYPE_EX,
]:
continue
if score.data.get_int('medal') == self.PLAY_MEDAL_NO_PLAY:
continue
clear_medal[score.id] = clear_medal[score.id] | self.__format_medal_for_score(score)
hiscore_index = (score.id * 4) + {

View File

@ -238,6 +238,8 @@ class PopnMusicTuneStreet(PopnMusicBase):
self.CHART_TYPE_EASY,
]:
continue
if score.data.get_int('medal') == self.PLAY_MEDAL_NO_PLAY:
continue
flags = self.__format_flags_for_score(score)

View File

@ -167,7 +167,7 @@ class PopnMusicUsaNeko(PopnMusicBase):
phase.add_child(Node.s16('event_id', phaseid))
phase.add_child(Node.s16('phase', phase_value))
# Gather course informatino and course ranking for users.
# Gather course information and course ranking for users.
course_infos, achievements, profiles = Parallel.execute([
lambda: self.data.local.game.get_all_time_sensitive_settings(self.game, self.version, 'course'),
lambda: self.data.local.user.get_all_achievements(self.game, self.version),
@ -555,6 +555,8 @@ class PopnMusicUsaNeko(PopnMusicBase):
self.CHART_TYPE_EX,
]:
continue
if score.data.get_int('medal') == self.PLAY_MEDAL_NO_PLAY:
continue
points = score.points
medal = score.data.get_int('medal')
@ -617,6 +619,8 @@ class PopnMusicUsaNeko(PopnMusicBase):
self.CHART_TYPE_EX,
]:
continue
if score.data.get_int('medal') == self.PLAY_MEDAL_NO_PLAY:
continue
music = Node.void('music')
root.add_child(music)
@ -757,6 +761,8 @@ class PopnMusicUsaNeko(PopnMusicBase):
self.CHART_TYPE_EX,
]:
continue
if score.data.get_int('medal') == self.PLAY_MEDAL_NO_PLAY:
continue
music = Node.void('music')
root.add_child(music)

View File

@ -94,6 +94,7 @@ class PopnMusicLapistoriaClient(BaseClient):
# Extract and return score data
medals: Dict[int, List[int]] = {}
scores: Dict[int, List[int]] = {}
courses: Dict[int, Dict[str, int]] = {}
for child in resp.child('player22').children:
if child.name == 'music':
songid = child.child_value('music_num')
@ -108,7 +109,27 @@ class PopnMusicLapistoriaClient(BaseClient):
scores[songid] = [0, 0, 0, 0]
scores[songid][chart] = points
return {'medals': medals, 'scores': scores}
if child.name == "course":
courseid = child.child_value('course_id')
medal = child.child_value('clear_medal')
combo = child.child_value('max_cmbo')
stage1 = child.child_value('stage1_score')
stage2 = child.child_value('stage2_score')
stage3 = child.child_value('stage3_score')
stage4 = child.child_value('stage4_score')
total = child.child_value('total_score')
courses[courseid] = {
'id': courseid,
'medal': medal,
'combo': combo,
'stage1': stage1,
'stage2': stage2,
'stage3': stage3,
'stage4': stage4,
'total': total,
}
return {'medals': medals, 'scores': scores, 'courses': courses}
else:
raise Exception(f'Unrecognized message type \'{msg_type}\'')
@ -164,6 +185,49 @@ class PopnMusicLapistoriaClient(BaseClient):
resp = self.exchange('', call)
self.assert_path(resp, "response/player22/@status")
def verify_player22_write_course(self, ref_id: str, course: Dict[str, int]) -> None:
call = self.call_node()
# Construct node
player22 = Node.void('player22')
call.add_child(player22)
player22.set_attribute('method', 'write_course')
player22.add_child(Node.s16('pref', 51))
player22.add_child(Node.string('location_id', 'JP-1'))
player22.add_child(Node.string('ref_id', ref_id))
player22.add_child(Node.string('data_id', ref_id))
player22.add_child(Node.string('name', self.NAME))
player22.add_child(Node.s16('chara_num', 1543))
player22.add_child(Node.s32('play_id', 0))
player22.add_child(Node.s16('course_id', course['id']))
player22.add_child(Node.s16('stage1_music_num', 148))
player22.add_child(Node.u8('stage1_sheet_num', 1))
player22.add_child(Node.s16('stage2_music_num', 550))
player22.add_child(Node.u8('stage2_sheet_num', 1))
player22.add_child(Node.s16('stage3_music_num', 1113))
player22.add_child(Node.u8('stage3_sheet_num', 1))
player22.add_child(Node.s16('stage4_music_num', 341))
player22.add_child(Node.u8('stage4_sheet_num', 1))
player22.add_child(Node.u8('norma_type', 2))
player22.add_child(Node.s32('norma_1_num', 5))
player22.add_child(Node.s32('norma_2_num', 0))
player22.add_child(Node.u8('clear_medal', course['medal']))
player22.add_child(Node.u8('clear_norma', 2))
player22.add_child(Node.s32('total_score', course['total']))
player22.add_child(Node.s16('max_combo', course['combo']))
for stage, music in enumerate([148, 550, 1113, 341]):
stagenode = Node.void('stage')
player22.add_child(stagenode)
stagenode.add_child(Node.u8('stage', stage))
stagenode.add_child(Node.s16('music_num', music))
stagenode.add_child(Node.u8('sheet_num', 1))
stagenode.add_child(Node.s32('score', course[f'stage{stage + 1}']))
# Swap with server
resp = self.exchange('', call)
self.assert_path(resp, "response/player22/@status")
def verify_player22_new(self, ref_id: str) -> None:
call = self.call_node()
@ -250,6 +314,8 @@ class PopnMusicLapistoriaClient(BaseClient):
for i in range(4):
if score[i] != 0:
raise Exception('Got nonzero scores count on a new card!')
for _ in scores['courses']:
raise Exception('Got nonzero courses count on a new card!')
for phase in [1, 2]:
if phase == 1:
@ -338,6 +404,29 @@ class PopnMusicLapistoriaClient(BaseClient):
# Sleep so we don't end up putting in score history on the same second
time.sleep(1)
# Write a random course so we know we can retrieve them.
course = {
'id': random.randint(1, 100),
'medal': 2,
'combo': random.randint(10, 100),
'stage1': random.randint(70000, 100000),
'stage2': random.randint(70000, 100000),
'stage3': random.randint(70000, 100000),
'stage4': random.randint(70000, 100000),
}
course['total'] = sum(course[f'stage{i + 1}'] for i in range(4))
self.verify_player22_write_course(ref_id, course)
# Now, grab the profile one more time and see that it is there.
scores = self.verify_player22_read(ref_id, msg_type='query')
if len(scores['courses']) != 1:
raise Exception("Did not get a course back after saving!")
if course['id'] not in scores['courses']:
raise Exception("Did not get expected course back after saving!")
for key in ['medal', 'combo', 'stage1', 'stage2', 'stage3', 'stage4', 'total']:
if course[key] != scores['courses'][course['id']][key]:
raise Exception(f'Expected a {key} of \'{course[key]}\' but got \'{scores["courses"][course["id"]][key]}\'')
else:
print("Skipping score checks for existing card")

View File

@ -240,6 +240,7 @@ class DBConstants:
MUSECA_CLEAR_TYPE_CLEARED: Final[int] = 200
MUSECA_CLEAR_TYPE_FULL_COMBO: Final[int] = 300
POPN_MUSIC_PLAY_MEDAL_NO_PLAY: Final[int] = 50
POPN_MUSIC_PLAY_MEDAL_CIRCLE_FAILED: Final[int] = 100
POPN_MUSIC_PLAY_MEDAL_DIAMOND_FAILED: Final[int] = 200
POPN_MUSIC_PLAY_MEDAL_STAR_FAILED: Final[int] = 300

View File

@ -633,9 +633,9 @@ class UserData(BaseData):
return [UserID(result['userid']) for result in cursor.fetchall()]
def get_all_achievements(self, game: GameConstants, version: int) -> List[Tuple[UserID, Achievement]]:
def get_all_achievements(self, game: GameConstants, version: int, achievementid: Optional[int] = None, achievementtype: Optional[str] = None) -> List[Tuple[UserID, Achievement]]:
"""
Given a game/version, find all achievements for al players.
Given a game/version, find all achievements for all players.
Parameters:
game - Enum value identifier of the game looking up the user.
@ -649,7 +649,14 @@ class UserData(BaseData):
"refid.userid AS userid FROM achievement, refid WHERE refid.game = :game AND "
"refid.version = :version AND refid.refid = achievement.refid"
)
cursor = self.execute(sql, {'game': game.value, 'version': version})
params: Dict[str, Any] = {'game': game.value, 'version': version}
if achievementtype is not None:
sql += " AND achievement.type = :type"
params['type'] = achievementtype
if achievementid is not None:
sql += " AND achievement.id = :id"
params['id'] = achievementid
cursor = self.execute(sql, params)
achievements = []
for result in cursor.fetchall():

View File

@ -15,7 +15,7 @@ from bemani.frontend.types import g
bishi_pages = Blueprint(
'bishi_pages',
__name__,
url_prefix='/bishi',
url_prefix=f'/{GameConstants.BISHI_BASHI.value}',
template_folder=templates_location,
static_folder=static_location,
)

View File

@ -15,7 +15,7 @@ from bemani.frontend.types import g
ddr_pages = Blueprint(
'ddr_pages',
__name__,
url_prefix='/ddr',
url_prefix=f'/{GameConstants.DDR.value}',
template_folder=templates_location,
static_folder=static_location,
)

View File

@ -14,7 +14,7 @@ from bemani.frontend.types import g
iidx_pages = Blueprint(
'iidx_pages',
__name__,
url_prefix='/iidx',
url_prefix=f'/{GameConstants.IIDX.value}',
template_folder=templates_location,
static_folder=static_location,
)

View File

@ -14,7 +14,7 @@ from bemani.frontend.types import g
jubeat_pages = Blueprint(
'jubeat_pages',
__name__,
url_prefix='/jubeat',
url_prefix=f'/{GameConstants.JUBEAT.value}',
template_folder=templates_location,
static_folder=static_location,
)

View File

@ -15,7 +15,7 @@ from bemani.frontend.types import g
museca_pages = Blueprint(
'museca_pages',
__name__,
url_prefix='/museca',
url_prefix=f'/{GameConstants.MUSECA.value}',
template_folder=templates_location,
static_folder=static_location,
)

View File

@ -15,7 +15,7 @@ from bemani.frontend.types import g
popn_pages = Blueprint(
'popn_pages',
__name__,
url_prefix='/pnm',
url_prefix=f'/{GameConstants.POPN_MUSIC.value}',
template_folder=templates_location,
static_folder=static_location,
)

View File

@ -26,7 +26,7 @@ class PopnMusicFrontend(FrontendBase):
VersionConstants.POPN_MUSIC_TUNE_STREET: 0,
VersionConstants.POPN_MUSIC_FANTASIA: 2,
VersionConstants.POPN_MUSIC_SUNNY_PARK: 2,
VersionConstants.POPN_MUSIC_LAPISTORIA: 4,
VersionConstants.POPN_MUSIC_LAPISTORIA: 2,
VersionConstants.POPN_MUSIC_ECLALE: 4,
VersionConstants.POPN_MUSIC_USANEKO: 4,
}
@ -40,6 +40,7 @@ class PopnMusicFrontend(FrontendBase):
formatted_score['combo'] = score.data.get_int('combo', -1)
formatted_score['medal'] = score.data.get_int('medal')
formatted_score['status'] = {
PopnMusicBase.PLAY_MEDAL_NO_PLAY: "No Play",
PopnMusicBase.PLAY_MEDAL_CIRCLE_FAILED: "○ Failed",
PopnMusicBase.PLAY_MEDAL_DIAMOND_FAILED: "◇ Failed",
PopnMusicBase.PLAY_MEDAL_STAR_FAILED: "☆ Failed",

View File

@ -15,7 +15,7 @@ from bemani.frontend.types import g
reflec_pages = Blueprint(
'reflec_pages',
__name__,
url_prefix='/reflec',
url_prefix=f'/{GameConstants.REFLEC_BEAT.value}',
template_folder=templates_location,
static_folder=static_location,
)

View File

@ -15,7 +15,7 @@ from bemani.frontend.types import g
sdvx_pages = Blueprint(
'sdvx_pages',
__name__,
url_prefix='/sdvx',
url_prefix=f'/{GameConstants.SDVX.value}',
template_folder=templates_location,
static_folder=static_location,
)