1
0
mirror of synced 2024-09-24 03:18:22 +02:00

Implement rivals support for supported Pop'n Music games.

This commit is contained in:
Jennifer Taylor 2020-05-12 21:03:01 +00:00
parent e126b845f6
commit 19c7de499d
11 changed files with 594 additions and 26 deletions

View File

@ -2,7 +2,6 @@ Things that I have not gotten around to doing.
- IIDX favorites viewer and editor for frontend. The data is all available in profile, but the interface was never built.
- DDR calorie tracker and workout stats. Again, the data is all available but the interface was never built.
- Rivals support for Pop'n Music series.
- Lobby for all games except Reflec Beat. Reflec is the only game with lobby support right now, but this should be fairly easy to add to other games since the backend infra is all there. Correct global IP detection is even built-in to the server and passed forward if you are using a proxy.
- Prettify the frontend. Its a bit utilitarian right now, aside from some minor color flare.
- Make the frontend work better on mobile. It works well enough, but it could be a much better experience.

View File

@ -1,13 +1,13 @@
# vim: set fileencoding=utf-8
import binascii
import copy
from typing import Dict, Optional
from typing import Dict, List, Optional
from bemani.backend.popn.base import PopnMusicBase
from bemani.backend.popn.lapistoria import PopnMusicLapistoria
from bemani.common import Time, ValidatedDict, VersionConstants
from bemani.data import UserID
from bemani.data import UserID, Link
from bemani.protocol import Node
@ -306,6 +306,93 @@ class PopnMusicEclale(PopnMusicBase):
return root
def handle_player23_friend_request(self, request: Node) -> Node:
refid = request.attribute('ref_id')
no = int(request.attribute('no', '-1'))
root = Node.void('player23')
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, ValidatedDict] = {}
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.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.get_int('extid')))) # Eclale formats on its own
friend.add_child(Node.string('name', rivalprofile.get_str('name', 'なし')))
friend.add_child(Node.s16('chara', 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
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('clearmedal', 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]))
return root
def handle_player23_write_music_request(self, request: Node) -> Node:
refid = request.child_value('ref_id')
@ -443,8 +530,19 @@ class PopnMusicEclale(PopnMusicBase):
account.add_child(Node.s16_array('my_best', most_played))
account.add_child(Node.s16_array('latest_music', last_played))
# TODO: Hook up rivals for Pop'n music?
account.add_child(Node.u8('active_fr_num', 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))
# player statistics
statistics = self.get_play_statistics(userid)

View File

