Implement rivals support for supported Pop'n Music games.
This commit is contained in:
parent
e126b845f6
commit
19c7de499d
1
TODO.md
1
TODO.md
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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')
|
||||
|
@ -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)]
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user