1
0
mirror of synced 2024-11-30 16:54:30 +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 import traceback
from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Type 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.common import Model, ValidatedDict, Profile, PlayStatistics, GameConstants, Time
from bemani.data import Config, Data, Machine, UserID, RemoteUser 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. List of statuses we return to the game for various reasons.
""" """
SUCCESS = 0 SUCCESS: Final[int] = 0
NO_PROFILE = 109 NO_PROFILE: Final[int] = 109
NOT_ALLOWED = 110 NOT_ALLOWED: Final[int] = 110
NOT_REGISTERED = 112 NOT_REGISTERED: Final[int] = 112
INVALID_PIN = 116 INVALID_PIN: Final[int] = 116
class Factory: class Factory(ABC):
""" """
The base class every game factory inherits from. Defines a create method The base class every game factory inherits from. Defines a create method
which should return some game class which can handle packets. Game classes 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. Dispatch will look up in order to handle calls.
""" """
MANAGED_CLASSES: List[Type["Base"]] = [] MANAGED_CLASSES: List[Type["Base"]]
@classmethod @classmethod
@abstractmethod
def register_all(cls) -> None: def register_all(cls) -> None:
""" """
Subclasses of this class should use this function to register themselves Subclasses of this class should use this function to register themselves
with Base, using Base.register(). Factories specify the game code that with Base, using Base.register(). Factories specify the game code that
they support, which Base will use when routing requests. they support, which Base will use when routing requests.
""" """
raise Exception('Override this in subclass!') raise NotImplementedError('Override this in subclass!')
@classmethod @classmethod
def run_scheduled_work(cls, data: Data, config: Config) -> None: def run_scheduled_work(cls, data: Data, config: Config) -> None:
@ -84,6 +86,7 @@ class Factory:
yield (game.game, game.version, game.get_settings()) yield (game.game, game.version, game.get_settings())
@classmethod @classmethod
@abstractmethod
def create(cls, data: Data, config: Config, model: Model, parentmodel: Optional[Model]=None) -> Optional['Base']: 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. 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 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. 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): class Base(ABC):

View File

