1589 lines
68 KiB
Python
1589 lines
68 KiB
Python
# vim: set fileencoding=utf-8
|
|
from abc import ABC, abstractmethod
|
|
import binascii
|
|
import random
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
from typing_extensions import Final
|
|
|
|
from bemani.backend.popn.base import PopnMusicBase
|
|
from bemani.common import Time, ID, Profile, ValidatedDict, Parallel
|
|
from bemani.data import Data, UserID, Achievement, Link
|
|
from bemani.protocol import Node
|
|
|
|
|
|
class PopnMusicModernBase(PopnMusicBase, ABC):
|
|
# Chart type, as returned from the game
|
|
GAME_CHART_TYPE_EASY: Final[int] = 0
|
|
GAME_CHART_TYPE_NORMAL: Final[int] = 1
|
|
GAME_CHART_TYPE_HYPER: Final[int] = 2
|
|
GAME_CHART_TYPE_EX: Final[int] = 3
|
|
|
|
# Medal type, as returned from the game
|
|
GAME_PLAY_MEDAL_CIRCLE_FAILED: Final[int] = 1
|
|
GAME_PLAY_MEDAL_DIAMOND_FAILED: Final[int] = 2
|
|
GAME_PLAY_MEDAL_STAR_FAILED: Final[int] = 3
|
|
GAME_PLAY_MEDAL_EASY_CLEAR: Final[int] = 4
|
|
GAME_PLAY_MEDAL_CIRCLE_CLEARED: Final[int] = 5
|
|
GAME_PLAY_MEDAL_DIAMOND_CLEARED: Final[int] = 6
|
|
GAME_PLAY_MEDAL_STAR_CLEARED: Final[int] = 7
|
|
GAME_PLAY_MEDAL_CIRCLE_FULL_COMBO: Final[int] = 8
|
|
GAME_PLAY_MEDAL_DIAMOND_FULL_COMBO: Final[int] = 9
|
|
GAME_PLAY_MEDAL_STAR_FULL_COMBO: Final[int] = 10
|
|
GAME_PLAY_MEDAL_PERFECT: Final[int] = 11
|
|
|
|
# Rank type, as returned from the game
|
|
GAME_PLAY_RANK_E: Final[int] = 1
|
|
GAME_PLAY_RANK_D: Final[int] = 2
|
|
GAME_PLAY_RANK_C: Final[int] = 3
|
|
GAME_PLAY_RANK_B: Final[int] = 4
|
|
GAME_PLAY_RANK_A: Final[int] = 5
|
|
GAME_PLAY_RANK_AA: Final[int] = 6
|
|
GAME_PLAY_RANK_AAA: Final[int] = 7
|
|
GAME_PLAY_RANK_S: Final[int] = 8
|
|
|
|
# Biggest ID in the music DB
|
|
GAME_MAX_MUSIC_ID: int
|
|
|
|
# Biggest deco part ID in the game
|
|
GAME_MAX_DECO_ID: int
|
|
|
|
# Return the local2 and lobby2 service so that Pop'n Music 24+ will
|
|
# send game packets.
|
|
extra_services: List[str] = [
|
|
"local2",
|
|
"lobby2",
|
|
]
|
|
|
|
@classmethod
|
|
def run_scheduled_work(
|
|
cls, data: Data, config: Dict[str, Any]
|
|
) -> List[Tuple[str, Dict[str, Any]]]:
|
|
"""
|
|
Once a week, insert a new course.
|
|
"""
|
|
events = []
|
|
if data.local.network.should_schedule(
|
|
cls.game, cls.version, "course", "weekly"
|
|
):
|
|
# Generate a new course list, save it to the DB.
|
|
start_time, end_time = data.local.network.get_schedule_duration("weekly")
|
|
all_songs = [
|
|
song.id
|
|
for song in data.local.music.get_all_songs(cls.game, cls.version)
|
|
]
|
|
if all_songs:
|
|
course_song = random.choice(all_songs)
|
|
data.local.game.put_time_sensitive_settings(
|
|
cls.game,
|
|
cls.version,
|
|
"course",
|
|
{
|
|
"start_time": start_time,
|
|
"end_time": end_time,
|
|
"music": course_song,
|
|
},
|
|
)
|
|
events.append(
|
|
(
|
|
"pnm_course",
|
|
{
|
|
"version": cls.version,
|
|
"song": course_song,
|
|
},
|
|
)
|
|
)
|
|
|
|
# Mark that we did some actual work here.
|
|
data.local.network.mark_scheduled(
|
|
cls.game, cls.version, "course", "weekly"
|
|
)
|
|
return events
|
|
|
|
def __score_to_rank(self, score: int) -> int:
|
|
if score < 50000:
|
|
return self.GAME_PLAY_RANK_E
|
|
if score < 62000:
|
|
return self.GAME_PLAY_RANK_D
|
|
if score < 72000:
|
|
return self.GAME_PLAY_RANK_C
|
|
if score < 82000:
|
|
return self.GAME_PLAY_RANK_B
|
|
if score < 90000:
|
|
return self.GAME_PLAY_RANK_A
|
|
if score < 95000:
|
|
return self.GAME_PLAY_RANK_AA
|
|
if score < 98000:
|
|
return self.GAME_PLAY_RANK_AAA
|
|
return self.GAME_PLAY_RANK_S
|
|
|
|
def handle_lobby24_requests(self, request: Node) -> Node:
|
|
# Stub out the entire lobby24 service
|
|
return Node.void("lobby24")
|
|
|
|
def handle_pcb24_error_request(self, request: Node) -> Node:
|
|
return Node.void("pcb24")
|
|
|
|
def handle_pcb24_boot_request(self, request: Node) -> Node:
|
|
return Node.void("pcb24")
|
|
|
|
def handle_pcb24_write_request(self, request: Node) -> Node:
|
|
# Update the name of this cab for admin purposes
|
|
self.update_machine_name(request.child_value("pcb_setting/name"))
|
|
return Node.void("pcb24")
|
|
|
|
@abstractmethod
|
|
def get_common_config(self) -> Tuple[Dict[int, int], bool]:
|
|
"""
|
|
Return a tuple of configuration options for sending the common node back
|
|
to the client. The first parameter is a dictionary whose keys are event
|
|
IDs and values are the event phase number. The second parameter is a bool
|
|
representing whether or not to send areas.
|
|
"""
|
|
|
|
def __construct_common_info(self, root: Node) -> None:
|
|
phases, send_areas = self.get_common_config()
|
|
|
|
for phaseid, phase_value in phases.items():
|
|
phase = Node.void("phase")
|
|
root.add_child(phase)
|
|
phase.add_child(Node.s16("event_id", phaseid))
|
|
phase.add_child(Node.s16("phase", phase_value))
|
|
|
|
# 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
|
|
),
|
|
lambda: self.data.local.user.get_all_profiles(self.game, self.version),
|
|
]
|
|
)
|
|
# Sort courses by newest to oldest so we can grab the newest 256.
|
|
course_infos = sorted(
|
|
course_infos,
|
|
key=lambda c: c["start_time"],
|
|
reverse=True,
|
|
)
|
|
# Sort achievements within course ID from best to worst ranking.
|
|
achievements_by_course_id: Dict[
|
|
int, Dict[str, List[Tuple[UserID, Achievement]]]
|
|
] = {}
|
|
type_to_chart_lut: Dict[str, str] = {
|
|
f"course_{self.GAME_CHART_TYPE_EASY}": "loc_ranking_e",
|
|
f"course_{self.GAME_CHART_TYPE_NORMAL}": "loc_ranking_n",
|
|
f"course_{self.GAME_CHART_TYPE_HYPER}": "loc_ranking_h",
|
|
f"course_{self.GAME_CHART_TYPE_EX}": "loc_ranking_ex",
|
|
}
|
|
for uid, ach in achievements:
|
|
if ach.type[:7] != "course_":
|
|
continue
|
|
if ach.id not in achievements_by_course_id:
|
|
achievements_by_course_id[ach.id] = {
|
|
"loc_ranking_e": [],
|
|
"loc_ranking_n": [],
|
|
"loc_ranking_h": [],
|
|
"loc_ranking_ex": [],
|
|
}
|
|
achievements_by_course_id[ach.id][type_to_chart_lut[ach.type]].append(
|
|
(uid, ach)
|
|
)
|
|
for courseid in achievements_by_course_id:
|
|
for chart in [
|
|
"loc_ranking_e",
|
|
"loc_ranking_n",
|
|
"loc_ranking_h",
|
|
"loc_ranking_ex",
|
|
]:
|
|
achievements_by_course_id[courseid][chart] = sorted(
|
|
achievements_by_course_id[courseid][chart],
|
|
key=lambda uid_and_ach: uid_and_ach[1].data.get_int("score"),
|
|
reverse=True,
|
|
)
|
|
|
|
# Cache of userID to profile
|
|
userid_to_profile: Dict[UserID, Profile] = {
|
|
uid: profile for (uid, profile) in profiles
|
|
}
|
|
|
|
# Course ranking info for the last 256 courses
|
|
for course_info in course_infos[:256]:
|
|
course_id = int(course_info["start_time"] / 604800)
|
|
course_rankings = achievements_by_course_id.get(course_id, {})
|
|
|
|
ranking_info = Node.void("ranking_info")
|
|
root.add_child(ranking_info)
|
|
ranking_info.add_child(Node.s16("course_id", course_id))
|
|
ranking_info.add_child(
|
|
Node.u64("start_date", course_info["start_time"] * 1000)
|
|
)
|
|
ranking_info.add_child(Node.u64("end_date", course_info["end_time"] * 1000))
|
|
ranking_info.add_child(Node.s32("music_id", course_info["music"]))
|
|
|
|
# Top 20 rankings for each particular chart.
|
|
for name in [
|
|
"loc_ranking_e",
|
|
"loc_ranking_n",
|
|
"loc_ranking_h",
|
|
"loc_ranking_ex",
|
|
]:
|
|
chart_rankings = course_rankings.get(name, [])
|
|
|
|
for pos, (uid, ach) in enumerate(chart_rankings[:20]):
|
|
profile = userid_to_profile.get(
|
|
uid, Profile(self.game, self.version, "", 0)
|
|
)
|
|
|
|
subnode = Node.void(name)
|
|
ranking_info.add_child(subnode)
|
|
subnode.add_child(Node.s16("rank", pos + 1))
|
|
subnode.add_child(Node.string("name", profile.get_str("name")))
|
|
subnode.add_child(
|
|
Node.s16("chara_num", profile.get_int("chara", -1))
|
|
)
|
|
subnode.add_child(
|
|
Node.s32("total_score", ach.data.get_int("score"))
|
|
)
|
|
subnode.add_child(
|
|
Node.u8("clear_type", ach.data.get_int("clear_type"))
|
|
)
|
|
subnode.add_child(
|
|
Node.u8("clear_rank", ach.data.get_int("clear_rank"))
|
|
)
|
|
|
|
if send_areas:
|
|
for area_id in range(1, 16):
|
|
area = Node.void("area")
|
|
root.add_child(area)
|
|
area.add_child(Node.s16("area_id", area_id))
|
|
area.add_child(Node.u64("end_date", 0))
|
|
area.add_child(Node.s16("medal_id", area_id))
|
|
area.add_child(Node.bool("is_limit", False))
|
|
|
|
for choco_id in range(0, 5):
|
|
choco = Node.void("choco")
|
|
root.add_child(choco)
|
|
choco.add_child(Node.s16("choco_id", choco_id))
|
|
choco.add_child(Node.s32("param", -1))
|
|
|
|
# Set up goods, educated guess here.
|
|
for goods_id in range(self.GAME_MAX_DECO_ID):
|
|
if goods_id < 15:
|
|
price = 30
|
|
elif goods_id < 30:
|
|
price = 40
|
|
elif goods_id < 45:
|
|
price = 60
|
|
elif goods_id < 60:
|
|
price = 80
|
|
elif goods_id < 98:
|
|
price = 200
|
|
else:
|
|
price = 250
|
|
goods = Node.void("goods")
|
|
root.add_child(goods)
|
|
goods.add_child(Node.s32("item_id", goods_id + 1))
|
|
goods.add_child(Node.s16("item_type", 3))
|
|
goods.add_child(Node.s32("price", price))
|
|
goods.add_child(Node.s16("goods_type", 0))
|
|
|
|
# Ignoring NAVIfes node, we don't set these.
|
|
# fes = Node.void('fes')
|
|
# fes.add_child(Node.s16('fes_id', -1))
|
|
# fes.add_child(Node.s32('gauge_count', -1))
|
|
# fes.add_child(Node.s32_array('gauge', [-1, -1, -1, -1, -1, -1]))
|
|
# fes.add_child(Node.s32_array('music', [-1, -1, -1, -1, -1, -1]))
|
|
# fes.add_child(Node.s16('r', -1))
|
|
# fes.add_child(Node.s16('g', -1))
|
|
# fes.add_child(Node.s16('b', -1))
|
|
# fes.add_child(Node.s16('poster', -1))
|
|
|
|
# Calculate most popular characters
|
|
profiles = self.data.remote.user.get_all_profiles(self.game, self.version)
|
|
charas: Dict[int, int] = {}
|
|
for _userid, profile in profiles:
|
|
chara = profile.get_int("chara", -1)
|
|
if chara <= 0:
|
|
continue
|
|
if chara not in charas:
|
|
charas[chara] = 1
|
|
else:
|
|
charas[chara] = charas[chara] + 1
|
|
|
|
# Order a typle by most popular character to least popular character
|
|
charamap = sorted(
|
|
[(c, charas[c]) for c in charas],
|
|
key=lambda c: c[1],
|
|
reverse=True,
|
|
)
|
|
|
|
# Top 20 Popular characters
|
|
for rank, (charaid, _usecount) in enumerate(charamap[:20]):
|
|
popular = Node.void("popular")
|
|
root.add_child(popular)
|
|
popular.add_child(Node.s16("rank", rank + 1))
|
|
popular.add_child(Node.s16("chara_num", charaid))
|
|
|
|
# Top 500 Popular music
|
|
for songid, _plays in self.data.local.music.get_hit_chart(
|
|
self.game, self.music_version, 500
|
|
):
|
|
popular_music = Node.void("popular_music")
|
|
root.add_child(popular_music)
|
|
popular_music.add_child(Node.s16("music_num", songid))
|
|
|
|
# Ignoring recommended music, we don't set this
|
|
# recommend = Node.void('recommend')
|
|
# root.add_child(recommend)
|
|
# recommend.add_child(Node.s32_array('music_no', [-1] * 30))
|
|
|
|
# Ignoring mission points, we don't set these.
|
|
# mission_point = Node.void('mission_point')
|
|
# mission_point.add_child(Node.s32('point', -1))
|
|
# mission_point.add_child(Node.s32('bonus_point', -1))
|
|
|
|
# Ignoring medals, we don't set these.
|
|
# medal = Node.void('medal')
|
|
# medal.add_child(Node.s16('medal_id', -1))
|
|
# medal.add_child(Node.s16('percent', -1))
|
|
|
|
# Ignoring chara ranking, we don't set these.
|
|
# chara_ranking = Node.void('chara_ranking')
|
|
# chara_ranking.add_child(Node.s32('rank', -1))
|
|
# chara_ranking.add_child(Node.s32('kind_id', -1))
|
|
# chara_ranking.add_child(Node.s32('point', -1))
|
|
# chara_ranking.add_child(Node.s32('month', -1))
|
|
|
|
def handle_info24_common_request(self, root: Node) -> Node:
|
|
root = Node.void("info24")
|
|
self.__construct_common_info(root)
|
|
return root
|
|
|
|
def handle_player24_new_request(self, request: Node) -> Node:
|
|
refid = request.child_value("ref_id")
|
|
name = request.child_value("name")
|
|
root = self.new_profile_by_refid(refid, name)
|
|
if root is None:
|
|
root = Node.void("player24")
|
|
root.add_child(Node.s8("result", 2))
|
|
return root
|
|
|
|
def handle_player24_conversion_request(self, request: Node) -> Node:
|
|
refid = request.child_value("ref_id")
|
|
name = request.child_value("name")
|
|
chara = request.child_value("chara")
|
|
achievements: List[Achievement] = []
|
|
for node in request.children:
|
|
if node.name == "item":
|
|
itemid = node.child_value("id")
|
|
itemtype = node.child_value("type")
|
|
param = node.child_value("param")
|
|
is_new = node.child_value("is_new")
|
|
get_time = node.child_value("get_time")
|
|
|
|
achievements.append(
|
|
Achievement(
|
|
itemid,
|
|
f"item_{itemtype}",
|
|
0,
|
|
{
|
|
"param": param,
|
|
"is_new": is_new,
|
|
"get_time": get_time,
|
|
},
|
|
)
|
|
)
|
|
root = self.new_profile_by_refid(refid, name, chara, achievements=achievements)
|
|
if root is None:
|
|
root = Node.void("player24")
|
|
root.add_child(Node.s8("result", 2))
|
|
return root
|
|
|
|
def handle_player24_read_request(self, request: Node) -> Node:
|
|
refid = request.child_value("ref_id")
|
|
root = self.get_profile_by_refid(refid, self.OLD_PROFILE_FALLTHROUGH)
|
|
if root is None:
|
|
root = Node.void("player24")
|
|
root.add_child(Node.s8("result", 2))
|
|
return root
|
|
|
|
def handle_player24_write_request(self, request: Node) -> Node:
|
|
refid = request.child_value("ref_id")
|
|
|
|
if refid is not None:
|
|
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
|
|
else:
|
|
userid = None
|
|
|
|
if userid is not None:
|
|
oldprofile = self.get_profile(userid) or Profile(
|
|
self.game, self.version, refid, 0
|
|
)
|
|
newprofile = self.unformat_profile(userid, request, oldprofile)
|
|
|
|
if newprofile is not None:
|
|
self.put_profile(userid, newprofile)
|
|
|
|
return Node.void("player24")
|
|
|
|
def handle_player24_update_ranking_request(self, request: Node) -> Node:
|
|
refid = request.child_value("ref_id")
|
|
root = Node.void("player24")
|
|
|
|
if refid is not None:
|
|
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
|
|
else:
|
|
userid = None
|
|
|
|
if userid is not None:
|
|
course_id = request.child_value("course_id")
|
|
chart = request.child_value("sheet_num")
|
|
score = request.child_value("total_score")
|
|
clear_type = request.child_value("clear_type")
|
|
clear_rank = request.child_value("clear_rank")
|
|
prefecture = request.child_value("pref")
|
|
loc_id = ID.parse_machine_id(request.child_value("location_id"))
|
|
course_type = f"course_{chart}"
|
|
|
|
old_course = self.data.local.user.get_achievement(
|
|
self.game,
|
|
self.version,
|
|
userid,
|
|
course_id,
|
|
course_type,
|
|
)
|
|
if old_course is None:
|
|
old_course = ValidatedDict()
|
|
|
|
new_course = ValidatedDict(
|
|
{
|
|
"score": max(score, old_course.get_int("score")),
|
|
"clear_type": max(clear_type, old_course.get_int("clear_type")),
|
|
"clear_rank": max(clear_rank, old_course.get_int("clear_rank")),
|
|
"pref": prefecture,
|
|
"lid": loc_id,
|
|
"count": old_course.get_int("count") + 1,
|
|
}
|
|
)
|
|
|
|
self.data.local.user.put_achievement(
|
|
self.game,
|
|
self.version,
|
|
userid,
|
|
course_id,
|
|
course_type,
|
|
new_course,
|
|
)
|
|
|
|
# Handle fetching all scores
|
|
uids_and_courses, profile = Parallel.execute(
|
|
[
|
|
lambda: self.data.local.user.get_all_achievements(
|
|
self.game, self.version
|
|
),
|
|
lambda: self.get_profile(userid)
|
|
or Profile(self.game, self.version, "", 0),
|
|
]
|
|
)
|
|
|
|
# Grab a sorted list of all scores for this course and chart
|
|
global_uids_and_courses = sorted(
|
|
[
|
|
(uid, ach)
|
|
for (uid, ach) in uids_and_courses
|
|
if ach.type == course_type and ach.id == course_id
|
|
],
|
|
key=lambda uid_and_course: uid_and_course[1].data.get_int("score"),
|
|
reverse=True,
|
|
)
|
|
# Grab smaller lists that contain only sorted for our prefecture/location
|
|
pref_uids_and_courses = [
|
|
(uid, ach)
|
|
for (uid, ach) in global_uids_and_courses
|
|
if ach.data.get_int("pref") == prefecture
|
|
]
|
|
loc_uids_and_courses = [
|
|
(uid, ach)
|
|
for (uid, ach) in global_uids_and_courses
|
|
if ach.data.get_int("lid") == loc_id
|
|
]
|
|
|
|
def _get_rank(uac: List[Tuple[UserID, Achievement]]) -> Optional[int]:
|
|
for rank, (uid, _) in enumerate(uac):
|
|
if uid == userid:
|
|
return rank + 1
|
|
return None
|
|
|
|
for nodename, ranklist in [
|
|
("all_ranking", global_uids_and_courses),
|
|
("pref_ranking", pref_uids_and_courses),
|
|
("location_ranking", loc_uids_and_courses),
|
|
]:
|
|
# Grab the rank, bail if we don't have any answer since the game doesn't
|
|
# require a response.
|
|
rank = _get_rank(ranklist)
|
|
if rank is None:
|
|
continue
|
|
|
|
# Send back the data for this ranking.
|
|
node = Node.void(nodename)
|
|
root.add_child(node)
|
|
node.add_child(Node.string("name", profile.get_str("name", "なし")))
|
|
node.add_child(Node.s16("chara_num", profile.get_int("chara", -1)))
|
|
node.add_child(Node.s32("total_score", new_course.get_int("score")))
|
|
node.add_child(Node.u8("clear_type", new_course.get_int("clear_type")))
|
|
node.add_child(Node.u8("clear_rank", new_course.get_int("clear_rank")))
|
|
node.add_child(Node.s16("player_count", len(ranklist)))
|
|
node.add_child(Node.s16("player_rank", rank))
|
|
|
|
return root
|
|
|
|
def handle_player24_friend_request(self, request: Node) -> Node:
|
|
refid = request.attribute("ref_id")
|
|
no = int(request.attribute("no", "-1"))
|
|
|
|
root = Node.void("player24")
|
|
if no < 0:
|
|
root.add_child(Node.s8("result", 2))
|
|
return root
|
|
|
|
# Look up our own user ID based on the RefID provided.
|
|
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
|
|
if userid is None:
|
|
root.add_child(Node.s8("result", 2))
|
|
return root
|
|
|
|
# Grab the links that we care about.
|
|
links = self.data.local.user.get_links(self.game, self.version, userid)
|
|
profiles: Dict[UserID, Profile] = {}
|
|
rivals: List[Link] = []
|
|
for link in links:
|
|
if link.type != "rival":
|
|
continue
|
|
|
|
other_profile = self.get_profile(link.other_userid)
|
|
if other_profile is None:
|
|
continue
|
|
profiles[link.other_userid] = other_profile
|
|
rivals.append(link)
|
|
|
|
# Somehow requested an invalid profile.
|
|
if no >= len(rivals):
|
|
root.add_child(Node.s8("result", 2))
|
|
return root
|
|
rivalid = links[no].other_userid
|
|
rivalprofile = profiles[rivalid]
|
|
scores = self.data.remote.music.get_scores(
|
|
self.game, self.music_version, rivalid
|
|
)
|
|
|
|
# First, output general profile info.
|
|
friend = Node.void("friend")
|
|
root.add_child(friend)
|
|
friend.add_child(Node.s16("no", no))
|
|
friend.add_child(
|
|
Node.string("g_pm_id", self.format_extid(rivalprofile.extid))
|
|
) # UsaNeko formats on its own
|
|
friend.add_child(Node.string("name", rivalprofile.get_str("name", "なし")))
|
|
friend.add_child(Node.s16("chara_num", rivalprofile.get_int("chara", -1)))
|
|
# This might be for having non-active or non-confirmed friends, but setting to 0 makes the
|
|
# ranking numbers disappear and the player icon show a questionmark.
|
|
friend.add_child(Node.s8("is_open", 1))
|
|
|
|
for score in scores:
|
|
# Skip any scores for chart types we don't support
|
|
if score.chart not in [
|
|
self.CHART_TYPE_EASY,
|
|
self.CHART_TYPE_NORMAL,
|
|
self.CHART_TYPE_HYPER,
|
|
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")
|
|
|
|
music = Node.void("music")
|
|
friend.add_child(music)
|
|
music.set_attribute("music_num", str(score.id))
|
|
music.set_attribute(
|
|
"sheet_num",
|
|
str(
|
|
{
|
|
self.CHART_TYPE_EASY: self.GAME_CHART_TYPE_EASY,
|
|
self.CHART_TYPE_NORMAL: self.GAME_CHART_TYPE_NORMAL,
|
|
self.CHART_TYPE_HYPER: self.GAME_CHART_TYPE_HYPER,
|
|
self.CHART_TYPE_EX: self.GAME_CHART_TYPE_EX,
|
|
}[score.chart]
|
|
),
|
|
)
|
|
music.set_attribute("score", str(points))
|
|
music.set_attribute("clearrank", str(self.__score_to_rank(score.points)))
|
|
music.set_attribute(
|
|
"cleartype",
|
|
str(
|
|
{
|
|
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,
|
|
self.PLAY_MEDAL_EASY_CLEAR: self.GAME_PLAY_MEDAL_EASY_CLEAR,
|
|
self.PLAY_MEDAL_CIRCLE_CLEARED: self.GAME_PLAY_MEDAL_CIRCLE_CLEARED,
|
|
self.PLAY_MEDAL_DIAMOND_CLEARED: self.GAME_PLAY_MEDAL_DIAMOND_CLEARED,
|
|
self.PLAY_MEDAL_STAR_CLEARED: self.GAME_PLAY_MEDAL_STAR_CLEARED,
|
|
self.PLAY_MEDAL_CIRCLE_FULL_COMBO: self.GAME_PLAY_MEDAL_CIRCLE_FULL_COMBO,
|
|
self.PLAY_MEDAL_DIAMOND_FULL_COMBO: self.GAME_PLAY_MEDAL_DIAMOND_FULL_COMBO,
|
|
self.PLAY_MEDAL_STAR_FULL_COMBO: self.GAME_PLAY_MEDAL_STAR_FULL_COMBO,
|
|
self.PLAY_MEDAL_PERFECT: self.GAME_PLAY_MEDAL_PERFECT,
|
|
}[medal]
|
|
),
|
|
)
|
|
|
|
achievements = self.data.local.user.get_achievements(
|
|
self.game, self.version, rivalid
|
|
)
|
|
for achievement in achievements:
|
|
if achievement.type[:7] == "course_":
|
|
sheet = int(achievement.type[7:])
|
|
|
|
course_data = Node.void("course_data")
|
|
root.add_child(course_data)
|
|
course_data.add_child(Node.s16("course_id", achievement.id))
|
|
course_data.add_child(
|
|
Node.u8("clear_type", achievement.data.get_int("clear_type"))
|
|
)
|
|
course_data.add_child(
|
|
Node.u8("clear_rank", achievement.data.get_int("clear_rank"))
|
|
)
|
|
course_data.add_child(
|
|
Node.s32("total_score", achievement.data.get_int("score"))
|
|
)
|
|
course_data.add_child(
|
|
Node.s32("update_count", achievement.data.get_int("count"))
|
|
)
|
|
course_data.add_child(Node.u8("sheet_num", sheet))
|
|
|
|
return root
|
|
|
|
def handle_player24_read_score_request(self, request: Node) -> Node:
|
|
refid = request.child_value("ref_id")
|
|
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
|
|
if userid is None:
|
|
return Node.void("player24")
|
|
|
|
root = Node.void("player24")
|
|
scores = self.data.remote.music.get_scores(
|
|
self.game, self.music_version, userid
|
|
)
|
|
for score in scores:
|
|
# Skip any scores for chart types we don't support
|
|
if score.chart not in [
|
|
self.CHART_TYPE_EASY,
|
|
self.CHART_TYPE_NORMAL,
|
|
self.CHART_TYPE_HYPER,
|
|
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)
|
|
music.add_child(Node.s16("music_num", score.id))
|
|
music.add_child(
|
|
Node.u8(
|
|
"sheet_num",
|
|
{
|
|
self.CHART_TYPE_EASY: self.GAME_CHART_TYPE_EASY,
|
|
self.CHART_TYPE_NORMAL: self.GAME_CHART_TYPE_NORMAL,
|
|
self.CHART_TYPE_HYPER: self.GAME_CHART_TYPE_HYPER,
|
|
self.CHART_TYPE_EX: self.GAME_CHART_TYPE_EX,
|
|
}[score.chart],
|
|
)
|
|
)
|
|
music.add_child(Node.s32("score", score.points))
|
|
music.add_child(
|
|
Node.u8(
|
|
"clear_type",
|
|
{
|
|
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,
|
|
self.PLAY_MEDAL_EASY_CLEAR: self.GAME_PLAY_MEDAL_EASY_CLEAR,
|
|
self.PLAY_MEDAL_CIRCLE_CLEARED: self.GAME_PLAY_MEDAL_CIRCLE_CLEARED,
|
|
self.PLAY_MEDAL_DIAMOND_CLEARED: self.GAME_PLAY_MEDAL_DIAMOND_CLEARED,
|
|
self.PLAY_MEDAL_STAR_CLEARED: self.GAME_PLAY_MEDAL_STAR_CLEARED,
|
|
self.PLAY_MEDAL_CIRCLE_FULL_COMBO: self.GAME_PLAY_MEDAL_CIRCLE_FULL_COMBO,
|
|
self.PLAY_MEDAL_DIAMOND_FULL_COMBO: self.GAME_PLAY_MEDAL_DIAMOND_FULL_COMBO,
|
|
self.PLAY_MEDAL_STAR_FULL_COMBO: self.GAME_PLAY_MEDAL_STAR_FULL_COMBO,
|
|
self.PLAY_MEDAL_PERFECT: self.GAME_PLAY_MEDAL_PERFECT,
|
|
}[score.data.get_int("medal")],
|
|
)
|
|
)
|
|
music.add_child(Node.u8("clear_rank", self.__score_to_rank(score.points)))
|
|
music.add_child(Node.s16("cnt", score.plays))
|
|
|
|
return root
|
|
|
|
def handle_player24_write_music_request(self, request: Node) -> Node:
|
|
refid = request.child_value("ref_id")
|
|
|
|
root = Node.void("player24")
|
|
if refid is None:
|
|
return root
|
|
|
|
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
|
|
if userid is None:
|
|
return root
|
|
|
|
songid = request.child_value("music_num")
|
|
chart = {
|
|
self.GAME_CHART_TYPE_EASY: self.CHART_TYPE_EASY,
|
|
self.GAME_CHART_TYPE_NORMAL: self.CHART_TYPE_NORMAL,
|
|
self.GAME_CHART_TYPE_HYPER: self.CHART_TYPE_HYPER,
|
|
self.GAME_CHART_TYPE_EX: self.CHART_TYPE_EX,
|
|
}[request.child_value("sheet_num")]
|
|
medal = request.child_value("clear_type")
|
|
points = request.child_value("score")
|
|
combo = request.child_value("combo")
|
|
stats = {
|
|
"cool": request.child_value("cool"),
|
|
"great": request.child_value("great"),
|
|
"good": request.child_value("good"),
|
|
"bad": request.child_value("bad"),
|
|
}
|
|
medal = {
|
|
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,
|
|
self.GAME_PLAY_MEDAL_EASY_CLEAR: self.PLAY_MEDAL_EASY_CLEAR,
|
|
self.GAME_PLAY_MEDAL_CIRCLE_CLEARED: self.PLAY_MEDAL_CIRCLE_CLEARED,
|
|
self.GAME_PLAY_MEDAL_DIAMOND_CLEARED: self.PLAY_MEDAL_DIAMOND_CLEARED,
|
|
self.GAME_PLAY_MEDAL_STAR_CLEARED: self.PLAY_MEDAL_STAR_CLEARED,
|
|
self.GAME_PLAY_MEDAL_CIRCLE_FULL_COMBO: self.PLAY_MEDAL_CIRCLE_FULL_COMBO,
|
|
self.GAME_PLAY_MEDAL_DIAMOND_FULL_COMBO: self.PLAY_MEDAL_DIAMOND_FULL_COMBO,
|
|
self.GAME_PLAY_MEDAL_STAR_FULL_COMBO: self.PLAY_MEDAL_STAR_FULL_COMBO,
|
|
self.GAME_PLAY_MEDAL_PERFECT: self.PLAY_MEDAL_PERFECT,
|
|
}[medal]
|
|
self.update_score(
|
|
userid, songid, chart, points, medal, combo=combo, stats=stats
|
|
)
|
|
|
|
if request.child_value("is_image_store") == 1:
|
|
self.broadcast_score(userid, songid, chart, medal, points, combo, stats)
|
|
|
|
return root
|
|
|
|
def handle_player24_start_request(self, request: Node) -> Node:
|
|
root = Node.void("player24")
|
|
root.add_child(Node.s32("play_id", 0))
|
|
self.__construct_common_info(root)
|
|
return root
|
|
|
|
def handle_player24_logout_request(self, request: Node) -> Node:
|
|
return Node.void("player24")
|
|
|
|
def handle_player24_buy_request(self, request: Node) -> Node:
|
|
refid = request.child_value("ref_id")
|
|
|
|
if refid is not None:
|
|
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
|
|
else:
|
|
userid = None
|
|
|
|
if userid is not None:
|
|
itemid = request.child_value("id")
|
|
itemtype = request.child_value("type")
|
|
itemparam = request.child_value("param")
|
|
|
|
price = request.child_value("price")
|
|
lumina = request.child_value("lumina")
|
|
|
|
if lumina >= price:
|
|
# Update player lumina balance
|
|
profile = self.get_profile(userid) or Profile(
|
|
self.game, self.version, refid, 0
|
|
)
|
|
profile.replace_int("player_point", lumina - price)
|
|
self.put_profile(userid, profile)
|
|
|
|
# Grant the object
|
|
self.data.local.user.put_achievement(
|
|
self.game,
|
|
self.version,
|
|
userid,
|
|
itemid,
|
|
f"item_{itemtype}",
|
|
{
|
|
"param": itemparam,
|
|
"is_new": True,
|
|
},
|
|
)
|
|
|
|
return Node.void("player24")
|
|
|
|
def format_conversion(self, userid: UserID, profile: Profile) -> Node:
|
|
root = Node.void("player24")
|
|
root.add_child(Node.string("name", profile.get_str("name", "なし")))
|
|
root.add_child(Node.s16("chara", profile.get_int("chara", -1)))
|
|
root.add_child(Node.s8("con_type", 0))
|
|
root.add_child(Node.s8("result", 1))
|
|
|
|
# Scores
|
|
scores = self.data.remote.music.get_scores(
|
|
self.game, self.music_version, userid
|
|
)
|
|
for score in scores:
|
|
# Skip any scores for chart types we don't support
|
|
if score.chart not in [
|
|
self.CHART_TYPE_EASY,
|
|
self.CHART_TYPE_NORMAL,
|
|
self.CHART_TYPE_HYPER,
|
|
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)
|
|
music.add_child(Node.s16("music_num", score.id))
|
|
music.add_child(
|
|
Node.u8(
|
|
"sheet_num",
|
|
{
|
|
self.CHART_TYPE_EASY: self.GAME_CHART_TYPE_EASY,
|
|
self.CHART_TYPE_NORMAL: self.GAME_CHART_TYPE_NORMAL,
|
|
self.CHART_TYPE_HYPER: self.GAME_CHART_TYPE_HYPER,
|
|
self.CHART_TYPE_EX: self.GAME_CHART_TYPE_EX,
|
|
}[score.chart],
|
|
)
|
|
)
|
|
music.add_child(Node.s32("score", score.points))
|
|
music.add_child(
|
|
Node.u8(
|
|
"clear_type",
|
|
{
|
|
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,
|
|
self.PLAY_MEDAL_EASY_CLEAR: self.GAME_PLAY_MEDAL_EASY_CLEAR,
|
|
self.PLAY_MEDAL_CIRCLE_CLEARED: self.GAME_PLAY_MEDAL_CIRCLE_CLEARED,
|
|
self.PLAY_MEDAL_DIAMOND_CLEARED: self.GAME_PLAY_MEDAL_DIAMOND_CLEARED,
|
|
self.PLAY_MEDAL_STAR_CLEARED: self.GAME_PLAY_MEDAL_STAR_CLEARED,
|
|
self.PLAY_MEDAL_CIRCLE_FULL_COMBO: self.GAME_PLAY_MEDAL_CIRCLE_FULL_COMBO,
|
|
self.PLAY_MEDAL_DIAMOND_FULL_COMBO: self.GAME_PLAY_MEDAL_DIAMOND_FULL_COMBO,
|
|
self.PLAY_MEDAL_STAR_FULL_COMBO: self.GAME_PLAY_MEDAL_STAR_FULL_COMBO,
|
|
self.PLAY_MEDAL_PERFECT: self.GAME_PLAY_MEDAL_PERFECT,
|
|
}[score.data.get_int("medal")],
|
|
)
|
|
)
|
|
music.add_child(Node.u8("clear_rank", self.__score_to_rank(score.points)))
|
|
music.add_child(Node.s16("cnt", score.plays))
|
|
|
|
return root
|
|
|
|
def format_extid(self, extid: int) -> str:
|
|
data = str(extid)
|
|
crc = abs(binascii.crc32(data.encode("ascii"))) % 10000
|
|
return f"{data}{crc:04d}"
|
|
|
|
def format_profile(self, userid: UserID, profile: Profile) -> Node:
|
|
root = Node.void("player24")
|
|
|
|
# Mark this as a current profile
|
|
root.add_child(Node.s8("result", 0))
|
|
|
|
# Basic account info
|
|
account = Node.void("account")
|
|
root.add_child(account)
|
|
account.add_child(Node.string("g_pm_id", self.format_extid(profile.extid)))
|
|
account.add_child(Node.string("name", profile.get_str("name", "なし")))
|
|
account.add_child(Node.s16("area_id", profile.get_int("area_id")))
|
|
account.add_child(Node.s16("use_navi", profile.get_int("use_navi")))
|
|
account.add_child(Node.s16("read_news", profile.get_int("read_news")))
|
|
account.add_child(
|
|
Node.s16_array("nice", profile.get_int_array("nice", 30, [-1] * 30))
|
|
)
|
|
account.add_child(
|
|
Node.s16_array(
|
|
"favorite_chara", profile.get_int_array("favorite_chara", 20, [-1] * 20)
|
|
)
|
|
)
|
|
account.add_child(
|
|
Node.s16_array(
|
|
"special_area", profile.get_int_array("special_area", 8, [-1] * 8)
|
|
)
|
|
)
|
|
account.add_child(
|
|
Node.s16_array(
|
|
"chocolate_charalist",
|
|
profile.get_int_array("chocolate_charalist", 5, [-1] * 5),
|
|
)
|
|
)
|
|
account.add_child(
|
|
Node.s32("chocolate_sp_chara", profile.get_int("chocolate_sp_chara", -1))
|
|
)
|
|
account.add_child(
|
|
Node.s32("chocolate_pass_cnt", profile.get_int("chocolate_pass_cnt"))
|
|
)
|
|
account.add_child(
|
|
Node.s32("chocolate_hon_cnt", profile.get_int("chocolate_hon_cnt"))
|
|
)
|
|
account.add_child(
|
|
Node.s16_array(
|
|
"teacher_setting",
|
|
profile.get_int_array("teacher_setting", 10, [-1] * 10),
|
|
)
|
|
)
|
|
account.add_child(
|
|
Node.bool("welcom_pack", False)
|
|
) # Set to true to grant extra stage no matter what.
|
|
account.add_child(Node.s32("ranking_node", profile.get_int("ranking_node")))
|
|
account.add_child(
|
|
Node.s32("chara_ranking_kind_id", profile.get_int("chara_ranking_kind_id"))
|
|
)
|
|
account.add_child(
|
|
Node.s8("navi_evolution_flg", profile.get_int("navi_evolution_flg"))
|
|
)
|
|
account.add_child(
|
|
Node.s32("ranking_news_last_no", profile.get_int("ranking_news_last_no"))
|
|
)
|
|
account.add_child(Node.s32("power_point", profile.get_int("power_point")))
|
|
account.add_child(
|
|
Node.s32("player_point", profile.get_int("player_point", 300))
|
|
)
|
|
account.add_child(
|
|
Node.s32_array(
|
|
"power_point_list",
|
|
profile.get_int_array("power_point_list", 20, [-1] * 20),
|
|
)
|
|
)
|
|
|
|
# Tutorial handling is all sorts of crazy in UsaNeko. the tutorial flag
|
|
# is split into two values. The game uses the flag modulo 100 for navigation
|
|
# tutorial progress, and the flag divided by 100 for the hold note tutorial.
|
|
# The hold note tutorial will activate the first time you choose a song with
|
|
# hold notes in it, regardless of whether you say yes/no. The total times you
|
|
# have ever played Pop'n Music also factors in, as the game will only attempt
|
|
# to offer you the basic "how to play" tutorial screen and song on the playthrough
|
|
# attempt where the "total_play_cnt" value is 1. The game expects this to be 1-based,
|
|
# and if you set it to 0 for the first playthorough then it will play a mandatory
|
|
# cursed tutorial stage on the second profile load using the chart of your last
|
|
# played song and keysounds of system menu entries. Valid values for each of the
|
|
# two tutorial values is as follows:
|
|
#
|
|
# Lower values:
|
|
# 0 - Brand new profile and user has not been prompted to choose any tutorials.
|
|
# Prompts the user for the nagivation tutorial. If the user selects "no" then
|
|
# moves the tutorial state to "1" at the end of the round. If the user selects
|
|
# "yes" then moves the tutorial state to "3" immediately and starts the navigation
|
|
# tutorial. If the total play count for this user is "1" when this value is hit,
|
|
# the game will offer a basic "how to play" tutorial that can be played or skipped.
|
|
# 1 - Prompt the user on the mode select screen asking them if they want to see
|
|
# the navigation tutorial. If the user selects "no" then moves the tutorial state
|
|
# to "2" after the round. If the user selects "yes" then moves the tutorial state
|
|
# to "3" immediately. If the total play count for this user is "1" when this value
|
|
# is hit, then the game will bug out and play the hold note tutorial and then crash.
|
|
# 2 - Prompt the user on the mode select screen asking them if they want to see
|
|
# the navigation tutorial. If the user selects "no" then moves the tutorial state
|
|
# to "8" immediately. If the user selects "yes" then moves the tutorial state
|
|
# to "3" immediately. If the total play count for this user is "1" when this value
|
|
# is hit, then the game will bug out and play the hold note tutorial and then crash.
|
|
# 3 - Display some tutorial elements on most screens, and then advance the tutorial
|
|
# state to "4" on profile save.
|
|
# 4 - Display some tutorial elements on most screens, and then advance the tutorial
|
|
# state to "5" on profile save.
|
|
# 5 - Display some tutorial elements on most screens, and then prompt user with a
|
|
# repeat tutorial question. If the user selects "no" then moves the tutorial
|
|
# state to "8". If the user selects "yes" then moves the tutorial state to "3".
|
|
# 6 - Do nothing, display nothing, but advance the tutorial state to "7" at the
|
|
# end of the game. It seems that nothing requests this state.
|
|
# 7 - Display guide information prompt on the option select screen. Game moves
|
|
# this to "8" after this tutorial has been displayed. It appears that there is
|
|
# code to go to this state instead of "8" when selecting "no" on the navigation
|
|
# tutorial prompt but only when the total play count is "1". That crashes the game
|
|
# as documented above, so it is not clear how this state was ever reachable.
|
|
# 8 - Do not display any more tutorial stuff, this is a terminal state.
|
|
#
|
|
# Upper values:
|
|
# 0 - Brand new profile and user has not been asked for the above navigation tutorial
|
|
# or shown an optional "how to play" tutorial. The game will advance this to "1"
|
|
# after going through the mode and character select screens, but only if the total
|
|
# play count is "1".
|
|
# 1 - Hold note tutorial has not been activated yet and will be displayed when
|
|
# the player chooses a song with hold notes. Game moves this to "2" after this
|
|
# tutorial has been activated.
|
|
# 2 - Hold note tutorial was displayed to the user, but the mini-tutorial showing
|
|
# the hold note indicator that pops up after the hold note tutorial has not
|
|
# been displayed yet. Presumably this is just in case you play a hold note
|
|
# song on your last stage. Game moves this to "3" after this tutorial has been
|
|
# displayed.
|
|
# 3 - All hold note tutorials are finished, this is a terminal state.
|
|
statistics = self.get_play_statistics(userid)
|
|
account.add_child(
|
|
Node.s16(
|
|
"tutorial",
|
|
profile.get_int("tutorial", 100 if statistics.total_plays > 1 else 0),
|
|
)
|
|
)
|
|
|
|
# Stuff we never change
|
|
account.add_child(Node.s8("staff", 0))
|
|
account.add_child(Node.s16("item_type", 0))
|
|
account.add_child(Node.s16("item_id", 0))
|
|
account.add_child(Node.s8("is_conv", 0))
|
|
account.add_child(Node.s16_array("license_data", [-1] * 20))
|
|
|
|
# Song statistics
|
|
last_played = [
|
|
x[0]
|
|
for x in self.data.local.music.get_last_played(
|
|
self.game, self.music_version, userid, 10
|
|
)
|
|
]
|
|
most_played = [
|
|
x[0]
|
|
for x in self.data.local.music.get_most_played(
|
|
self.game, self.music_version, userid, 20
|
|
)
|
|
]
|
|
while len(last_played) < 10:
|
|
last_played.append(-1)
|
|
while len(most_played) < 20:
|
|
most_played.append(-1)
|
|
|
|
account.add_child(Node.s16_array("my_best", most_played))
|
|
account.add_child(Node.s16_array("latest_music", last_played))
|
|
|
|
# Player statistics
|
|
account.add_child(Node.s16("total_play_cnt", statistics.total_plays))
|
|
account.add_child(Node.s16("today_play_cnt", statistics.today_plays))
|
|
account.add_child(Node.s16("consecutive_days", statistics.consecutive_days))
|
|
account.add_child(Node.s16("total_days", statistics.total_days))
|
|
account.add_child(Node.s16("interval_day", 0))
|
|
|
|
# Number of rivals that are active for this version.
|
|
links = self.data.local.user.get_links(self.game, self.version, userid)
|
|
rivalcount = 0
|
|
for link in links:
|
|
if link.type != "rival":
|
|
continue
|
|
|
|
if not self.has_profile(link.other_userid):
|
|
continue
|
|
|
|
# This profile is valid.
|
|
rivalcount += 1
|
|
account.add_child(Node.u8("active_fr_num", rivalcount))
|
|
|
|
# eAmuse account link
|
|
eaappli = Node.void("eaappli")
|
|
root.add_child(eaappli)
|
|
eaappli.add_child(
|
|
Node.s8(
|
|
"relation",
|
|
1 if self.data.triggers.has_broadcast_destination(self.game) else -1,
|
|
)
|
|
)
|
|
|
|
# Player info
|
|
info = Node.void("info")
|
|
root.add_child(info)
|
|
info.add_child(Node.u16("ep", profile.get_int("ep")))
|
|
|
|
# Player config
|
|
config = Node.void("config")
|
|
root.add_child(config)
|
|
config.add_child(Node.u8("mode", profile.get_int("mode")))
|
|
config.add_child(Node.s16("chara", profile.get_int("chara", -1)))
|
|
config.add_child(Node.s16("music", profile.get_int("music", -1)))
|
|
config.add_child(Node.u8("sheet", profile.get_int("sheet")))
|
|
config.add_child(Node.s8("category", profile.get_int("category", -1)))
|
|
config.add_child(Node.s8("sub_category", profile.get_int("sub_category", -1)))
|
|
config.add_child(
|
|
Node.s8("chara_category", profile.get_int("chara_category", -1))
|
|
)
|
|
config.add_child(Node.s16("course_id", profile.get_int("course_id", -1)))
|
|
config.add_child(Node.s8("course_folder", profile.get_int("course_folder", -1)))
|
|
config.add_child(
|
|
Node.s8("ms_banner_disp", profile.get_int("ms_banner_disp", -1))
|
|
)
|
|
config.add_child(Node.s8("ms_down_info", profile.get_int("ms_down_info", -1)))
|
|
config.add_child(Node.s8("ms_side_info", profile.get_int("ms_side_info", -1)))
|
|
config.add_child(Node.s8("ms_raise_type", profile.get_int("ms_raise_type", -1)))
|
|
config.add_child(Node.s8("ms_rnd_type", profile.get_int("ms_rnd_type", -1)))
|
|
config.add_child(Node.s8("banner_sort", profile.get_int("banner_sort", -1)))
|
|
|
|
# Player options
|
|
option = Node.void("option")
|
|
option_dict = profile.get_dict("option")
|
|
root.add_child(option)
|
|
option.add_child(Node.s16("hispeed", option_dict.get_int("hispeed")))
|
|
option.add_child(Node.u8("popkun", option_dict.get_int("popkun")))
|
|
option.add_child(Node.bool("hidden", option_dict.get_bool("hidden")))
|
|
option.add_child(Node.s16("hidden_rate", option_dict.get_int("hidden_rate")))
|
|
option.add_child(Node.bool("sudden", option_dict.get_bool("sudden")))
|
|
option.add_child(Node.s16("sudden_rate", option_dict.get_int("sudden_rate")))
|
|
option.add_child(Node.s8("randmir", option_dict.get_int("randmir")))
|
|
option.add_child(Node.s8("gauge_type", option_dict.get_int("gauge_type")))
|
|
option.add_child(Node.u8("ojama_0", option_dict.get_int("ojama_0")))
|
|
option.add_child(Node.u8("ojama_1", option_dict.get_int("ojama_1")))
|
|
option.add_child(Node.bool("forever_0", option_dict.get_bool("forever_0")))
|
|
option.add_child(Node.bool("forever_1", option_dict.get_bool("forever_1")))
|
|
option.add_child(
|
|
Node.bool("full_setting", option_dict.get_bool("full_setting"))
|
|
)
|
|
option.add_child(Node.u8("judge", option_dict.get_int("judge")))
|
|
option.add_child(Node.s8("guide_se", option_dict.get_int("guide_se")))
|
|
|
|
# Player custom category
|
|
custom_cate = Node.void("custom_cate")
|
|
root.add_child(custom_cate)
|
|
custom_cate.add_child(Node.s8("valid", 0))
|
|
custom_cate.add_child(Node.s8("lv_min", -1))
|
|
custom_cate.add_child(Node.s8("lv_max", -1))
|
|
custom_cate.add_child(Node.s8("medal_min", -1))
|
|
custom_cate.add_child(Node.s8("medal_max", -1))
|
|
custom_cate.add_child(Node.s8("friend_no", -1))
|
|
custom_cate.add_child(Node.s8("score_flg", -1))
|
|
|
|
# Navi data
|
|
navi_data = Node.void("navi_data")
|
|
root.add_child(navi_data)
|
|
if "navi_points" in profile:
|
|
navi_data.add_child(
|
|
Node.s32_array("raisePoint", profile.get_int_array("navi_points", 5))
|
|
)
|
|
|
|
game_config = self.get_game_config()
|
|
if game_config.get_bool("force_unlock_songs"):
|
|
songs = {
|
|
song.id
|
|
for song in self.data.local.music.get_all_songs(
|
|
self.game, self.music_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))
|
|
item.add_child(Node.u16("param", 15))
|
|
item.add_child(Node.bool("is_new", False))
|
|
item.add_child(Node.u64("get_time", 0))
|
|
|
|
# Set up achievements
|
|
achievements = self.data.local.user.get_achievements(
|
|
self.game, self.version, userid
|
|
)
|
|
for achievement in achievements:
|
|
if achievement.type[:5] == "item_":
|
|
itemtype = int(achievement.type[5:])
|
|
param = achievement.data.get_int("param")
|
|
is_new = achievement.data.get_bool("is_new")
|
|
get_time = achievement.data.get_int("get_time")
|
|
|
|
# Item type can be 0-6 inclusive and is the type of the unlock/item.
|
|
# Item 0 is music unlocks. In this case, the id is the song ID according
|
|
# to the game. Unclear what the param is supposed to be, but i've seen
|
|
# seen 8 and 0. Might be what chart is available?
|
|
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))
|
|
item.add_child(Node.u16("id", achievement.id))
|
|
item.add_child(Node.u16("param", param))
|
|
item.add_child(Node.bool("is_new", is_new))
|
|
item.add_child(Node.u64("get_time", get_time))
|
|
|
|
elif achievement.type == "chara":
|
|
friendship = achievement.data.get_int("friendship")
|
|
|
|
chara = Node.void("chara_param")
|
|
root.add_child(chara)
|
|
chara.add_child(Node.u16("chara_id", achievement.id))
|
|
chara.add_child(Node.u16("friendship", friendship))
|
|
|
|
elif achievement.type == "navi":
|
|
# There should only be 12 of these.
|
|
friendship = achievement.data.get_int("friendship")
|
|
|
|
# This relies on the above Navi data section to ensure the navi_param
|
|
# node is created.
|
|
navi_param = Node.void("navi_param")
|
|
navi_data.add_child(navi_param)
|
|
navi_param.add_child(Node.u16("navi_id", achievement.id))
|
|
navi_param.add_child(Node.s32("friendship", friendship))
|
|
|
|
elif achievement.type == "area":
|
|
# There should only be 16 of these.
|
|
index = achievement.data.get_int("index")
|
|
points = achievement.data.get_int("points")
|
|
cleared = achievement.data.get_bool("cleared")
|
|
diary = achievement.data.get_int("diary")
|
|
|
|
area = Node.void("area")
|
|
root.add_child(area)
|
|
area.add_child(Node.u32("area_id", achievement.id))
|
|
area.add_child(Node.u8("chapter_index", index))
|
|
area.add_child(Node.u16("gauge_point", points))
|
|
area.add_child(Node.bool("is_cleared", cleared))
|
|
area.add_child(Node.u32("diary", diary))
|
|
|
|
elif achievement.type[:7] == "course_":
|
|
sheet = int(achievement.type[7:])
|
|
|
|
course_data = Node.void("course_data")
|
|
root.add_child(course_data)
|
|
course_data.add_child(Node.s16("course_id", achievement.id))
|
|
course_data.add_child(
|
|
Node.u8("clear_type", achievement.data.get_int("clear_type"))
|
|
)
|
|
course_data.add_child(
|
|
Node.u8("clear_rank", achievement.data.get_int("clear_rank"))
|
|
)
|
|
course_data.add_child(
|
|
Node.s32("total_score", achievement.data.get_int("score"))
|
|
)
|
|
course_data.add_child(
|
|
Node.s32("update_count", achievement.data.get_int("count"))
|
|
)
|
|
course_data.add_child(Node.u8("sheet_num", sheet))
|
|
|
|
elif achievement.type == "fes":
|
|
index = achievement.data.get_int("index")
|
|
points = achievement.data.get_int("points")
|
|
cleared = achievement.data.get_bool("cleared")
|
|
|
|
fes = Node.void("fes")
|
|
root.add_child(fes)
|
|
fes.add_child(Node.u32("fes_id", achievement.id))
|
|
fes.add_child(Node.u8("chapter_index", index))
|
|
fes.add_child(Node.u16("gauge_point", points))
|
|
fes.add_child(Node.bool("is_cleared", cleared))
|
|
|
|
# Handle daily mission. Note that we should be presenting 3 random IDs
|
|
# in the range of 1-228 inclusive, and presenting three new ones per day.
|
|
achievements = self.data.local.user.get_time_based_achievements(
|
|
self.game,
|
|
self.version,
|
|
userid,
|
|
since=Time.beginning_of_today(),
|
|
until=Time.end_of_today(),
|
|
)
|
|
achievements = sorted(achievements, key=lambda a: a.timestamp)
|
|
daily_missions: Dict[int, ValidatedDict] = {}
|
|
|
|
# Find the newest version of each daily mission completion,
|
|
# since we've sorted by time above. If we haven't started for
|
|
# today, the defaults will be set after this loop so we at least
|
|
# give the game the right ID.
|
|
for achievement in achievements:
|
|
if achievement.type == "mission":
|
|
daily_missions[achievement.id] = achievement.data
|
|
|
|
while len(daily_missions) < 3:
|
|
new_id = random.randint(1, 228)
|
|
if new_id not in daily_missions:
|
|
daily_missions[new_id] = ValidatedDict()
|
|
|
|
for i, (daily_id, data) in enumerate(daily_missions.items()):
|
|
if i >= 3:
|
|
break
|
|
|
|
points = data.get_int("points")
|
|
complete = data.get_int("complete")
|
|
|
|
mission = Node.void("mission")
|
|
root.add_child(mission)
|
|
mission.add_child(Node.u32("mission_id", daily_id))
|
|
mission.add_child(Node.u32("gauge_point", points))
|
|
mission.add_child(Node.u32("mission_comp", complete))
|
|
|
|
# Player netvs section
|
|
netvs = Node.void("netvs")
|
|
root.add_child(netvs)
|
|
netvs.add_child(Node.s16_array("record", [0] * 6))
|
|
netvs.add_child(Node.string("dialog", ""))
|
|
netvs.add_child(Node.string("dialog", ""))
|
|
netvs.add_child(Node.string("dialog", ""))
|
|
netvs.add_child(Node.string("dialog", ""))
|
|
netvs.add_child(Node.string("dialog", ""))
|
|
netvs.add_child(Node.string("dialog", ""))
|
|
netvs.add_child(Node.s8_array("ojama_condition", [0] * 74))
|
|
netvs.add_child(Node.s8_array("set_ojama", [0] * 3))
|
|
netvs.add_child(Node.s8_array("set_recommend", [0] * 3))
|
|
netvs.add_child(Node.u32("netvs_play_cnt", 0))
|
|
|
|
# Character customizations
|
|
customize = Node.void("customize")
|
|
root.add_child(customize)
|
|
customize.add_child(Node.u16("effect_left", profile.get_int("effect_left")))
|
|
customize.add_child(Node.u16("effect_center", profile.get_int("effect_center")))
|
|
customize.add_child(Node.u16("effect_right", profile.get_int("effect_right")))
|
|
customize.add_child(Node.u16("hukidashi", profile.get_int("hukidashi")))
|
|
customize.add_child(Node.u16("comment_1", profile.get_int("comment_1")))
|
|
customize.add_child(Node.u16("comment_2", profile.get_int("comment_2")))
|
|
|
|
# Stamp stuff
|
|
stamp = Node.void("stamp")
|
|
root.add_child(stamp)
|
|
stamp.add_child(Node.s16("stamp_id", profile.get_int("stamp_id")))
|
|
stamp.add_child(Node.s16("cnt", profile.get_int("stamp_cnt")))
|
|
|
|
return root
|
|
|
|
def unformat_profile(
|
|
self, userid: UserID, request: Node, oldprofile: Profile
|
|
) -> Profile:
|
|
newprofile = oldprofile.clone()
|
|
|
|
account = request.child("account")
|
|
if account is not None:
|
|
newprofile.replace_int("tutorial", account.child_value("tutorial"))
|
|
newprofile.replace_int("read_news", account.child_value("read_news"))
|
|
newprofile.replace_int("area_id", account.child_value("area_id"))
|
|
newprofile.replace_int("use_navi", account.child_value("use_navi"))
|
|
newprofile.replace_int("ranking_node", account.child_value("ranking_node"))
|
|
newprofile.replace_int(
|
|
"chara_ranking_kind_id", account.child_value("chara_ranking_kind_id")
|
|
)
|
|
newprofile.replace_int(
|
|
"navi_evolution_flg", account.child_value("navi_evolution_flg")
|
|
)
|
|
newprofile.replace_int(
|
|
"ranking_news_last_no", account.child_value("ranking_news_last_no")
|
|
)
|
|
newprofile.replace_int("power_point", account.child_value("power_point"))
|
|
newprofile.replace_int("player_point", account.child_value("player_point"))
|
|
|
|
newprofile.replace_int_array("nice", 30, account.child_value("nice"))
|
|
newprofile.replace_int_array(
|
|
"favorite_chara", 20, account.child_value("favorite_chara")
|
|
)
|
|
newprofile.replace_int_array(
|
|
"special_area", 8, account.child_value("special_area")
|
|
)
|
|
newprofile.replace_int_array(
|
|
"chocolate_charalist", 5, account.child_value("chocolate_charalist")
|
|
)
|
|
newprofile.replace_int(
|
|
"chocolate_sp_chara", account.child_value("chocolate_sp_chara")
|
|
)
|
|
newprofile.replace_int(
|
|
"chocolate_pass_cnt", account.child_value("chocolate_pass_cnt")
|
|
)
|
|
newprofile.replace_int(
|
|
"chocolate_hon_cnt", account.child_value("chocolate_hon_cnt")
|
|
)
|
|
newprofile.replace_int(
|
|
"chocolate_giri_cnt", account.child_value("chocolate_giri_cnt")
|
|
)
|
|
newprofile.replace_int(
|
|
"chocolate_kokyu_cnt", account.child_value("chocolate_kokyu_cnt")
|
|
)
|
|
newprofile.replace_int_array(
|
|
"teacher_setting", 10, account.child_value("teacher_setting")
|
|
)
|
|
newprofile.replace_int_array(
|
|
"power_point_list", 20, account.child_value("power_point_list")
|
|
)
|
|
|
|
info = request.child("info")
|
|
if info is not None:
|
|
newprofile.replace_int("ep", info.child_value("ep"))
|
|
|
|
stamp = request.child("stamp")
|
|
if stamp is not None:
|
|
newprofile.replace_int("stamp_id", stamp.child_value("stamp_id"))
|
|
newprofile.replace_int("stamp_cnt", stamp.child_value("cnt"))
|
|
|
|
config = request.child("config")
|
|
if config is not None:
|
|
newprofile.replace_int("mode", config.child_value("mode"))
|
|
newprofile.replace_int("chara", config.child_value("chara"))
|
|
newprofile.replace_int("music", config.child_value("music"))
|
|
newprofile.replace_int("sheet", config.child_value("sheet"))
|
|
newprofile.replace_int("category", config.child_value("category"))
|
|
newprofile.replace_int("sub_category", config.child_value("sub_category"))
|
|
newprofile.replace_int(
|
|
"chara_category", config.child_value("chara_category")
|
|
)
|
|
newprofile.replace_int("course_id", config.child_value("course_id"))
|
|
newprofile.replace_int("course_folder", config.child_value("course_folder"))
|
|
newprofile.replace_int(
|
|
"ms_banner_disp", config.child_value("ms_banner_disp")
|
|
)
|
|
newprofile.replace_int("ms_down_info", config.child_value("ms_down_info"))
|
|
newprofile.replace_int("ms_side_info", config.child_value("ms_side_info"))
|
|
newprofile.replace_int("ms_raise_type", config.child_value("ms_raise_type"))
|
|
newprofile.replace_int("ms_rnd_type", config.child_value("ms_rnd_type"))
|
|
newprofile.replace_int("banner_sort", config.child_value("banner_sort"))
|
|
|
|
option_dict = newprofile.get_dict("option")
|
|
option = request.child("option")
|
|
if option is not None:
|
|
option_dict.replace_int("hispeed", option.child_value("hispeed"))
|
|
option_dict.replace_int("popkun", option.child_value("popkun"))
|
|
option_dict.replace_bool("hidden", option.child_value("hidden"))
|
|
option_dict.replace_int("hidden_rate", option.child_value("hidden_rate"))
|
|
option_dict.replace_bool("sudden", option.child_value("sudden"))
|
|
option_dict.replace_int("sudden_rate", option.child_value("sudden_rate"))
|
|
option_dict.replace_int("randmir", option.child_value("randmir"))
|
|
option_dict.replace_int("gauge_type", option.child_value("gauge_type"))
|
|
option_dict.replace_int("ojama_0", option.child_value("ojama_0"))
|
|
option_dict.replace_int("ojama_1", option.child_value("ojama_1"))
|
|
option_dict.replace_bool("forever_0", option.child_value("forever_0"))
|
|
option_dict.replace_bool("forever_1", option.child_value("forever_1"))
|
|
option_dict.replace_bool("full_setting", option.child_value("full_setting"))
|
|
option_dict.replace_int("judge", option.child_value("judge"))
|
|
option_dict.replace_int("guide_se", option.child_value("guide_se"))
|
|
newprofile.replace_dict("option", option_dict)
|
|
|
|
customize = request.child("customize")
|
|
if customize is not None:
|
|
newprofile.replace_int("effect_left", customize.child_value("effect_left"))
|
|
newprofile.replace_int(
|
|
"effect_center", customize.child_value("effect_center")
|
|
)
|
|
newprofile.replace_int(
|
|
"effect_right", customize.child_value("effect_right")
|
|
)
|
|
newprofile.replace_int("hukidashi", customize.child_value("hukidashi"))
|
|
newprofile.replace_int("comment_1", customize.child_value("comment_1"))
|
|
newprofile.replace_int("comment_2", customize.child_value("comment_2"))
|
|
|
|
navi_data = request.child("navi_data")
|
|
if navi_data is not None:
|
|
newprofile.replace_int_array(
|
|
"navi_points", 5, navi_data.child_value("raisePoint")
|
|
)
|
|
|
|
# Extract navi achievements
|
|
for node in navi_data.children:
|
|
if node.name == "navi_param":
|
|
navi_id = node.child_value("navi_id")
|
|
friendship = node.child_value("friendship")
|
|
|
|
self.data.local.user.put_achievement(
|
|
self.game,
|
|
self.version,
|
|
userid,
|
|
navi_id,
|
|
"navi",
|
|
{
|
|
"friendship": friendship,
|
|
},
|
|
)
|
|
|
|
# Extract achievements
|
|
game_config = self.get_game_config()
|
|
for node in request.children:
|
|
if node.name == "item":
|
|
itemid = node.child_value("id")
|
|
itemtype = node.child_value("type")
|
|
param = node.child_value("param")
|
|
is_new = node.child_value("is_new")
|
|
get_time = node.child_value("get_time")
|
|
|
|
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,
|
|
userid,
|
|
itemid,
|
|
f"item_{itemtype}",
|
|
{
|
|
"param": param,
|
|
"is_new": is_new,
|
|
"get_time": get_time,
|
|
},
|
|
)
|
|
|
|
elif node.name == "chara_param":
|
|
charaid = node.child_value("chara_id")
|
|
friendship = node.child_value("friendship")
|
|
|
|
self.data.local.user.put_achievement(
|
|
self.game,
|
|
self.version,
|
|
userid,
|
|
charaid,
|
|
"chara",
|
|
{
|
|
"friendship": friendship,
|
|
},
|
|
)
|
|
|
|
elif node.name == "area":
|
|
area_id = node.child_value("area_id")
|
|
index = node.child_value("chapter_index")
|
|
points = node.child_value("gauge_point")
|
|
cleared = node.child_value("is_cleared")
|
|
diary = node.child_value("diary")
|
|
|
|
self.data.local.user.put_achievement(
|
|
self.game,
|
|
self.version,
|
|
userid,
|
|
area_id,
|
|
"area",
|
|
{
|
|
"index": index,
|
|
"points": points,
|
|
"cleared": cleared,
|
|
"diary": diary,
|
|
},
|
|
)
|
|
|
|
elif node.name == "mission":
|
|
# If you don't send the right values on login, then
|
|
# the game sends 0 for mission_id three times. Skip
|
|
# those values since they're bogus.
|
|
mission_id = node.child_value("mission_id")
|
|
if mission_id > 0:
|
|
points = node.child_value("gauge_point")
|
|
complete = node.child_value("mission_comp")
|
|
|
|
self.data.local.user.put_time_based_achievement(
|
|
self.game,
|
|
self.version,
|
|
userid,
|
|
mission_id,
|
|
"mission",
|
|
{
|
|
"points": points,
|
|
"complete": complete,
|
|
},
|
|
)
|
|
|
|
# Unlock NAVI-kun and Kenshi Yonezu after one play
|
|
for songid in [1592, 1608]:
|
|
self.data.local.user.put_achievement(
|
|
self.game,
|
|
self.version,
|
|
userid,
|
|
songid,
|
|
"item_0",
|
|
{
|
|
"param": 0xF,
|
|
"is_new": False,
|
|
"get_time": Time.now(),
|
|
},
|
|
)
|
|
|
|
# Keep track of play statistics
|
|
self.update_play_statistics(userid)
|
|
|
|
return newprofile
|