@ -1,13 +1,13 @@
# vim: set fileencoding=utf-8
import copy
from typing import Optional
from typing import Dict, List, Optional
from bemani.backend.popn.base import PopnMusicBase
from bemani.backend.popn.tunestreet import PopnMusicTuneStreet
from bemani.backend.base import Status
from bemani.common import ValidatedDict, VersionConstants, Time, ID
from bemani.data import Score, UserID
from bemani.data import Score, Link, UserID
from bemani.protocol import Node
@ -130,11 +130,24 @@ class PopnMusicFantasia(PopnMusicBase):
today_count = statistics.get_int('today_plays', 0)
else:
today_count = 0
base.add_child(Node.u8('active_fr_num', 0)) # TODO: Hook up rivals code?
base.add_child(Node.s32('total_play_cnt', statistics.get_int('total_plays', 0)))
base.add_child(Node.s16('today_play_cnt', today_count))
base.add_child(Node.s16('consecutive_days', statistics.get_int('consecutive_days', 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
base.add_child(Node.u8('active_fr_num', rivalcount))
last_played = [x[0] for x in self.data.local.music.get_last_played(self.game, self.version, userid, 3)]
most_played = [x[0] for x in self.data.local.music.get_most_played(self.game, self.version, userid, 20)]
while len(last_played) < 3:
@ -418,6 +431,86 @@ class PopnMusicFantasia(PopnMusicBase):
return root
elif method == 'friend':
refid = request.attribute('ref_id')
root = Node.void('playerdata')
# 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.set_attribute('status', str(Status.NO_PROFILE))
return root
# Grab the links that we care about.
links = self.data.local.user.get_links(self.game, self.version, userid)
profiles: Dict[UserID, ValidatedDict] = {}
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)
for rival in links[:2]:
rivalid = rival.other_userid
rivalprofile = profiles[rivalid]
scores = self.data.remote.music.get_scores(self.game, self.version, rivalid)
# First, output general profile info.
friend = Node.void('friend')
root.add_child(friend)
# 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('open', 1))
# Set up some sane defaults.
friend.add_child(Node.string('name', rivalprofile.get_str('name', 'なし')))
friend.add_child(Node.string('g_pm_id', ID.format_extid(rivalprofile.get_int('extid'))))
friend.add_child(Node.s16('chara', rivalprofile.get_int('chara', -1)))
# Perform hiscore/medal conversion.
hiscore_array = [0] * int((((self.GAME_MAX_MUSIC_ID * 4) * 17) + 7) / 8)
clear_medal = [0] * self.GAME_MAX_MUSIC_ID
for score in scores:
if score.id > self.GAME_MAX_MUSIC_ID:
continue
# 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
points = score.points
clear_medal[score.id] = clear_medal[score.id] | self.__format_medal_for_score(score)
hiscore_index = (score.id * 4) + {
self.CHART_TYPE_EASY: self.GAME_CHART_TYPE_EASY_POSITION,
self.CHART_TYPE_NORMAL: self.GAME_CHART_TYPE_NORMAL_POSITION,
self.CHART_TYPE_HYPER: self.GAME_CHART_TYPE_HYPER_POSITION,
self.CHART_TYPE_EX: self.GAME_CHART_TYPE_EX_POSITION,
}[score.chart]
hiscore_byte_pos = int((hiscore_index * 17) / 8)
hiscore_bit_pos = int((hiscore_index * 17) % 8)
hiscore_value = points << hiscore_bit_pos
hiscore_array[hiscore_byte_pos] = hiscore_array[hiscore_byte_pos] | (hiscore_value & 0xFF)
hiscore_array[hiscore_byte_pos + 1] = hiscore_array[hiscore_byte_pos + 1] | ((hiscore_value >> 8) & 0xFF)
hiscore_array[hiscore_byte_pos + 2] = hiscore_array[hiscore_byte_pos + 2] | ((hiscore_value >> 16) & 0xFF)
hiscore = bytes(hiscore_array)
friend.add_child(Node.u16_array('clear_medal', clear_medal))
friend.add_child(Node.binary('hiscore', hiscore))
return root
# Invalid method
return None
@ -458,3 +551,7 @@ class PopnMusicFantasia(PopnMusicBase):
# Invalid method
return None
def handle_lobby_request(self, request: Node) -> Optional[Node]:
# Stub out the entire lobby service
return Node.void('lobby')

View File

@ -1,13 +1,13 @@
# vim: set fileencoding=utf-8
import copy
from typing import Optional
from typing import Dict, List, Optional
from bemani.backend.popn.base import PopnMusicBase
from bemani.backend.popn.sunnypark import PopnMusicSunnyPark
from bemani.backend.base import Status
from bemani.common import ValidatedDict, VersionConstants, Time, ID
from bemani.data import UserID
from bemani.data import UserID, Link
from bemani.protocol import Node
@ -176,6 +176,93 @@ class PopnMusicLapistoria(PopnMusicBase):
return root
elif method == 'friend':
refid = request.attribute('ref_id')
no = int(request.attribute('no', '-1'))
root = Node.void('player22')
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, ValidatedDict] = {}
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.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', ID.format_extid(rivalprofile.get_int('extid'))))
friend.add_child(Node.string('name', rivalprofile.get_str('name', 'なし')))
friend.add_child(Node.s16('chara', 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
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('clearmedal', 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]))
return root
elif method == 'conversion':
refid = request.child_value('ref_id')
name = request.child_value('name')
@ -263,13 +350,26 @@ class PopnMusicLapistoria(PopnMusicBase):
today_count = statistics.get_int('today_plays', 0)
else:
today_count = 0
account.add_child(Node.u8('active_fr_num', 0)) # TODO: Hook up rivals code?
account.add_child(Node.s16('total_play_cnt', statistics.get_int('total_plays', 0)))
account.add_child(Node.s16('today_play_cnt', today_count))
account.add_child(Node.s16('consecutive_days', statistics.get_int('consecutive_days', 0)))
account.add_child(Node.s16('total_days', statistics.get_int('total_days', 0)))
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))
# Add scores section
last_played = [x[0] for x in self.data.local.music.get_last_played(self.game, self.version, userid, 5)]
most_played = [x[0] for x in self.data.local.music.get_most_played(self.game, self.version, userid, 10)]