@ -18,6 +18,7 @@ class PopnMusicBase(CoreHandler, CardManagerHandler, PASELIHandler, Base):
game = GameConstants.POPN_MUSIC game = GameConstants.POPN_MUSIC
# Play medals, as saved into/loaded from the DB # 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_CIRCLE_FAILED = DBConstants.POPN_MUSIC_PLAY_MEDAL_CIRCLE_FAILED
PLAY_MEDAL_DIAMOND_FAILED = DBConstants.POPN_MUSIC_PLAY_MEDAL_DIAMOND_FAILED PLAY_MEDAL_DIAMOND_FAILED = DBConstants.POPN_MUSIC_PLAY_MEDAL_DIAMOND_FAILED
PLAY_MEDAL_STAR_FAILED = DBConstants.POPN_MUSIC_PLAY_MEDAL_STAR_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 # Range check medals
if medal not in [ if medal not in [
self.PLAY_MEDAL_NO_PLAY,
self.PLAY_MEDAL_CIRCLE_FAILED, self.PLAY_MEDAL_CIRCLE_FAILED,
self.PLAY_MEDAL_DIAMOND_FAILED, self.PLAY_MEDAL_DIAMOND_FAILED,
self.PLAY_MEDAL_STAR_FAILED, self.PLAY_MEDAL_STAR_FAILED,

View File

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

View File

@ -198,6 +198,8 @@ class PopnMusicFantasia(PopnMusicBase):
self.CHART_TYPE_EX, self.CHART_TYPE_EX,
]: ]:
continue 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) clear_medal[score.id] = clear_medal[score.id] | self.__format_medal_for_score(score)
hiscore_index = (score.id * 4) + { hiscore_index = (score.id * 4) + {
@ -289,6 +291,8 @@ class PopnMusicFantasia(PopnMusicBase):
self.CHART_TYPE_EX, self.CHART_TYPE_EX,
]: ]:
continue 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) clear_medal[score.id] = clear_medal[score.id] | self.__format_medal_for_score(score)
hiscore_index = (score.id * 4) + { hiscore_index = (score.id * 4) + {
@ -567,6 +571,8 @@ class PopnMusicFantasia(PopnMusicBase):
self.CHART_TYPE_EX, self.CHART_TYPE_EX,
]: ]:
continue 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) clear_medal[score.id] = clear_medal[score.id] | self.__format_medal_for_score(score)
hiscore_index = (score.id * 4) + { hiscore_index = (score.id * 4) + {

View File

@ -1,12 +1,12 @@
# vim: set fileencoding=utf-8 # vim: set fileencoding=utf-8
import copy import copy
from typing import Dict, List from typing import Any, Dict, List
from bemani.backend.popn.base import PopnMusicBase from bemani.backend.popn.base import PopnMusicBase
from bemani.backend.popn.sunnypark import PopnMusicSunnyPark from bemani.backend.popn.sunnypark import PopnMusicSunnyPark
from bemani.backend.base import Status 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.data import UserID, Link
from bemani.protocol import Node from bemani.protocol import Node
@ -23,6 +23,7 @@ class PopnMusicLapistoria(PopnMusicBase):
GAME_CHART_TYPE_EX = 3 GAME_CHART_TYPE_EX = 3
# Medal type, as returned from the game # Medal type, as returned from the game
GAME_PLAY_MEDAL_NO_PLAY = 0
GAME_PLAY_MEDAL_CIRCLE_FAILED = 1 GAME_PLAY_MEDAL_CIRCLE_FAILED = 1
GAME_PLAY_MEDAL_DIAMOND_FAILED = 2 GAME_PLAY_MEDAL_DIAMOND_FAILED = 2
GAME_PLAY_MEDAL_STAR_FAILED = 3 GAME_PLAY_MEDAL_STAR_FAILED = 3
@ -41,48 +42,101 @@ class PopnMusicLapistoria(PopnMusicBase):
def previous_version(self) -> PopnMusicBase: def previous_version(self) -> PopnMusicBase:
return PopnMusicSunnyPark(self.data, self.config, self.model) 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: 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 = { phases = {
# Unknown event (0-16) # Default song phase availability (0-16)
0: 0, 0: 16,
# Unknown event (0-11) # Card phase (0-11)
1: 0, 1: 11,
# Pop'n Aura, max (0-11) (remove all aura requirements) # Pop'n Aura, max (0-11) (remove all aura requirements)
2: 11, 2: 11,
# Story (0-24) # Story (0-24)
3: 1, 3: story_phase,
# BEMANI ruins Discovery! (0-2) # BEMANI ruins Discovery! 0 = off, 1 = active, 2 = off
4: 0, 4: 0,
# Unknown event, something to do with net taisen (0-2) # Unknown event, something to do with net taisen (0-2)
5: 0, 5: 2,
# Unknown event (0-1) # Unknown event (0-1)
6: 0, 6: 1,
# Unknown event (0-1) # Unknown event (0-1)
7: 0, 7: 1,
# Unknown event (0-1) # Unknown event (0-1)
8: 0, 8: 1,
# Unknown event (0-11) # Course mode phase (0-11)
9: 0, 9: 11,
# Unknown event (0-2) # Pon's Fate Purification Plan, 0 = off, 1 = active, 2 = off
10: 0, 10: 0,
# Unknown event (0-3) # Unknown event (0-3)
11: 0, 11: 3,
# Unknown event (0-1) # Unknown event (0-1)
12: 0, 12: 1,
# Unknown event (0-2) # Appears to be unlocks for course mode including KAC stuff.
13: 0, 13: 2,
# Unknown event (0-4) # Unknown event (0-4)
14: 0, 14: 4,
# Unknown event (0-2) # Unknown event (0-2)
15: 0, 15: 2,
# Unknown event (0-2) # Unknown event (0-2)
16: 0, 16: 2,
# Unknown event (0-12) # Unknown event (0-12)
17: 0, 17: 0,
# Unknown event (0-2) # Unknown event (0-2)
18: 0, 18: 2,
# Unknown event (0-7) # Bemani Summer Diary, 0 = off, 1-6 are phases, 7 = off
19: 0, 19: 0,
} }
@ -198,6 +252,7 @@ class PopnMusicLapistoria(PopnMusicBase):
rivalid = links[no].other_userid rivalid = links[no].other_userid
rivalprofile = profiles[rivalid] rivalprofile = profiles[rivalid]
scores = self.data.remote.music.get_scores(self.game, self.version, 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. # First, output general profile info.
friend = Node.void('friend') friend = Node.void('friend')
@ -234,6 +289,7 @@ class PopnMusicLapistoria(PopnMusicBase):
}[score.chart])) }[score.chart]))
music.set_attribute('score', str(points)) music.set_attribute('score', str(points))
music.set_attribute('clearmedal', str({ 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_CIRCLE_FAILED: self.GAME_PLAY_MEDAL_CIRCLE_FAILED,
self.PLAY_MEDAL_DIAMOND_FAILED: self.GAME_PLAY_MEDAL_DIAMOND_FAILED, self.PLAY_MEDAL_DIAMOND_FAILED: self.GAME_PLAY_MEDAL_DIAMOND_FAILED,
self.PLAY_MEDAL_STAR_FAILED: self.GAME_PLAY_MEDAL_STAR_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, self.PLAY_MEDAL_PERFECT: self.GAME_PLAY_MEDAL_PERFECT,
}[medal])) }[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 return root
def handle_player22_conversion_request(self, request: Node) -> Node: def handle_player22_conversion_request(self, request: Node) -> Node:
@ -287,6 +364,7 @@ class PopnMusicLapistoria(PopnMusicBase):
'bad': request.child_value('bad') 'bad': request.child_value('bad')
} }
medal = { 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_CIRCLE_FAILED: self.PLAY_MEDAL_CIRCLE_FAILED,
self.GAME_PLAY_MEDAL_DIAMOND_FAILED: self.PLAY_MEDAL_DIAMOND_FAILED, self.GAME_PLAY_MEDAL_DIAMOND_FAILED: self.PLAY_MEDAL_DIAMOND_FAILED,
self.GAME_PLAY_MEDAL_STAR_FAILED: self.PLAY_MEDAL_STAR_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) self.update_score(userid, songid, chart, points, medal, combo=combo, stats=stats)
return root 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: def format_profile(self, userid: UserID, profile: Profile) -> Node:
root = Node.void('player22') root = Node.void('player22')
@ -380,6 +571,7 @@ class PopnMusicLapistoria(PopnMusicBase):
music.add_child(Node.s16('cnt', score.plays)) music.add_child(Node.s16('cnt', score.plays))
music.add_child(Node.s32('score', points)) music.add_child(Node.s32('score', points))
music.add_child(Node.u8('clear_type', { 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_CIRCLE_FAILED: self.GAME_PLAY_MEDAL_CIRCLE_FAILED,
self.PLAY_MEDAL_DIAMOND_FAILED: self.GAME_PLAY_MEDAL_DIAMOND_FAILED, self.PLAY_MEDAL_DIAMOND_FAILED: self.GAME_PLAY_MEDAL_DIAMOND_FAILED,
self.PLAY_MEDAL_STAR_FAILED: self.GAME_PLAY_MEDAL_STAR_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_1', customize_dict.get_int('comment_1')))
customize.add_child(Node.u16('comment_2', customize_dict.get_int('comment_2'))) 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 # Set up achievements
achievements = self.data.local.user.get_achievements(self.game, self.version, userid) achievements = self.data.local.user.get_achievements(self.game, self.version, userid)
for achievement in achievements: for achievement in achievements:
@ -481,6 +684,20 @@ class PopnMusicLapistoria(PopnMusicBase):
itemtype = achievement.data.get_int('type') itemtype = achievement.data.get_int('type')
param = achievement.data.get_int('param') 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') item = Node.void('item')
root.add_child(item) root.add_child(item)
item.add_child(Node.u8('type', itemtype)) 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.bool('is_cleared', cleared))
story.add_child(Node.u32('clear_chapter', clear_chapter)) 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 return root
def unformat_profile(self, userid: UserID, request: Node, oldprofile: Profile) -> Profile: def unformat_profile(self, userid: UserID, request: Node, oldprofile: Profile) -> Profile:
@ -584,6 +829,7 @@ class PopnMusicLapistoria(PopnMusicBase):
self.update_play_statistics(userid) self.update_play_statistics(userid)
# Extract achievements # Extract achievements
game_config = self.get_game_config()
for node in request.children: for node in request.children:
if node.name == 'item': if node.name == 'item':
if not node.child_value('is_new'): if not node.child_value('is_new'):
@ -594,6 +840,10 @@ class PopnMusicLapistoria(PopnMusicBase):
itemtype = node.child_value('type') itemtype = node.child_value('type')
param = node.child_value('param') 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.data.local.user.put_achievement(
self.game, self.game,
self.version, self.version,
@ -698,6 +948,7 @@ class PopnMusicLapistoria(PopnMusicBase):
music.add_child(Node.u8('clear_type', 0)) music.add_child(Node.u8('clear_type', 0))
music.add_child(Node.s32('old_score', points)) music.add_child(Node.s32('old_score', points))
music.add_child(Node.u8('old_clear_type', { 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_CIRCLE_FAILED: self.GAME_PLAY_MEDAL_CIRCLE_FAILED,
self.PLAY_MEDAL_DIAMOND_FAILED: self.GAME_PLAY_MEDAL_DIAMOND_FAILED, self.PLAY_MEDAL_DIAMOND_FAILED: self.GAME_PLAY_MEDAL_DIAMOND_FAILED,
self.PLAY_MEDAL_STAR_FAILED: self.GAME_PLAY_MEDAL_STAR_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, self.CHART_TYPE_EX,
]: ]:
continue 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) clear_medal[score.id] = clear_medal[score.id] | self.__format_medal_for_score(score)
hiscore_index = (score.id * 4) + { hiscore_index = (score.id * 4) + {
@ -348,6 +350,8 @@ class PopnMusicSunnyPark(PopnMusicBase):
self.CHART_TYPE_EX, self.CHART_TYPE_EX,
]: ]:
continue 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) 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, self.CHART_TYPE_EX,
]: ]:
continue 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) clear_medal[score.id] = clear_medal[score.id] | self.__format_medal_for_score(score)
hiscore_index = (score.id * 4) + { hiscore_index = (score.id * 4) + {

View File

@ -238,6 +238,8 @@ class PopnMusicTuneStreet(PopnMusicBase):
self.CHART_TYPE_EASY, self.CHART_TYPE_EASY,
]: ]:
continue continue
if score.data.get_int('medal') == self.PLAY_MEDAL_NO_PLAY:
continue
flags = self.__format_flags_for_score(score) 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('event_id', phaseid))
phase.add_child(Node.s16('phase', phase_value)) 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([ 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.game.get_all_time_sensitive_settings(self.game, self.version, 'course'),
lambda: self.data.local.user.get_all_achievements(self.game, self.version), lambda: self.data.local.user.get_all_achievements(self.game, self.version),
@ -555,6 +555,8 @@ class PopnMusicUsaNeko(PopnMusicBase):
self.CHART_TYPE_EX, self.CHART_TYPE_EX,
]: ]:
continue continue
if score.data.get_int('medal') == self.PLAY_MEDAL_NO_PLAY:
continue
points = score.points points = score.points
medal = score.data.get_int('medal') medal = score.data.get_int('medal')
@ -617,6 +619,8 @@ class PopnMusicUsaNeko(PopnMusicBase):
self.CHART_TYPE_EX, self.CHART_TYPE_EX,
]: ]:
continue continue
if score.data.get_int('medal') == self.PLAY_MEDAL_NO_PLAY:
continue
music = Node.void('music') music = Node.void('music')
root.add_child(music) root.add_child(music)
@ -757,6 +761,8 @@ class PopnMusicUsaNeko(PopnMusicBase):
self.CHART_TYPE_EX, self.CHART_TYPE_EX,
]: ]:
continue continue
if score.data.get_int('medal') == self.PLAY_MEDAL_NO_PLAY:
continue
music = Node.void('music') music = Node.void('music')
root.add_child(music) root.add_child(music)