View File

@ -1,13 +1,13 @@
# vim: set fileencoding=utf-8
import copy
from typing import Optional
from typing import Dict, List, Optional
from bemani.backend.popn.base import PopnMusicBase
from bemani.backend.popn.fantasia import PopnMusicFantasia
from bemani.backend.base import Status
from bemani.common import ValidatedDict, VersionConstants, Time, ID
from bemani.data import UserID
from bemani.data import UserID, Link
from bemani.protocol import Node
@ -91,11 +91,24 @@ class PopnMusicSunnyPark(PopnMusicBase):
today_count = statistics.get_int('today_plays', 0)
else:
today_count = 0
base.add_child(Node.u8('active_fr_num', 0)) # TODO: Hook up rivals code?
base.add_child(Node.s32('total_play_cnt', statistics.get_int('total_plays', 0)))
base.add_child(Node.s16('today_play_cnt', today_count))
base.add_child(Node.s16('consecutive_days', statistics.get_int('consecutive_days', 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
base.add_child(Node.u8('active_fr_num', rivalcount))
last_played = [x[0] for x in self.data.local.music.get_last_played(self.game, self.version, userid, 3)]
most_played = [x[0] for x in self.data.local.music.get_most_played(self.game, self.version, userid, 20)]
while len(last_played) < 3:
@ -135,7 +148,13 @@ class PopnMusicSunnyPark(PopnMusicBase):
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')]
clear_medal[score.id] = clear_medal[score.id] | (medal << (score.chart * 4))
medal_pos = {
self.CHART_TYPE_EASY: self.GAME_CHART_TYPE_EASY_POSITION,
self.CHART_TYPE_NORMAL: self.GAME_CHART_TYPE_NORMAL_POSITION,
self.CHART_TYPE_HYPER: self.GAME_CHART_TYPE_HYPER_POSITION,
self.CHART_TYPE_EX: self.GAME_CHART_TYPE_EX_POSITION,
}[score.chart]
clear_medal[score.id] = clear_medal[score.id] | (medal << (medal_pos * 4))
hiscore_index = (score.id * 4) + {
self.CHART_TYPE_EASY: self.GAME_CHART_TYPE_EASY_POSITION,
@ -525,5 +544,117 @@ class PopnMusicSunnyPark(PopnMusicBase):
return root
elif method == 'friend':
refid = request.attribute('ref_id')
root = Node.void('playerdata')
# 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.set_attribute('status', str(Status.NO_PROFILE))
return root
# Grab the links that we care about.
links = self.data.local.user.get_links(self.game, self.version, userid)
profiles: Dict[UserID, ValidatedDict] = {}
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)
for rival in links[:2]:
rivalid = rival.other_userid
rivalprofile = profiles[rivalid]
scores = self.data.remote.music.get_scores(self.game, self.version, rivalid)
# First, output general profile info.
friend = Node.void('friend')
root.add_child(friend)
# 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('open', 1))
# Set up some sane defaults.
friend.add_child(Node.string('name', rivalprofile.get_str('name', 'なし')))
friend.add_child(Node.string('g_pm_id', ID.format_extid(rivalprofile.get_int('extid'))))
friend.add_child(Node.s16('chara', rivalprofile.get_int('chara', -1)))
# Set up player avatar.
avatar_dict = rivalprofile.get_dict('avatar')
friend.add_child(Node.u8('hair', avatar_dict.get_int('hair', 0)))
friend.add_child(Node.u8('face', avatar_dict.get_int('face', 0)))
friend.add_child(Node.u8('body', avatar_dict.get_int('body', 0)))
friend.add_child(Node.u8('effect', avatar_dict.get_int('effect', 0)))
friend.add_child(Node.u8('object', avatar_dict.get_int('object', 0)))
friend.add_child(Node.u8_array('comment', avatar_dict.get_int_array('comment', 2)))
# Perform hiscore/medal conversion.
hiscore_array = [0] * int((((self.GAME_MAX_MUSIC_ID * 4) * 17) + 7) / 8)
clear_medal = [0] * self.GAME_MAX_MUSIC_ID
for score in scores:
if score.id > self.GAME_MAX_MUSIC_ID:
continue
# 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
points = score.points + rivalprofile.get_int('extid') % 100
medal = {
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_CIRCLE_CLEARED, # Map approximately
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')]
medal_pos = {
self.CHART_TYPE_EASY: self.GAME_CHART_TYPE_EASY_POSITION,
self.CHART_TYPE_NORMAL: self.GAME_CHART_TYPE_NORMAL_POSITION,
self.CHART_TYPE_HYPER: self.GAME_CHART_TYPE_HYPER_POSITION,
self.CHART_TYPE_EX: self.GAME_CHART_TYPE_EX_POSITION,
}[score.chart]
clear_medal[score.id] = clear_medal[score.id] | (medal << (medal_pos * 4))
hiscore_index = (score.id * 4) + {
self.CHART_TYPE_EASY: self.GAME_CHART_TYPE_EASY_POSITION,
self.CHART_TYPE_NORMAL: self.GAME_CHART_TYPE_NORMAL_POSITION,
self.CHART_TYPE_HYPER: self.GAME_CHART_TYPE_HYPER_POSITION,
self.CHART_TYPE_EX: self.GAME_CHART_TYPE_EX_POSITION,
}[score.chart]
hiscore_byte_pos = int((hiscore_index * 17) / 8)
hiscore_bit_pos = int((hiscore_index * 17) % 8)
hiscore_value = points << hiscore_bit_pos
hiscore_array[hiscore_byte_pos] = hiscore_array[hiscore_byte_pos] | (hiscore_value & 0xFF)
hiscore_array[hiscore_byte_pos + 1] = hiscore_array[hiscore_byte_pos + 1] | ((hiscore_value >> 8) & 0xFF)
hiscore_array[hiscore_byte_pos + 2] = hiscore_array[hiscore_byte_pos + 2] | ((hiscore_value >> 16) & 0xFF)
hiscore = bytes(hiscore_array)
friend.add_child(Node.u16_array('clear_medal', clear_medal))
friend.add_child(Node.binary('hiscore', hiscore))
return root
# Invalid method
return None
def handle_lobby_request(self, request: Node) -> Optional[Node]:
# Stub out the entire lobby service
return Node.void('lobby')

View File

@ -135,6 +135,9 @@ class PopnMusicTuneStreet(PopnMusicBase):
binary_profile[63] = (profile.get_int('music') >> 8) & 0xFF
binary_profile[64] = profile.get_int('sheet') & 0xFF
binary_profile[65] = profile.get_int('category') & 0xFF
# This might be the count of friends, since Tune Street *does* support
# rivals. However, I can no longer get it running on my cabinet or locally
# so there's no way for me to test.
binary_profile[67] = profile.get_int('medal_and_friend') & 0xFF
# Format Scores

View File

@ -7,7 +7,7 @@ from typing import Any, Dict, List, Optional, Tuple
from bemani.backend.popn.base import PopnMusicBase
from bemani.backend.popn.eclale import PopnMusicEclale
from bemani.common import Time, ID, ValidatedDict, VersionConstants, Parallel
from bemani.data import Data, UserID, Achievement
from bemani.data import Data, UserID, Achievement, Link
from bemani.protocol import Node
@ -496,6 +496,108 @@ class PopnMusicUsaNeko(PopnMusicBase):
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, ValidatedDict] = {}
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.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.get_int('extid')))) # UsaNeko formats on its own
friend.add_child(Node.string('name', rivalprofile.get_str('name', 'なし')))
friend.add_child(Node.s16('chara', 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
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)
@ -751,8 +853,19 @@ class PopnMusicUsaNeko(PopnMusicBase):
account.add_child(Node.s16('total_days', statistics.get_int('total_days', 0)))
account.add_child(Node.s16('interval_day', 0))
# TODO: Hook up rivals for Pop'n Music?
account.add_child(Node.u8('active_fr_num', 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')

View File

@ -3,7 +3,7 @@ import re
from typing import Any, Dict
from flask import Blueprint, request, Response, url_for, abort, g # type: ignore
from bemani.common import ID, GameConstants
from bemani.common import ID, GameConstants, VersionConstants
from bemani.data import UserID
from bemani.frontend.app import loginrequired, jsonify, render_react
from bemani.frontend.popn.popn import PopnMusicFrontend
@ -361,12 +361,18 @@ def updatename() -> Dict[str, Any]:
def viewrivals() -> Response:
frontend = PopnMusicFrontend(g.data, g.config, g.cache)
rivals, playerinfo = frontend.get_rivals(g.userID)
# There is no support for Rivals in Tune Street.
if VersionConstants.POPN_MUSIC_TUNE_STREET in rivals:
del rivals[VersionConstants.POPN_MUSIC_TUNE_STREET]
return render_react(
'Pop\'n Music Rivals',
'popn/rivals.react.js',
{
'userid': str(g.userID),
'rivals': rivals,
'max_active_rivals': frontend.max_active_rivals,
'players': playerinfo,
'versions': {version: name for (game, version, name) in frontend.all_games()},
},
@ -387,6 +393,10 @@ def listrivals() -> Dict[str, Any]:
frontend = PopnMusicFrontend(g.data, g.config, g.cache)
rivals, playerinfo = frontend.get_rivals(g.userID)
# There is no support for Rivals in Tune Street.
if VersionConstants.POPN_MUSIC_TUNE_STREET in rivals:
del rivals[VersionConstants.POPN_MUSIC_TUNE_STREET]
return {
'rivals': rivals,
'players': playerinfo,

View File

@ -2,7 +2,7 @@
from typing import Any, Dict, Iterator, Tuple
from bemani.backend.popn import PopnMusicFactory, PopnMusicBase
from bemani.common import ValidatedDict, GameConstants
from bemani.common import ValidatedDict, GameConstants, VersionConstants
from bemani.data import Attempt, Score, Song, UserID
from bemani.frontend.base import FrontendBase
@ -20,6 +20,17 @@ class PopnMusicFrontend(FrontendBase):
valid_rival_types = ['rival']
max_active_rivals = {
# Technically there is support for Rivals in Tune Street but I
# couldn't get it booting anymore to test.
VersionConstants.POPN_MUSIC_TUNE_STREET: 0,
VersionConstants.POPN_MUSIC_FANTASIA: 2,
VersionConstants.POPN_MUSIC_SUNNY_PARK: 2,
VersionConstants.POPN_MUSIC_LAPISTORIA: 4,
VersionConstants.POPN_MUSIC_ECLALE: 4,
VersionConstants.POPN_MUSIC_USANEKO: 4,
}
def all_games(self) -> Iterator[Tuple[str, int, str]]:
yield from PopnMusicFactory.all_games()

View File

@ -3,7 +3,7 @@ import re
from typing import Any, Dict
from flask import Blueprint, request, Response, url_for, abort, g # type: ignore
from bemani.common import ID, GameConstants
from bemani.common import ID, GameConstants, VersionConstants
from bemani.data import UserID
from bemani.frontend.app import loginrequired, jsonify, render_react
from bemani.frontend.sdvx.sdvx import SoundVoltexFrontend
@ -355,10 +355,10 @@ def viewrivals() -> Response:
rivals, playerinfo = frontend.get_rivals(g.userID)
# There were no rivals in SDVX 1 or 2.
if 1 in rivals:
del rivals[1]
if 2 in rivals:
del rivals[2]
if VersionConstants.SDVX_BOOTH in rivals:
del rivals[VersionConstants.SDVX_BOOTH]
if VersionConstants.SDVX_INFINITE_INFECTION in rivals:
del rivals[VersionConstants.SDVX_INFINITE_INFECTION]
return render_react(
'SDVX Rivals',
@ -386,6 +386,12 @@ def listrivals() -> Dict[str, Any]:
frontend = SoundVoltexFrontend(g.data, g.config, g.cache)
rivals, playerinfo = frontend.get_rivals(g.userID)
# There were no rivals in SDVX 1 or 2.
if VersionConstants.SDVX_BOOTH in rivals:
del rivals[VersionConstants.SDVX_BOOTH]
if VersionConstants.SDVX_INFINITE_INFECTION in rivals:
del rivals[VersionConstants.SDVX_INFINITE_INFECTION]
return {
'rivals': rivals,
'players': playerinfo,

View File

@ -128,7 +128,7 @@ var rivals_view = React.createClass({
}
}
if (count >= 4) { avail = false; }
if (count >= window.max_active_rivals[this.state.version]) { avail = false; }
return (
<span>