View File

@ -94,6 +94,7 @@ class PopnMusicLapistoriaClient(BaseClient):
# Extract and return score data # Extract and return score data
medals: Dict[int, List[int]] = {} medals: Dict[int, List[int]] = {}
scores: Dict[int, List[int]] = {} scores: Dict[int, List[int]] = {}
courses: Dict[int, Dict[str, int]] = {}
for child in resp.child('player22').children: for child in resp.child('player22').children:
if child.name == 'music': if child.name == 'music':
songid = child.child_value('music_num') songid = child.child_value('music_num')
@ -108,7 +109,27 @@ class PopnMusicLapistoriaClient(BaseClient):
scores[songid] = [0, 0, 0, 0] scores[songid] = [0, 0, 0, 0]
scores[songid][chart] = points 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: else:
raise Exception(f'Unrecognized message type \'{msg_type}\'') raise Exception(f'Unrecognized message type \'{msg_type}\'')
@ -164,6 +185,49 @@ class PopnMusicLapistoriaClient(BaseClient):
resp = self.exchange('', call) resp = self.exchange('', call)
self.assert_path(resp, "response/player22/@status") 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: def verify_player22_new(self, ref_id: str) -> None:
call = self.call_node() call = self.call_node()
@ -250,6 +314,8 @@ class PopnMusicLapistoriaClient(BaseClient):
for i in range(4): for i in range(4):
if score[i] != 0: if score[i] != 0:
raise Exception('Got nonzero scores count on a new card!') 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]: for phase in [1, 2]:
if phase == 1: 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 # Sleep so we don't end up putting in score history on the same second
time.sleep(1) 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: else:
print("Skipping score checks for existing card") 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_CLEARED: Final[int] = 200
MUSECA_CLEAR_TYPE_FULL_COMBO: Final[int] = 300 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_CIRCLE_FAILED: Final[int] = 100
POPN_MUSIC_PLAY_MEDAL_DIAMOND_FAILED: Final[int] = 200 POPN_MUSIC_PLAY_MEDAL_DIAMOND_FAILED: Final[int] = 200
POPN_MUSIC_PLAY_MEDAL_STAR_FAILED: Final[int] = 300 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()] 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: Parameters:
game - Enum value identifier of the game looking up the user. 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.userid AS userid FROM achievement, refid WHERE refid.game = :game AND "
"refid.version = :version AND refid.refid = achievement.refid" "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 = [] achievements = []
for result in cursor.fetchall(): for result in cursor.fetchall():

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,7 +26,7 @@ class PopnMusicFrontend(FrontendBase):
VersionConstants.POPN_MUSIC_TUNE_STREET: 0, VersionConstants.POPN_MUSIC_TUNE_STREET: 0,
VersionConstants.POPN_MUSIC_FANTASIA: 2, VersionConstants.POPN_MUSIC_FANTASIA: 2,
VersionConstants.POPN_MUSIC_SUNNY_PARK: 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_ECLALE: 4,
VersionConstants.POPN_MUSIC_USANEKO: 4, VersionConstants.POPN_MUSIC_USANEKO: 4,
} }
@ -40,6 +40,7 @@ class PopnMusicFrontend(FrontendBase):
formatted_score['combo'] = score.data.get_int('combo', -1) formatted_score['combo'] = score.data.get_int('combo', -1)
formatted_score['medal'] = score.data.get_int('medal') formatted_score['medal'] = score.data.get_int('medal')
formatted_score['status'] = { formatted_score['status'] = {
PopnMusicBase.PLAY_MEDAL_NO_PLAY: "No Play",
PopnMusicBase.PLAY_MEDAL_CIRCLE_FAILED: "○ Failed", PopnMusicBase.PLAY_MEDAL_CIRCLE_FAILED: "○ Failed",
PopnMusicBase.PLAY_MEDAL_DIAMOND_FAILED: "◇ Failed", PopnMusicBase.PLAY_MEDAL_DIAMOND_FAILED: "◇ Failed",
PopnMusicBase.PLAY_MEDAL_STAR_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 = Blueprint(
'reflec_pages', 'reflec_pages',
__name__, __name__,
url_prefix='/reflec', url_prefix=f'/{GameConstants.REFLEC_BEAT.value}',
template_folder=templates_location, template_folder=templates_location,
static_folder=static_location, static_folder=static_location,
) )

View File

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